diff --git a/THREADS.md b/THREADS.md index d8e79db..0d3ee62 100644 --- a/THREADS.md +++ b/THREADS.md @@ -9476,3 +9476,73 @@ anchors are your GLB), but the tooling is now waiting, not TODO. For A: N7's same-client double offer reads odd under "the other goes to somebody else". Gate 4 is JOHN'S: partly.party/hardyards β€” seven nights, the board, the Vasilaros cap, and the Karalis pool yard in the pool. Your three sentences steer Sprint 18. + +[E] 2026-07-28 β€” 🎨 **THE PALETTE HAS BEEN ONE GAMMA STEP TOO LIGHT SINCE SPRINT 1, ON EVERY + OBJECT IN THE GAME. FOUND, MEASURED, FIXED AT SOURCE, PINNED.** (Integrator wearing E's hat, + John's ask: "more detailed textures and objects". The most detailed thing available turned out + to be the art we already had, drawn correctly.) + + **THE BUG.** `hex_to_rgba` (build_yard_assets.py:165) divided a palette hex by 255 and stopped + β€” the docstring says it was "lifted from racks_to_glb.py, kept deliberately close to the + original", and the original's bug came with it. But `Base Color` and `Emission Color` are + LINEAR sockets. The exporter copies what it finds into `baseColorFactor` (also linear), and + three converts linearβ†’sRGB to display. An sRGB fraction parked in a linear socket therefore + renders one whole gamma step too light and too desaturated β€” every material, every asset, + seventeen sprints. + + **MEASURED OFF THE SHIPPED GLB, before and after:** + + | PAL entry | its own comment | factor before | displayed before | factor after | displayed after | + |---|---|---|---|---|---| + | `leaf_gum #7C8F5E` | "sage/olive, **not lawn green**" | 0.4863 | **#B9C5A3** | 0.2016 | **#7C8F5E** βœ“ | + | `bark_gum #BFB8A8` | "eucalypt: **pale, chalky**" | 0.7490 | **#E0DDD4** | 0.5210 | **#BFB8A8** βœ“ | + + The game was drawing a NEAR-WHITE gum trunk where E wrote "pale, chalky", and a grey-green + where E wrote "not lawn green". **THREADS' own [E] entry blaming Blender's contact-sheet view + transform for a near-white canopy was half right β€” the view transform was real, and it masked + this, which shipped.** It is also why the ped has always looked like he belongs to a richer + render than the yard: he is the only textured object in the scene and his maps were correct, + so he alone was never washed out. + + **THE FIX** is `_srgb_to_linear` on the three colour channels (not alpha β€” alpha is coverage, + not colour), the exact IEC 61966-2-1 curve rather than a 2.2 approximation because three's own + decode is that curve. Both call sites are colour sockets, both convert. Full factory rebuild: + **ALL ASSETS PASS**, 37 GLBs re-exported, one-time churn that is exactly what a deterministic + factory is for. Nothing was hand-edited; `--only` still reproduces a full run. + + **THE PIN** (e.test): for three PAL materials, `mat.color.getHexString(SRGBColorSpace)` β€” the + exact question a player's eye asks β€” must equal the authored hex. It compares the FACTORY's + intent against what the RUNTIME draws, which is the one comparison neither end can fake by + agreeing with itself. **Mutation-checked red-then-green**: restoring the pre-fix GLB from HEAD + reds it with "PAL.leaf_gum is authored #7C8F5E but tree_gum_01/Mat_Leaf DISPLAYS #B9C5A3". + Selftest **523/0/0** (522 + the pin). + + **WHY THIS AND NOT NEW TEXTURES (John asked for textures; here is the honest reason he got a + gamma fix first).** A texture is an sRGB map multiplied into `mat.color`. Wire a correctly + decoded map on top of a base that is already a gamma step too bright and the result is muddy + AND inconsistent with every surface you have not touched yet. The palette had to be right + before any map goes on top of it. **Verified by eye in the running game, same camera, same + night, before and after** β€” canopy reads eucalypt olive instead of chalky sage, sleepers read + hardwood instead of pale tan, the shed reads Colorbond grey instead of near-white, and the ped + stops looking pasted in. + + **STILL OPEN, and the next shift's work β€” filed with evidence rather than done half:** + Β· **7 of 8 factory textures have ZERO runtime consumers** (`grass_atlas` β€” now ten sprints β€” + plus `hail_pips`, `moon`, `plant_shred`, `pond_normal`, `pond_water`, `sail_tears`). Only + `sail_weave` is wired (sail.js:1275). Their systems are all LIVE now β€” ponding, breaks, + hail, garden damage, the night sky β€” so this is detail the game already paid for and does + not draw. Wire them, or bin them, but the ten-sprint drift stops. + Β· **THE LAWN is 25–50% of frame** (measured across five camera poses), flat #4a7c3f, no map, + and it is what the sail's shadow lands on β€” "the shadow IS the product". Its `PlaneGeometry` + already emits clean 0..1 UVs, so it needs NO factory UV work: one procedural seamless tile + + one loader block. Biggest visual win left, cheapest wiring in the repo. + Β· ⚠️ **AND THE REASON THE OTHER SURFACES CANNOT HAVE MAPS YET:** every factory mesh carries + Blender's untouched default primitive unwrap. Measured by parsing TEXCOORD_0 out of the + binaries: texel density varies **95.8Γ— WITHIN a single mesh** on `fence_panel_v1` + (0.3817–36.5497), 92.0Γ— on `house_yardside_v1`'s roof; `uvArea` is *exactly* 0.3750 and U is + *exactly* [0.125, 0.875] on fence, shed, table, pergola, carport and all four debris tubs β€” + the literal default cube cross. There is no `smart_project`/`cube_project` anywhere in the + factory's ~4,300 lines. So a `hasAttribute('uv')` check PASSES and a wood grain would still + arrive 10Γ— finer on a paling's edge than its face. **UV-unwrapping the factory is its own + gate** β€” it must land with a texel-density assert, or the next lane ships stretched wood and + calls it detail. diff --git a/tools/blender/asset_report.json b/tools/blender/asset_report.json index 900cc5e..dd6ee24 100644 --- a/tools/blender/asset_report.json +++ b/tools/blender/asset_report.json @@ -955,5 +955,42 @@ "problems": [] } ], - "debris": [] + "debris": [ + { + "file": "BlueCrate_v2.glb", + "dims": [ + 0.36, + 0.36, + 0.29 + ], + "sane": true + }, + { + "file": "BlackTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WhiteTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WoodenBin_v2.glb", + "dims": [ + 0.35, + 0.36, + 0.31 + ], + "sane": true + } + ] } \ No newline at end of file diff --git a/tools/blender/build_yard_assets.py b/tools/blender/build_yard_assets.py index 394c399..cead034 100644 --- a/tools/blender/build_yard_assets.py +++ b/tools/blender/build_yard_assets.py @@ -162,12 +162,49 @@ WATER_DEEP = (0.22, 0.28, 0.26) # ============================================================================ # UTILS β€” lifted from racks_to_glb.py, kept deliberately close to the original # ============================================================================ +def _srgb_to_linear(c): + """One sRGB channel (0..1) -> linear. The exact IEC 61966-2-1 curve, not a + 2.2 approximation, because three.js's own decode is this curve and a + mismatched gamma is what this function exists to stop.""" + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + def hex_to_rgba(hex_str, alpha=1.0): + """A palette hex (which is an sRGB colour, as read off any colour picker) + -> the LINEAR floats Blender's colour sockets actually want. + + THE BUG THIS FIXES, MEASURED (2026-07-28, integrator wearing E's hat). + This was lifted from racks_to_glb.py and kept "deliberately close to the + original", and the original divided by 255 and stopped. But `Base Color` + and `Emission Color` are LINEAR sockets; the glTF exporter writes what it + finds straight into `baseColorFactor` (also linear); and three.js then + converts linear -> sRGB for display. So an sRGB fraction parked in that + socket comes out one whole gamma step too light and too desaturated, on + every object in the game, since Sprint 1. + + Read out of the shipped tree_gum_01_v1.glb before the fix: + + PAL entry baseColorFactor displayed as + leaf_gum #7C8F5E 0.4863 0.5608 0.3686 #B9C5A3 + bark_gum #BFB8A8 0.7490 0.7216 0.6588 #E0DDD4 + + i.e. the comment beside leaf_gum says "sage/olive, not lawn green" and the + game drew a pale grey-green; the comment beside bark_gum says "pale, + chalky" and the game drew a near-white trunk. THREADS has an [E] entry + blaming Blender's contact-sheet view transform for a canopy reading + near-white β€” the view transform was real, but so was this, and this one + shipped. It is also why the ped (the one properly textured object in the + yard) has always looked like he belonged to a richer render than the yard. + + Alpha is NOT converted: it is coverage, not colour. + """ h = (hex_str or "#888888").lstrip("#") if len(h) != 6: h = "888888" - return (int(h[0:2], 16) / 255.0, int(h[2:4], 16) / 255.0, - int(h[4:6], 16) / 255.0, alpha) + return (_srgb_to_linear(int(h[0:2], 16) / 255.0), + _srgb_to_linear(int(h[2:4], 16) / 255.0), + _srgb_to_linear(int(h[4:6], 16) / 255.0), + alpha) _MAT_CACHE = {} diff --git a/tools/blender/contact_sheet.png b/tools/blender/contact_sheet.png index 963346c..0367cd2 100644 Binary files a/tools/blender/contact_sheet.png and b/tools/blender/contact_sheet.png differ diff --git a/web/world/js/tests/e.test.js b/web/world/js/tests/e.test.js index fc71306..6467e26 100644 --- a/web/world/js/tests/e.test.js +++ b/web/world/js/tests/e.test.js @@ -159,6 +159,54 @@ export default async function run(t) { assert(failed.size === 0, `failed to load: ${lost}`); }); + /** + * THE COLOUR THE PALETTE ASKED FOR IS THE COLOUR THE PLAYER SEES. + * + * Shipped broken from Sprint 1 to Sprint 17 and nothing could see it: + * `hex_to_rgba` divided a palette hex by 255 and put that sRGB fraction + * straight into Blender's LINEAR `Base Color` socket. The exporter copies it + * to `baseColorFactor` (linear), and three converts linear -> sRGB to + * display β€” so every surface in the game rendered one whole gamma step too + * light and too desaturated. Measured before the fix: bark_gum #BFB8A8 + * displayed #E0DDD4 (a near-white gum trunk), leaf_gum #7C8F5E displayed + * #B9C5A3. THREADS has an [E] entry blaming Blender's contact-sheet view + * transform for a near-white canopy; that was real too, but it masked this. + * + * The pin closes the loop the same way the post-rating pin does: it asserts + * the FACTORY's authored intent against what the RUNTIME will draw. The + * expected values are the PAL entries in build_yard_assets.py verbatim β€” + * literals here on purpose, because the whole failure mode was the two ends + * agreeing with each other while disagreeing with the author. + * + * `getHexString(SRGBColorSpace)` is the exact question a player asks: three + * has already decoded baseColorFactor into its linear working space, so this + * re-encodes it the way the display will. Comparing the raw factor instead + * would re-implement the bug inside its own test. + */ + t.test('the palette hex the factory authored is the hex the game DISPLAYS', () => { + // [asset, material, PAL key, authored hex] β€” build_yard_assets.py PAL @102-130 + const PINS = [ + ['tree_gum_01', 'Mat_Leaf', 'leaf_gum', '7c8f5e'], + ['tree_gum_01', 'Mat_Bark', 'bark_gum', 'bfb8a8'], + ['tree_gum_01', 'Mat_Leaf2', 'leaf_gum_2', '6b7e52'], + ]; + const seen = []; + for (const [assetName, matName, palKey, want] of PINS) { + const g = loaded.get(assetName); + assert(g, `${assetName} did not load`); + let mat = null; + g.scene.traverse((o) => { if (o.isMesh && o.material?.name === matName) mat = o.material; }); + assert(mat, `${assetName} has no material named ${matName} β€” the pin is aimed at a material that moved`); + const got = mat.color.getHexString(THREE.SRGBColorSpace); + assert(got === want, + `PAL.${palKey} is authored #${want.toUpperCase()} but ${assetName}/${matName} DISPLAYS #${got.toUpperCase()}. ` + + 'That is the sRGB-into-a-linear-socket bug (or its over-correction) back again β€” fix hex_to_rgba ' + + 'in tools/blender/build_yard_assets.py and re-run the factory; do not touch the GLB.'); + seen.push(`${palKey} #${got}`); + } + return seen.join(', '); + }); + // The anchor of the whole scale system. If this is wrong, every judgement // made against the contact sheet was made against a lie. t.test('ref_capsule is 1.70 m tall on +Y β€” the scale everything is judged against', () => { diff --git a/web/world/models/bike_kid_01_v1.glb b/web/world/models/bike_kid_01_v1.glb index 23a79ef..1d22be9 100644 Binary files a/web/world/models/bike_kid_01_v1.glb and b/web/world/models/bike_kid_01_v1.glb differ diff --git a/web/world/models/broom_01_v1.glb b/web/world/models/broom_01_v1.glb index 75259e1..5d9ba64 100644 Binary files a/web/world/models/broom_01_v1.glb and b/web/world/models/broom_01_v1.glb differ diff --git a/web/world/models/carabiner_v1.glb b/web/world/models/carabiner_v1.glb index a4ff4f2..8ef17ae 100644 Binary files a/web/world/models/carabiner_v1.glb and b/web/world/models/carabiner_v1.glb differ diff --git a/web/world/models/carport_01_v1.glb b/web/world/models/carport_01_v1.glb index bcec19e..aa44a3c 100644 Binary files a/web/world/models/carport_01_v1.glb and b/web/world/models/carport_01_v1.glb differ diff --git a/web/world/models/carport_01_wrecked_v1.glb b/web/world/models/carport_01_wrecked_v1.glb index 4b8d5d3..c4e827e 100644 Binary files a/web/world/models/carport_01_wrecked_v1.glb and b/web/world/models/carport_01_wrecked_v1.glb differ diff --git a/web/world/models/debris/tramp_01_v1.glb b/web/world/models/debris/tramp_01_v1.glb index b979151..7660913 100644 Binary files a/web/world/models/debris/tramp_01_v1.glb and b/web/world/models/debris/tramp_01_v1.glb differ diff --git a/web/world/models/debris/wheelie_bin_01_v1.glb b/web/world/models/debris/wheelie_bin_01_v1.glb index 01b89f3..5275d27 100644 Binary files a/web/world/models/debris/wheelie_bin_01_v1.glb and b/web/world/models/debris/wheelie_bin_01_v1.glb differ diff --git a/web/world/models/fence_panel_snapped_v1.glb b/web/world/models/fence_panel_snapped_v1.glb index ff0f705..ba0d097 100644 Binary files a/web/world/models/fence_panel_snapped_v1.glb and b/web/world/models/fence_panel_snapped_v1.glb differ diff --git a/web/world/models/fence_panel_v1.glb b/web/world/models/fence_panel_v1.glb index a6562ac..70ed68f 100644 Binary files a/web/world/models/fence_panel_v1.glb and b/web/world/models/fence_panel_v1.glb differ diff --git a/web/world/models/fence_post_v1.glb b/web/world/models/fence_post_v1.glb index 5ab8fc2..041d0c2 100644 Binary files a/web/world/models/fence_post_v1.glb and b/web/world/models/fence_post_v1.glb differ diff --git a/web/world/models/garden_bed_v1.glb b/web/world/models/garden_bed_v1.glb index 3db2fa0..84eec28 100644 Binary files a/web/world/models/garden_bed_v1.glb and b/web/world/models/garden_bed_v1.glb differ diff --git a/web/world/models/garden_gnome_01_broken_v1.glb b/web/world/models/garden_gnome_01_broken_v1.glb index cdcc281..c27ffd3 100644 Binary files a/web/world/models/garden_gnome_01_broken_v1.glb and b/web/world/models/garden_gnome_01_broken_v1.glb differ diff --git a/web/world/models/garden_gnome_01_v1.glb b/web/world/models/garden_gnome_01_v1.glb index f74f106..648c46a 100644 Binary files a/web/world/models/garden_gnome_01_v1.glb and b/web/world/models/garden_gnome_01_v1.glb differ diff --git a/web/world/models/gate_v1.glb b/web/world/models/gate_v1.glb index 2c84e44..f7b400c 100644 Binary files a/web/world/models/gate_v1.glb and b/web/world/models/gate_v1.glb differ diff --git a/web/world/models/glasshouse_01_v1.glb b/web/world/models/glasshouse_01_v1.glb index 706149d..f687c53 100644 Binary files a/web/world/models/glasshouse_01_v1.glb and b/web/world/models/glasshouse_01_v1.glb differ diff --git a/web/world/models/glasshouse_01_wrecked_v1.glb b/web/world/models/glasshouse_01_wrecked_v1.glb index 9306a62..371d08c 100644 Binary files a/web/world/models/glasshouse_01_wrecked_v1.glb and b/web/world/models/glasshouse_01_wrecked_v1.glb differ diff --git a/web/world/models/hail_stone_01_v1.glb b/web/world/models/hail_stone_01_v1.glb index 1d8b811..6558e2d 100644 Binary files a/web/world/models/hail_stone_01_v1.glb and b/web/world/models/hail_stone_01_v1.glb differ diff --git a/web/world/models/house_yardside_v1.glb b/web/world/models/house_yardside_v1.glb index 6daf5bf..39a06e1 100644 Binary files a/web/world/models/house_yardside_v1.glb and b/web/world/models/house_yardside_v1.glb differ diff --git a/web/world/models/house_yardside_wrecked_v1.glb b/web/world/models/house_yardside_wrecked_v1.glb index 49ea885..a76e279 100644 Binary files a/web/world/models/house_yardside_wrecked_v1.glb and b/web/world/models/house_yardside_wrecked_v1.glb differ diff --git a/web/world/models/ladder_01_v1.glb b/web/world/models/ladder_01_v1.glb index f38ab4a..48b6e44 100644 Binary files a/web/world/models/ladder_01_v1.glb and b/web/world/models/ladder_01_v1.glb differ diff --git a/web/world/models/pergola_01_v1.glb b/web/world/models/pergola_01_v1.glb index 1e6d29d..db4a856 100644 Binary files a/web/world/models/pergola_01_v1.glb and b/web/world/models/pergola_01_v1.glb differ diff --git a/web/world/models/pergola_01_wrecked_v1.glb b/web/world/models/pergola_01_wrecked_v1.glb index eed4ff6..6885c0a 100644 Binary files a/web/world/models/pergola_01_wrecked_v1.glb and b/web/world/models/pergola_01_wrecked_v1.glb differ diff --git a/web/world/models/pool_kit_01_v1.glb b/web/world/models/pool_kit_01_v1.glb index 1d08869..6518560 100644 Binary files a/web/world/models/pool_kit_01_v1.glb and b/web/world/models/pool_kit_01_v1.glb differ diff --git a/web/world/models/ref_capsule_v1.glb b/web/world/models/ref_capsule_v1.glb index 6fe92cd..57c2e75 100644 Binary files a/web/world/models/ref_capsule_v1.glb and b/web/world/models/ref_capsule_v1.glb differ diff --git a/web/world/models/sail_post_corroded_v1.glb b/web/world/models/sail_post_corroded_v1.glb index 7453546..03e1fc3 100644 Binary files a/web/world/models/sail_post_corroded_v1.glb and b/web/world/models/sail_post_corroded_v1.glb differ diff --git a/web/world/models/sail_post_corroded_wrecked_v1.glb b/web/world/models/sail_post_corroded_wrecked_v1.glb index 69be741..9579274 100644 Binary files a/web/world/models/sail_post_corroded_wrecked_v1.glb and b/web/world/models/sail_post_corroded_wrecked_v1.glb differ diff --git a/web/world/models/sail_post_v1.glb b/web/world/models/sail_post_v1.glb index 55d4dfb..b8ea7c4 100644 Binary files a/web/world/models/sail_post_v1.glb and b/web/world/models/sail_post_v1.glb differ diff --git a/web/world/models/shackle_v1.glb b/web/world/models/shackle_v1.glb index 08e0739..cc3be28 100644 Binary files a/web/world/models/shackle_v1.glb and b/web/world/models/shackle_v1.glb differ diff --git a/web/world/models/shed_01_v1.glb b/web/world/models/shed_01_v1.glb index f067333..9b58650 100644 Binary files a/web/world/models/shed_01_v1.glb and b/web/world/models/shed_01_v1.glb differ diff --git a/web/world/models/shed_table_v1.glb b/web/world/models/shed_table_v1.glb index 75064dd..4aefe39 100644 Binary files a/web/world/models/shed_table_v1.glb and b/web/world/models/shed_table_v1.glb differ diff --git a/web/world/models/swing_set_01_v1.glb b/web/world/models/swing_set_01_v1.glb index aa716d7..ff2ed26 100644 Binary files a/web/world/models/swing_set_01_v1.glb and b/web/world/models/swing_set_01_v1.glb differ diff --git a/web/world/models/swing_set_01_wrecked_v1.glb b/web/world/models/swing_set_01_wrecked_v1.glb index 2fdcb6a..7554e0d 100644 Binary files a/web/world/models/swing_set_01_wrecked_v1.glb and b/web/world/models/swing_set_01_wrecked_v1.glb differ diff --git a/web/world/models/tree_gum_01_v1.glb b/web/world/models/tree_gum_01_v1.glb index 6475479..4d08b51 100644 Binary files a/web/world/models/tree_gum_01_v1.glb and b/web/world/models/tree_gum_01_v1.glb differ diff --git a/web/world/models/tree_gum_02_v1.glb b/web/world/models/tree_gum_02_v1.glb index 39c8448..f17d0ca 100644 Binary files a/web/world/models/tree_gum_02_v1.glb and b/web/world/models/tree_gum_02_v1.glb differ diff --git a/web/world/models/tree_jacaranda_01_v1.glb b/web/world/models/tree_jacaranda_01_v1.glb index 54c196d..c287ca8 100644 Binary files a/web/world/models/tree_jacaranda_01_v1.glb and b/web/world/models/tree_jacaranda_01_v1.glb differ diff --git a/web/world/models/turnbuckle_v1.glb b/web/world/models/turnbuckle_v1.glb index 2a0e0cc..c32cc3d 100644 Binary files a/web/world/models/turnbuckle_v1.glb and b/web/world/models/turnbuckle_v1.glb differ diff --git a/web/world/models/washing_line_01_v1.glb b/web/world/models/washing_line_01_v1.glb index f212d8f..a0a5037 100644 Binary files a/web/world/models/washing_line_01_v1.glb and b/web/world/models/washing_line_01_v1.glb differ