1
0

fix: aerial fog follows the camera on the tick, not on OrbitControls change

Drive, crow and aircraft follow-cameras write the pose directly each
frame and never fire `change`. Fog lived on that event, throttled at
2% of altitude, so a descent under a follow camera fell back to the
one-hertz clock and stepped the haze four times instead of easing it.
A 300 ms fog dip got at most one sample.

The recompute now runs on the city's tick, after `roadTraffic.tick`
and the actor/aircraft `setPose` writes — the same pump
`applyReliefRamp` already uses, for the same reason: a camera-dependent
term cannot live on an event the descent does not fire. The 2%
altitude throttle is unchanged, so a chapter flight is still a few
dozen recomputations rather than sixty a second. Orbit `change` still
drives handover and prefetch; those are overview-only.

`aerialFogMoved` is the throttle as a function of two altitudes.
`followCameraFog.test.ts` writes a Drive pose without `controls.update`,
asserts `change` does not fire, and still pulls the fog in from the
new pose.
This commit is contained in:
2026-08-24 22:54:51 -07:00
parent bda7a61acd
commit 92126df420
7 changed files with 310 additions and 75 deletions
+19 -32
View File
@@ -54,40 +54,27 @@ things fall out of it that somebody has to pick up.
### Aerial perspective is wired, and the coarse seam is now a live defect
Fog now follows camera altitude (`aerialReach` in `engine/atmosphere.ts`, §12.2).
`main.ts` recomputes the rig on the controls' `change` event, throttled at 2% of
the current altitude, which collapses a chapter flight to a few dozen
recomputations instead of sixty a second.
`main.ts` recomputes the rig on the city's tick (`onCameraFrame`, after the
follow cameras write the pose), throttled at 2% of the current altitude, which
collapses a chapter flight to a few dozen recomputations instead of sixty a
second. Drive, crow and aircraft follow-cameras write the pose directly and do
not fire OrbitControls `change`; they used to fall back to the one-hertz clock
and a fast descent stepped the fog four times. That path is on the frame loop.
What that does **not** cover is a camera moved by something other than
`OrbitControls`: the drive, actor and aircraft follow-cameras set the pose
directly each frame and do not fire `change`. Those poses fall back to the
one-hertz clock tick, so a fast descent under a follow camera steps the fog up
to four times instead of easing it.
The state-to-metro transition is a **fog dip** — collapse the outgoing board's
`setAerialFog` toward the camera over ~0.3 s, swap in the obscured frame, lift
the incoming board's over ~0.5 s — chosen over a crossfade because a crossfade
cannot afford the triangles (ca+sf is 2,640,307 against the Bay Area's
2,600,000 cap) and because a dip is the only transition that hides the 4.2x
vertical-exaggeration pop, the 4 km projection slide and the vanishing 2 km
freeways at once, since all three happen at maximum obscuration.
**Promoted from hypothetical to live, 2026-08-23.** This entry used to end "a
continuous state-to-city descent *would* need this on the frame loop rather than
on an event". That descent is now the thing being built: the state-to-metro
transition is a **fog dip** — collapse the outgoing board's `setAerialFog`
toward the camera over ~0.3 s, swap in the obscured frame, lift the incoming
board's over ~0.5 s — chosen over a crossfade because a crossfade cannot afford
the triangles (ca+sf is 2,640,307 against the Bay Area's 2,600,000 cap) and
because a dip is the only transition that hides the 4.2x vertical-exaggeration
pop, the 4 km projection slide and the vanishing 2 km freeways at once, since
all three happen at maximum obscuration.
That makes fog a **transition mechanism** rather than an ambience setting, and a
mechanism driven by an event the descent does not fire is a mechanism that does
not run. Two specific consequences:
- A dip driven from a follow camera — leaving the corridor drive into the Bay
Area is the obvious case — gets the one-hertz tick, so a 300 ms collapse gets
at most one step. The dip either does not happen or happens as a single jump.
- The 2%-of-altitude throttle is a threshold on *altitude*, and a dip changes
near/far with the altitude held still. Nothing in the current trigger notices
that at all.
So this moves onto the frame loop, and it moves as part of the transition work
rather than after it.
That makes fog a **transition mechanism** rather than an ambience setting. The
dip itself is already on the app pump (`stepFogDip`); aerial perspective under
a follow camera is now on the same kind of pump. What is still true: the 2%
throttle is a threshold on *altitude*, and a dip changes near/far with the
altitude held still — which is why the dip writes `setAerialFog` itself rather
than going through `refreshAerialFog`.
### The north is authored, and three things in it are thin