Skip to main content

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:

GitHub

Components

ComponentRole
ShaderPassRendererOwns the fullscreen draw path, swap framebuffers, named targets, shader inputs, and presentation step.
ShaderPassPipelineChains existing passes with named intermediate render targets.
ShaderPassDescribes one standalone texture-processing effect and its optional subpasses.
ShaderSubPassDescribes 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:

SourceMeaning
originalThe texture passed to renderToTexture() or renderToScreen().
previousThe 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:

  1. Application geometry produces one scene color texture plus semantic surface attachments.
  2. ShaderPassRenderer orders fullscreen effects and owns their internal named and temporal targets.
  3. 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 valueProducerConsumers
sourceTextureGBuffer.colorTextureEvery shader pass through previous or original.
depthTextureGBuffer.depthTextureDOF, depth-aware blur, SSAO, GTAO, SSGI, SSR, outlines, contact shadows, TAA, motion blur, fog, clustered volumetric lighting.
normalTextureGBuffer.normalRoughnessTextureSSAO, GTAO, SSGI, SSR, normal-aware outlines, contact-shadow filtering.
velocityTextureGBuffer.velocityTextureGTAO/SSGI/SSR/volumetric temporal reprojection, TAA, and motion blur.
named extrasGBuffer.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:

  1. Trace cosine-weighted hemisphere rays through the shared scene depth and view normals.
  2. Reproject indirect-radiance history with velocity and reject perspective-correct depth disocclusions.
  3. Save current depth for the next-frame history comparison.
  4. Denoise diffuse bounce horizontally while preserving depth and normal edges.
  5. Repeat the bilateral denoising vertically.
  6. 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:

  1. Trace stochastic, roughness-aware reflection rays against the G-buffer depth at configurable resolution, defaulting to full resolution.
  2. Reproject persistent reflection history with screen-space velocity and reject depth disocclusions.
  3. Save current depth into the reflection history target for the next frame.
  4. Denoise reflection radiance horizontally using roughness, scene depth, and normals.
  5. Repeat the depth/normal-aware denoising vertically.
  6. 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:

  1. March configurable-resolution view rays through exponential world-height density.
  2. 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.
  3. Trace radial screen-depth visibility toward a configurable sun position to produce recognizable, depth-occluded crepuscular god rays.
  4. Reproject empty-space atmospheric history with current/previous camera transforms, apply G-buffer velocity to opaque surfaces, and reject linear-depth disocclusions.
  5. Capture compact current linear depth for the next frame.
  6. Denoise the radiance/transmittance result with separable depth-aware blur.
  7. 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:

  1. Extract center-weighted logarithmic luminance from floating-point scene color.
  2. Reduce four successively smaller luminance-pyramid levels into a near-global geometric mean.
  3. Adapt persistent exposure history with independent brightening and darkening response rates.
  4. 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: true for 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.

QualityLevelsGaussian portableGaussian WebGPUDual-Kawase portableDual-Kawase WebGPU
low28 render6 render + 1 compute4 render2 render + 1 compute
medium312 render9 render + 1 compute6 render3 render + 1 compute
high416 render12 render + 1 compute8 render4 render + 1 compute
ultra520 render15 render + 1 compute10 render5 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:

Use the HDR order: temporal effects, auto exposure, bloom, then tone mapping.

PhaseTypical workWhy
Geometry and opaque surface captureMRT scene color, normal-roughness, velocity, depth, material extrasEstablish one coherent surface snapshot.
Opaque lighting resolveDeferred PBR lighting, contact shadows, other direct-light correctionsThese still need unwarped depth, normals, and material terms.
Surface effectsSSAO/GTAO, SSGI, SSR, outlines, depth-aware blurThese consume the original semantic attachments.
Participating mediaHeight fog or clustered volumetric lightingComposite extinction and in-scattering over completed opaque light transport.
Transparency resolveWBOIT or A-buffer resolve pipelineResolve translucent geometry before temporal accumulation when it should participate in TAA.
Temporal effectsTAA, then motion blurReproject the composed image before display-space processing.
Display effectsAuto exposure, bloom, color adjustment, vignette, tone mappingThese 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.