The light set every shaded fragment stage shares (§68, R-17 2026-08-09):
the uniform array declarations and the one function that turns a light index
plus a world position into an irradiance and a direction to shade with.
One string, spliced into both LIT_FRAGMENT_SHADER_SOURCE and
gl-standard.ts's stage, rather than two copies: the falloff and the cone
are one model and a scene mixing a LitMaterial with a StandardMaterial
under the same lamp must not be able to disagree about it. It also ships the
text once in a bundle instead of twice, which on this backend is real bytes
(§86).
Names, and why the count is an int uniform
punctualCount is a uniform, not a #define, so one linked program shades
a scene with any number of lamps from zero to
@fourjs/render!MAX_PUNCTUAL_LIGHTS — the uniform-switch argument
useMap records, one size up. That it is an int uniform is also the whole
byte-identity story: GL initializes it to 0, so a program whose scene has
no punctual light never uploads it, the loop below never runs, and the frame
emits exactly the GL sequence it emitted before this chunk existed.
The model (§68, "physically coherent units where practical")
d = |lightPosition − p|
attenuation = 1 / max(d², 1e-8)
× clamp(1 − (d / range)⁴, 0, 1) when range > 0
× clamp((cos θ − cos outer) × z, 0, 1) when the light is a spot
Inverse-square because a point emitter obeys it; the range window and the
cone ramp are KHR_lights_punctual's, so a loaded glTF light transfers
without reinterpretation. punctualParams[i].z is the precomputed
1 / max(cos inner − cos outer, 1e-6) — see @fourjs/render's lights.ts,
which packs it.
The max(d², 1e-8) is the same placement rule R-13 fixed for roughness:
the guard lives where the division does. A surface at the light's exact
position renders very bright, never NaN.
No 1/π appears anywhere, here or in either lobe that consumes this — the
engine's light colour × intensity is an irradiance already divided by π
(R-13, 2026-08-08), which is what lets a point light and the directional
light add up to one lighting model.
The light set every shaded fragment stage shares (§68, R-17 2026-08-09): the uniform array declarations and the one function that turns a light index plus a world position into an irradiance and a direction to shade with.
One string, spliced into both
LIT_FRAGMENT_SHADER_SOURCEandgl-standard.ts's stage, rather than two copies: the falloff and the cone are one model and a scene mixing aLitMaterialwith aStandardMaterialunder the same lamp must not be able to disagree about it. It also ships the text once in a bundle instead of twice, which on this backend is real bytes (§86).Names, and why the count is an
intuniformpunctualCountis a uniform, not a#define, so one linked program shades a scene with any number of lamps from zero to @fourjs/render!MAX_PUNCTUAL_LIGHTS — the uniform-switch argumentuseMaprecords, one size up. That it is anintuniform is also the whole byte-identity story: GL initializes it to0, so a program whose scene has no punctual light never uploads it, the loop below never runs, and the frame emits exactly the GL sequence it emitted before this chunk existed.The model (§68, "physically coherent units where practical")
Inverse-square because a point emitter obeys it; the range window and the cone ramp are
KHR_lights_punctual's, so a loaded glTF light transfers without reinterpretation.punctualParams[i].zis the precomputed1 / max(cos inner − cos outer, 1e-6)— see@fourjs/render'slights.ts, which packs it.The
max(d², 1e-8)is the same placement rule R-13 fixed forroughness: the guard lives where the division does. A surface at the light's exact position renders very bright, neverNaN.No
1/πappears anywhere, here or in either lobe that consumes this — the engine's light colour × intensity is an irradiance already divided by π (R-13, 2026-08-08), which is what lets a point light and the directional light add up to one lighting model.