The town is inhabited: 23 towns finally differ, 181 back yards have a Hills Hoist with washing on it, a magpie owns one street for six weeks of the year, and six point sources put a life off-screen. MEASURED, walked, not bookmarked — two port-isolated no-store servers (:8472 = `git archive HEAD`, :8471 = treatment, assets symlinked so E's mid-round wave is identical on both sides), 102 8-metre spine steps at yaw 0, each frame fully streamed, through the shell's own composer: worst draws 224 -> 225 (+1) worst tris 89,502 -> 93,298 (+3,796 of ~75k spare) sum draws over 102 frames 16,676 -> 16,663 (-13) The citizen sim and the tram are hidden identically on both sides, and that IS the measurement: with them live the same step read control 346 vs treatment 232, because peds spawn on wall-clock time. `?yards=0` reproduces HEAD exactly (224 / 89,502 / 16,676). Isolated at a fixed pose: washing +1 draw / +512 tris, magpie +1 draw / +182 tris, every yard box prop +0 draws. §1.2 THE CHARACTER VECTOR — +0 draws, +0 tris, classic byte-identical BY CONSTRUCTION. `townCharacter(null)` returns the frozen v1 literals and a null tint keeps the SAME material cache key, so classic gets the same object, not a copy of it: default-boot ground hashes identical to HEAD mesh-for-mesh, `?classic=1` hashes unmoved from R37. There is no `if (classic)` in the ground path. The sky feeds the BASE skyName, never `setSky` (a one-way latch that would have killed R32's dawn dome on all 23 towns). The vacuous arm is per-town: `newtown_godverse` / `redhill_godverse` carry `state: ''`, so a per-corpus arm would have dropped exactly those two silently. 21 distinct palettes across 23 towns. `gravel` and `reddust` were not unwired but UNWIREABLE — their `use` values named no slot ground.js had; making the slot addressable fixed it at the root, and both are selected now. E's three new grounds landed mid-round and the upgrade was a name change in one table, which is what procedural-first is for. §1.4 BACKYARD AUSTRALIA — inside boxMats, +0 draws. Houses inset off their boundaries so there is a yard; 36 `yard` lots that had rendered as blank shopfront sheds since v1 are now fenced blocks with a hoist, a shed and often a tank. Collision byte-identical (the lot rect is where the fence stands). THE WASHING SHIPS, and I wrote the inverted weight rather than dropping it. The shipped top-weighted mode moves a hem 0.10 cm calm / 0.67 cm gusty; `pegged` gives 4.5 cm / 31 cm — ~52x, and at the hem instead of the pegs. The `canopy` string is byte-for-byte unchanged, so the gum-tree covenant argument is untouched. It is ONE TOWN-WIDE InstancedMesh (1 draw at any N, cap 256, proximity-first off the R37 chunk seam) rather than per-chunk, which would have been +1 x 31 live chunks = 4x the whole margin. Zero on every real town (no hoists ⇒ no mesh) and zero at night. §1.3 THE MAGPIE — +1 draw on screen, exactly 0 otherwise; 182 tris of 900. Territory scales with the road graph: 6 on the 6.78 km synthetic town, 157 on bendigo's 188.6 km / 2,593 edges, nearest 120 m from spawn. The latch sets, the day roll clears it, localStorage gains no key and the save never mentions it. `swoopPos()` is exported and pure. The hat stays cut. MY OWN SEASON MECHANISM WAS VACUOUS AND THE SWEEP CAUGHT IT: keyed on citySeed alone it produced a CONSTANT, because the town key is deliberately off the plan — 24 of 24 towns were 214 days from swooping season, and it would have gated green because `?magpie=1` forces it. Re-keyed on (citySeed, townKey): `newtown_real` is in season on arrival on the default seed, marrickville is 8 days out, and 12.9% of 1,000 seeds put the synthetic default in season against a design ratio of 11.5%. §1.1 THE AUDIO LAYER — six new mixer layers, zero draws, zero tris, seeded anchors, distance falloff, AND A StereoPannerNode PER LAYER, so direction is real (measured spread -0.71..+0.85) instead of a limitation written into the notes. Day-of-week derived locally. All six anchored on the default boot; 3 of 6 report `no-anchor` on real towns rather than being faked onto shopfronts. The falsifiability control — one manifest key renamed away, served from a third port — makes that source report `no-entry`, 0 plays, never `audible`, with the other five unchanged and zero console errors. TWO DEFECTS MY OWN GREEN NUMBERS COULD NOT SEE, both caught by a screenshot: the washing shipped at 256 instances / +1 draw / cap respected and was INVISIBLE behind its own back fence (per-instance peg height, and a fence taller than the hoist), and the magpie perched on THIN AIR over the road. R37's lesson relearned at full price. Fence 1.60 / hoist 2.15 measured against the object; territories now snap to furniture.js's own streetlight rule. LANE E'S PROPS, ADJUDICATED: hoist + aerial permanently primitive (E's spike killed them, and primitive is also the +0-draw mechanism). `paling_fence` at 1,121 tris x 905 runs = 1.01M triangles — no. The `magpie` GLB is HELD FOR THE TINT: it reads as a crow, and a magpie seen for 1.2 s in peripheral vision IS its white bar. My 182-tri bird already carries it, at a fifth of the triangles and no fetch. furniture.js's "novelty_record is 26.5k tris" was stale by six epochs — corrected to 8,000. Zero fetch delta: `?noassets=1` fetches the same 23 assets, same list, on HEAD and on R38. 0 console errors on 10 boots (default, classic, yards=0, washing=0&magpie=0&ambient=0, noassets, magpie=1, darwin, hobart, bendigo, the patched-manifest control) beyond the known PointerLock ones. Shots: docs/shots/laneB_r38/. Detail + every number: LANE_B_NOTES §38.
97 lines
5.8 KiB
JavaScript
97 lines
5.8 KiB
JavaScript
// PROCITY Lane B — wind.js (round 17, v3.2 — "wind sway ships or dies")
|
||
// Weather-driven vertex-shader sway for street foliage. Parked since R7 with the note "touches shared
|
||
// materials"; the R16 standard is measurement-over-brief, and the measurement here is: the gum-tree
|
||
// material is B-owned street geometry (furniture.js), so this is ENTIRELY Lane B — no C/E material seam.
|
||
//
|
||
// ONE shared uniform set, referenced by the tree material via applyWind(). Amplitude stays 0 until the
|
||
// weather system drives it (updateWind, called from weather.update). With weather OFF — ?weather=0, or
|
||
// ?classic=1 where weather is never constructed — updateWind never runs, uWindAmp stays 0, the injected
|
||
// displacement is exactly `... * 0.0 == 0.0`, and trees render byte-identical to no-sway (verified). So
|
||
// the classic covenant and ?weather=0 both hold by construction.
|
||
//
|
||
// SCOPE (measured, per R16 discipline): gum-tree BILLBOARDS only. Awnings are rigid posted-verandah box
|
||
// slabs (buildings.js) — a structural verandah roof does not flutter, so swaying it would read wrong; it
|
||
// is intentionally excluded (not a cross-lane blocker — a physical one). Fabric-canopy flutter, if ever
|
||
// wanted, is a separate v3.3 item (the striped canopies share the rigid awning InstancedMesh today).
|
||
//
|
||
// Cost: ~0 draws — a handful of ALU ops added to the tree InstancedMesh's existing vertex program.
|
||
|
||
export const windUniforms = {
|
||
uWindTime: { value: 0 },
|
||
uWindAmp: { value: 0 }, // 0 = dead calm ⇒ zero displacement ⇒ byte-identical (weather off / classic)
|
||
};
|
||
|
||
// ── R38 §1.4: THE WEIGHT IS UPSIDE DOWN FOR ANYTHING PEGGED AT THE TOP ──────────────────────────
|
||
// The `canopy` weight below is `wH = clamp(y/topY,0,1); wH *= wH` — top-weighted, base planted.
|
||
// That is correct for a gum and **exactly wrong for a sheet on a Hills Hoist**, which is pegged at
|
||
// the top and free at the hem. Measured on the shipped numbers: a sheet hanging 0.6–1.6 m under the
|
||
// canopy weight gets wH = (1.6/4.4)² = 0.132 at its TOP and 0.019 at its hem, so with the shipped
|
||
// amplitude (`updateWind`: 0.05 calm → 0.35 gusty) the free end of the washing moves
|
||
// **0.10 cm calm / 0.67 cm gusty** and the pegged end moves the most — visually inert, and inverted.
|
||
// The `pegged` mode weights by distance BELOW the peg line instead: wH = 1 at the hem, 0 at the
|
||
// pegs ⇒ **5.0 cm calm / 35 cm gusty at the hem, a 52× gain**, and the top stays put because that
|
||
// is where the pegs are. It also flutters faster and adds a Z-flap, because a sheet is a sail and a
|
||
// gum is a spring. (Fable's R38 binding correction 6, taken as the "write the mode" branch.)
|
||
//
|
||
// `wing` (§1.3, the magpie) is the third weight: |x| from the body centreline, displacement in Y,
|
||
// so a wing beats and a body does not. Still on uWindAmp ⇒ still exactly 0 at dead calm.
|
||
//
|
||
// **The `canopy` string is byte-for-byte what it was pre-R38**, so the gum-tree program compiles to
|
||
// the same source it always has and the covenant argument for trees is unchanged.
|
||
const MODES = {
|
||
canopy: (topY) => [
|
||
`float wH = clamp(position.y / ${topY.toFixed(1)}, 0.0, 1.0); wH *= wH;`, // top-weighted, base planted
|
||
'float wS = sin(uWindTime + wPhase) + 0.4 * sin(uWindTime * 2.3 + wPhase * 1.7);',
|
||
'transformed.x += wS * uWindAmp * wH;',
|
||
'transformed.z += cos(uWindTime * 0.8 + wPhase) * uWindAmp * wH * 0.5;',
|
||
],
|
||
// Pegged at the TOP of the local geometry (y ∈ [0, topY], pegs at topY): free at the hem.
|
||
pegged: (topY) => [
|
||
`float wH = clamp((${topY.toFixed(2)} - position.y) / ${topY.toFixed(2)}, 0.0, 1.0); wH *= wH;`,
|
||
'float wS = sin(uWindTime * 2.1 + wPhase) + 0.5 * sin(uWindTime * 4.7 + wPhase * 2.3);',
|
||
'transformed.x += wS * uWindAmp * wH;',
|
||
'transformed.z += sin(uWindTime * 3.3 + wPhase * 1.3) * uWindAmp * wH * 0.75;',
|
||
'transformed.y -= abs(wS) * uWindAmp * wH * 0.12;', // a lifting sheet also shortens
|
||
],
|
||
// Wing beat: weighted by span from the body centreline, displaced in Y.
|
||
wing: (topY) => [
|
||
`float wH = clamp(abs(position.x) / ${topY.toFixed(2)}, 0.0, 1.0);`,
|
||
'float wS = sin(uWindTime * 9.0 + wPhase);',
|
||
'transformed.y += wS * uWindAmp * wH * 3.0;',
|
||
'transformed.x -= abs(wS) * uWindAmp * wH * 0.35 * sign(position.x);',
|
||
],
|
||
};
|
||
|
||
// Patch a material's vertex program: displace vertices by a mode-specific weight × the shared
|
||
// amplitude. A per-instance phase from world position desyncs neighbours. Guarded for instanced use
|
||
// so the same material would still compile if ever drawn non-instanced.
|
||
// `topY` is the mode's reference length: canopy → tree height, pegged → drop below the pegs,
|
||
// wing → half-span.
|
||
export function applyWind(material, { topY = 4.4, mode = 'canopy' } = {}) {
|
||
const body = (MODES[mode] || MODES.canopy)(topY);
|
||
material.onBeforeCompile = (shader) => {
|
||
shader.uniforms.uWindTime = windUniforms.uWindTime;
|
||
shader.uniforms.uWindAmp = windUniforms.uWindAmp;
|
||
shader.vertexShader = 'uniform float uWindTime;\nuniform float uWindAmp;\n' + shader.vertexShader;
|
||
shader.vertexShader = shader.vertexShader.replace(
|
||
'#include <begin_vertex>',
|
||
[
|
||
'#include <begin_vertex>',
|
||
'float wPhase = position.x * 0.1 + position.z * 0.1;',
|
||
'#ifdef USE_INSTANCING',
|
||
' wPhase = instanceMatrix[3].x * 0.15 + instanceMatrix[3].z * 0.15;',
|
||
'#endif',
|
||
...body,
|
||
].join('\n')
|
||
);
|
||
};
|
||
material.needsUpdate = true;
|
||
}
|
||
|
||
// Drive from weather.update(dt): base breeze on any weather-on boot (clear included), gusts scaling with
|
||
// rain intensity. NEVER called when weather is absent ⇒ uWindAmp stays 0 (byte-identical, see header).
|
||
export function updateWind(dt, intensity = 0) {
|
||
windUniforms.uWindTime.value += dt * 1.6;
|
||
windUniforms.uWindAmp.value = 0.05 + Math.max(0, intensity) * 0.30; // calm ~0.05 m top sway → gusty ~0.35 m
|
||
}
|