Skip to content

@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.

A simple radiance cascades scene lit from SDF surfaces
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");
}

@moduletypegpu

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");
}

@moduletypegpu

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

@seeinitFromDevice. *

@example

When given no options, the function will ask the browser for a suitable GPU device.

const root = await tgpu.init();

@example

If there are specific options that should be used when requesting a device, you can pass those in.

const adapterOptions: GPURequestAdapterOptions = ...;
const deviceDescriptor: GPUDeviceDescriptor = ...;
const root = await tgpu.init({ adapter: adapterOptions, device: deviceDescriptor });

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");
}

@moduletypegpu

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.Vec2f
export vec2f

Schema representing vec2f - a vector with 2 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

vec2f
],
import d
d
.
const f32: d.F32
export 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.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
)((
uv: d.v2f
uv
) => {
'use gpu';
const
const circle: number
circle
=
import sdf
sdf
.
function sdDisk(point: d.v2f, radius: number): number
export sdDisk

Signed distance function for a disk (filled circle)

@parampoint Point to evaluate

@paramradius Radius of the disk

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.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

vec2f
(0.5), 0.18);
const
const wall: number
wall
=
import sdf
sdf
.
function sdRoundedBox2d(point: d.v2f, size: d.v2f, cornerRadius: number): number
export sdRoundedBox2d

Signed distance function for a rounded 2d box

@parampoint Point to evaluate

@paramsize Half-dimensions of the box

@paramcornerRadius Box corner radius

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.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

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.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

vec2f
(0.42, 0.03), 0.01);
return
import sdf
sdf
.
function opUnion(d1: number, d2: number): number
export opUnion

Union operator for combining two SDFs Returns the minimum distance between two SDFs

@paramd1 First SDF distance

@paramd2 Second SDF distance

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");
}

@moduletypegpu

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.Vec2f
export vec2f

Schema representing vec2f - a vector with 2 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

vec2f
],
import d
d
.
const vec3f: d.Vec3f
export vec3f

Schema representing vec3f - a vector with 3 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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");
}

@moduletypegpu

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.Vec2f
export vec2f

Schema representing vec2f - a vector with 2 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec2f(); // (0.0, 0.0) const vector = d.vec2f(1); // (1.0, 1.0) const vector = d.vec2f(0.5, 0.1); // (0.5, 0.1)

@example const buffer = root.createBuffer(d.vec2f, d.vec2f(0, 1)); // buffer holding a d.vec2f value, with an initial value of vec2f(0, 1);

vec2f
],
import d
d
.
const vec4f: d.Vec4f
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

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.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

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.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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.

A common setup is to generate an SDF texture with @typegpu/sdf and feed it into the radiance runner.

A radiance cascades scene using a generated SDF texture
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.

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.

The executor has a small lifecycle:

  • runner.run() dispatches all cascade passes and writes the current radiance field.
  • runner.output is the sampled/storage rgba16float result.
  • 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.

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.

A radiance cascades scene with custom ray marching haze
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).

Most users only need createRadianceCascades. The package also exports lower-level pieces for custom runners and experiments:

ExportPurpose
defaultRayMarchThe built-in ray marcher used by the runner
RayMarchResultReturn struct for custom ray marchers
getCascadeDimComputes internal cascade texture dimensions
sdfSlot, colorSlot, rayMarchSlot, sdfResolutionSlotSlots used by the internal cascade compute pass

See the package source for details that are not yet covered here.