SHADOW_GLSL:"uniform bool useShadow;\nuniform sampler2D shadowMap;\nuniform mat4 shadowMatrix;\nuniform float shadowBias;\nuniform float shadowNormalBias;\nuniform float shadowTexelSize;\n\nfloat shadowFactor(vec3 worldPosition, vec3 n) {\n vec4 lightSpace = shadowMatrix * vec4(worldPosition + n * shadowNormalBias, 1.0);\n vec3 c = (lightSpace.xyz / lightSpace.w) * 0.5 + 0.5;\n if (any(lessThan(c, vec3(0.0))) || any(greaterThan(c, vec3(1.0)))) {\n return 1.0;\n }\n float receiver = c.z - shadowBias;\n float lit = 0.0;\n for (int y = -1; y <= 1; y += 1) {\n for (int x = -1; x <= 1; x += 1) {\n float occluder = texture(shadowMap, c.xy + vec2(x, y) * shadowTexelSize).r;\n lit += receiver <= occluder ? 1.0 : 0.0;\n }\n }\n return lit / 9.0;\n}\n"
The receiver chunk both shaded fragment stages splice in (§69; R-18,
2026-08-09) — the shared half of shadowing, exactly as
PUNCTUAL_LIGHT_GLSL is the shared half of the light set.
p = worldPosition + n · shadowNormalBias
c = (shadowMatrix · vec4(p, 1)).xyz / w, mapped from [-1,1] to [0,1]
factor = (1/9) Σ over the 3×3 texel neighbourhood of c.xy
[ c.z − shadowBias ≤ depth(tap) ]
Four properties are load-bearing and each is a decision:
Outside the volume is lit, not shadowed. A receiver whose mapped
coordinate leaves the unit cube on any axis — past the near or far plane,
or outside the map's own rectangle — returns 1.0. One any(lessThan) || any(greaterThan) covers all three axes, which is both shorter and exactly
the claim. An under-sized extent then reads as "shadows stop here", which
an author can see and fix, rather than as "the world beyond the box went
black".
The perspective divide is performed although this tier's projection is
orthographic (w is 1). It costs one division per fragment and is what
lets §69's spot-light shadows reuse this chunk unchanged when they land.
shadowBias is subtracted from the receiver rather than added to the
caster, so the sign convention matches every other engine's: a positive
bias moves the surface towards the light.
The normal bias offsets the sample position, not the depth. It is a
world-space displacement (§40 metres), the only form that scales correctly
with a surface's slope; a constant depth bias cannot.
Why explicit taps and not hardware PCF
GLES 3.00 offers sampler2DShadow with TEXTURE_COMPARE_MODE, which gives a
free 2×2 comparison-filtered tap. This tier uses a plain sampler2D with
NEAREST filtering and nine explicit taps instead:
the comparison mode is sampler state, so it would have to be set on the
attachment gl-render-target.ts allocates — coupling that cache to what a
particular consumer intends to do with a depth texture, which is exactly
the coupling its design avoids;
a 2×2 hardware tap is a smaller filter than §69's "percentage-closer
filtering" suggests, and widening it later would mean nine shadow samples
rather than nine plain ones — no cheaper;
what the taps do is arithmetic this engine can state and test. What a
driver does inside a shadow sampler is not something a fake GL context can
assert anything about, and §33's language is worth the two instructions.
The loop bounds are constants, so the nine taps unroll; the tap offset
arrives as shadowTexelSize = 1 / mapSize, computed once per frame on the
CPU rather than once per fragment here.
The receiver chunk both shaded fragment stages splice in (§69; R-18, 2026-08-09) — the shared half of shadowing, exactly as PUNCTUAL_LIGHT_GLSL is the shared half of the light set.
Four properties are load-bearing and each is a decision:
1.0. Oneany(lessThan) || any(greaterThan)covers all three axes, which is both shorter and exactly the claim. An under-sizedextentthen reads as "shadows stop here", which an author can see and fix, rather than as "the world beyond the box went black".wis 1). It costs one division per fragment and is what lets §69's spot-light shadows reuse this chunk unchanged when they land.shadowBiasis subtracted from the receiver rather than added to the caster, so the sign convention matches every other engine's: a positive bias moves the surface towards the light.Why explicit taps and not hardware PCF
GLES 3.00 offers
sampler2DShadowwithTEXTURE_COMPARE_MODE, which gives a free 2×2 comparison-filtered tap. This tier uses a plainsampler2DwithNEARESTfiltering and nine explicit taps instead:gl-render-target.tsallocates — coupling that cache to what a particular consumer intends to do with a depth texture, which is exactly the coupling its design avoids;The loop bounds are constants, so the nine taps unroll; the tap offset arrives as
shadowTexelSize = 1 / mapSize, computed once per frame on the CPU rather than once per fragment here.