@typegpu/radiance-cascades
The @typegpu/radiance-cascades package provides a small TypeGPU runner for computing 2D radiance cascades.
It is designed for screen-space lighting where your scene can be described by:
- an SDF callback sampled in normalized UV space,
- a color callback sampled at hit points,
- an output texture that stores the resolved radiance field.
The runner owns the cascade textures and dispatches the compute passes in the right order. You provide the scene functions and call run() when the scene changes.
Basic usage
Section titled “Basic usage”
import * as import rc
rc from '@typegpu/radiance-cascades';import * as import sdf
sdf from '@typegpu/sdf';import const tgpu: { const: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/comptime").comptime; resolve: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").init; initFromDevice: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").initFromDevice; slot: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/slot").slot; lazy: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/tgpuUnstable");}
tgpu, { import d
d, import std
std } from 'typegpu';
const const root: TgpuRoot
root = await const tgpu: { const: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/comptime").comptime; resolve: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").init; initFromDevice: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").initFromDevice; slot: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/slot").slot; lazy: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/tgpuUnstable");}
tgpu.init: (options?: InitOptions) => Promise<TgpuRoot>
Requests a new GPU device and creates a root around it.
If a specific device should be used instead, use
init();const const previewSize: { width: number; height: number;}
previewSize = { width: number
width: 512, height: number
height: 512 };
const const sceneSdf: TgpuFn<(uv: d.Vec2f) => d.F32>
sceneSdf = const tgpu: { const: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/comptime").comptime; resolve: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").init; initFromDevice: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").initFromDevice; slot: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/slot").slot; lazy: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/tgpuUnstable");}
tgpu.fn: <[d.Vec2f], d.F32>(argTypes: [d.Vec2f], returnType: d.F32) => TgpuFnShell<[d.Vec2f], d.F32> (+2 overloads)
fn([import d
d.const vec2f: d.Vec2fexport vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f], import d
d.const f32: d.F32export f32
A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)
Can also be called to cast a value to an f32.
f32)((uv: d.v2f
uv) => { 'use gpu'; const const circle: number
circle = import sdf
sdf.function sdDisk(point: d.v2f, radius: number): numberexport sdDisk
Signed distance function for a disk (filled circle)
sdDisk(uv: d.v2f
uv - import d
d.function vec2f(xy: number): d.v2f (+3 overloads)export vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f(0.5), 0.18); const const wall: number
wall = import sdf
sdf.function sdRoundedBox2d(point: d.v2f, size: d.v2f, cornerRadius: number): numberexport sdRoundedBox2d
Signed distance function for a rounded 2d box
sdRoundedBox2d(uv: d.v2f
uv - import d
d.function vec2f(x: number, y: number): d.v2f (+3 overloads)export vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f(0.5, 0.82), import d
d.function vec2f(x: number, y: number): d.v2f (+3 overloads)export vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f(0.42, 0.03), 0.01); return import sdf
sdf.function opUnion(d1: number, d2: number): numberexport opUnion
Union operator for combining two SDFs
Returns the minimum distance between two SDFs
opUnion(const circle: number
circle, const wall: number
wall);});
const const surfaceColor: TgpuFn<(uv: d.Vec2f) => d.Vec3f>
surfaceColor = const tgpu: { const: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/comptime").comptime; resolve: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").init; initFromDevice: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").initFromDevice; slot: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/slot").slot; lazy: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/tgpuUnstable");}
tgpu.fn: <[d.Vec2f], d.Vec3f>(argTypes: [d.Vec2f], returnType: d.Vec3f) => TgpuFnShell<[d.Vec2f], d.Vec3f> (+2 overloads)
fn([import d
d.const vec2f: d.Vec2fexport vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f], import d
d.const vec3f: d.Vec3fexport vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f)((uv: d.v2f
uv) => { 'use gpu'; return import std
std.mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)export mix
mix(import d
d.function vec3f(x: number, y: number, z: number): d.v3f (+5 overloads)export vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f(1, 0.82, 0.5), import d
d.function vec3f(x: number, y: number, z: number): d.v3f (+5 overloads)export vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f(0.28, 0.52, 1), uv: d.v2f
uv.v2f.x: number
x);});
const const runner: rc.RadianceCascadesExecutor<OutputTexture>
runner = import rc
rc.function createRadianceCascades(options: CascadesOptions<undefined> & { size: Size;}): rc.RadianceCascadesExecutor (+1 overload)export createRadianceCascades
createRadianceCascades({ root: TgpuRoot
root, size: Size
size: const previewSize: { width: number; height: number;}
previewSize, sdfResolution: Size
sdfResolution: { width: number
width: 1024, height: number
height: 1024 }, sdf: (uv: d.v2f) => number
sdf: const sceneSdf: TgpuFn<(uv: d.Vec2f) => d.F32>
sceneSdf, color: (uv: d.v2f) => d.v3f
color: const surfaceColor: TgpuFn<(uv: d.Vec2f) => d.Vec3f>
surfaceColor,});
const runner: rc.RadianceCascadesExecutor<OutputTexture>
runner.function run(): void
run();
const const radianceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
radianceView = const runner: rc.RadianceCascadesExecutor<OutputTexture>
runner.output: OutputTexture
output.TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.createView<d.WgslTexture2d<d.F32>>(schema: d.WgslTexture2d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & { sampleType?: "float" | "unfilterable-float";}) | undefined): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView(import d
d.function texture2d(): d.WgslTexture2d<d.F32> (+1 overload)export texture2d
texture2d());const const sampler: TgpuFixedSampler
sampler = const root: TgpuRoot
root.TgpuRoot.createSampler(props: WgslSamplerProps): TgpuFixedSampler
createSampler({ WgslSamplerProps.magFilter?: GPUFilterMode
Specifies the sampling behavior when the sample footprint is smaller than or equal to one
texel.
magFilter: 'linear', WgslSamplerProps.minFilter?: GPUFilterMode
Specifies the sampling behavior when the sample footprint is larger than one texel.
minFilter: 'linear' });
const const renderPreview: TgpuFn<(uv: d.Vec2f) => d.Vec4f>
renderPreview = const tgpu: { const: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/function/comptime").comptime; resolve: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").init; initFromDevice: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/root/init").initFromDevice; slot: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/slot").slot; lazy: typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("/home/runner/work/TypeGPU/TypeGPU/packages/typegpu/src/tgpuUnstable");}
tgpu.fn: <[d.Vec2f], d.Vec4f>(argTypes: [d.Vec2f], returnType: d.Vec4f) => TgpuFnShell<[d.Vec2f], d.Vec4f> (+2 overloads)
fn([import d
d.const vec2f: d.Vec2fexport vec2f
Schema representing vec2f - a vector with 2 elements of type f32.
Also a constructor function for this vector value.
vec2f], import d
d.const vec4f: d.Vec4fexport vec4f
Schema representing vec4f - a vector with 4 elements of type f32.
Also a constructor function for this vector value.
vec4f)((uv: d.v2f
uv) => { 'use gpu'; const const dist: number
dist = const sceneSdf: (uv: d.v2f) => number
sceneSdf(uv: d.v2f
uv); const const edge: number
edge = import std
std.function max(fst: number, ...rest: number[]): number (+1 overload)export max
max(import std
std.function fwidth(value: number): number (+1 overload)export fwidth
fwidth(const dist: number
dist), 0.001); const const surface: number
surface = 1 - import std
std.function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)export smoothstep
smoothstep(-const edge: number
edge, const edge: number
edge, const dist: number
dist); const const radiance: d.v3f
radiance = import std
std.textureSampleLevel<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, sampler: d.sampler, coords: d.v2f, level: number): d.v4f (+13 overloads)export textureSampleLevel
textureSampleLevel(const radianceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
radianceView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, uv: d.v2f
uv, 0).xyz: d.v3f
xyz;
const const bg: d.v3f
bg = import std
std.mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)export mix
mix(import d
d.function vec3f(x: number, y: number, z: number): d.v3f (+5 overloads)export vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f(0.04, 0.05, 0.07), import d
d.function vec3f(x: number, y: number, z: number): d.v3f (+5 overloads)export vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f(0.11, 0.15, 0.22), uv: d.v2f
uv.v2f.y: number
y); const const lit: d.v3f
lit = const bg: d.v3f
bg + const radiance: d.v3f
radiance * 1.55; const const color: d.v3f
color = import std
std.mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)export mix
mix(const lit: d.v3f
lit, const surfaceColor: (uv: d.v2f) => d.v3f
surfaceColor(uv: d.v2f
uv), const surface: number
surface);
return import d
d.function vec4f(v0: AnyNumericVec3Instance, w: number): d.v4f (+9 overloads)export vec4f
Schema representing vec4f - a vector with 4 elements of type f32.
Also a constructor function for this vector value.
vec4f(import std
std.min<d.v3f>(fst: d.v3f, ...rest: d.v3f[]): d.v3f (+1 overload)export min
min(const color: d.v3f
color, import d
d.function vec3f(xyz: number): d.v3f (+5 overloads)export vec3f
Schema representing vec3f - a vector with 3 elements of type f32.
Also a constructor function for this vector value.
vec3f(1)), 1);});size controls the output texture resolution when the runner creates the texture for you. sdfResolution should match the resolution of the SDF source you are sampling from, or the resolution you use to think about texel-sized marching thresholds.
Using a generated SDF texture
Section titled “Using a generated SDF texture”A common setup is to generate an SDF texture with @typegpu/sdf and feed it into the radiance runner.

import * as rc from '@typegpu/radiance-cascades';import * as sdf from '@typegpu/sdf';import tgpu, { d, std } from 'typegpu';
const root = await tgpu.init();const floodSize = { width: 2048, height: 2048 };const previewSize = { width: 512, height: 512 };
const sourceTexture = root .createTexture({ size: [floodSize.width, floodSize.height], format: 'rgba8unorm', }) .$usage('storage', 'sampled');
// Draw or bake the source mask into sourceTexture first.const sourceLayout = tgpu.bindGroupLayout({ source: { texture: d.texture2d() },});const sourceBindGroup = root.createBindGroup(sourceLayout, { source: sourceTexture.createView(),});
const floodRunner = sdf .createJumpFlood({ root, size: floodSize, classify: (coord) => { 'use gpu'; return std.textureLoad(sourceLayout.$.source, coord, 0).w > 0.5; }, getSdf: (_coord, size, signedDist) => { 'use gpu'; return signedDist / d.f32(std.min(size.x, size.y)); }, getColor: (_coord, _size, _signedDist, insidePx) => { 'use gpu'; const source = std.textureLoad(sourceLayout.$.source, insidePx, 0); return d.vec4f(source.xyz, 1); }, }) .with(sourceBindGroup);
floodRunner.run();
const floodSdfView = floodRunner.sdfOutput.createView();const floodColorView = floodRunner.colorOutput.createView();const sampler = root.createSampler({ magFilter: 'linear', minFilter: 'linear' });
const runner = rc.createRadianceCascades({ root, size: previewSize, sdfResolution: floodSize, sdf: (uv) => { 'use gpu'; if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1) { return 1; } return std.textureSampleLevel(floodSdfView.$, sampler.$, uv, 0).x; }, color: (uv) => { 'use gpu'; return std.textureSampleLevel(floodColorView.$, sampler.$, uv, 0).xyz; },});
runner.run();This is the same shape used by the Radiance Cascades (with drawing) example: draw into a texture, rebuild an SDF with jump flooding, then update the radiance field from that SDF.
Output textures
Section titled “Output textures”If you pass no output, the runner creates an rgba16float texture with storage and sampled usage and exposes it as runner.output.
You can also pass your own output texture or storage texture view:
const output = root .createTexture({ size: [width, height], format: 'rgba16float', }) .$usage('storage', 'sampled');
const runner = rc.createRadianceCascades({ root, output, sdfResolution, sdf: sceneSdf, color: surfaceColor,});When a storage view cannot provide its size, pass size explicitly alongside output.
Updating and cleanup
Section titled “Updating and cleanup”The executor has a small lifecycle:
runner.run()dispatches all cascade passes and writes the current radiance field.runner.outputis the sampled/storagergba16floatresult.runner.with(bindGroup)returns an executor with an extra bind group attached to all internal passes.runner.destroy()releases the cascade textures, and also releases the output texture if the runner created it.
Use with(bindGroup) when your SDF, color, or ray marching callback reads resources that are not captured through TypeGPU accessors.
Custom ray marching
Section titled “Custom ray marching”By default, the runner uses defaultRayMarch, which sphere-traces through the supplied SDF and samples color at the first hit.
For custom attenuation, translucent surfaces, or non-SDF sources, pass a rayMarch callback.
This example adds near-surface haze and lets part of the light continue after a hit.

const runner = rc.createRadianceCascades({ root, size, sdfResolution, sdf: sceneSdf, color: surfaceColor, rayMarch: (probePos, rayDir, startT, endT, eps, minStep, bias) => { 'use gpu'; let color = d.vec3f(); let transmittance = d.f32(1); let t = startT;
for (let step = 0; step < 64; step++) { if (t > endT || transmittance < 0.02) { break; }
const pos = probePos + rayDir * t; if (std.any(std.lt(pos, d.vec2f(0))) || std.any(std.gt(pos, d.vec2f(1)))) { break; }
const signedDist = sceneSdf(pos); const stepSize = std.max(signedDist + bias, minStep); const haze = std.exp(-std.abs(signedDist) * 34) * stepSize * 18; const hazeColor = std.mix(d.vec3f(1, 0.38, 0.14), d.vec3f(0.22, 0.56, 1), pos.x); color += hazeColor * haze * transmittance;
if (signedDist + bias <= eps) { color += surfaceColor(pos) * transmittance * 0.65; transmittance *= 0.35; break; }
transmittance *= std.max(1 - haze * 0.08, 0.72); t += stepSize; }
return rc.RayMarchResult({ color, transmittance, }); },});Custom ray marchers should return RayMarchResult, where color is accumulated radiance and transmittance is how much light should continue to the next cascade (1 means no hit, 0 means fully blocked).
Advanced exports
Section titled “Advanced exports”Most users only need createRadianceCascades. The package also exports lower-level pieces for custom runners and experiments:
| Export | Purpose |
|---|---|
defaultRayMarch | The built-in ray marcher used by the runner |
RayMarchResult | Return struct for custom ray marchers |
getCascadeDim | Computes internal cascade texture dimensions |
sdfSlot, colorSlot, rayMarchSlot, sdfResolutionSlot | Slots used by the internal cascade compute pass |
See the package source for details that are not yet covered here.