ReadonlyambientThe scene ambient term; [0, 0, 0] when the root offers none.
ReadonlydirectionWorld-space unit vector the light travels along; (0, 0, -1) — the
direction an unrotated light shines — when there is no light. Owned by
the record and rewritten by the next collection.
ReadonlydirectionalThe light's color premultiplied by its intensity — the one product every
shaded fragment needs, computed once per frame here rather than per
fragment there. [0, 0, 0] when there is no light, which makes the
Lambert term vanish and is why a backend needs no shader variant for the
unlit-scene case.
Whether a directional light was found. When false the other two
directional fields hold their documented no-light values, so a backend
may upload them unconditionally.
Whether a hemisphere light was found. When false the three
hemisphere fields hold their documented no-light values (zeros), so a
backend may upload them unconditionally — or, on GL, upload nothing
at all: a bool uniform's initial value is false, the same
compatibility contract SceneLights.punctualCount states.
Whether the frame's directional light casts a shadow map (§69; R-18,
2026-08-09) — true only when that light exists, sets castShadow, and
offers a computeShadowMatrix.
A backend that sees false here must issue no shadow call at all —
not a framebuffer bind, not a uniform, not a texture. That is the same
compatibility contract SceneLights.punctualCount states, kept the
same way: a bool uniform's initial value in GL is false, so a scene lit
the way every scene was lit before §69 shipped emits byte-for-byte the GL
sequence it always did.
ReadonlyhemisphereGround colour premultiplied by intensity. [0, 0, 0] when there is
no hemisphere.
ReadonlyhemisphereSky colour premultiplied by intensity. [0, 0, 0] when there is no
hemisphere, which makes the mix vanish.
ReadonlyhemisphereWorld-space unit vector toward the sky; (0, 1, 0) — §7a's world up,
the direction an unrotated hemisphere faces — when there is no
hemisphere. Owned by the record and rewritten by the next collection.
The no-light up is the identity axis rather than zero so a backend that uploads unconditionally still has a finite, documented vector; the zero sky and ground are what make the mix contribute nothing.
ReadonlypunctualEach light's colour premultiplied by its intensity, r, g, b per light —
the same product directionalColor carries, for the same reason
(computed once per frame here rather than per fragment there).
How many entries of the four arrays below are live — 0 through
MAX_PUNCTUAL_LIGHTS (R-17, 2026-08-09).
A backend that sees 0 here must upload nothing at all. That is not
an optimisation, it is the compatibility contract: an int uniform's
initial value in GL is 0, so a scene lit the way every scene was lit
before this field existed issues byte-for-byte the GL sequence it always
did — the same technique R-38's permissive layer mask and R-15's
bool encode switch used.
ReadonlypunctualEach light's cone axis, x, y, z per light: the world-space unit vector
the light travels along, matching SceneLights.direction's
convention. A point light has no axis and writes (0, 0, 0).
ReadonlypunctualFour scalars per light, packed so the fragment stage needs no branching on a light kind beyond one comparison:
x range, or 0 for unbounded — the KHR_lights_punctual window
y cos(outerConeAngle); 0 for a point light
z 1 / max(cos inner − cos outer, 1e-6); 0 for a point light
w 1 for a spot light, 0 for a point light
The reciprocal in z is precomputed here, once per light per frame,
rather than in the shader once per light per fragment; the max is what
makes an inner ≥ outer cone a hard edge instead of a division by zero
(see @fourjs/scene's SpotLight).
ReadonlypunctualWorld-space positions, x, y, z per light, MAX_PUNCTUAL_LIGHTS entries
long and only the first punctualCount of them meaningful.
Float32Array rather than the number[] the fields above use, because
these are uploaded as a block: a backend hands the array straight to
uniform3fv with no per-frame copy into scratch, which is the whole
reason the packing is done here instead of there. The trailing entries are
zeroed by every collection so a transcript of the upload is deterministic
rather than showing the previous frame's leftovers.
Constant depth bias, in clip-space depth units (§69); 0 when nothing casts.
Edge of the square shadow map in texels; 0 when nothing casts. The
backend allocates its off-screen depth surface at this size and derives
the PCF tap offset (1 / shadowMapSize) from it.
ReadonlyshadowWorld space → shadow-map clip space (§69), written by the casting light's
own computeShadowMatrix. Identity when SceneLights.hasShadow is
false, and owned by the record — read it, upload it, do not mutate it.
Normal-space bias, in metres (§69, §40); 0 when nothing casts.
The frame's flattened lighting (§68), as a backend consumes it. One mutable record, rewritten in place by every collectSceneLights call — the pooling policy every per-frame structure in this package follows (plan D7). Copy anything you need to keep.