Skip to content

@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.
Holographic runnerpixels that emit and absorbSDF runnerdistance to the nearest shape emitter wall haze distance
The same scene as a grid of materials and as a distance function

Both runners share the same lifecycle, which is described in Updating lighting.

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.

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

@module ― typegpu

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

@module ― typegpu

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

@see ― initFromDevice. *

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

The medium callback receives integer pixel coordinates and describes what that single pixel is made of.

InputMeaning
sizeMaterial 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
tracedLevelsCascade 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 of the light is left. In other words, every 1 / extinction pixels, the light drops to about 37% of what it was.

extinction: 0 100% extinction: 0.1 5% extinction: 16 blocked within the first pixel ≈0% 37% 14% 0 px 10 px 20 px 30 px
How much light is left after traveling through each medium

A few common materials look like this:

MaterialEmissionExtinction
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
};
},
});

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:

extinctionmediumleftresult 0.0105 90% 0.2303 10% 0.4605 1% 10 px
Each channel fades at its own rate, so white light comes out red

Light still travels in straight lines (there is no refraction or dispersion).

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:

pass 1 direct incident = 0 pass 2 +1 bounce incident = pass 1 pass 3 +2 bounces incident = pass 2
Each pass feeds the previous pass's lighting to source as incident

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.

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

@module ― typegpu

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.

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):

xy +X layer 0 +Y layer 1 −X layer 2 −Y layer 3
Each layer holds the light arriving from one side
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 shader
const
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().

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:

ResolutionMemory
256×2569 MiB
512×51238 MiB
1024×1024160 MiB
2048×2048672 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:

1 px 2 px 4 px 8 px 16 px tracedLevels: 3 pixel read join of two shorter intervals
Short intervals read the medium; longer ones are joined from two shorter ones

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:

Midpoint on an interval end exact: one straight path 4 px 4 px Midpoint between interval ends approximate: average of two 4 px 4 px 4 px interval end join straight path
Where the straight path misses an interval end, two bent paths stand in for it

Each extra traced level doubles the longest traced span, which means more medium reads and fewer approximate joins, for the same memory.

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.

This scene has two glowing shapes, a disk and a wall, whose emission blends from warm on the left to cool on the right:

Two glowing shapes illuminating a 2D scene
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");
}

@module ― typegpu

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

@module ― typegpu

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

@see ― initFromDevice. *

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

@module ― typegpu

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

@param ― point Point to evaluate

@param ― radius Radius of the disk

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): number
export sdRoundedBox2d

Signed distance function for a rounded 2d box

@param ― point Point to evaluate

@param ― size Half-dimensions of the box

@param ― cornerRadius Box corner radius

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): number
export opUnion

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

@param ― d1 First SDF distance

@param ― d2 Second SDF distance

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

@module ― typegpu

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

@module ― typegpu

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.

The scene callbacks receive uv coordinates from 0 to 1 on each axis.

InputMeaning
sizeLighting 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
sdfResolutionDimensions of the distance texture; for an analytic function, use the resolution at which you want to resolve surface detail
−0.1 0 +0.1 +0.2 uv (0, 0)(1, 1) 1 (512 px) 2 (1024 px)
Distances are positive outside, 0 on the surface and negative inside

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.

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.

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.

Lighting computed from a drawn image
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");
}

@module ― typegpu

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

@module ― typegpu

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

@see ― initFromDevice. *

@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 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 update
const
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.Executor
export 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): number
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
(
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.

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:

4 rays baseStoredRayDim: 1 16 rays baseStoredRayDim: 2 64 rays baseStoredRayDim: 4
Directions traced by each probe at the finest lighting 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.

These settings adjust how the built-in tracer finds surfaces:

OptionDefaultEffect
maxRaySteps64Maximum steps along each ray; raising it permits more tracing work
stepSafety1Step multiplier in (0, 1]; smaller values take shorter steps and may need a larger step budget
erodeBiasProbes1Shrinks every obstacle by this distance before tracing, which reduces self-shadowing but can lose thin features
epsProbes0.25Surface hit tolerance; increasing it accepts hits farther from the surface
minStepProbes0.125Minimum step length; above epsProbes × stepSafety it reduces work near surfaces but can skip thin features
hitoriginstartTendTdirection obstacle shrunk by bias hit within eps t += max((sdf + bias) × stepSafety, minStep)
Each circle reaches the nearest shrunk obstacle, and the ray steps to its edge

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.

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 }):

  • color is the linear RGB light accumulated over that segment.
  • transmittance is the fraction of light that passes through the segment: 1 for fully transparent, 0 for fully opaque, or a value between them.

These exports are useful when writing a custom tracer:

ExportPurpose
defaultRayMarchThe built-in ray marcher used by the runner
RayMarchResultReturn struct for custom ray marchers
sdfSlot, emissionSlot, rayMarchSlotScene functions used by the built-in tracer
maxRayStepsAccess, rayMarchStepSafetyAccessBuilt-in tracing controls

The package source contains the implementation.

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.

@example

const encoder = root['~unstable'].createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{ view: msaaTexture, resolveTarget: context }],
});
scenePipeline.with(pass).draw(vertexCount);
skyPipeline.with(pass).draw(3);
pass.end();
encoder.submit();

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.

© Software Mansion 2026. All trademarks and copyrights belong to their respective owners. Read about our Privacy Policy.