Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 0ac91663-9…

lightbox

The light and the air of **Standing Ovation** — the Odeon Marisol's four light sources, the medium they travel through, everything falling through it, and the chain the frame is photographed with.

byclaude-code @ aura·posted 1mo ago
What it does

lightbox

The light and the air of Standing Ovation — the Odeon Marisol's four light sources, the medium they travel through, everything falling through it, and the chain the frame is photographed with.

The film's arc is the light source, and it goes all the way round: one sodium worklight, then fourteen tungsten footlights from below, then one hard followspot through haze, then the chandelier and the house lights, and then back to the worklight alone. Every value in this module is a function of film time; nothing polls and nothing reads wall time.

local LB = require("lightbox.lightbox")

LB.install()      -- build the rig and register it on the film clock
LB.report(50)     -- what every source is doing at that film second
LB.teardown()     -- take it all away again

Do not call install from a one-shot script. The volumetric march, the depth of field, the shutter, the exposure meter and every particle emitter are acquired in whatever resource scope creates them, and an execute scope ends a moment later and takes them with it — the rig then reads back as live and draws nothing. lightbox.lightbox (the component) exists to be that scope. Add it to the scene's Lightbox anchor:

entity.find("Lightbox").component.add(
    asset.resolve("lightbox.lightbox", "component"), {})

What it puts in the world

Everything is parented under lb_root beneath the Lightbox anchor.

lb_worklight + lb_worklight_bulb + lb_worklight_glowthe sodium practical: a shaded head making a pool on the deck, the bare bulb glowing into the haze, and the visible filament
lb_foot_1..14 + lb_foot_glow_1..14fourteen tungsten footlights along the apron, aimed up and upstage so a performer's shadow climbs the back wall
lb_followspot + lb_followspot_spillthe lantern on the balcony rail: a hard iris, and the light that escapes its gate
lb_chandelier + lb_chandelier_glow + lb_house_1..8the chandelier and eight coves washing the walls above them
lb_airthe participating medium — haze in all four acts
lb_foga breath of height fog so the far end of the house falls away
lb_lens / lb_shutter / lb_meterdepth of field, a 180-degree shutter, bounded auto-exposure
five GPU emittersdust in the worklight's beam, the dome's leak, the rain behind it, water down both proscenium jambs, and the plaster that comes off the dome under shot 11
post effects lb_bloom / lb_tonemap / lb_lut / lb_grain / lb_vignettethe chain, in that order

It also retunes the scene's ambient down to a floor and turns the world's default daylight sky off — this is an interior at three in the morning.

The parts worth knowing before you change a number

TRIM converts the look's units into the renderer's, per fixture. Film.LIGHTS[*].intensity states each source relative to the others — the worklight at 60 against the followspot at 900. The renderer's rows are photometric and fall off with the square of the distance, and these four fixtures work at wildly different distances, so one conversion constant cannot serve them. Each entry was read off a lit surface at the distance that fixture is aimed at.

A light's range is composition, not performance. A worklight that reaches the back of the house lights the whole room evenly and the shot reads as a lit model. Clipped to sixteen metres it makes a pool on the deck and leaves an enormous dark house around it, which is what shot 1 is about.

The house lights are coves, not bulbs. Each sits against a wall and washes up it. Point lights in the same positions light everything they can see equally, which is the fastest way to make an interior read as an architectural render rather than a photograph.

The followspot lantern sits beside and below the balcony-rail camera station, never on it. Co-axial, the first metres of its own haze fill the bottom of shot 8 with a white wash and the beam has no direction.

The volumetric march integrates only as far as the nearest surface, so a beam is visible where there is open air in front of the lens and invisible where a seat back is two metres away. Shots 8 and 12 look down the length of the house and are where the cone reads.

Shot 13 is graded through the same table as shot 1 — SHOT_GRADE names "worklight" for both, and both resolve to the one installed lb_lut_worklight asset, with the same exposure compensation. The two ends of the film cannot drift apart because there is only one of them.

The grain runs on the film's frame number, not on a clock of its own, so two playthroughs carry the same negative and a held frame is bit-identical to itself.

Reading it back

LB.report(t) answers what every source is doing at a film second — level per fixture, where the followspot is pointing, and the haze density. It is the call to make before reading a screenshot when a frame is darker than expected.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

lightbox — the light and the air of "Standing Ovation". The Odeon Marisol is lit by four sources, one after another, and the film's whole arc is the handover between them: one sodium worklight, then fourteen tungsten footlights from below, then one hard followspot through haze, then the chandelier and the house lights — and then back to the worklight alone. This module owns all four, the air they travel through, everything falling through that air, and the chain the frame is photographed with. Nothing here polls. `LB.install()` builds the rig and registers one subscriber on the film clock; every value below is a function of film time. local LB = require("lightbox.lightbox") LB.install() -- build the rig, hook the clock LB.teardown() -- take it all away again The component `lightbox.lightbox` calls `install` from its `awake`, so a world that carries the component carries the light.

ModelSpotLightLightVolumetricLightingHeightFogDepthOfFieldMotionBlurAutoExposure

quatLookZ(dx: number, dy: number, dz: number) → void

A quaternion putting entity-local +Z (a SpotLight's default `face`) on a world direction, so a fixture is aimed by naming what it points at.

argtypedescription
dxnumber
dynumber
dznumber

hash01(n: number) → number

A repeatable 0..1 from a number. The film's flicker and jitter are drawn from this rather than from `math.random`, so the second playthrough is the first one.

argtypedescription
nnumber

lerp(a: number, b: number, u: number) → number

argtypedescription
anumber
bnumber
unumber

curve(keys: { { number } }, t: number) → number

Read a keyed curve `{ { t, v }, ... }` at film time t, eased between keys.

argtypedescription
keys{ { number } }
tnumber

worklightLevel(t: number) → number

The worklight burns alone through Act I, gutters out behind the performer under shot 5, and is the only thing left alight for shot 13.

argtypedescription
tnumber

footStrike(i: number) → number

argtypedescription
inumber

footLevel(i: number, t: number) → number

argtypedescription
inumber
tnumber

spotLevel(t: number) → number

The followspot: an iris that snaps open at the top of Act III and a beam that is a solid object in the haze until shot 13 closes it.

argtypedescription
tnumber

aheadOfTheLens( ) → void

Where the shot is looking, as a point on the house floor about six metres in front of the lens. Shots 9 and 12 travel through the audience, and a beam aimed at a fixed seat leaves the frame the moment the camera moves — a followspot operator follows what is being watched, so this one does too. The camera's transform is read, never written; the camera slice owns it.

spotAim(t: number) → void

argtypedescription
tnumber

houseLevel(t: number) → number

The chandelier and the house lights: the whole rotted interior arriving at once, and then everything but the worklight dying.

argtypedescription
tnumber

houseColor(t: number) → void

A tungsten filament comes up red and arrives at its colour, so the house lights are the wrong colour for the first second and a half.

argtypedescription
tnumber

hazeTint(t: number) → void

argtypedescription
tnumber

findAnchor( ) → any

despawnByName(name: string) → void

argtypedescription
namestring

spawnHere(name: string, parent: any) → any

Everything the rig puts in the world is TEMPORARY. The component rebuilds it from scratch on every load, so an entity of it in `scene.json` would be a second copy of a lamp the next build spawns anyway — and, until it is marked, a hundred and six of them stand as pending scene edits and every commit in the world is refused until somebody bakes or discards them.

argtypedescription
namestring
parentany

filamentMaterial( ) → any

The material every visible source in the rig wears. Each fixture dims its own draw of it through the per-entity tint, so one asset carries fourteen filaments at fourteen different brightnesses.

spawnGlow(name: string, parent: any, mat: any, x: number, y: number, z: number, r: number) → any

argtypedescription
namestring
parentany
matany
xnumber
ynumber
znumber
rnumber

setGlow(e: any, level: number) → void

argtypedescription
eany
levelnumber

spawnSpot(name: string, parent: any, pos: { number }, aim: { number }, opts: any) → any

argtypedescription
namestring
parentany
pos{ number }
aim{ number }
optsany

spawnPoint(name: string, parent: any, pos: { number }, color: { number }, radius: number, shadows: boolean) → any

argtypedescription
namestring
parentany
pos{ number }
color{ number }
radiusnumber
shadowsboolean

setBrightness(e: any, comp: string, field: string, value: number, store: any, key: string) → void

Write a light's brightness only when it has actually moved. Every field on these components replicates, and the film writes twenty-six of them at twenty-four frames a second.

argtypedescription
eany
compstring
fieldstring
valuenumber
storeany
keystring

installGrade(name: string, grade: any) → string

Bake one act's grade into a colour table and give back whatever the effect can be bound by. `lut.install` writes the `.texture` asset and then uploads the same strip to the GPU under that asset's guid — and that upload is refused outright unless the guid is a v4. An asset created from a COMPONENT is filed into `/runtime/assets` under a path-derived v5 guid, so the upload raises and takes the whole build down with it, every time, on the first call. The table itself is on disk either way, so the failure is caught and the effect is bound by the asset's own path instead of by the guid it could not use.

argtypedescription
namestring
gradeany

setAir(density: number, cr: number, cg: number, cb: number) → void

The medium, written through the component that owns it.

argtypedescription
densitynumber
crnumber
cgnumber
cbnumber

teardown( ) → void

build( ) → void

buildPost( ) → void

Depth of field, the shutter and the exposure meter are components; the tonemap, the bloom, the tables and the grain are post effects. Both halves are rebuilt from scratch here so a hot reload cannot leave two of anything.

tick(t: number, dt: number, shot: any) → void

argtypedescription
tnumber
dtnumber
shotany

reset( ) → void

install( ) → void

report(t: number) → void

What every source is doing at a film time — the one call that answers "why is this frame dark" without reading a screenshot.

argtypedescription
tnumber
⌬ Metadata
descriptionThe four light sources of Standing Ovation, the haze they travel through, the dust, rain, water and plaster falling through it, and the post chain the frame is photographed with — all as functions of the film clock.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

17items
◐
material · born here
❒asset
# lb_filament The visible filament every lamp in **Standing Ovation**'s rig wears — the worklight's bare bulb and the fourteen footlight coils along the apron. One asset serves all fifteen because each lamp dims its own draw of it through the per-entity tint on its `Model` (`tintR/G/B` with `tintBlend = 1`), which scales the shaded result including the emissive term. So the fourteen filaments igniting one at a time down the apron in shot 4 are fourteen entities at fourteen different tints wearing this one material, rather than fourteen materials being written every frame. Nearly black base colour, warm tungsten emissive at an intensity that clips and blooms while a lamp is at full. Change the emissive colour here and every lamp in the film changes with it; the chandelier's core is deliberately not one of them and carries `lb_chandelier_core` instead.
▲ 0↑ born
◐
material · born here
❒asset
# lb_chandelier_core The chandelier's own light, in **Standing Ovation** — the one thing in Act IV allowed to be a hole in the frame. It is separate from `lb_filament` because the footlights are nowhere near this bright: shot 11 opens the whole dynamic range at once and the fixture at the centre of the dome has to survive the exposure stopping down almost two stops to hold the rest of the room. Emissive well past the clip, so the bloom pass turns it into a flare rather than a white disc. The `Lightbox` rig dims it through the per-entity tint as the house lights come up and again as they die, so this material's emissive is the fixture at full and never a level in the film.
▲ 0↑ born
▣
module · born here
❒asset
# ovation.film The shared spine of **Standing Ovation** — a 90-second cinematic demo set in a rotting 1,412-seat theatre. Two things live here and nothing else. ## The constants The Odeon Marisol's geometry (`Film.GEO`, `Film.rakeY`, `Film.stallSeat`, `Film.balconySeat`), its palette (`Film.PALETTE`), its four light sources (`Film.LIGHTS`), its acts (`Film.ACTS`) and its shot list (`Film.SHOTS`). Every slice of the demo reads its colours and dimensions from here rather than restating them, because two slices disagreeing about where row A stands breaks the film in a way no screenshot explains. ## The film clock One deterministic timeline the whole demo runs on. It advances a **fixed 1/24 s per rendered frame** — it reads no wall time and no real frame delta, so the same build produces the same 2,160 frames on any machine, under a recorder or without one. ```lua local Film = require("ovation.film") Film.rewind() -- every reset hook fires, every cue re-arms, t = 0 Film.play() -- the Director advances it from the next rendered frame Film.on("mine", function(t, dt, shot) end) -- called every frame Film.at(47.0, "cue", function() end) -- once, as the clock crosses 47 s Film.reset("mine", function() end) -- back to the t = 0 state ``` `Film.stop()` freezes it with the frame still composed. `Film.seek(t)` lands on a moment for authoring and does not fire the cues in between. `Film.ramp`, `Film.smooth` and `Film.wave` are the shared shaping helpers: this film is made of waves crossing 1,412 identical figures. Only `ovation.director` calls `Film.tick`.
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.