diff --git a/B-progress.md b/B-progress.md index 1b7afee..7d9b31f 100644 --- a/B-progress.md +++ b/B-progress.md @@ -4,6 +4,20 @@ plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's `generatePlan` (which landed mid-session). Everything below was verified live in a browser. +## Round 5 (Fable's ROUND5 → Lane B) — v2 window trick, behind `?winmap=1` · qa GREEN (4/4) + +- **Parallax interior-mapping window glass** (V2_IDEAS "Vuntra window trick"), behind **`?winmap=1` + default-off.** One shared `ShaderMaterial` on the existing window geometry does single-cube interior + mapping — each fragment rays into a virtual room box and shades back wall + seeded shelves + side + walls + floor/ceiling. **Zero extra geometry, 1 window draw/chunk, per-shop variation via attributes + (aTangent/aTint/aSeed), not per-shop materials.** Seeded from `shop.seed`. +- **§3.5 held:** night → open shop's room glows warm, closed shop's room dark (reuses the closed-facade + `uHour/uNight/aHours`). Screenshots: `docs/shots/laneB/winmap_{day,night_open_vs_closed,night_closed}.png`. +- **v2 prime law held & proven:** flag-OFF byte-identical to v1 (windows `MeshStandardMaterial`, no + winmap attrs); flag-ON worst view **122–168 draws** (≤300); shader **100% procedural — 0 fetch delta** + vs `?noassets` baseline; 0 console errors; `GOLDEN.hash` untouched. `?winmap` is self-read in + buildings.js → **no index.html seam for F** (just a flags-table row). File: `buildings.js`. + ## Round 4 (Fable's ROUND4 → Lane B) — done · qa.sh --strict GREEN (4/4) - **B1 — closed-facade visuals (§3.5, headline).** Closed shops → **dark windows + red CLOSED plate**; diff --git a/docs/LANES/LANE_B_NOTES.md b/docs/LANES/LANE_B_NOTES.md index f2454b0..0ae3c28 100644 --- a/docs/LANES/LANE_B_NOTES.md +++ b/docs/LANES/LANE_B_NOTES.md @@ -1,5 +1,34 @@ # LANE B — NOTES (measured) +## Round 5 (Fable's ROUND5 → Lane B) — v2: the Vuntra window trick, behind `?winmap=1` + +- **Parallax interior-mapping glass** (V2_IDEAS "window trick"). Behind **`?winmap=1`, default-off.** + Single-cube interior mapping in one shared `ShaderMaterial` on the existing window geometry: each + glass fragment casts the view ray into a virtual room box and shades the surface it hits (back wall + + seeded shelf silhouettes with product blocks, side walls, floor/ceiling) — a plausible room with + real parallax, **zero extra geometry, still 1 window draw/chunk**. Per-shop variation rides + attributes (`aTangent` = window right vector, `aTint` = seeded room colour, `aSeed` = shelf + variation), **not per-shop materials**. Verified live: rooms are visible through the glass with + parallax day + night. +- **§3.5 respected** via the same `uHour/uNight` + `aHours` the closed-facade system uses: at night an + **open shop's room glows warm** ("Video Regal" lit), a **closed shop's room is dark** ("Second Time" + dark) — screenshots in `docs/shots/laneB/winmap_*.png`. +- **v2 prime law held.** `WINMAP` is read straight from the URL in **buildings.js** — **no shell seam, + so F only needs a flags-table row, no `index.html` wiring.** Flag-OFF path is byte-identical to v1: + `windowQuad` collapses to the v1 `hoursQuad` (no extra attributes) and the finalize uses the v1 + `windowMaterial()` — verified: flag-off windows are `MeshStandardMaterial` with no `aTangent`; + flag-on are the interior `ShaderMaterial` with `aTangent`. Seeded from `shop.seed` (deterministic). +- **Budget + fetches:** flag-on worst continuous-walk view with citizens **122–168 draws** (≤300); + the shader is **100% procedural** — measured **0 fetch delta** vs the `?noassets` baseline (both 22 + = the pre-existing facade/ground skin JPEGs). `?noassets=1&winmap=1` adds no network. **0 console + errors** in all modes. `qa.sh --strict` GREEN (4/4); `GOLDEN.hash` untouched (no plan change). + +**→ Fable (F2 flags table):** `?winmap=1` — owner Lane B — state landed, default-off — needs **no +index.html wiring** (self-read in buildings.js). Smoke: boot `?winmap=1`, walk a block, glass shows +rooms; night open=warm / closed=dark. + +--- + ## Round 4 (Fable's ROUND4 → Lane B) — done - **B1 — closed-facade visuals (§3.5, the headline).** Closed shops now render **dark windows + a diff --git a/docs/shots/laneB/winmap_day.png b/docs/shots/laneB/winmap_day.png new file mode 100644 index 0000000..6da1a7d Binary files /dev/null and b/docs/shots/laneB/winmap_day.png differ diff --git a/docs/shots/laneB/winmap_night_closed.png b/docs/shots/laneB/winmap_night_closed.png new file mode 100644 index 0000000..d441766 Binary files /dev/null and b/docs/shots/laneB/winmap_night_closed.png differ diff --git a/docs/shots/laneB/winmap_night_open_vs_closed.png b/docs/shots/laneB/winmap_night_open_vs_closed.png new file mode 100644 index 0000000..69b46fe Binary files /dev/null and b/docs/shots/laneB/winmap_night_open_vs_closed.png differ diff --git a/web/js/world/buildings.js b/web/js/world/buildings.js index c32b7b5..c5e596a 100644 --- a/web/js/world/buildings.js +++ b/web/js/world/buildings.js @@ -52,6 +52,84 @@ function windowMaterial() { return m; } +// ── v2: parallax interior-mapping window glass (?winmap=1, default-off) ────────────────────────── +// Single-cube interior mapping: each glass fragment casts the view ray into a virtual room box and +// shades the surface it hits (back wall + shelf silhouettes, side walls, floor/ceiling) — a plausible +// room seen through the window with real parallax, zero extra geometry. One SHARED ShaderMaterial for +// all windows (per-shop variation via aTint/aSeed attributes), same 1 draw/chunk as v1. Respects §3.5 +// via the same uHour/uNight + aHours: closed-at-night rooms go dark, open rooms glow warm. Fully +// procedural (no fetch). Flag-off path is untouched (v1 windowMaterial()). +const WINMAP = (() => { try { const p = new URLSearchParams(location.search); return p.has('winmap') && p.get('winmap') !== '0'; } catch (e) { return false; } })(); +const ROOM_TINTS = [ // muted 90s interior wall colours (RGB 0..1), seeded per shop + [0.56, 0.44, 0.33], [0.42, 0.47, 0.52], [0.52, 0.47, 0.37], [0.47, 0.42, 0.46], + [0.54, 0.50, 0.41], [0.40, 0.45, 0.40], [0.58, 0.49, 0.40], [0.45, 0.48, 0.55], +]; +function roomTint(shop) { return ROOM_TINTS[((shop ? shop.seed : 12345) >>> 0) % ROOM_TINTS.length]; } + +let _winmapMat = null; +function interiorWindowMaterial() { + if (_winmapMat) return _winmapMat; + _winmapMat = new THREE.ShaderMaterial({ + transparent: true, + uniforms: { uHour: _hoursUniforms.uHour, uNight: _hoursUniforms.uNight, uRoomDepth: { value: 0.85 } }, + vertexShader: ` + attribute vec2 aHours; attribute vec3 aTangent; attribute vec3 aTint; attribute float aSeed; + varying vec2 vUv; varying vec3 vWorldPos; varying vec3 vNormalW; varying vec3 vTangentW; + varying vec2 vHours; varying vec3 vTint; varying float vSeed; + void main(){ + vUv = uv; vWorldPos = position; vNormalW = normal; vTangentW = aTangent; + vHours = aHours; vTint = aTint; vSeed = aSeed; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + }`, + fragmentShader: ` + precision highp float; + uniform float uHour; uniform float uNight; uniform float uRoomDepth; + varying vec2 vUv; varying vec3 vWorldPos; varying vec3 vNormalW; varying vec3 vTangentW; + varying vec2 vHours; varying vec3 vTint; varying float vSeed; + float h1(float n){ return fract(sin(n * 127.1) * 43758.5453); } + void main(){ + vec3 N = normalize(vNormalW); + vec3 T = normalize(vTangentW); + vec3 Bb = normalize(cross(N, T)); + vec3 rayDir = normalize(vWorldPos - cameraPosition); + vec3 rd = vec3(dot(rayDir, T), dot(rayDir, Bb), -dot(rayDir, N)); + rd.z = max(rd.z, 1e-3); + vec2 uv = vUv; + float tz = uRoomDepth / rd.z; // back wall + float tx = (abs(rd.x) > 1e-4) ? (((rd.x > 0.0 ? 1.0 : 0.0) - uv.x) / rd.x) : 1e9; + float ty = (abs(rd.y) > 1e-4) ? (((rd.y > 0.0 ? 1.0 : 0.0) - uv.y) / rd.y) : 1e9; + if (tx <= 0.0) tx = 1e9; if (ty <= 0.0) ty = 1e9; + float t = min(tz, min(tx, ty)); + vec3 hit = vec3(uv, 0.0) + rd * t; + float depth = clamp(hit.z / uRoomDepth, 0.0, 1.0); + vec3 wall = vTint; + vec3 col; + if (t >= tz - 1e-5) { // back wall + shelves + col = wall * 0.82; + for (int i = 0; i < 3; i++){ + float fy = 0.22 + float(i) * 0.26 + (h1(vSeed + float(i) * 3.7) - 0.5) * 0.06; + float band = smoothstep(0.05, 0.03, abs(hit.y - fy)); + col = mix(col, wall * 0.42, band * 0.7); + float bx = fract(hit.x * 6.0 + h1(vSeed + float(i))); + float prod = step(0.15, bx) * step(bx, 0.85) * step(fy, hit.y) * step(hit.y, fy + 0.11); + col = mix(col, wall * 1.3 + vec3(0.05, 0.03, 0.02), prod * 0.45); + } + } else if (t >= tx - 1e-5) { + col = wall * 0.6; // side wall + } else { + col = (rd.y < 0.0) ? wall * 0.5 : (wall * 1.05 + 0.04); // floor / ceiling + } + col *= mix(1.0, 0.55, depth); // depth falloff + float open = step(vHours.x, uHour) * step(uHour + 0.0001, vHours.y); + float lit = (uNight > 0.5) ? (open > 0.5 ? 1.0 : 0.10) : 0.72; // §3.5: closed@night dark + col *= lit; + if (uNight > 0.5 && open > 0.5) col = mix(col, col * vec3(1.0, 0.85, 0.6) * 1.7, 0.65); // warm glow + gl_FragColor = vec4(col, 0.94); + }`, + }); + return _winmapMat; +} + function plateTexture() { if (_plateTex) return _plateTex; const c = document.createElement('canvas'); c.width = 256; c.height = 96; @@ -120,6 +198,29 @@ function hoursQuad(c0, c1, c2, c3, hours, uv) { return g; } +// A window quad. Under ?winmap it carries the extra interior-mapping attributes (aTangent = the +// window's world-space right vector, aTint = the seeded room colour, aSeed = shelf variation). With +// the flag off it is exactly the v1 hoursQuad, so flag-off is byte-identical. +function windowQuad(c0, c1, c2, c3, hours, shop) { + const g = hoursQuad(c0, c1, c2, c3, hours); + if (!WINMAP) return g; + const tx = c1[0] - c0[0], ty = c1[1] - c0[1], tz = c1[2] - c0[2]; + const tl = Math.hypot(tx, ty, tz) || 1; + const tan = [tx / tl, ty / tl, tz / tl]; + const tint = roomTint(shop); + const seed = shop ? ((shop.seed >>> 0) % 997) / 997 : 0.5; + const tanA = new Float32Array(18), tintA = new Float32Array(18), seedA = new Float32Array(6); + for (let i = 0; i < 6; i++) { + tanA[i * 3] = tan[0]; tanA[i * 3 + 1] = tan[1]; tanA[i * 3 + 2] = tan[2]; + tintA[i * 3] = tint[0]; tintA[i * 3 + 1] = tint[1]; tintA[i * 3 + 2] = tint[2]; + seedA[i] = seed; + } + g.setAttribute('aTangent', new THREE.BufferAttribute(tanA, 3)); + g.setAttribute('aTint', new THREE.BufferAttribute(tintA, 3)); + g.setAttribute('aSeed', new THREE.BufferAttribute(seedA, 1)); + return g; +} + // Transform a local (lx,ly,lz) point to world via a lot's (x,z,ry). function toWorld(lot, lx, ly, lz, out) { const cos = Math.cos(lot.ry || 0), sin = Math.sin(lot.ry || 0); @@ -238,10 +339,10 @@ export function buildChunkBuildings(chunkData, ctx) { const winW = Math.max(0.7, (w - dw) / 2 - 0.6), winH = 1.5, wy = 1.55, zw = zf + 0.025; const offs = [-(dw / 2 + 0.4 + winW / 2), (dw / 2 + 0.4 + winW / 2)]; for (const ox of offs) { - windowGeos.push(hoursQuad( + windowGeos.push(windowQuad( toWorld(lot, ox - winW / 2, wy - winH / 2, zw, []), toWorld(lot, ox + winW / 2, wy - winH / 2, zw, []), toWorld(lot, ox + winW / 2, wy + winH / 2, zw, []), toWorld(lot, ox - winW / 2, wy + winH / 2, zw, []), - hrs, + hrs, shop, )); } // CLOSED plate over the shopfront — visible only when the shop is shut (shader gates on aHours) @@ -305,10 +406,10 @@ export function buildChunkBuildings(chunkData, ctx) { toWorld(lot, 0.5, 2.0, zf, []), toWorld(lot, -0.5, 2.0, zf, []), )); for (const ox of [-w / 2 + 1.2, w / 2 - 1.2]) { - windowGeos.push(hoursQuad( + windowGeos.push(windowQuad( toWorld(lot, ox - 0.6, 1.1, zf, []), toWorld(lot, ox + 0.6, 1.1, zf, []), toWorld(lot, ox + 0.6, 2.0, zf, []), toWorld(lot, ox - 0.6, 2.0, zf, []), - [0, 24], // homes glow at night (always "open") + [0, 24], null, // homes glow at night (always "open") )); } colliders.push(lotCollider(lot)); @@ -412,10 +513,11 @@ export function buildChunkBuildings(chunkData, ctx) { } // ── windows (merged, ONE shared material; emissive gated per-shop by the aHours shader) ── + // ?winmap swaps in the parallax interior-mapping glass (v2); default is the v1 window material. if (windowGeos.length) { const merged = mergeGeometries(windowGeos, false); windowGeos.forEach((g) => g.dispose()); - const mesh = new THREE.Mesh(merged, windowMaterial()); // shared → NOT disposed per chunk + const mesh = new THREE.Mesh(merged, WINMAP ? interiorWindowMaterial() : windowMaterial()); // shared → NOT disposed group.add(mesh); disposables.push(merged); }