Shader Passes
A shader pass is a shader module that can run as a fullscreen texture-processing
stage. The pass descriptor lives in @luma.gl/shadertools; the renderer that
executes pass chains lives in @luma.gl/engine as ShaderPassRenderer.
Choose an effect and adjust its parameters to see a shader pass update the source texture live:
Components
| Component | Role |
|---|---|
ShaderPassRenderer | Owns the fullscreen draw path, swap framebuffers, named targets, shader inputs, and presentation step. |
ShaderPassPipeline | Chains existing passes with named intermediate render targets. |
ShaderPass | Describes one standalone texture-processing effect and its optional subpasses. |
ShaderSubPass | Describes one draw inside a pass, including source routing, output routing, and subpass uniforms. |
Use a plain ShaderPass when each stage only needs the original input texture
or the previous result. Use a ShaderPassPipeline when later steps need named
intermediate textures such as an extracted highlight texture and its blurred
version.
Execution Model
ShaderPassRenderer receives a source texture, runs each pass or pipeline step,
and either returns the final texture or presents it to the current framebuffer.
It always exposes two logical texture sources:
| Source | Meaning |
|---|---|
original | The texture passed to renderToTexture() or renderToScreen(). |
previous | The current output in the shared pass chain. |
Pipelines may add named render targets. The renderer validates routing, manages
their size, and prevents a subpass from reading and writing the same named
target in one draw. Transient targets with non-overlapping lifetimes can set
aliasFor to reuse an earlier allocation with identical dimensions, format, and sampler;
persistent history targets cannot be aliased. Pipelines can also declare an optional WebGPU
compute replacement while retaining equivalent fragment steps as the WebGL or unsupported-device
fallback.
Built-in effects consume previous, so the shaderPasses array has strict
ordered-composition semantics even when it mixes plain ShaderPass objects and
multi-step ShaderPassPipeline objects. Route an input from original only
when an effect intentionally needs to bypass all preceding color processing.
Scene-aware effects may also sample application-owned depth, normal, or velocity attachments. Color adjustments can be placed anywhere in the chain, but an effect that warps screen coordinates should run after scene-aware effects unless the application applies the same transform to those auxiliary attachments.
Composable Scene Render Stack
The advanced-effects path has three separate responsibilities:
- Application geometry produces one scene color texture plus semantic surface attachments.
ShaderPassRendererorders fullscreen effects and owns their internal named and temporal targets.- The application presents the final texture, or passes it to another explicit workflow such as transparency capture and resolve.
On WebGPU, experimental GBuffer packages the common
surface attachments without owning scene traversal or material shading. Multiple render targets
(MRT) means one fragment shader writes several color attachments in the same render pass:
| Render-stack value | Producer | Consumers |
|---|---|---|
sourceTexture | GBuffer.colorTexture | Every shader pass through previous or original. |
depthTexture | GBuffer.depthTexture | DOF, depth-aware blur, SSAO, GTAO, SSGI, SSR, outlines, contact shadows, TAA, motion blur, fog, clustered volumetric lighting. |
normalTexture | GBuffer.normalRoughnessTexture | SSAO, GTAO, SSGI, SSR, normal-aware outlines, contact-shadow filtering. |
velocityTexture | GBuffer.velocityTexture | GTAO/SSGI/SSR/volumetric temporal reprojection, TAA, and motion blur. |
| named extras | GBuffer.getExtraColorTexture(name) | Application-specific material, debug, lighting, or resolve passes. |
import {ShaderPassRenderer} from '@luma.gl/engine';
import {
createBloomShaderPassPipeline,
createMotionBlurShaderPassPipeline,
createGTAOShaderPassPipeline,
createHDRAutoExposureShaderPassPipeline,
createSSGIShaderPassPipeline,
createSSRShaderPassPipeline,
createTAAShaderPassPipeline,
toneMapping
} from '@luma.gl/effects';
import {GBuffer} from '@luma.gl/experimental';
const gBuffer = new GBuffer(device, {width, height});
// Geometry shaders write color, normalRoughness, and velocity in one MRT render pass.
const scenePass = device.beginRenderPass({
framebuffer: gBuffer.framebuffer,
clearColors: [
new Float32Array([0, 0, 0, 1]),
new Float32Array([0.5, 0.5, 1, 1]),
new Float32Array([0, 0, 0, 0])
],
clearDepth: 1
});
sceneModel.draw(scenePass);
scenePass.end();
const effects = new ShaderPassRenderer(device, {
colorFormat: 'rgba16float',
shaderPasses: [
createGTAOShaderPassPipeline(),
createSSGIShaderPassPipeline(),
createSSRShaderPassPipeline(),
createTAAShaderPassPipeline(),
createMotionBlurShaderPassPipeline(),
createHDRAutoExposureShaderPassPipeline(),
createBloomShaderPassPipeline(),
toneMapping
]
});
effects.renderToScreen({
sourceTexture: gBuffer.colorTexture,
bindings: gBuffer.getShaderPassBindings()
});
GBuffer is intentionally a target and binding contract, not a scene renderer. Experimental
deferredLighting consumes two named material
extras plus depth and normal-roughness, reconstructs view position, and writes a Cook-Torrance
lighting result into the same ordered color chain:
const renderer = new ShaderPassRenderer(device, {
colorFormat: 'rgba16float',
shaderPasses: [
createDeferredLightingShaderPassPipeline(),
createGTAOShaderPassPipeline(),
createSSGIShaderPassPipeline(),
createSSRShaderPassPipeline(),
createTAAShaderPassPipeline(),
createHDRAutoExposureShaderPassPipeline(),
createBloomShaderPassPipeline(),
toneMapping
]
});
More specialized clustered-lighting or visibility-buffer workflows can replace the first resolve while preserving the same effect-facing depth, normal, velocity, and scene-color contract.
GTAO defaults to its backward-compatible full-color composite. Deferred applications that can isolate ambient light should request the physically accurate ambient-only mode instead:
import {createGTAOShaderPassPipeline} from '@luma.gl/effects';
import {createDeferredAmbientLightingShaderPassPipeline} from '@luma.gl/experimental';
const ambientRenderer = new ShaderPassRenderer(device, {
shaderPasses: [createDeferredAmbientLightingShaderPassPipeline()]
});
const ambientLightingTexture = ambientRenderer.renderToTexture({
sourceTexture: gBuffer.colorTexture,
bindings: {
depthTexture: gBuffer.depthTexture,
baseColorMetallicTexture: gBuffer.getExtraColorTexture('baseColorMetallic'),
emissiveOcclusionTexture: gBuffer.getExtraColorTexture('emissiveOcclusion')
},
uniforms: {deferredAmbientLighting: {ambientColor: [0.04, 0.04, 0.05]}}
});
const effects = new ShaderPassRenderer(device, {
shaderPasses: [
createDeferredLightingShaderPassPipeline(),
createGTAOShaderPassPipeline({composition: 'ambient-only'})
]
});
effects.renderToScreen({
sourceTexture: gBuffer.colorTexture,
bindings: {...gBuffer.getShaderPassBindings(), ambientLightingTexture}
});
Ambient-only composition subtracts ambientLightingTexture * (1 - visibility) from the lit
scene. Direct light, material emission, and alpha therefore remain unchanged. The separate
ambient texture is an explicit application-owned integration boundary; the effect does not
depend on hidden cross-pipeline render targets.
For side-by-side choices between reflections, ambient occlusion, light assignment, shadows, transparency, blur, and temporal effects, see Rendering Techniques and Tradeoffs.
Screen-space diffuse global illumination
createSSGIShaderPassPipeline() gathers already-lit scene radiance from the hemisphere above
each visible surface. Its stages mirror the reusable temporal render-stack contract:
- Trace cosine-weighted hemisphere rays through the shared scene depth and view normals.
- Reproject indirect-radiance history with velocity and reject perspective-correct depth disocclusions.
- Save current depth for the next-frame history comparison.
- Denoise diffuse bounce horizontally while preserving depth and normal edges.
- Repeat the bilateral denoising vertically.
- Add stabilized colored bounce to
previous, or expose indirect-radiance/confidence debug views.
SSGI adds diffuse energy; GTAO removes unavailable ambient energy, while SSR adds directional specular reflection. They are complementary effects rather than interchangeable copies. Place SSGI after the direct-light/GTAO resolve and before SSR when mirror reflections should include the newly bounced illumination.
Screen-space reflection composition
createSSRShaderPassPipeline() consumes the already-lit previous color plus the shared depth,
normal/roughness, and velocity attachments. Its six explicit stages demonstrate how a complex
effect remains one composable pipeline:
- Trace stochastic, roughness-aware reflection rays against the G-buffer depth at configurable resolution, defaulting to full resolution.
- Reproject persistent reflection history with screen-space velocity and reject depth disocclusions.
- Save current depth into the reflection history target for the next frame.
- Denoise reflection radiance horizontally using roughness, scene depth, and normals.
- Repeat the depth/normal-aware denoising vertically.
- Composite the stabilized HDR reflection into
previous, or expose reflection/confidence debug views.
Mirror-like materials retain narrow highlights, while rough surfaces accumulate wider glossy lobes. Because tracing samples existing scene color, its cost depends on visible pixels and ray steps instead of drawing every reflected object again. Off-screen geometry cannot contribute; screen-edge confidence fades reduce the resulting discontinuities.
Clustered volumetric lighting
createClusteredVolumetricLightingShaderPassPipeline() turns the same clustered point-light
storage buffers used by deferred shading into actual participating-media illumination:
- March configurable-resolution view rays through exponential world-height density.
- Integrate a best-scoring bounded set from the compute-retained cluster candidates plus directional light using an anisotropic phase function; work never falls back to scanning every active light per ray step.
- Trace radial screen-depth visibility toward a configurable sun position to produce recognizable, depth-occluded crepuscular god rays.
- Reproject empty-space atmospheric history with current/previous camera transforms, apply G-buffer velocity to opaque surfaces, and reject linear-depth disocclusions.
- Capture compact current linear depth for the next frame.
- Denoise the radiance/transmittance result with separable depth-aware blur.
- Composite Beer-Lambert extinction and in-scattered light, or expose volume/transmittance diagnostics.
This is the higher-fidelity alternative to createVolumetricFogShaderPassPipeline(), whose
compact height fog does not evaluate the real scene-light storage buffers. Both remain composable
ordered pipelines; normally choose one atmospheric implementation rather than stacking both.
Unlike the generic GBuffer examples above, clustered volumetric lighting requires more than
gBuffer.getShaderPassBindings(). Encode a ClusteredLightGrid for the current point-light buffer
each frame, add pointLights, clusteredLightGrid.getShaderPassBindings(), depth, and velocity to
the renderer bindings, and merge clusteredLightGrid.getShaderPassUniforms(nearPlane, farPlane)
into the clusteredVolumetricTrace uniforms alongside its camera, media, and light settings.
Provide inverseViewProjectionMatrix and previousViewProjectionMatrix to
clusteredVolumetricTemporal, plus inverseProjectionMatrix to both the temporal and linear-depth
history-copy stages. See
the ClusteredLightGrid usage guide for the
buffer setup and encode sequence.
SSAO, GTAO, screen-space global illumination, reflections, and clustered volumetric lighting
default to full-resolution intermediate framebuffers. Pass resolutionScale: 0.5, for example,
to explicitly trade edge quality for fewer shaded pixels and smaller history textures.
Adaptive HDR exposure and cinematic bloom
createHDRAutoExposureShaderPassPipeline() meters and adapts scene brightness entirely on the GPU:
- Extract center-weighted logarithmic luminance from floating-point scene color.
- Reduce four successively smaller luminance-pyramid levels into a near-global geometric mean.
- Adapt persistent exposure history with independent brightening and darkening response rates.
- Apply the adapted exposure to HDR scene color or visualize luminance as a false-color heat map.
Pair it with createBloomShaderPassPipeline(), which extracts HDR highlights at half resolution,
then progressively filters and reconstructs two to five rgba16float pyramid levels.
quality: 'ultra' reaches one-thirty-second resolution. resolutionScale controls the entire
pyramid without clamping highlight radiance to 8-bit normalized color. The pipeline supports:
- Exposure-aware highlight thresholds, soft knees, isolated-highlight suppression, configurable scatter, tint, and horizontal or vertical anamorphic stretching.
- Separable Gaussian blur or
blurAlgorithm: 'dual-kawase', which reconstructs the downsampled pyramid without the separate Gaussian passes. - Normalized tent reconstruction or
reconstruction: 'bicubic', which uses four bilinear samples for a B-spline reconstruction. energyConserving: truefor thresholdless normalized scattering instead of additive glow.- Optional lens dirt, chromatic ghosts, radial halos, and aperture-diffraction starbursts.
- Neighborhood-clamped temporal history, optional motion/depth reprojection, disocclusion rejection, and exposure-corrected history.
- WebGPU compute downsampling with automatic WebGL and unsupported-device render-pass fallback.
downsample: 'auto' fuses extraction and the complete downsampling pyramid into one WebGPU
compute dispatch when the chosen format supports storage writes and the device has enough storage
bindings. downsample: 'render' forces the portable fragment implementation. Expired extraction
textures are reused for reconstruction by default; set reuseRenderTargets: false to retain
separate allocations for inspection.
| Quality | Levels | Gaussian portable | Gaussian WebGPU | Dual-Kawase portable | Dual-Kawase WebGPU |
|---|---|---|---|---|---|
low | 2 | 8 render | 6 render + 1 compute | 4 render | 2 render + 1 compute |
medium | 3 | 12 render | 9 render + 1 compute | 6 render | 3 render + 1 compute |
high | 4 | 16 render | 12 render + 1 compute | 8 render | 4 render + 1 compute |
ultra | 5 | 20 render | 15 render + 1 compute | 10 render | 5 render + 1 compute |
These counts describe bloom-pipeline work only; optional lens/history passes and renderer source-seeding or presentation passes are additional.
Enable optional photographic optics through lens: aperture-diffraction starbursts, mirrored
chromatic ghosts, and radial halos share one extra half-resolution pass. lens.dirtIntensity
samples an application-provided lensDirtTexture during the existing composite and therefore adds
no pass or intermediate render target. Pass that mask through
renderer.renderToScreen({sourceTexture, bindings: {lensDirtTexture}}).
temporalStability enables neighborhood-clamped, persistent half-resolution glow history for one
additional pass. temporalReprojection: true additionally consumes caller-provided
velocityTexture and depthTexture bindings, rejects disocclusions, and stores previous depth in
the existing history alpha channel. previousExposure corrects history when exposure changes.
The default configuration allocates neither history nor lens artifacts. Screen-space diffraction
costs eight samples per ray, while each ghost costs one sample or three when chromatic aberration
is enabled.
For full-kernel optical convolution, GPUConvolutionBloom from @luma.gl/experimental provides a
separate WebGPU implementation. It accepts generated or measured point-spread functions, including
independent red, green, and blue kernels; packs those channels into one forward and one inverse FFT
schedule; and uses zero-padded guard bands to prevent opposite-edge wraparound. Optional lens
artifacts, sampled dirt, and temporal history execute in its existing final compute pass. An
optional GPU-resident exposure texture supplies adapted exposure without CPU readback. At
1920 x 1080 with quarter-resolution sampling and the default 12.5% guard band, the FFT uses a
1024 x 512 transform, 48 MiB of complex buffers, and 45 steady-state compute dispatches.
Disabling the guard band reduces that configuration to a 512 x 512 transform, 24 MiB, and
43 dispatches while removing edge-wrap protection. Changing the optical kernel adds
21 initialization dispatches for the guarded configuration.
Keep bloom in linear floating-point scene color before tone mapping. The stock deck.gl
PostProcessEffect accepts shader-pass modules, but does not execute named-target
ShaderPassPipeline graphs or the WebGPU FFT renderer. Its current intermediate buffers default
to rgba8unorm, so an integration requiring unclamped HDR highlights must first arrange
floating-point scene/postprocessing targets.
Related technical references:
- Unreal Engine bloom documentation documents FFT convolution, image-defined optical kernels, energy conservation, lens dirt, and edge-padding controls.
- AMD FidelityFX Single Pass Downsampler describes workgroup-local image reduction in a single compute dispatch.
- Google Filament imaging pipeline documents camera exposure, scene-linear bloom, and processing before tone mapping.
- deck.gl PostProcessEffect documents deck.gl's existing shader-module postprocessing interface.
Use the HDR order: temporal effects, auto exposure, bloom, then tone mapping.
Recommended ordering
| Phase | Typical work | Why |
|---|---|---|
| Geometry and opaque surface capture | MRT scene color, normal-roughness, velocity, depth, material extras | Establish one coherent surface snapshot. |
| Opaque lighting resolve | Deferred PBR lighting, contact shadows, other direct-light corrections | These still need unwarped depth, normals, and material terms. |
| Surface effects | SSAO/GTAO, SSGI, SSR, outlines, depth-aware blur | These consume the original semantic attachments. |
| Participating media | Height fog or clustered volumetric lighting | Composite extinction and in-scattering over completed opaque light transport. |
| Transparency resolve | WBOIT or A-buffer resolve pipeline | Resolve translucent geometry before temporal accumulation when it should participate in TAA. |
| Temporal effects | TAA, then motion blur | Reproject the composed image before display-space processing. |
| Display effects | Auto exposure, bloom, color adjustment, vignette, tone mapping | These operate on final color and usually do not need scene attachments. |
This is a default, not a hard rule. A debug view may intentionally bypass earlier color processing
through original, and a stylized stack may place display-space effects earlier.
Temporal history and resize
Pass pipelines with persistent history targets keep those textures inside ShaderPassRenderer.
Call renderer.resetHistory() after a camera cut, a discontinuous animation jump, or a semantic
change in the G-buffer. When the drawing size changes, call both gBuffer.resize() and
renderer.resize(); resizing invalidates history because old pixels no longer describe the same
screen locations.
Transparency composition
Opaque geometry should populate the G-buffer first. WBOITRenderer
and ABufferRenderer keep transparent geometry
capture separate, then expose resolve as ordinary ShaderPassPipeline steps. Put the chosen
resolve pipeline into the same ordered shaderPasses array so transparency participates in later
effects without creating a second postprocessing system.
The Advanced Effects example shows the full MRT surface pass feeding shadows, SSAO, SSR, fog, outlines, TAA, motion blur, and debug views.
The Deferred Illumination Lab shows one five-target geometry pass feeding clustered directional/point lighting, temporally stabilized GTAO, diffuse screen-space global illumination, roughness-aware screen-space reflections, clustered volumetric lighting, adaptive exposure, HDR bloom, tone mapping, and direct G-buffer/AO/bounce/reflection/volume debug views.
When To Use Shader Passes
- Postprocessing color, blur, bloom, depth-of-field, and temporal effects.
- Fullscreen effects whose inputs and outputs are textures.
- Reusable effects that should be configured as shader modules but executed by an engine-owned pass renderer.
For FXAA, TAA, and the ordering between resolved render targets and postprocessing, see Antialiasing and Multisampling.
Do not use shader passes for ordinary geometry shading. Use Model with
modules or plugins when the shader participates in a model's vertex and
fragment pipeline.
Minimal Shape
import {ShaderPassRenderer} from '@luma.gl/engine';
const renderer = new ShaderPassRenderer(device, {
shaderPasses: [myShaderPass, myShaderPassPipeline]
});
const outputTexture = renderer.renderToTexture({sourceTexture});
For descriptor fields, see ShaderPass.
For execution methods and routing details, see
ShaderPassRenderer.
The current built-in effect catalog is under
Shader Pass Catalog.