@typegpu/radiance-cascades
@typegpu/radiance-cascades computes lighting for 2D scenes. You describe the scene with GPU functions, and the package gives you back a texture holding the light that reaches every pixel. All that is left is to sample it in your own rendering shader.
There are two runners to choose from:
- Holographic (
@typegpu/radiance-cascades/holographic) lights a grid of pixels that emit and absorb light. It suits raster scenes, thin occluders and participating media such as fog or smoke. - SDF (
@typegpu/radiance-cascades) traces rays through a signed distance function. It suits analytic shapes and custom tracing.
Both runners share the same lifecycle, which is described in Updating lighting.
Holographic radiance cascades
Section titled “Holographic radiance cascades”The holographic runner traces short segments through the pixel grid and merges them into longer paths, so it does not need a distance function at all.
Basic usage
Section titled “Basic usage”This scene has a warm, opaque emitter on the left and a wall with a gap in the middle:
import * as import hrc
hrc from '@typegpu/radiance-cascades/holographic';import { const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/typegpu/src/tgpuUnstable");}
tgpu, import d
d, import std
std } from 'typegpu';
const const root: TgpuRoot
root = await const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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 runner: hrc.Executor
runner = import hrc
hrc.function create(options: Options): hrc.Executor
Four-sector HRC, using the interval and fluence recurrences of Freeman et al. (2025).
create({ root: TgpuRoot
root, size: { width: number; height: number;}
size: { width: number
width: 512, height: number
height: 512 }, medium: (pixel: d.v2u) => { emission: d.v3f; extinction: number;}
medium: (pixel: d.v2u
pixel) => { 'use gpu'; const const position: d.v2f
position = import d
d.const vec2f: d.Vec2f(v: AnyNumericVec2Instance) => d.v2f (+3 overloads)
vec2f(pixel: d.v2u
pixel) + 0.5; if (import std
std.length<d.v2f>(value: d.v2f): number (+1 overload)export length
length(const position: d.v2f
position - import d
d.const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)
vec2f(128, 256)) < 12) { return { emission: d.v3f
emission: import d
d.const vec3f: d.Vec3f(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f(16, 8, 4), extinction: number
extinction: 16 }; } if (pixel: d.v2u
pixel.v2u.x: number
x === 256 && (pixel: d.v2u
pixel.v2u.y: number
y < 240 || pixel: d.v2u
pixel.v2u.y: number
y > 272)) { return { emission: d.v3f
emission: import d
d.const vec3f: d.Vec3f() => d.v3f (+5 overloads)
vec3f(), extinction: number
extinction: 16 }; } return { emission: d.v3f
emission: import d
d.const vec3f: d.Vec3f() => d.v3f (+5 overloads)
vec3f(), extinction: number
extinction: 0 }; },});
const runner: hrc.Executor
runner.function run(options?: RunOptions): void
Records the solve into one compute pass. A supplied encoder is not submitted and a
supplied pass is not ended; with neither, the work is submitted on its own.
run();
const const lightView: TgpuTextureView<d.WgslTexture2d<d.F32>>
lightView = const runner: hrc.Executor
runner.output: OutputTexture
Angular mean radiance, normalized by 2π
output.TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.createView<d.WgslTexture2d<d.F32>>(schema: d.WgslTexture2d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & { sampleType?: "float" | "unfilterable-float" | undefined;}) | undefined): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView(import d
d.function texture2d(): d.WgslTexture2d<d.F32> (+1 overload)export texture2d
texture2d());runner.run() submits the lighting update. Afterwards, runner.output holds the light reaching each pixel: the wall at x = 256 blocks it everywhere except the gap between rows 240 and 272. Using the lighting shows how to combine it with your scene.
Scene inputs
Section titled “Scene inputs”The medium callback receives integer pixel coordinates and describes what that single pixel is made of.
| Input | Meaning |
|---|---|
size | Material grid and lighting texture dimensions in pixels |
medium(pixel) | Emitted light and extinction of one pixel |
transmission | 'scalar' (default) for one extinction value, or 'rgb' for one per channel |
source(pixel, incident) | Optional extra emission; see Sources and scattering |
tracedLevels | Cascade levels traced directly before merging; see Quality and cost |
Emission is linear radiance added per pixel of travel. Extinction controls how quickly light fades while it travels through a pixel. It is a nonnegative value in inverse pixels: after s pixels, a fraction 1 / extinction pixels, the light drops to about 37% of what it was.
A few common materials look like this:
| Material | Emission | Extinction |
|---|---|---|
| Empty | [0, 0, 0] | 0 |
| Nearly opaque wall | [0, 0, 0] | 16 |
| Nearly opaque warm emitter | [16, 8, 4] | 16 |
| Transparent glowing gas | [0.01, 0.005, 0] | 0 |
An opaque emitter appears with radiance emission / extinction, so the warm emitter above shows up as [1, 0.5, 0.25]. Extinction 16 lets through about 10⁻⁷ of the light crossing one pixel, which is opaque for all practical purposes. (Use finite values, since infinity is not treated as fully opaque.)
The medium callback runs many times per pixel, so for anything more expensive than a few shapes, it is worth rasterizing the scene into a texture first and reading it back:
const const runner: hrc.Executor
runner = import hrc
hrc.function create(options: Options): hrc.Executor
Four-sector HRC, using the interval and fluence recurrences of Freeman et al. (2025).
create({ root: TgpuRoot
root, size: { width: number; height: number;}
size: { width: number
width: 512, height: number
height: 512 }, medium: (pixel: d.v2u) => { emission: d.v3f; extinction: number;}
medium: (pixel: d.v2u
pixel) => { 'use gpu'; const const material: d.v4f
material = import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const materialView: TgpuTextureView<d.WgslTexture2d<d.F32>>
materialView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0); return { emission: d.v3f
emission: const material: d.v4f
material.rgb: d.v3f
rgb, extinction: number
extinction: const material: d.v4f
material.v4f.a: number
a }; },});Colored transmission
Section titled “Colored transmission”By default, extinction is a single number, so every color channel fades at the same rate. With transmission: 'rgb', extinction becomes a vec3f and each channel is attenuated separately:
const const runner: hrc.Executor
runner = import hrc
hrc.function create(options: Options): hrc.Executor
Four-sector HRC, using the interval and fluence recurrences of Freeman et al. (2025).
create({ root: TgpuRoot
root, size: { width: number; height: number;}
size: { width: number
width: 512, height: number
height: 512 }, transmission: "rgb"
transmission: 'rgb', medium: (pixel: d.v2u) => { emission: d.v3f; extinction: d.v3f;}
medium: (pixel: d.v2u
pixel) => { 'use gpu'; return { emission: d.v3f
emission: import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const emissionView: TgpuTextureView<d.WgslTexture2d<d.F32>>
emissionView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0).rgb: d.v3f
rgb, extinction: d.v3f
extinction: import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const extinctionView: TgpuTextureView<d.WgslTexture2d<d.F32>>
extinctionView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0).rgb: d.v3f
rgb, }; },});To get transmission T through a thickness of s pixels, use extinction = -log(T) / s per channel. For example, [0.9, 0.1, 0.01] over 10 pixels gives roughly [0.0105, 0.2303, 0.4605], which turns white light red:
Light still travels in straight lines (there is no refraction or dispersion).
Sources and scattering
Section titled “Sources and scattering”A medium emits the same light no matter how brightly it is lit. For light that depends on the lighting itself, such as fog glowing where a beam passes through it, add a source callback. It receives a pixel and incident (the light arriving at that pixel) and returns extra emission for it.
The arriving light is not known until the lighting is computed, so run({ iterations }) computes it several times. On the first pass, source receives zero incident, so fog adds nothing and only direct light appears. Each later pass gives source the previous pass’s lighting (the same values as runner.output), which adds one more bounce:
Every run() starts over from darkness, so bounces do not carry over between runs.
A scattering medium, such as fog or smoke, does two things: it takes light out of every beam passing through it, and sends that light out again in new directions. For isotropic scattering, where light leaves equally in all directions, we cover the first part by adding the scattering coefficient to extinction, and the second by returning it multiplied by incident from source:
const const runner: hrc.Executor
runner = import hrc
hrc.function create(options: Options): hrc.Executor
Four-sector HRC, using the interval and fluence recurrences of Freeman et al. (2025).
create({ root: TgpuRoot
root, size: { width: number; height: number;}
size: { width: number
width: 512, height: number
height: 512 }, transmission: "rgb"
transmission: 'rgb', medium: (pixel: d.v2u) => { emission: d.v3f; extinction: d.v3f;}
medium: (pixel: d.v2u
pixel) => { 'use gpu'; const const material: d.v4f
material = import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const mediumView: TgpuTextureView<d.WgslTexture2d<d.F32>>
mediumView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0); return { emission: d.v3f
emission: import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const emissionView: TgpuTextureView<d.WgslTexture2d<d.F32>>
emissionView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0).rgb: d.v3f
rgb, extinction: d.v3f
extinction: const material: d.v4f
material.rgb: d.v3f
rgb + const material: d.v4f
material.v4f.a: number
a, }; }, source?: ((pixel: d.v2u, incident: d.v3f) => d.v3f) | undefined
Additional emission, evaluated once per pixel before each solve
source: (pixel: d.v2u
pixel, incident: d.v3f
incident) => { 'use gpu'; return import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const mediumView: TgpuTextureView<d.WgslTexture2d<d.F32>>
mediumView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, pixel: d.v2u
pixel, 0).v4f.a: number
a * incident: d.v3f
incident; },});
const runner: hrc.Executor
runner.function run(options?: RunOptions): void
Records the solve into one compute pass. A supplied encoder is not submitted and a
supplied pass is not ended; with neither, the work is submitted on its own.
run({ iterations?: number | undefined
Each run starts from zero incident light. Default: 1
iterations: 3 });Here mediumView stores absorption in .rgb and scattering in .a, both in inverse pixels like extinction. Three iterations give direct light plus two bounces; thick media need more iterations before the result stops changing.
incident is averaged over all directions, so source cannot tell where the light came from. That is enough for isotropic scattering, but not for media that scatter mostly forward or backward. Neither callback may read runner.output or runner.sectors; use incident instead.
Using the lighting
Section titled “Using the lighting”runner.output is a texture covering the whole grid, holding the light that reaches each pixel. To use it, sample it with the same uv as your scene: solid parts of the scene keep their own color, and the light shows everywhere else.
const const lightView: TgpuTextureView<d.WgslTexture2d<d.F32>>
lightView = const runner: hrc.Executor
runner.output: OutputTexture
Angular mean radiance, normalized by 2π
output.TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.createView<d.WgslTexture2d<d.F32>>(schema: d.WgslTexture2d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & { sampleType?: "float" | "unfilterable-float" | undefined;}) | 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 | undefined
Specifies the sampling behavior when the sample footprint is smaller than or equal to one
texel.
magFilter: 'linear', WgslSamplerProps.minFilter?: GPUFilterMode | undefined
Specifies the sampling behavior when the sample footprint is larger than one texel.
minFilter: 'linear' });
const const fragment: TgpuFragmentFn<{ uv: d.Vec2f;}, d.Vec4f>
fragment = const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/typegpu/src/tgpuUnstable");}
tgpu.fragmentFn: <{ uv: d.Vec2f;}, d.Vec4f>(options: { in: { uv: d.Vec2f; }; out: d.Vec4f;}) => TgpuFragmentFnShell<{ uv: d.Vec2f;}, d.Vec4f> (+1 overload)
fragmentFn({ in: { uv: d.Vec2f;}
in: { uv: d.Vec2f
uv: import d
d.const vec2f: d.Vec2f
vec2f }, out: d.Vec4f
out: import d
d.const vec4f: d.Vec4f
vec4f })(({ uv: d.v2f
uv }) => { 'use gpu'; const const scene: d.v4f
scene = 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 sceneView: TgpuTextureView<d.WgslTexture2d<d.F32>>
sceneView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, uv: d.v2f
uv, 0); const const light: d.v3f
light = 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 lightView: TgpuTextureView<d.WgslTexture2d<d.F32>>
lightView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, uv: d.v2f
uv, 0).rgb: d.v3f
rgb; return import d
d.const vec4f: d.Vec4f(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f(const scene: d.v4f
scene.rgb: d.v3f
rgb + const light: d.v3f
light * (1 - const scene: d.v4f
scene.v4f.a: number
a), 1);});Here sceneView holds what you drew, with alpha 1 on solid pixels. The light values are linear and can go above 1 near bright emitters, so if they look blown out, scale them down or apply tone mapping before returning.
Light from each direction
Section titled “Light from each direction”Most scenes only need output. When you also need to know which way the light comes from (for example, to shade a surface by its normal), use runner.sectors. It is a texture array whose four layers each hold the light arriving from one side (+Y points down):
const const sectorsView: TgpuTextureView<d.WgslTexture2dArray<d.F32>>
sectorsView = const runner: hrc.Executor
runner.sectors: SectorTexture
Radiance from the +X, +Y, −X and −Y sectors, one layer each
sectors.TgpuTexture<{ size: [number, number, number]; format: "rgba16float"; }>.createView<d.WgslTexture2dArray<d.F32>>(schema: d.WgslTexture2dArray<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & { sampleType?: "float" | "unfilterable-float" | undefined;}) | undefined): TgpuTextureView<d.WgslTexture2dArray<d.F32>> (+3 overloads)
createView(import d
d.function texture2dArray(): d.WgslTexture2dArray<d.F32> (+1 overload)export texture2dArray
texture2dArray());
// in a shaderconst const fromAbove: d.v3f
fromAbove = import std
std.textureSampleLevel<d.texture2dArray<d.F32>>(texture: d.texture2dArray<d.F32>, sampler: d.sampler, coords: d.v2f, arrayIndex: number, level: number): d.v4f (+13 overloads)export textureSampleLevel
textureSampleLevel(const sectorsView: TgpuTextureView<d.WgslTexture2dArray<d.F32>>
sectorsView.TgpuTextureView<WgslTexture2dArray<F32>>.$: d.texture2dArray<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, const uv: d.v2f
uv, 3, 0).rgb: d.v3f
rgb;The four layers add up to output before its smoothing. The Holographic Global Illumination example uses them to light surfaces by their normals.
Both textures are rgba16float and are owned by the runner, which destroys them when you call destroy().
Quality and cost
Section titled “Quality and cost”size controls both the material grid and the lighting resolution. Intermediate intervals are kept for the reconstruction, so memory grows as O(N² log N). For scalar transmission without a source:
| Resolution | Memory |
|---|---|
| 256×256 | 9 MiB |
| 512×512 | 38 MiB |
| 1024×1024 | 160 MiB |
| 2048×2048 | 672 MiB |
RGB transmission stores 12 bytes per interval instead of 8, and a source adds 8 bytes per pixel and one dispatch per iteration. runner.byteSize reports the exact allocation for the current configuration.
Light is gathered along intervals that double in length from one cascade level to the next. tracedLevels sets how many of the shortest levels read the medium directly; each longer interval is joined from two intervals of the level below. The default of 3 traces spans of 1, 2 and 4 pixels:
Shorter intervals end only on every other pixel row. A join is exact when the straight path’s midpoint lands on one of those ends; otherwise, the runner averages two bent paths through the ends on either side:
Each extra traced level doubles the longest traced span, which means more medium reads and fewer approximate joins, for the same memory.
SDF radiance cascades
Section titled “SDF radiance cascades”The SDF runner traces rays through a signed distance function. The built-in tracer treats surfaces as opaque, so surface reflection and additional light bounces are up to your application logic.
Basic usage
Section titled “Basic usage”This scene has two glowing shapes, a disk and a wall, whose emission blends from warm on the left to cool on the right:

import * as import rc
rc from '@typegpu/radiance-cascades';import * as import sdf
sdf from '@typegpu/sdf';import { const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/typegpu/src/tgpuUnstable");}
tgpu, import d
d, import std
std } from 'typegpu';
const const root: TgpuRoot
root = await const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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.Vec2f
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.const vec2f: d.Vec2f(xy: number) => d.v2f (+3 overloads)
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.const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)
vec2f(0.5, 0.82), import d
d.const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)
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 emission: TgpuFn<(uv: d.Vec2f) => d.Vec3f>
emission = const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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.Vec2f
vec2f], import d
d.const vec3f: d.Vec3f
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.const vec3f: d.Vec3f(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f(1, 0.82, 0.5), import d
d.const vec3f: d.Vec3f(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f(0.28, 0.52, 1), uv: d.v2f
uv.v2f.x: number
x);});
const const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner = import rc
rc.function create(options: CascadesOptions & { output?: undefined; size: OutputSize;}): rc.RadianceCascadesExecutor (+1 overload)export create
create({ root: TgpuRoot
root, size: OutputSize
size: const previewSize: { width: number; height: number;}
previewSize, sdfResolution: { width: number; height: number;}
sdfResolution: { width: number
width: 1024, height: number
height: 1024 }, sdf: (uv: d.v2f) => number
Signed distance in scene units: the shorter side measures 1.
sdf: const sceneSdf: TgpuFn<(uv: d.Vec2f) => d.F32>
sceneSdf, emission: (uv: d.v2f) => d.v3f
Emitted linear RGB; zero for an obstacle that does not emit light.
emission,});
const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner.function run(commandEncoder?: TgpuCommandEncoder): void
run();
const const radianceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
radianceView = const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner.output: TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag
output.TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.createView<d.WgslTexture2d<d.F32>>(schema: d.WgslTexture2d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & { sampleType?: "float" | "unfilterable-float" | undefined;}) | 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 | undefined
Specifies the sampling behavior when the sample footprint is smaller than or equal to one
texel.
magFilter: 'linear', WgslSamplerProps.minFilter?: GPUFilterMode | undefined
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("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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.Vec2f
vec2f], import d
d.const vec4f: d.Vec4f
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.const vec3f: d.Vec3f(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f(0.04, 0.05, 0.07), import d
d.const vec3f: d.Vec3f(x: number, y: number, z: number) => d.v3f (+5 overloads)
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 emission: (uv: d.v2f) => d.v3f
emission(uv: d.v2f
uv), const surface: number
surface);
return import d
d.const vec4f: d.Vec4f(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
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.const vec3f: d.Vec3f(xyz: number) => d.v3f (+5 overloads)
vec3f(1)), 1);});Just like in Using the lighting, renderPreview draws the shapes with their own color and shows the light everywhere else.
Scene inputs
Section titled “Scene inputs”The scene callbacks receive uv coordinates from 0 to 1 on each axis.
| Input | Meaning |
|---|---|
size | Lighting texture dimensions in pixels; these can be smaller than the canvas |
sdf(uv) | Distance to the nearest surface: positive outside, zero on the surface, negative inside |
emission(uv) | Emitted light in linear RGB, not material color; zero for a non-emitting obstacle, with values above 1 allowed |
sdfResolution | Dimensions of the distance texture; for an analytic function, use the resolution at which you want to resolve surface detail |
While uv runs from 0 to 1 along both axes, distances are measured in units of the shorter side, so in a 1024 × 512 scene, 10 pixels is 10 / 512. Generated distance textures use the same units. sdfResolution sets the minimum hit tolerance and step size (it does not allocate a distance texture).
For rectangular scenes, the scene aspect defaults to the output texture’s width divided by its height. Set renderAspect if your lighting texture has a different aspect from the scene; for example, a square lighting texture covering a 1024 × 512 scene needs renderAspect: 2.
Output textures
Section titled “Output textures”By default, runner.output is an rgba16float texture with storage and sampled usage. You can also bring your own output texture or storage view. The runner destroys its own output when you call destroy(), but caller-supplied output remains yours to release.
const const output: TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag
output = const root: TgpuRoot
root .TgpuRoot.createTexture<number, number, number, [number, number], "rgba16float", number, number, GPUTextureFormat[], GPUTextureDimension>(props: CreateTextureOptions<[number, number], "rgba16float", number, number, GPUTextureFormat[], GPUTextureDimension>): TgpuTexture<{ size: [number, number]; format: "rgba16float";}>
createTexture({ size: [number, number]
The width, height, and depth or layer count of the texture.
size: [const width: number
width, const height: number
height], format: "rgba16float"
The format of the texture.
format: 'rgba16float', }) .TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.$usage<["storage", "sampled"]>(usages_0: "storage", usages_1: "sampled"): TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag
$usage('storage', 'sampled');
const const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner = import rc
rc.create<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>(options: CascadesOptions & { output: TgpuTexture<{ size: [number, number]; format: "rgba16float"; }> & StorageFlag & SampledFlag;}): rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag> (+1 overload)export create
create({ root: TgpuRoot
root, output: TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & SampledFlag
output, sdfResolution: { width: number; height: number;}
sdfResolution, sdf: (uv: d.v2f) => number
Signed distance in scene units: the shorter side measures 1.
sdf: const sceneSdf: (uv: d.v2f) => number
sceneSdf, emission: (uv: d.v2f) => d.v3f
Emitted linear RGB; zero for an obstacle that does not emit light.
emission,});When a storage view cannot provide its size, pass size explicitly alongside output.
Image-based scenes
Section titled “Image-based scenes”For scenes drawn into an image, @typegpu/sdf can generate the distance texture for you. In this example, alpha marks solid pixels and RGB stores their emitted light.

import * as import rc
rc from '@typegpu/radiance-cascades';import * as import sdf
sdf from '@typegpu/sdf';import { const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/typegpu/src/tgpuUnstable");}
tgpu, import d
d, import std
std } from 'typegpu';
const const root: TgpuRoot
root = await const tgpu: { const: typeof import("node_modules/typegpu/src/core/constant/tgpuConstant").constant; fn: typeof import("node_modules/typegpu/src/core/function/tgpuFn").fn; comptime: typeof import("node_modules/typegpu/src/core/function/comptime").comptime; resolve: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolve; resolveWithContext: typeof import("node_modules/typegpu/src/core/resolve/tgpuResolve").resolveWithContext; init: typeof import("node_modules/typegpu/src/core/root/init").init; initFromDevice: typeof import("node_modules/typegpu/src/core/root/init").initFromDevice; slot: typeof import("node_modules/typegpu/src/core/slot/slot").slot; lazy: typeof import("node_modules/typegpu/src/core/slot/lazy").lazy; ... 10 more ...; '~unstable': typeof import("node_modules/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 floodSize: { width: number; height: number;}
floodSize = { width: number
width: 2048, height: number
height: 2048 };const const previewSize: { width: number; height: number;}
previewSize = { width: number
width: 512, height: number
height: 512 };
const const sourceTexture: TgpuTexture<{ size: [number, number]; format: "rgba8unorm";}> & StorageFlag & SampledFlag
sourceTexture = const root: TgpuRoot
root .TgpuRoot.createTexture<number, number, number, [number, number], "rgba8unorm", number, number, GPUTextureFormat[], GPUTextureDimension>(props: CreateTextureOptions<[number, number], "rgba8unorm", number, number, GPUTextureFormat[], GPUTextureDimension>): TgpuTexture<{ size: [number, number]; format: "rgba8unorm";}>
createTexture({ size: [number, number]
The width, height, and depth or layer count of the texture.
size: [const floodSize: { width: number; height: number;}
floodSize.width: number
width, const floodSize: { width: number; height: number;}
floodSize.height: number
height], format: "rgba8unorm"
The format of the texture.
format: 'rgba8unorm', }) .TgpuTexture<{ size: [number, number]; format: "rgba8unorm"; }>.$usage<["storage", "sampled"]>(usages_0: "storage", usages_1: "sampled"): TgpuTexture<{ size: [number, number]; format: "rgba8unorm";}> & StorageFlag & SampledFlag
$usage('storage', 'sampled');
// Fill sourceTexture with the scene before running the lighting updateconst const sourceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
sourceView = const sourceTexture: TgpuTexture<{ size: [number, number]; format: "rgba8unorm";}> & StorageFlag & SampledFlag
sourceTexture.TgpuTexture<{ size: [number, number]; format: "rgba8unorm"; }>.createView(): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView();
const const floodRunner: sdf.JumpFlood.Executor
floodRunner = import sdf
sdf.function createJumpFlood(options: JumpFloodOptions): sdf.JumpFlood.Executorexport createJumpFlood
Create a Jump Flood Algorithm executor with separate SDF and color output textures.
createJumpFlood({ root: TgpuRoot
root, size: { width: number; height: number;}
size: const floodSize: { width: number; height: number;}
floodSize,
classify: (coord: d.v2u, size: d.v2u) => boolean
Classify function that determines which pixels are "inside" for the SDF.
Returns true if the pixel is inside, false if outside.
classify: (coord: d.v2u
coord) => { 'use gpu'; return import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const sourceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
sourceView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, coord: d.v2u
coord, 0).v4f.w: number
w > 0.5; },
getSdf: (coord: d.v2u, size: d.v2u, signedDist: number, insidePx: d.v2u, outsidePx: d.v2u) => number
Get the SDF value to store. Receives signed distance in pixels.
getSdf: (_coord: d.v2u
_coord, size: d.v2u
size, signedDist: number
signedDist) => { 'use gpu'; return signedDist: number
signedDist / import d
d.function f32(v?: number | boolean): numberexport 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(import std
std.function min(fst: number, ...rest: number[]): number (+1 overload)export min
min(size: d.v2u
size.v2u.x: number
x, size: d.v2u
size.v2u.y: number
y)); },
getColor: (coord: d.v2u, size: d.v2u, signedDist: number, insidePx: d.v2u, outsidePx: d.v2u) => d.v4f
Get the color value to store.
getColor: (_coord: d.v2u
_coord, _size: d.v2u
_size, _signedDist: number
_signedDist, insidePx: d.v2u
insidePx) => { 'use gpu'; const const source: d.v4f
source = import std
std.textureLoad<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, coords: d.v2i | d.v2u, level: number): d.v4f (+12 overloads)export textureLoad
textureLoad(const sourceView: TgpuTextureView<d.WgslTexture2d<d.F32>>
sourceView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, insidePx: d.v2u
insidePx, 0); return import d
d.const vec4f: d.Vec4f(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f(const source: d.v4f
source.xyz: d.v3f
xyz, 1); },});
const const floodSdfView: TgpuTextureView<d.WgslTexture2d<d.F32>>
floodSdfView = const floodRunner: sdf.JumpFlood.Executor
floodRunner.sdfOutput: sdf.JumpFlood.SdfTexture
The SDF output texture (rgba16float).
sdfOutput.TgpuTexture<{ size: [number, number]; format: "rgba16float"; }>.createView(): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView();const const floodColorView: TgpuTextureView<d.WgslTexture2d<d.F32>>
floodColorView = const floodRunner: sdf.JumpFlood.Executor
floodRunner.colorOutput: sdf.JumpFlood.ColorTexture
The color output texture (rgba8unorm).
colorOutput.TgpuTexture<{ size: [number, number]; format: "rgba8unorm"; }>.createView(): TgpuTextureView<d.WgslTexture2d<d.F32>> (+3 overloads)
createView();const const sampler: TgpuFixedSampler
sampler = const root: TgpuRoot
root.TgpuRoot.createSampler(props: WgslSamplerProps): TgpuFixedSampler
createSampler({ WgslSamplerProps.magFilter?: GPUFilterMode | undefined
Specifies the sampling behavior when the sample footprint is smaller than or equal to one
texel.
magFilter: 'linear', WgslSamplerProps.minFilter?: GPUFilterMode | undefined
Specifies the sampling behavior when the sample footprint is larger than one texel.
minFilter: 'linear' });
const const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner = import rc
rc.function create(options: CascadesOptions & { output?: undefined; size: OutputSize;}): rc.RadianceCascadesExecutor (+1 overload)export create
create({ root: TgpuRoot
root, size: OutputSize
size: const previewSize: { width: number; height: number;}
previewSize, sdfResolution: { width: number; height: number;}
sdfResolution: const floodSize: { width: number; height: number;}
floodSize,
sdf: (uv: d.v2f) => number
Signed distance in scene units: the shorter side measures 1.
sdf: (uv: d.v2f
uv) => { 'use gpu'; return 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 floodSdfView: TgpuTextureView<d.WgslTexture2d<d.F32>>
floodSdfView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, uv: d.v2f
uv, 0).v4f.x: number
x; },
emission: (uv: d.v2f) => d.v3f
Emitted linear RGB; zero for an obstacle that does not emit light.
emission: (uv: d.v2f
uv) => { 'use gpu'; return 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 floodColorView: TgpuTextureView<d.WgslTexture2d<d.F32>>
floodColorView.TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$, const sampler: TgpuFixedSampler
sampler.TgpuSampler.$: d.sampler
$, uv: d.v2f
uv, 0).xyz: d.v3f
xyz; },});
function function updateLighting(): void
updateLighting() { const floodRunner: sdf.JumpFlood.Executor
floodRunner.function run(): void
Run the jump flood algorithm.
run(); const runner: rc.RadianceCascadesExecutor<TgpuTexture<{ size: [number, number]; format: "rgba16float";}> & StorageFlag & SampledFlag>
runner.function run(commandEncoder?: TgpuCommandEncoder): void
run();}
function updateLighting(): void
updateLighting();Call updateLighting() again whenever you edit the source image.
Quality and cost
Section titled “Quality and cost”size controls the lighting resolution. A smaller texture costs less, but resolves less spatial detail. baseStoredRayDim sets how many directions each lighting sample traces, independently of that resolution:
More directions can help with small lights and thin obstacles, but the improvement depends on the scene, so compare the result before increasing this setting. Each step also doubles the internal textures on each axis: at 4, a lighting texture with a diagonal of 4096 pixels or more exceeds the default 8192-pixel texture size limit.
mergeMode offers a separate quality/cost trade-off:
'hardware'(default) uses the faster interpolation path.'bilinear-fix'performs additional visibility checks, which can reduce light leaking around obstacles at a higher cost.
Advanced usage
Section titled “Advanced usage”Tracing controls
Section titled “Tracing controls”These settings adjust how the built-in tracer finds surfaces:
| Option | Default | Effect |
|---|---|---|
maxRaySteps | 64 | Maximum steps along each ray; raising it permits more tracing work |
stepSafety | 1 | Step multiplier in (0, 1]; smaller values take shorter steps and may need a larger step budget |
erodeBiasProbes | 1 | Shrinks every obstacle by this distance before tracing, which reduces self-shadowing but can lose thin features |
epsProbes | 0.25 | Surface hit tolerance; increasing it accepts hits farther from the surface |
minStepProbes | 0.125 | Minimum step length; above epsProbes × stepSafety it reduces work near surfaces but can skip thin features |
The three distances ending in Probes use lighting-sample spacing as their unit: 1 is the distance between neighboring samples at the finest lighting resolution. The hit tolerance and minimum step are also limited by sdfResolution, so going below the source texture’s detail level has no effect.
Custom tracing
Section titled “Custom tracing”When you need different light transport, such as attenuation or partial transmission, you can replace the built-in tracer with a rayMarch callback. It is used in both merge modes and receives a ray and the segment to trace:
const rayMarch: (probePos: d.v2f, rayDir: d.v2f, startT: number, endT: number, eps: number, minStep: number, bias: number) => d.InferGPU<typeof rc.RayMarchResult>
rayMarch(const origin: d.v2f
origin, const direction: d.v2f
direction, const startT: number
startT, const endT: number
endT, const eps: number
eps, const minStep: number
minStep, const bias: number
bias);Trace positions with origin + direction * t, for t between startT and endT. The distances use the same scene units as sdf. The runner supplies eps, minStep and bias from the tracing controls above. The diagram in that section shows how the built-in tracer uses them, but your callback may use them differently or ignore them altogether. maxRaySteps and stepSafety only control the built-in tracer.
Return rc.RayMarchResult({ color, transmittance }):
coloris the linear RGB light accumulated over that segment.transmittanceis the fraction of light that passes through the segment:1for fully transparent,0for fully opaque, or a value between them.
Tracing utilities
Section titled “Tracing utilities”These exports are useful when writing a custom tracer:
| Export | Purpose |
|---|---|
defaultRayMarch | The built-in ray marcher used by the runner |
RayMarchResult | Return struct for custom ray marchers |
sdfSlot, emissionSlot, rayMarchSlot | Scene functions used by the built-in tracer |
maxRayStepsAccess, rayMarchStepSafetyAccess | Built-in tracing controls |
The package source contains the implementation.
Updating lighting
Section titled “Updating lighting”Create a runner once and call run() whenever the buffers or textures read by its scene callbacks change. Each call submits a lighting update. If the size or configuration changes, call destroy() and create a new runner.
To prepare the GPU pipelines ahead of the first update, use await runner.initAsync(). This is optional (initSync() provides a synchronous alternative).
To record an update without submitting it, pass a command encoder and submit it yourself after recording the rest of your frame. The SDF runner takes run(encoder). The holographic runner takes run({ encoder }), or run({ pass }) to record into an open compute pass, and also accepts raw WebGPU encoders and passes:
const const encoder: TgpuCommandEncoder
encoder = const root: TgpuRoot
root['~unstable'].function createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): TgpuCommandEncoder
Creates a
TgpuCommandEncoder
for batching multiple render/compute
passes (and draws within them) into a single submission.
createCommandEncoder();const const pass: TgpuComputePass
pass = const encoder: TgpuCommandEncoder
encoder.TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass
Begins recording a compute pass
beginComputePass();const producer: TgpuComputePipeline
producer.TgpuComputePipeline.with(pass: TgpuComputePass): TgpuComputePipeline (+7 overloads)
Directs subsequent dispatches into the given compute pass, letting multiple
pipelines share one pass (and one submission).
with(const pass: TgpuComputePass
pass).TgpuComputePipeline.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups(64, 64);const runner: hrc.Executor
runner.function run(options?: RunOptions): void
Records the solve into one compute pass. A supplied encoder is not submitted and a
supplied pass is not ended; with neither, the work is submitted on its own.
run({ pass?: TgpuComputePass | GPUComputePassEncoder | undefined
pass });const pass: TgpuComputePass
pass.TgpuComputePass.end(): void
Completes the recording of this compute pass
end();const encoder: TgpuCommandEncoder
encoder.TgpuCommandEncoder.submit(): void
Finishes the recording and submits the resulting command buffer to the device queue
submit();If a callback uses an explicit bind group layout, attach its resources with runner.with(bindGroup) and use the returned runner. Captured TypeGPU views, samplers and accessors are bound automatically.
Examples
Section titled “Examples”- Holographic Radiance Cascades (with drawing) paints emitters and occluders into a texture that serves as the medium.
- Holographic Global Illumination lights surfaces from
sectorsand adds diffuse bounces. - Radiance Cascades is a from-scratch implementation of the algorithm, independent of this package.
© Software Mansion 2026. All trademarks and copyrights belong to their respective owners. Read about our Privacy Policy.