diff --git a/web/world/js/skyfx.js b/web/world/js/skyfx.js index 9675264..c71eb7b 100644 --- a/web/world/js/skyfx.js +++ b/web/world/js/skyfx.js @@ -656,15 +656,25 @@ export function createSkyFx(o = {}) { // fixed post-change instant — def + seed in, same wall out, every run. const FRONT_LEAD = 12; // s before the change the wall starts rising const FRONT_FADE = 8; // s after the swing completes for it to clear - const FRONT_ARC = 2.2; // radians of horizon the wall spans (~126°) + const FRONT_ARC = 2.6; // radians of horizon the wall spans (~149°) const FRONT_MAX_OP = 0.85; const frontEvents = (def.events || []) .filter((e) => e.type === 'windchange' && Number.isFinite(e.t)) .map((e) => ({ t0: e.t, over: e.over ?? 6, az: null })); - // Band from ~34° above the horizon down to it, centred (in local space) on - // azimuth π: SphereGeometry's (x,z) = (-cosφ, sinφ)·sinθ, so φ=0 sits at - // atan2(z,x) = π. rotation.y = π − A then carries the centre to azimuth A. - const frontGeo = new THREE.SphereGeometry(170, 24, 8, -FRONT_ARC / 2, FRONT_ARC, Math.PI * 0.30, Math.PI * 0.20); + // Band from ~34° above the horizon down to ~12° BELOW it, centred (in local + // space) on azimuth π: SphereGeometry's (x,z) = (-cosφ, sinφ)·sinθ, so φ=0 + // sits at atan2(z,x) = π. rotation.y = π − A then carries the centre to A. + // + // SPRINT13 gate 2.2 — why the band overshoots the equator: the old band + // stopped AT the horizontal plane through the camera, but from in-yard eye + // height the ground's true horizon sits ~0.6° BELOW that plane (the dip), + // so a sliver of bright sky showed under the wall's base and the bank read + // as a floating slab — the QA sighting, reproduced on dev_skyfx before this + // fix. Overshooting to −12° buries the base behind the ground from any eye + // height that matters (it survives scale.y's 0.3 floor: −37 m compressed to + // −11 m is still −3.7° elevation, below the dip). The ground plane occludes + // the overshoot by depth, so nothing is drawn twice. + const frontGeo = new THREE.SphereGeometry(170, 24, 10, -FRONT_ARC / 2, FRONT_ARC, Math.PI * 0.30, Math.PI * 0.27); { // Soft edges via vertex alpha, or the band reads as a floating rectangle // (checked by eye on dev_skyfx.html — the hard phi/theta cut was exactly @@ -672,11 +682,15 @@ export function createSkyFx(o = {}) { // the arc ends and across the top, so it sits ON the horizon like a bank // of weather instead of hanging in the sky like a screen. // Sphere uv: x runs 0..1 across the arc, y is 1 at the band top, 0 at the - // horizon edge. + // (now sub-horizon) bottom edge. + // The 1.6 exponent is gate 2.2's other half: 0.75 left the outer tenth of + // the arc at ~0.36 alpha, which from in-yard eye height is the "hard + // vertical seam" the QA saw at the bank's ends. 1.6 holds the same solid + // core (the arc is wider to compensate) but takes the ends to <0.12. const uv = frontGeo.attributes.uv; const rgba = new Float32Array(uv.count * 4); for (let i = 0; i < uv.count; i++) { - const across = Math.pow(Math.sin(Math.PI * uv.getX(i)), 0.75); + const across = Math.pow(Math.sin(Math.PI * uv.getX(i)), 1.6); const up = 1 - smoothstep(0.45, 1, uv.getY(i)); rgba[i * 4] = 1; rgba[i * 4 + 1] = 1; rgba[i * 4 + 2] = 1; rgba[i * 4 + 3] = across * up; diff --git a/web/world/js/tests/c.test.js b/web/world/js/tests/c.test.js index 0a1c262..8e8cac4 100644 --- a/web/world/js/tests/c.test.js +++ b/web/world/js/tests/c.test.js @@ -227,6 +227,31 @@ export default async function run(t) { 'dispose did not hand the sun back exactly (intensity / colour / shadow radius)'); }); + // SPRINT13 gate 2.2 — the wall's edges, judged from in-yard eye height. + // Two QA sightings, both geometry: a sliver of bright sky under the bank's + // base (it stopped AT the camera's horizontal plane, above the ground's true + // horizon), and a hard vertical seam at the arc ends (0.36 alpha at the + // outer tenth). The band now overshoots the equator and the end fade is + // steeper; these pins are what "grounded" and "feathered" mean in numbers. + t.test('the change-front wall grounds below the horizon and feathers its ends', () => { + const wind = createWind(storms.storm_03_southerly); + const sky = createSkyFx({ wind }); + const geo = sky.front.geometry; + geo.computeBoundingBox(); + assert(geo.boundingBox.min.y < -20, + `front base at y=${geo.boundingBox.min.y.toFixed(1)} — a band stopping at eye level floats a sky sliver under the wall`); + const uv = geo.attributes.uv, col = geo.attributes.color; + assert(col && col.itemSize === 4, 'front lost its vertex alpha — the edges are hard cuts again'); + let edgeMax = 0; + for (let i = 0; i < uv.count; i++) { + const u = uv.getX(i); + if (u <= 0.09 || u >= 0.91) edgeMax = Math.max(edgeMax, col.getW(i)); + } + assert(edgeMax < 0.2, + `arc-end alpha ${edgeMax.toFixed(2)} — the vertical seam QA saw at the bank's ends`); + sky.dispose(); + }); + // --- SPRINT2 §Lane C.3: rain has to stop at the cloth --- // Driven with a synthetic 4×4 m panel rather than a whole cloth sim: the thing // under test is the projection, and a flat panel makes the right answer