Skip to main content

Deferred Lighting

deferredLighting is an experimental WebGPU-only fullscreen shader pass that resolves physically based opaque lighting from a GBuffer. Geometry writes surface properties once; the resolve then reconstructs view-space position from depth and evaluates one directional light plus up to 64 point lights from a storage buffer.

The pass stays intentionally composable. It does not traverse a scene, own materials, or allocate the G-buffer. Applications choose their geometry, material packing, light animation, and the effect passes that run before or after the resolve.

GitHub

Material attachment contract

deferredLighting consumes the standard GBuffer depth and normal-roughness channels plus two named extra color attachments:

G-buffer outputSuggested formatMeaning
@location(0) scene colorrgba16floatBackground/fallback color when depth contains no opaque surface.
@location(1) normal-roughnessrgba8unormView-space normal encoded as normal * 0.5 + 0.5 in RGB; perceptual roughness in A.
@location(2) velocityrg16floatCurrent-minus-previous screen UV velocity for later temporal effects.
@location(3) baseColorMetallicrgba8unormLinear base color in RGB; metallic factor in A.
@location(4) emissiveOcclusionrgba8uintNormalized emissive color in RGB and ambient-occlusion factor in A, packed to 0–255 integers.
depth attachmentdepth24plusSampleable depth used to reconstruct view position.

The extra attachment names are conventions at the application boundary; GBuffer itself permits any names. The resolve bindings use baseColorMetallicTexture and emissiveOcclusionTexture. This layout keeps HDR scene color while totaling 32 color-attachment bytes per sample, so it runs on the portable WebGPU CORE limit without requesting a higher device limit.

Usage

import {Buffer} from '@luma.gl/core';
import {ShaderPassRenderer} from '@luma.gl/engine';
import {
createDeferredLightingShaderPassPipeline,
GBuffer,
makeDeferredPointLightBufferData,
MAX_DEFERRED_POINT_LIGHTS
} from '@luma.gl/experimental';

const gBuffer = new GBuffer(device, {
width,
height,
colorFormat: 'rgba16float',
extraColorAttachments: [
{name: 'baseColorMetallic', format: 'rgba8unorm'},
{name: 'emissiveOcclusion', format: 'rgba8uint'}
]
});

const pointLights = device.createBuffer({
data: makeDeferredPointLightBufferData(lights, MAX_DEFERRED_POINT_LIGHTS),
usage: Buffer.STORAGE | Buffer.COPY_DST
});

const renderer = new ShaderPassRenderer(device, {
shaderPasses: [createDeferredLightingShaderPassPipeline()]
});

renderer.renderToScreen({
sourceTexture: gBuffer.colorTexture,
bindings: {
depthTexture: gBuffer.depthTexture,
normalTexture: gBuffer.normalRoughnessTexture,
baseColorMetallicTexture: gBuffer.getExtraColorTexture('baseColorMetallic'),
emissiveOcclusionTexture: gBuffer.getExtraColorTexture('emissiveOcclusion'),
pointLights
},
uniforms: {
deferredLighting: {
inverseProjectionMatrix,
ambientColor: [0.03, 0.03, 0.05],
directionalLightDirectionView,
directionalLightColor: [1, 0.9, 0.8],
directionalLightIntensity: 2.5,
pointLightCount: lights.length
}
}
});

Point-light positions and the directional-light direction are view-space values. Update the storage buffer with pointLights.write(makeDeferredPointLightBufferData(...)) when lights move.

API

deferredLighting

The exported ShaderPass descriptor. It samples:

  • depthTexture
  • normalTexture
  • baseColorMetallicTexture
  • emissiveOcclusionTexture
  • pointLights

Its uniforms are inverseProjectionMatrix, ambientColor, directionalLightDirectionView, directionalLightColor, directionalLightIntensity, and pointLightCount.

createDeferredLightingShaderPassPipeline(): ShaderPassPipeline

Returns a one-step pipeline that reads the current previous color and writes the lighting result back into previous. Put it before SSAO, reflections, temporal accumulation, bloom, and tone mapping unless a deliberate stylized ordering needs otherwise.

makeDeferredPointLightBufferData(lights, maxLightCount?): Float32Array

Packs each light into two vec4f storage records:

position.xyz, range, color.rgb, intensity

The returned array is padded to maxLightCount, allowing one fixed-size GPU buffer to be reused across frames. MAX_DEFERRED_POINT_LIGHTS is 64 and matches the shader loop bound.

  • GBuffer owns the MRT attachments and standard depth, normal, and velocity bindings.
  • Clustered Lighting reuses the same material attachments with compute-built light lists for hundreds of local lights.
  • Shader Passes explains the ordered composable render stack.
  • Advanced Effects shows the broader scene-aware and temporal effect chain after opaque rendering.