film
The clock, the shot table and the palettes of GREEN SKY. Every other slice talks to this module and to nothing else.
film
The clock, the shot table and the palettes of GREEN SKY. Every other slice talks to this module and to nothing else.
The clock
A fixed 1/24 s per rendered frame, ignoring the real frame delta. The film is therefore byte-identical on every run whatever the frame rate, so it can be recorded a frame at a time and retimed on the host.
Film.FPS 24
Film.FIXED_DT 1/24
Film.DURATION 122 seconds
scenes/main.scene's entrypoint calls Film.build() once on load and
Film.tick() once per frame. There is no cutscene, no coroutine timeline
and no task.wait chain anywhere in the demo.
The shot table
Film.SHOTS — eighteen entries, { n, seconds, act, name, brief }.
seconds is the only timing authority in the film; greensky/camera owns
where each set-up stands. Shot numbers are stable and are never renumbered,
because every slice keys off them.
Film.shotAt(t) -> the shot on screen at t, and seconds into it
Film.shotStart(n) -> when shot n begins
Registering a slice
local Film = require("greensky.film")
local M = {}
function M.build() end -- once, on scene load
function M.update(c) end -- every frame
Film.register("pan", M, 10) -- lower order ticks first
The context c every update receives:
| field | meaning |
|---|---|
t / u | film seconds, and 0..1 across the whole film |
dt | always FIXED_DT |
frame | integer frame counter from 0 |
shot / shotT / shotU | the shot on screen, and progress through it |
cut | true on the first frame of a new shot |
act | 1, 2 or 3 |
storm / flood / clear | the three progressions, 0..1 |
Film.arcAt(t) gives those last three at any time, so a slice can ask what
act III looks like without waiting for it.
Two palettes, and they are not the same thing
Film.PALETTE is the light — what the sun, the sky and the ambient
are doing in the world, per act, plus the discharge. greensky/sky reads
it every frame.
Film.PAL is the photograph taken of that light — the response of
the camera's negative, per act. greensky/camera reads it every frame and
writes it into the gs_grade post-process pass. Its fields are ev
(exposure compensation in stops), exposure, contrast, pivot,
saturation, vignette, vigInner, grain, and the ASC-CDL triplets
lift (offset, moves the blacks), gain (slope, the whites) and gamma
(power, the mids).
Both carry dry, storm, clear and flash, and both are interpolated
by the same c.storm / c.clear ramps, so the light and the photograph of
it always agree about which act it is.
Film.mixGrade(a, b, x) mixes two Film.PAL entries.
Every gain in Film.PAL is at or below 1 on purpose: the post chain is
display-referred and hard-clipped at 1.0, so a gain above 1 clips the image
rather than opening it up. Level lives in ev, in stops.
The site
Film.SITE — the numbers more than one slice needs about the place: pan
radius, runway length and width, hangar position, cloud base and top, the
cloud volume's horizontal extent, where the storm enters and leaves, and
how deep the flood gets. Everything must live inside 8 km of every camera
station, because the far plane is hard-fixed at 8000.
Curves
Film.ramp(t, a, b) smoothstep, 0 before a and 1 after b
Film.lerp(a, b, x)
Film.mix3(a, b, x) mix two {r, g, b}
Film.clamp01(x)
Files
film.module/init.luau this module
../design/spec.md read this before changing any of it
../design/shotlist.md the eighteen shots as written
Scoped to this part · feeds back into the world's score.