look
The light, the grade and the luma gate of *Pastel Astral Dream*. One animator, registered as `"look"`, is a pure function of film time `t`: it aims and colours the key, sets the milky ambient floor, drives the post-process chain (`pad_bloom` + `padGrade`), and sets the lens — dep…
look
The light, the grade and the luma gate of Pastel Astral Dream. One animator,
registered as "look", is a pure function of film time t: it aims and colours
the key, sets the milky ambient floor, drives the post-process chain
(pad_bloom + padGrade), and sets the lens — depth of field, film grain, a
small flare.
The key is the sun field, re-aimed
There is no sun in this film. But the engine's sun field is the one light that
carries the cascade shadow set, and this film's arc is that its shadows invert —
so pad_key, a single Light { kind = "directional" }, holds it and carries
whichever source is the key at time t:
| act | source | direction the light travels | colour |
|---|---|---|---|
| I | cold starlight, high and behind | -0.42, -0.62, -0.66 | keyStar |
| II | the nebula front, above and LEFT | +0.74, -0.50, -0.45 | keyNebula |
| III | the cream sea and the column, BELOW | +0.22, +0.95, -0.22 | keyUnder |
Direction, colour and intensity are one weighted blend of the three, so the key swings through each transition instead of cutting. The weights are the film's own drivers and nothing else:
nebK = film.frontAt(t, x) -- has the front reached here
undK = max(film.uplight(t), film.column(t)) -- the sea and the column
wUnd = undK ; wNeb = nebK * (1-undK) ; wStar = (1-nebK) * (1-undK)
x is the camera's own world X, taken from film.eyeAt(t), so the light line
sweeps across the archipelago as the front does. pad_key_ring (the pale ringed
body) and pad_key_bounce (the sea's light, straight up) sit beside it as
DirectionalLights and do not swing.
Nothing else in this world may carry a Light { kind = "directional" } — a
second one moves the sun rather than joining it.
The grade
@user/pad/grade/padGrade.shader is one post-process pass doing the whole
photographic finish. One reading decides everything in it: whether a surface was
drawn at that pixel, read from reversed scene depth (0.0 where nothing was).
That gate is what lets this film be high key and keep a dark sky — the
confection and the void are exposed separately (exposure / skyExposure) and
lifted separately (lift / skyLift), so both halves of the film's rule hold at
once. The highlight shoulder rolls on the brightest channel and scales all three
together, which is what keeps pistachio, strawberry and marshmallow from all
printing as the same white at this exposure.
The luma gate
The gate is stated against the FINISHED frame, which carries the camera slice's
letterbox (shots.letterbox = 0.08 per side — 16 % of the frame at the void
colour, about 22 luma off the mean). A capture taken while the cutscene is not
playing has no bars, so:
barred = 0.84 * unbarred + 2.6 unbarred 130..190 == barred 110..190
M.EV[shot] is the per-shot exposure in stops and is where the gate is tuned.
Measure with a capture from the shot's own station at its midpoint — the eye,
aim and fov come from @user::pad.camera.poseAt(t) — and read the mean luma of
the returned frame.
Exports
M.build()— spawnspad_look_rootand its three lights; registers the chain. Idempotent, guarded on liveness.M.attach(film)— registers the"look"animator.M.at(film, t) -> table— the whole look attas numbers, reading nothing from the world, so it can be printed and diffed without rendering.M.midpoints(film) -> { number }— the thirteen shot midpoints.M.EV: { number }— per-shot exposure in stops. The gate.
Notes
- Exposure is manual. The
exposuresystem's adaptation is an integrator and settles over seconds, so the same seek would give a different frame depending on where the clock came from; this film scrubs, so exposure is a function oftonpadGradeinstead. - Focus comes from
@user::pad.camera.focusAt(t)when that slice is loaded. TheLENStable here supplies only fStop and focal length, plus a focus fallback. - The one thing here that is not a pure function of
tislensFx.tick, which advances the clock the film grain is drawn against. A capture that pins the renderer's temporal clock (deterministic) pins it too. - Never cache a component proxy across frames. A proxy's environment reaches
the engine's computed-property registry, and a cached one puts that registry
inside the graph every hot reload has to rebind —
@user::pad.filmreloaded straight into the 4096-node budget because of it. This module keeps entity IDs, which are strings, and re-resolves the component each call.
Scoped to this part · feeds back into the world's score.