ShadowMapRenderer
ShadowMapRenderer is an experimental WebGPU and WebGL2 light-space shadow system. It owns stable
cascaded directional maps, spot arrays, point-light maps, and the samplers used by the
shadow WGSL/GLSL module. Applications retain scene traversal and draw their own shadow casters.
Usage​
import {Model, ShaderInputs} from '@luma.gl/engine';
import {ShadowMapRenderer, shadow} from '@luma.gl/anari';
const renderer = new ShadowMapRenderer(device, {quality: 'balanced'});
const shaderInputs = new ShaderInputs({shadow});
const shadowProps = renderer.render({
camera: {viewMatrix, projectionMatrix, near: 0.1, far: 200},
directionalLights: [{direction: [0.4, 0.8, 0.3]}],
spotLights: [{position: [0, 10, 0], direction: [0, -1, 0], range: 40, outerConeAngle: 0.45}],
pointLights: [{position: [0, 4, 0], range: 18}],
drawShadowCasters: view => {
const casterModel = casterModels[getViewIndex(view)];
casterModel.shaderInputs.setProps({caster: {viewProjectionMatrix: view.viewProjectionMatrix}});
casterModel.setParameters(view.rasterParameters);
casterModel.draw(view.renderPass);
}
});
shaderInputs.setProps({shadow: shadowProps});
The callback runs once per directional cascade, spot map, and point cube face. It receives an already-open depth-only render pass. When several views are recorded before submission, use independent uniform buffers or models per view; repeatedly rewriting one uniform buffer would make all recorded draws observe its final value.
Camera projections default to math.gl's OpenGL [-1, 1] depth convention. Set
camera.clipDepth: 'zero-to-one' for an already-converted WebGPU projection. The returned light
camera matrices always use WebGPU [0, 1] depth for both caster rendering and receiver sampling.
WebGPU stores point maps as cube arrays and uses comparison samplers. WebGL2 stores the six faces
per light in a 2D depth array and performs explicit bilinear depth comparisons. Both use the same
cascade fitting, blocker search, contact-hardening PCSS, and quality settings. In a GLSL caster,
convert the returned clip position before assigning it: clip.z = clip.z * 2.0 - clip.w.
Shader integration​
Add shadow to a WebGPU or WebGL2 model and apply the returned visibility to each direct-light term:
let sunVisibility = shadow_getDirectionalFactor(worldPosition, worldNormal, viewDepth);
let spotVisibility = shadow_getSpotFactor(spotIndex, worldPosition, worldNormal);
let pointVisibility = shadow_getPointFactor(pointIndex, worldPosition, worldNormal);
color += sunDirect * sunVisibility;
color += spotDirect * spotVisibility;
color += pointDirect * pointVisibility;
Ambient, emissive, and indirect terms stay outside these multiplications. Shadows therefore cannot be implemented correctly as a color-only postprocess.
The module uses bind group 2 and binds depth32float texture_depth_2d_array resources for
directional and spot maps, a texture_depth_cube_array for point lights, one non-filtering depth
sampler, and one comparison sampler. PCSS performs explicit-LOD blocker search and variable-radius
comparison filtering.
Quality presets​
| Quality | Cascades | Directional | Spot | Point face | Blocker/filter | Contact scale/steps |
|---|---|---|---|---|---|---|
low | 3 | 1024 | 512 | 256 | 8 / 12 | 0.5 / 12 |
balanced | 4 | 1536 | 1024 | 512 | 16 / 24 | 0.75 / 24 |
cinematic | 4 | 2048 | 2048 | 1024 | 24 / 48 | 1.0 / 40 |
Capacities default to one directional, one spot, and one point light. Configurable hard limits are
one directional, four spot, and four point lights. setProps() rebuilds resources only when the
effective quality, capacity, cascade count, or map size changes.
Contact refinement​
createContactShadowCompositeShaderPass({quality}) creates a depth ray-march, two depth/normal-aware
bilateral passes, and a composite. Bind camera depth, view-space normals, projection matrices, the
primary directional-light vector, and a texture containing that light's already-shadowed direct
contribution. The composite subtracts only the occluded directional direct term, preserving ambient
and emissive lighting. Run contact refinement immediately after scene shading and before SSAO, SSR,
fog, outlines, TAA, and motion blur.
Lifecycle​
- The renderer rejects non-WebGPU devices and active light counts above configured capacities.
render()records commands but never submits, allocates, or reads GPU data.destroy()is idempotent and releases textures, layer views, framebuffers, and samplers.