Documentation
    Preparing search index...

    Variable SHADOW_GLSLConst

    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.

    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.