The shading pass multiplies each world position by the light's matrix, divides by w and maps -1..1 to 0..1: a texture coordinate plus the depth the light sees. A sampler2DShadow takes all three, and in COMPARE_REF_TO_TEXTURE mode the texture unit compares for you, returning 1 when the reference passes TEXTURE_COMPARE_FUNC (default LEQUAL) against the stored depth, else 0:
const map = GLH.framebuffer(gl, 4, 4, { color: false });
gl.bindFramebuffer(gl.FRAMEBUFFER, map.fbo);
gl.clearDepth(0.5); gl.clear(gl.DEPTH_BUFFER_BIT); // every texel stores depth 0.5
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, map.depth);
gl.activeTexture(gl.TEXTURE1); // GLH.evaluate's target: unit 1
for (const reference of [0.4, 0.5, 0.6]) {
const [lit] = GLH.evaluate(gl, `result = vec4(texture(uMap, vec3(0.5, 0.5, ${reference})));`,
'uniform highp sampler2DShadow uMap;');
console.log(`reference ${reference}: ${lit} (${lit ? 'lit' : 'in shadow'})`);
}Output
reference 0.4: 1 (lit) reference 0.5: 1 (lit) reference 0.6: 0 (in shadow)
Declare the sampler highp, since mediump depth can merge nearby surfaces. Fragments outside the map's 0..1 range read the clamped edge texel, so keep receivers inside the light's frustum.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; font-family: monospace; font-size: 10px; }
.names div { flex: 1; padding: 4px 2px; }
p { max-width: 600px; margin: 4px 0; font-family: monospace; }
</style>
<canvas id="c" width="600" height="200"></canvas>
<div class="names"><div>stored depths (grey)</div><div>texture(uMap, vec3(uv, 0.5)), NEAREST</div><div>same, LINEAR: 4 comparisons blended</div></div>
<p id="out"></p>
<script>
const gl = document.getElementById('c').getContext('webgl2');
function program(vs, fs) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(p, s);
}
gl.linkProgram(p);
return p;
}
const quadVS = `#version 300 es
out vec2 vUV;
void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`;
gl.bindVertexArray(gl.createVertexArray());
// A 16 x 16 depth texture: cleared to 1.0 (far), with a tilted "book" drawn at depth 0.3.
const SIZE = 16;
const map = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, map);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT24, SIZE, SIZE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, map, 0);
gl.drawBuffers([gl.NONE]);
gl.viewport(0, 0, SIZE, SIZE);
gl.enable(gl.DEPTH_TEST);
gl.clearDepth(1.0); gl.clear(gl.DEPTH_BUFFER_BIT);
const caster = program(`#version 300 es
void main() {
vec2 q = vec2[6](vec2(-1), vec2(1, -1), vec2(1), vec2(-1), vec2(1), vec2(-1, 1))[gl_VertexID];
float c = cos(0.4), s = sin(0.4);
vec2 p = mat2(c, s, -s, c) * (q * vec2(0.35, 0.6));
gl_Position = vec4(p, -0.4, 1.0); // NDC z -0.4 -> stored depth 0.3
}`, `#version 300 es
precision mediump float; void main() {}`);
gl.useProgram(caster);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// The book's check: a map cleared to 0.5 answers 1 (lit) for references at or below it.
const half = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, half);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT24, 4, 4);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, half, 0);
gl.clearDepth(0.5); gl.clear(gl.DEPTH_BUFFER_BIT); // every texel stores 0.5
gl.getExtension('EXT_color_buffer_float');
const result = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, result);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, 1, 1);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, null, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, result, 0);
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
gl.disable(gl.DEPTH_TEST);
gl.viewport(0, 0, 1, 1);
const probe = program(quadVS, `#version 300 es
precision highp float; uniform highp sampler2DShadow uMap; uniform float uRef; out vec4 c;
void main() { c = vec4(texture(uMap, vec3(0.5, 0.5, uRef))); }`);
gl.useProgram(probe);
gl.bindTexture(gl.TEXTURE_2D, half);
const answers = [0.4, 0.5, 0.6].map(ref => {
gl.uniform1f(gl.getUniformLocation(probe, 'uRef'), ref);
gl.drawArrays(gl.TRIANGLES, 0, 3);
const v = new Float32Array(4); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, v);
return `reference ${ref}: ${v[0]} (${v[0] ? 'lit' : 'in shadow'})`;
});
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
document.getElementById('out').textContent = 'map cleared to 0.5, LEQUAL: ' + answers.join(' | ');
// Draw the three views of the 16 x 16 map.
const grey = program(quadVS, `#version 300 es
precision highp float; uniform highp sampler2D uMap; in vec2 vUV; out vec4 c;
void main() { c = vec4(vec3(texture(uMap, vUV).r), 1.0); }`);
const compare = program(quadVS, `#version 300 es
precision highp float; uniform highp sampler2DShadow uMap; in vec2 vUV; out vec4 c;
void main() {
float lit = texture(uMap, vec3(vUV, 0.5)); // 1 where 0.5 <= stored depth, else 0
c = vec4(mix(vec3(0.25, 0.22, 0.2), vec3(0.95, 0.88, 0.7), lit), 1.0);
}`);
gl.bindTexture(gl.TEXTURE_2D, map);
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
[[grey, gl.NONE, gl.NEAREST], [compare, gl.COMPARE_REF_TO_TEXTURE, gl.NEAREST], [compare, gl.COMPARE_REF_TO_TEXTURE, gl.LINEAR]]
.forEach(([p, mode, filter], i) => {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, mode);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
gl.useProgram(p);
gl.viewport(i * 200 + 10, 5, 180, 190);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
</script>