Lane C S13 gate 2.1: the storm grade — the night finally looks like a night
QA played the southerly at 65 km/h under a noon-blue sky with razor midday shadows. The arithmetic said why: sky, sun and hemi all multiplied the weather by the author's palette (darkness 0.5 on the southerly), so the dial could zero the weather's say — 0.69 storminess became a 0.35 nudge. The grade floors it (storminess x lerp(0.5, 1, darkness)): a full gale now goes at least half way to slate on any palette while the wild night keeps its authored black. Plus the two things nothing keyed at all: the sun disc lerps toward slate and sun.shadow.radius lerps 1->7 on storminess (PCFSoft, so it is real blur). Deterministic — pure in t, same curve the rain uses, exposed as sky.stormGrade above the render gate for headless reads. dispose() hands back intensity, colour and radius exactly. Verified by eye in dev_skyfx: southerly t=55, 18.5 m/s reads slate weather. Selftest 337/0/0 (+1 test). Mutation-checked: the old formula and a dropped radius line both go red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8a3dc323bc
commit
a861151a4a
@ -23,6 +23,7 @@ const STORM_SKY = new THREE.Color(0x2a2f3a);
|
||||
const NIGHT_SKY = new THREE.Color(0x11141c);
|
||||
const WHITE = new THREE.Color(0xffffff);
|
||||
const FLASH_COL = new THREE.Color(0xdfe8ff);
|
||||
const SUN_STORM = new THREE.Color(0xc4cedb); // the noon disc gone slate (gate 2.1)
|
||||
|
||||
// ------------------------------------------------------------ rain shadow
|
||||
/**
|
||||
@ -709,6 +710,8 @@ export function createSkyFx(o = {}) {
|
||||
fogFar: scene && scene.fog ? scene.fog.far : 0,
|
||||
sun: sun ? sun.intensity : 0,
|
||||
hemi: hemi ? hemi.intensity : 0,
|
||||
sunColor: sun ? sun.color.clone() : null,
|
||||
sunRadius: sun && sun.shadow ? sun.shadow.radius : 1,
|
||||
};
|
||||
const baseSky = (scene && scene.background && scene.background.isColor)
|
||||
? scene.background.clone() : CALM_SKY.clone();
|
||||
@ -720,6 +723,7 @@ export function createSkyFx(o = {}) {
|
||||
}
|
||||
|
||||
let flash = 0; // decaying lightning brightness
|
||||
let gradeNow = 0; // the storm grade, gate 2.1 — pure in t, see step()
|
||||
let flashQueue = []; // {at, power} — double-strike
|
||||
let lastTelegraph = null;
|
||||
const camPos = new THREE.Vector3();
|
||||
@ -730,6 +734,9 @@ export function createSkyFx(o = {}) {
|
||||
get flash() { return flash; },
|
||||
/** 0..1 hail intensity right now — for the HUD ("HAIL" banner) and asserts. */
|
||||
get hailAmount() { return hailAmt; },
|
||||
/** 0..1 — the storm grade driving sky/sun/shadow-softness (gate 2.1).
|
||||
* storminess with a floor under the author's darkness dial; pure in t. */
|
||||
get stormGrade() { return gradeNow; },
|
||||
/** 0..1 — how far risen the change-front wall is (gate 3.4). Pure in t. */
|
||||
get changeFront() { return frontLevel; },
|
||||
/** Azimuth (radians, XZ) the front stands in — the quarter the new wind
|
||||
@ -845,6 +852,16 @@ export function createSkyFx(o = {}) {
|
||||
const speed = Math.hypot(w.x, w.z);
|
||||
const intensity = wind.rainAt(t);
|
||||
const storminess = clamp01(Math.max(intensity, speed / 26));
|
||||
// SPRINT13 gate 2.1 — the STORM GRADE. The QA pass played the southerly at
|
||||
// 65 km/h under a noon-blue sky, and the arithmetic says why: everything
|
||||
// below multiplied the weather by the author's palette (`darkness`), so a
|
||||
// darkness-0.5 storm at full blow only ever moved 0.35 toward slate — the
|
||||
// author's dial could zero the weather's say. The grade gives the weather
|
||||
// a floor: at full storminess even a darkness-0 sky goes half way to
|
||||
// slate, while the wild night (0.94) keeps reading as the night it is.
|
||||
// Same curve the rain already uses (storminess IS max(rain, wind)), pure
|
||||
// in t, computed above the render gate so a test can read it headless.
|
||||
gradeNow = storminess * lerp(0.5, 1, darkness);
|
||||
|
||||
// --- events: lightning + the ticker ---
|
||||
for (const ev of wind.eventsBetween(t - dt, t)) {
|
||||
@ -937,7 +954,7 @@ export function createSkyFx(o = {}) {
|
||||
if (!rendering) return;
|
||||
|
||||
// --- sky ---
|
||||
skyCol.copy(baseSky).lerp(target, storminess * darkness);
|
||||
skyCol.copy(baseSky).lerp(target, gradeNow);
|
||||
if (flash > 0) skyCol.lerp(FLASH_COL, Math.min(0.85, flash));
|
||||
if (scene) {
|
||||
if (scene.fog) {
|
||||
@ -946,8 +963,18 @@ export function createSkyFx(o = {}) {
|
||||
scene.fog.far = lerp(160, 55, storminess);
|
||||
}
|
||||
}
|
||||
if (sun) sun.intensity = lerp(original.sun, original.sun * 0.12, storminess * darkness) + flash * 2.2;
|
||||
if (hemi) hemi.intensity = lerp(original.hemi, original.hemi * 0.3, storminess * darkness) + flash * 1.2;
|
||||
if (sun) {
|
||||
sun.intensity = lerp(original.sun, original.sun * 0.12, gradeNow) + flash * 2.2;
|
||||
// The sun loses its noon edge two ways as the grade builds: the warm
|
||||
// disc goes slate (colour), and the shadows go soft (radius — the
|
||||
// renderer is PCFSoft, so radius is real blur, not a no-op). Softness
|
||||
// keys on STORMINESS, not the graded value: overcast is overcast even
|
||||
// under a light-palette storm, and razor-sharp noon shadows at 65 km/h
|
||||
// were the QA sighting this whole block answers.
|
||||
if (original.sunColor) sun.color.copy(original.sunColor).lerp(SUN_STORM, gradeNow * 0.85);
|
||||
if (sun.shadow) sun.shadow.radius = lerp(original.sunRadius, 7, storminess);
|
||||
}
|
||||
if (hemi) hemi.intensity = lerp(original.hemi, original.hemi * 0.3, gradeNow) + flash * 1.2;
|
||||
|
||||
dome.position.copy(camPos);
|
||||
dome.material.opacity = storminess * 0.85;
|
||||
@ -961,7 +988,7 @@ export function createSkyFx(o = {}) {
|
||||
// and the cloud texture is baked near-white. The crush is what makes night
|
||||
// look like night. It stops at 0.78 on purpose — the yard has no lights in
|
||||
// it yet, and a storm you can't see isn't a storm, it's a black screen.
|
||||
const nightAmt = storminess * darkness;
|
||||
const nightAmt = gradeNow;
|
||||
dome.material.color.copy(WHITE)
|
||||
.lerp(target, nightAmt * 0.92)
|
||||
.multiplyScalar(1 - 0.78 * nightAmt);
|
||||
@ -1039,7 +1066,11 @@ export function createSkyFx(o = {}) {
|
||||
original.fog.far = original.fogFar;
|
||||
}
|
||||
}
|
||||
if (sun) sun.intensity = original.sun;
|
||||
if (sun) {
|
||||
sun.intensity = original.sun;
|
||||
if (original.sunColor) sun.color.copy(original.sunColor);
|
||||
if (sun.shadow) sun.shadow.radius = original.sunRadius;
|
||||
}
|
||||
if (hemi) hemi.intensity = original.hemi;
|
||||
rain.dispose();
|
||||
hail.dispose();
|
||||
|
||||
@ -193,6 +193,40 @@ export default async function run(t) {
|
||||
sky.dispose();
|
||||
});
|
||||
|
||||
// SPRINT13 gate 2.1 — the storm grade. The QA sighting: 65 km/h + driving
|
||||
// rain under a noon-blue sky with razor midday shadows. Cause: sky and sun
|
||||
// both multiplied the weather by the author's palette (`darkness`, 0.5 on
|
||||
// the southerly), so the dial could zero the weather's say — and nothing
|
||||
// touched shadow softness at all. Pins: the grade floors the darkness dial,
|
||||
// the sun dims/goes slate/softens, and dispose hands all of it back.
|
||||
t.test('storm grade: a mid-darkness gale dims the sun, softens shadows, restores on dispose', () => {
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x9fc4e8);
|
||||
const camera = new THREE.PerspectiveCamera();
|
||||
const sun = new THREE.DirectionalLight(0xfff2dc, 2.0);
|
||||
const before = { sun: sun.intensity, color: sun.color.getHex(), radius: sun.shadow.radius };
|
||||
// a darkness-0.5 storm blowing 18 m/s (65 km/h) in full rain — the QA scene
|
||||
const gale = {
|
||||
seed: 1, def: { sky: { darkness: 0.5 } },
|
||||
sample: (p, t2, o) => o.set(18, 0, 0),
|
||||
speedAt: () => 18, rainAt: () => 1, rainMmPerHour: () => 30,
|
||||
gustTelegraph: () => null, eventsBetween: () => [], setSheltersFromTrees() {},
|
||||
};
|
||||
const sky = createSkyFx({ scene, camera, wind: gale, sun });
|
||||
fixedLoop(10, FIXED_DT, (dt, time) => sky.step(dt, time, {}));
|
||||
assert(sky.stormGrade > 0.7,
|
||||
`grade ${sky.stormGrade} — the darkness dial still zeroes the weather (the old formula reads 0.50 here)`);
|
||||
assert(sun.intensity < before.sun * 0.5,
|
||||
`sun at ${sun.intensity.toFixed(2)} of ${before.sun} — still noon at 65 km/h`);
|
||||
assert(sun.shadow.radius > 4,
|
||||
`shadow radius ${sun.shadow.radius} — razor-sharp midday shadows in a gale`);
|
||||
assert(sun.color.getHex() !== before.color, 'the sun disc never lost its noon warmth');
|
||||
sky.dispose();
|
||||
assert(sun.intensity === before.sun && sun.color.getHex() === before.color
|
||||
&& sun.shadow.radius === before.radius,
|
||||
'dispose did not hand the sun back exactly (intensity / colour / shadow radius)');
|
||||
});
|
||||
|
||||
// --- 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user