ashbloom.camera.rack
The lens and the ears of Ashbloom's 25 set-ups.
The set-ups themselves are not here. They are data — the pos / look / fov /
ease keys on each row of film.SHOTS — because the cutscene Runtime under
AshbloomDirector is what poses the camera, and it reads that table directly.
This module owns the two things that ride on those set-ups and cannot be
expressed as a row in a table.
The lens
sensorHeight is full frame, so every shot's focal length falls out of the field
of view it was composed at:
f = 12 / tan(fov / 2) -- 24° -> 56 mm, 14° -> 98 mm, 68° -> 18 mm
which keeps the lens honest through shot 13's 30° → 40° ramp, where a hand-written focal length would drift away from the framing mid-shot. Focus is racked to the subject: the distance from the shot's eye to the point it is looking at, re-evaluated every frame, so a shot that dollies pulls focus as it travels.
RACK[n] states only what the optics cannot derive — the aperture, where the
plane of focus sits along the eye → subject ray, and the blur ceiling:
| shots | f-stop | |
|---|---|---|
| macro | 2, 9, 12, 15, 20, 23 | f/1.4 – f/1.8 |
| close | 6, 8, 13, 16, 18, 24 | f/2.0 – f/2.8 |
| middle | 3, 10, 11, 19 | f/4 |
| wide | 1, 4, 14, 21, 25 | f/5.6 |
| very wide | 5, 7, 17, 22 | f/8 |
It drives @builtin::systems.depthOfField.depthOfField directly rather than
through a DepthOfField component. The component pushes its fields only when
they CHANGE, so the two do not fight; but one authoring surface per system is the
rule, and for this system the camera slice is it.
The ears
The scene's single AudioListener is re-posed on every cut, so the soundscape
turns with the film.
It does not re-parent to the cutscene camera. That camera is temporary and is despawned when the playback releases its stage, which would take the world's only listener with it.
One departure from riding the camera exactly: two set-ups stand a quarter of a kilometre outside the crater, and a listener out there hears no garden. The ears are pulled along the eye → subject ray until they are within 30 m of what the shot is looking at. Close shots are unaffected; the wides hear the bowl they are framing.
The guards
A camera inside the rock renders a flat single-colour frame and every read-back in the engine reports it as correct, so two checks answer for it:
local rack = require("ashbloom.camera.rack")
rack.probe(110) -- eye / at / fov / focal length / f-stop at a film time
rack.clearances() -- how far each of the 25 eyes clears the crater surface
clearances() asks ashbloom.crater.craterGeo.groundY — the same profile and
warp the crater mesh was cut from — at thirteen points across every shot. Under
0.40 m is inside the 34 cm ash mantle. The stage repeats the check on each cut
and says so in the log, and it warns once per anchor when a shot anchor moves
more than 2 m from where the set-ups were composed.
Joining in
rack.install() -- registers the "camera" stage at order 15; idempotent
AshbloomCamera.component calls it, so a freshly loaded world has the lens and
the listener without anyone requiring this module by hand.
Scoped to this part · feeds back into the world's score.