Skip to content

Light from textures

Store baked or changing light in textures and sample it through the lighting ambient hook.

Reduce the field to 64 texels per side to see blocky emitters and softer shadows. Glows are opaque emitting discs, and pillars are opaque non-emitting squares.

Every shaded pixel loops over point-light slots. Dozens of lamps or short-lived shots and blasts can make that expensive. Compute light into a texture at load time or each frame, then sample it at the shaded point through ambient. TypeGPU binds the texture in each pipeline using that lighting.

For stationary lamps, compute light once at load time into a 3D texture covering the scene:

// Fill once at load time over a yard 64 units wide and 4 units high
const
const baked: TgpuTexture<{
size: [64, 16, 64];
format: "rgba16float";
dimension: "3d";
}> & StorageFlag & SampledFlag
baked
=
const root: TgpuRoot
root
.
TgpuRoot.createTexture<number, number, number, [64, 16, 64], "rgba16float", number, number, GPUTextureFormat[], "3d">(props: CreateTextureOptions<[64, 16, 64], "rgba16float", number, number, GPUTextureFormat[], "3d">): TgpuTexture<{
size: [64, 16, 64];
format: "rgba16float";
dimension: "3d";
}>
createTexture
({
size: [64, 16, 64]

The width, height, and depth or layer count of the texture.

size
: [64, 16, 64],
dimension?: "3d" | undefined

Whether the texture is one-dimensional, an array of two-dimensional layers, or three-dimensional.

@default ― '2d'

dimension
: '3d',
format: "rgba16float"

The format of the texture.

format
: 'rgba16float' })
.
TgpuTexture<{ size: [64, 16, 64]; format: "rgba16float"; dimension: "3d"; }>.$usage<["storage", "sampled"]>(usages_0: "storage", usages_1: "sampled"): TgpuTexture<{
size: [64, 16, 64];
format: "rgba16float";
dimension: "3d";
}> & StorageFlag & SampledFlag
$usage
('storage', 'sampled');
const
const irradiance: TgpuTextureView<d.WgslTexture3d<d.F32>>
irradiance
=
const baked: TgpuTexture<{
size: [64, 16, 64];
format: "rgba16float";
dimension: "3d";
}> & StorageFlag & SampledFlag
baked
.
TgpuTexture<{ size: [64, 16, 64]; format: "rgba16float"; dimension: "3d"; }>.createView<d.WgslTexture3d<d.F32>>(schema: d.WgslTexture3d<d.F32>, viewDescriptor?: (TgpuTextureViewDescriptor & {
sampleType?: "float" | "unfilterable-float" | undefined;
}) | undefined): TgpuTextureView<d.WgslTexture3d<d.F32>> (+3 overloads)
createView
(
import d
d
.
texture3d<d.F32>(sampleType: d.F32): d.WgslTexture3d<d.F32> (+1 overload)
export texture3d
texture3d
(
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
));
const
const smooth: TgpuFixedSampler
smooth
=
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 yard: lighting.Lighting
yard
=
import lighting
lighting
.
function create<{
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
}>(root: TgpuRoot, options?: ({
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
} & lighting.Options & {
...;
}) | undefined): lighting.Lighting

Creates lighting with a sun, sky and fog. It can also read point lights from a table or a function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if points names no source or more than one, a light function has neither count nor candidates, a table comes with count, or candidates.ranges is anything other than a positive integer.

@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and attenuation

@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading reads

@binds ― 1 uniform: shading.shade, the sun, sky and fog

@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights

@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the device, the table's two parts

@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function binds

@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2 storage for a neighbour grid's range and slot

@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the depth array and its comparison sampler

@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views, beside their depth array and the comparison sampler

@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a shadow, its light space, the views of its cascades, a comparison sampler, the depth array and the passes that draw it; with pointShadow, the same for the lights' faces

@records ― shadow.record and pointShadow.record with what was added to them, before the passes that shade

@param ― options.shadow the sun's shadow, with a depth pass per cascade

@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth pass per face

@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits each slot up to the table's highest live one, or count with a function, unless candidates names the slots that may reach it

@returns ― lighting you destroy

create
(
const root: TgpuRoot
root
, {
shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
} & lighting.Shading & {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
}

Sets the functions that shading.shade calls. Default pbr, hemisphere, heightFog, filtered and reaching.

shading
: {
ambient: ((material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f) & shading.AmbientHook

Computes ambient light. Default shading.hemisphere.

ambient
: (
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
) => {
'use gpu';
const
const uvw: d.v3f
uvw
= (
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
.
position: d.v3f
position
+
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(32, 0, 32)) /
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(64, 4, 64);
const
const lamps: d.v3f
lamps
=
import std
std
.
textureSampleLevel<d.texture3d<d.F32>>(texture: d.texture3d<d.F32>, sampler: d.sampler, coords: d.v3f, level: number): d.v4f (+13 overloads)
export textureSampleLevel
textureSampleLevel
(
const irradiance: TgpuTextureView<d.WgslTexture3d<d.F32>>
irradiance
.
TgpuTextureView<WgslTexture3d<F32>>.$: d.texture3d<d.F32>
$
,
const smooth: TgpuFixedSampler
smooth
.
TgpuSampler.$: d.sampler
$
,
const uvw: d.v3f
uvw
, 0).
rgb: d.v3f
rgb
;
return
import shading
shading
.
const hemisphere: (material: shading.Material, point: shading.Point) => d.v3f

Computes ambient light by blending sky and ground colours according to the surface normal. Reflections sample the sky in the reflection direction. Roughness blends them towards diffuse light, and skyReflection scales them. The material's occlusion darkens both.

hemisphere
(
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
) +
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
.
color: d.v3f

Sets the diffuse colour of a dielectric or the reflection colour of a metal, in linear colour space.

color
*
const lamps: d.v3f
lamps
;
},
},
});
  • The volume uses 8 bytes per texel, 0.5 MB in this example, and one sample per shaded pixel.
  • Fill it with a kernel that sums lamp light at each texel or a radiance field computed over a yard map.

A lightmap stores light baked onto a mesh’s second uv set, which shading reads as point.uv1. Shade by it in the ambient hook with shading.surrounded:

const
const baked: images.Texture<[number, number]>
baked
=
import images
images
.
function create(root: TgpuRoot, picture: images.Picture, options?: images.Options): images.Texture

Creates a sampled texture from one image. 8-bit images become rgba8unorm-srgb, or rgba8unorm with color: 'linear'. Half-float images, and 8-bit ones with packed, become format. Throws if the pixel count doesn't match the size, the image is empty or larger than the device allows, packed comes with color, or format with 8-bit colour or data.

@param ― picture pixels from decode, decodeHdr or your own code, or a bitmap

@returns ― a texture you destroy, with every mip level unless mips is false

create
(
const root: TgpuRoot
root
,
const picture: images.Picture
picture
, {
Options.packed?: images.Unpack | undefined

Converts light stored in an 8-bit image, such as RGBM, to linear light once, at upload, so the texture filters and mips like any HDR texture. A 'use gpu' function that takes each texel as stored, unfiltered, and returns linear RGBA. The texture takes format. It reads raw values, so it takes no color. Default none.

packed
:
import images
images
.
const rgbm: (range: number) => images.Unpack

Unpacks RGBM, light stored as rgb × a × range in linear values, for packed.

@param ― range the brightest light the encoding holds

@returns ― a function for packed

rgbm
(8),
Options.mips?: boolean | undefined

Generates every mip level down to 1 × 1. Default true.

mips
: false });
const
const view: TgpuTextureView<d.WgslTexture2d<d.F32>>
view
=
const baked: images.Texture<[number, number]>
baked
.
TgpuTexture<{ size: [number, number]; format: Format; }>.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
.
texture2d<d.F32>(sampleType: d.F32): d.WgslTexture2d<d.F32> (+1 overload)
export texture2d
texture2d
(
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
));
const
const smooth: TgpuSampler
smooth
=
import images
images
.
function sampler(root: TgpuRoot, options?: images.SamplerOptions): TgpuSampler

Creates a filtering sampler for textures from this module.

@returns ― a sampler, which needs no destroy

sampler
(
const root: TgpuRoot
root
, {
SamplerOptions.wrap?: GPUAddressMode | undefined

Says what happens past the edge: WebGPU's address mode on every axis. Default 'repeat'.

wrap
: 'clamp-to-edge' });
const
const room: lighting.Lighting
room
=
import lighting
lighting
.
function create<{
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
}>(root: TgpuRoot, options?: ({
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
} & lighting.Options & {
...;
}) | undefined): lighting.Lighting

Creates lighting with a sun, sky and fog. It can also read point lights from a table or a function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if points names no source or more than one, a light function has neither count nor candidates, a table comes with count, or candidates.ranges is anything other than a positive integer.

@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and attenuation

@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading reads

@binds ― 1 uniform: shading.shade, the sun, sky and fog

@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights

@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the device, the table's two parts

@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function binds

@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2 storage for a neighbour grid's range and slot

@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the depth array and its comparison sampler

@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views, beside their depth array and the comparison sampler

@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a shadow, its light space, the views of its cascades, a comparison sampler, the depth array and the passes that draw it; with pointShadow, the same for the lights' faces

@records ― shadow.record and pointShadow.record with what was added to them, before the passes that shade

@param ― options.shadow the sun's shadow, with a depth pass per cascade

@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth pass per face

@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits each slot up to the table's highest live one, or count with a function, unless candidates names the slots that may reach it

@returns ― lighting you destroy

create
(
const root: TgpuRoot
root
, {
shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
} & lighting.Shading & {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
}

Sets the functions that shading.shade calls. Default pbr, hemisphere, heightFog, filtered and reaching.

shading
: {
ambient: ((material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f) & shading.AmbientHook

Computes ambient light. Default shading.hemisphere.

ambient
: (
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
) => {
'use gpu';
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 view: TgpuTextureView<d.WgslTexture2d<d.F32>>
view
.
TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$
,
const smooth: TgpuSampler
smooth
.
TgpuSampler.$: d.sampler
$
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
.
uv1: d.v2f

Holds the second uv set at the point, a mesh's uvs1, or zero.

uv1
, 0).
rgb: d.v3f
rgb
;
return
import shading
shading
.
const surrounded: (material: shading.Material, point: shading.Point, irradiance: d.v3f, reflection: d.v3f) => d.v3f

Shades a point by the light around it, for an ambient hook.

irradiance is the light a white matte surface at the point reflects, as environment.irradiance and a diffuse lightmap give it. It is used as given, so multiply it by material.occlusion if the light doesn't already include occlusion.

reflection is the light arriving along reflectionOf, as environment.reflection gives it. It is weighted by reflectance and darkened by specularOcclusion of the material's occlusion.

surrounded
(
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
,
const light: d.v3f
light
,
const light: d.v3f
light
);
},
},
});
  • If the bake already includes occlusion, pass its light as is. Otherwise multiply the diffuse light by material.occlusion.
  • To reflect an environment instead, sample it along shading.reflectionOf(point, material.roughness).
  • gltf.read keeps the second set with uvs1: true. Load 8-bit packed maps with packed. On React Native, use an encoding that leaves alpha at 255, as its decoder needs.
classroomBlender's Classroom, its daylight and lamps baked on a second uv set, sunbeams through its dust.
Two lightmaps hold the light from the sky and from the lamps. The sun's light and shadow are computed live. Turn each off to compare.
falling-sandSand, water and lava falling cell by cell through a cave lit by its own glow.
Drag to paint, or tap the cave first on a touch screen; turn bounce off to remove reflected light. Lava and crystal emit light, rock and sand block it, and walls and steam reflect it.

Shots, muzzle flashes and blasts can create dozens of lights lasting a fraction of a second. Draw their emission into a texture over the ground. @typegpu/radiance-cascades computes how much reaches each texel around obstacles such as towers and trees. The kit doesn’t depend on it, so add it yourself, in a version whose typegpu peer range covers your project’s TypeGPU, such as:

npm i @typegpu/radiance-cascades@0.12.3

Create a medium, a texture holding emission and extinction per texel. This kernel fills it from a table of glows:

import {
import table
table
} from '@typegpu/kit';
import * as
import hrc
hrc
from '@typegpu/radiance-cascades/holographic';
const
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
=
import table
table
.
function define<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>(name: "Glow", parts: {
host?: {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
} | undefined;
device?: {} | undefined;
}): table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>

Declares CPU-written host fields and GPU-written device fields using the records accepted by d.struct. Define it at module scope beside shaders. Throws if no fields are declared, a field appears in both parts, the CPU writer cannot write a field type such as f16, or a field uses a reserved member name such as alive.

name brands row ids as RowId<'Enemy'> and labels structs, buffers, WGSL functions and errors. It is not a lookup key.

@param ― name a label for ids and WGSL

@param ― parts the host fields the CPU writes and the device fields the GPU writes

@returns ― a definition whose getters shaders read and that table.create allocates

define
('Glow', {
host?: {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
} | undefined
host
: {
position: d.Vec2f
position
:
import d
d
.
const vec2f: d.Vec2f
vec2f
,
radius: d.F32
radius
:
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
,
color: d.Vec3f
color
:
import d
d
.
const vec3f: d.Vec3f
vec3f
}, // on the ground, x and z
});
const
const glows: table.Table<table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>>
glows
=
import table
table
.
function create<table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>>(root: TgpuRoot, def: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>, options: table.Options<table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>>): table.Table<table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>> (+1 overload)

Allocates GPU buffers and a CPU copy of host rows. Throws unless capacity is an integer in [1, 2^20].

@param ― def the definition its shaders were written against, from table.define

@returns ― a table you destroy

create
(
const root: TgpuRoot
root
,
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
, {
Options<T extends AnyTableDef>.capacity: number

Sets the table capacity for its lifetime. Must be an integer in [1, 2^20], with slots from 0 to capacity - 1. A row id uses enough bits for the slot and the rest for its generation. The generation has 29 bits at 8 rows and 12 bits at 2^20 rows.

capacity
: 256 });
const
const SIDE: 32
SIDE
= 32; // world units across the square of ground the field covers
const
const TEXELS: 256
TEXELS
= 256;
const
const OPAQUE: 4
OPAQUE
= 4; // extinction per texel
const
const medium: TgpuTexture<{
size: [256, 256];
format: "rgba16float";
}> & StorageFlag & SampledFlag
medium
=
const root: TgpuRoot
root
.
TgpuRoot.createTexture<number, number, number, [256, 256], "rgba16float", number, number, GPUTextureFormat[], GPUTextureDimension>(props: CreateTextureOptions<[256, 256], "rgba16float", number, number, GPUTextureFormat[], GPUTextureDimension>): TgpuTexture<{
size: [256, 256];
format: "rgba16float";
}>
createTexture
({
size: [256, 256]

The width, height, and depth or layer count of the texture.

size
: [
const TEXELS: 256
TEXELS
,
const TEXELS: 256
TEXELS
],
format: "rgba16float"

The format of the texture.

format
: 'rgba16float' })
.
TgpuTexture<{ size: [256, 256]; format: "rgba16float"; }>.$usage<["storage", "sampled"]>(usages_0: "storage", usages_1: "sampled"): TgpuTexture<{
size: [256, 256];
format: "rgba16float";
}> & StorageFlag & SampledFlag
$usage
('storage', 'sampled');
const
const writing: TgpuTextureView<d.WgslStorageTexture2d<"rgba16float", "write-only">>
writing
=
const medium: TgpuTexture<{
size: [256, 256];
format: "rgba16float";
}> & StorageFlag & SampledFlag
medium
.
TgpuTexture<{ size: [256, 256]; format: "rgba16float"; }>.createView<d.WgslStorageTexture2d<"rgba16float", "write-only">>(schema: d.WgslStorageTexture2d<"rgba16float", "write-only">, viewDescriptor?: TgpuTextureViewDescriptor): TgpuTextureView<d.WgslStorageTexture2d<"rgba16float", "write-only">> (+3 overloads)
createView
(
import d
d
.
textureStorage2d<"rgba16float", "write-only">(format: "rgba16float", access: "write-only"): d.WgslStorageTexture2d<"rgba16float", "write-only"> (+1 overload)
export textureStorage2d
textureStorage2d
('rgba16float', 'write-only'));
const
const reading: TgpuTextureView<d.WgslTexture2d<d.F32>>
reading
=
const medium: TgpuTexture<{
size: [256, 256];
format: "rgba16float";
}> & StorageFlag & SampledFlag
medium
.
TgpuTexture<{ size: [256, 256]; 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
.
texture2d<d.F32>(sampleType: d.F32): d.WgslTexture2d<d.F32> (+1 overload)
export texture2d
texture2d
(
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
));
const
const draw: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
draw
=
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
computeFn: <{
gid: d.BuiltinGlobalInvocationId;
}>(options: {
in: {
gid: d.BuiltinGlobalInvocationId;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
gid: d.BuiltinGlobalInvocationId;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [8, 8],
in: {
gid: d.BuiltinGlobalInvocationId;
}
in
: {
gid: d.BuiltinGlobalInvocationId
gid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
globalInvocationId: d.BuiltinGlobalInvocationId
globalInvocationId
},
})(({
gid: d.v3u
gid
}) => {
'use gpu';
if (
gid: d.v3u
gid
.
v3u.x: number
x
>=
const TEXELS: 256
TEXELS
||
gid: d.v3u
gid
.
v3u.y: number
y
>=
const TEXELS: 256
TEXELS
) return;
const
const ground: d.v2f
ground
= ((
import d
d
.
const vec2f: d.Vec2f
(v: AnyNumericVec2Instance) => d.v2f (+3 overloads)
vec2f
(
gid: d.v3u
gid
.
xy: d.v2u
xy
) + 0.5) /
const TEXELS: 256
TEXELS
- 0.5) *
const SIDE: 32
SIDE
;
let
let emission: d.v3f
emission
=
import d
d
.
const vec3f: d.Vec3f
() => d.v3f (+5 overloads)
vec3f
();
let
let extinction: number
extinction
=
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
(0); // a wall writes OPAQUE and no emission
for (let
let i: number
i
=
import d
d
.
function u32(v?: number | boolean): number
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

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

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
(0);
let i: number
i
<
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
.
span: TgpuGenericFn
() => number

Returns the highest live slot plus one.

span
();
let i: number
i
++) {
if (
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
.
alive: TgpuGenericFn
(i: number) => boolean

Tests whether slot i holds a row. Returns false beyond the span.

alive
(
let i: number
i
) &&
import std
std
.
distance<d.v2f>(a: d.v2f, b: d.v2f): number (+1 overload)
export distance
distance
(
const ground: d.v2f
ground
,
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
.
position: table.Getter
(i: number) => d.v2f
position
(
let i: number
i
)) <
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
.
radius: table.Getter
(i: number) => number
radius
(
let i: number
i
)) {
let emission: d.v3f
emission
=
const Glow: table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>
Glow
.
color: table.Getter
(i: number) => d.v3f
color
(
let i: number
i
) *
const OPAQUE: 4
OPAQUE
; // an opaque disc shines with its colour
let extinction: number
extinction
=
const OPAQUE: 4
OPAQUE
;
}
}
import std
std
.
textureStore<d.textureStorage2d<"rgba16float", "write-only">>(texture: d.textureStorage2d<"rgba16float", "write-only">, coords: d.v2i | d.v2u, value: d.v4f): void (+3 overloads)
export textureStore
textureStore
(
const writing: TgpuTextureView<d.WgslStorageTexture2d<"rgba16float", "write-only">>
writing
.
TgpuTextureView<WgslStorageTexture2d<"rgba16float", "write-only">>.$: d.textureStorage2d<"rgba16float", "write-only">
$
,
gid: d.v3u
gid
.
xy: d.v2u
xy
,
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
let emission: d.v3f
emission
,
let extinction: number
extinction
));
});
const
const drawing: TgpuComputePipeline
drawing
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const glows: table.Table<table.TableDef<"Glow", {
position: d.Vec2f;
radius: d.F32;
color: d.Vec3f;
}, {}>>
glows
.
Table<TableDef<"Glow", { position: Vec2f; radius: F32; color: Vec3f; }, {}>>.inject(): Inject

Fills the definition's accessors with this table's buffers.

inject
()).
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const draw: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
draw
});
const
const field: hrc.Executor
field
=
import hrc
hrc
.
function create(options: Options): hrc.Executor
export create

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
:
const TEXELS: 256
TEXELS
,
height: number
height
:
const TEXELS: 256
TEXELS
},
medium: (pixel: d.v2u) => {
emission: d.v3f;
extinction: number;
}
medium
: (
pixel: d.v2u
pixel
) => {
'use gpu';
const
const texel: d.v4f
texel
=
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 reading: TgpuTextureView<d.WgslTexture2d<d.F32>>
reading
.
TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$
,
pixel: d.v2u
pixel
, 0);
return {
emission: d.v3f
emission
:
const texel: d.v4f
texel
.
rgb: d.v3f
rgb
,
extinction: number
extinction
:
const texel: d.v4f
texel
.
v4f.a: number
a
};
},
});

Sample the field at the shaded point in ambient. Each frame, fill the medium and run the field in one compute pass:

import {
import frames
frames
} from '@typegpu/kit';
import {
import lighting
lighting
,
import shading
shading
} from '@typegpu/kit';
const
const radiance: TgpuTextureView<d.WgslTexture2d<d.F32>>
radiance
=
const field: hrc.Executor
field
.
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
.
texture2d<d.F32>(sampleType: d.F32): d.WgslTexture2d<d.F32> (+1 overload)
export texture2d
texture2d
(
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
));
const
const smooth: TgpuFixedSampler
smooth
=
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 light: lighting.Lighting
light
=
import lighting
lighting
.
function create<{
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
}>(root: TgpuRoot, options?: ({
readonly shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
};
} & lighting.Options & {
...;
}) | undefined): lighting.Lighting

Creates lighting with a sun, sky and fog. It can also read point lights from a table or a function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if points names no source or more than one, a light function has neither count nor candidates, a table comes with count, or candidates.ranges is anything other than a positive integer.

@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and attenuation

@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading reads

@binds ― 1 uniform: shading.shade, the sun, sky and fog

@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights

@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the device, the table's two parts

@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function binds

@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2 storage for a neighbour grid's range and slot

@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the depth array and its comparison sampler

@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views, beside their depth array and the comparison sampler

@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a shadow, its light space, the views of its cascades, a comparison sampler, the depth array and the passes that draw it; with pointShadow, the same for the lights' faces

@records ― shadow.record and pointShadow.record with what was added to them, before the passes that shade

@param ― options.shadow the sun's shadow, with a depth pass per cascade

@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth pass per face

@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits each slot up to the table's highest live one, or count with a function, unless candidates names the slots that may reach it

@returns ― lighting you destroy

create
(
const root: TgpuRoot
root
, {
shading: {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
} & lighting.Shading & {
readonly ambient: (material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f;
}

Sets the functions that shading.shade calls. Default pbr, hemisphere, heightFog, filtered and reaching.

shading
: {
ambient: ((material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}, point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}) => d.v3f) & shading.AmbientHook

Computes ambient light. Default shading.hemisphere.

ambient
: (
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
) => {
'use gpu';
// Offset two texels along the normal to leave opaque wall cells
const
const ground: d.v2f
ground
=
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
.
position: d.v3f
position
.
xz: d.v2f
xz
+
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
.
normal: d.v3f

Holds a unit surface normal.

normal
.
xz: d.v2f
xz
* ((2 *
const SIDE: 32
SIDE
) /
const TEXELS: 256
TEXELS
);
const
const glow: d.v3f
glow
=
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 radiance: TgpuTextureView<d.WgslTexture2d<d.F32>>
radiance
.
TgpuTextureView<WgslTexture2d<F32>>.$: d.texture2d<d.F32>
$
,
const smooth: TgpuFixedSampler
smooth
.
TgpuSampler.$: d.sampler
$
,
const ground: d.v2f
ground
/
const SIDE: 32
SIDE
+ 0.5, 0).
rgb: d.v3f
rgb
;
return
import shading
shading
.
const hemisphere: (material: shading.Material, point: shading.Point) => d.v3f

Computes ambient light by blending sky and ground colours according to the surface normal. Reflections sample the sky in the reflection direction. Roughness blends them towards diffuse light, and skyReflection scales them. The material's occlusion darkens both.

hemisphere
(
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
) +
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
.
color: d.v3f

Sets the diffuse colour of a dielectric or the reflection colour of a metal, in linear colour space.

color
*
const glow: d.v3f
glow
;
},
},
});
import frames
frames
.
function animate(loop: Pick<frames.Frames, "run"> & Partial<Pick<frames.Frames, "seconds">>, options?: frames.AnimateOptions): frames.Animation
export animate

Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps back, and stops while the surface is paused. Destroying the animation keeps the loop alive. Another animation on the same loop continues from its recorded time. A frame error stops the animation.

@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before

@returns ― an animation you destroy to stop the frames

animate
(
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const glows: {
flush(): void;
}
glows
.
function flush(): void
flush
();
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const compute: TgpuComputePass
compute
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const drawing: TgpuComputePipeline
drawing
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(
const TEXELS: 256
TEXELS
/ 8,
const TEXELS: 256
TEXELS
/ 8);
const field: hrc.Executor
field
.
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 compute: TgpuComputePass
compute
});
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
// Record passes using light after this compute pass
}),
);
  • run submits no commands. field.output retains the light until the next run. Use a cadence, such as 30 Hz, and rerun whenever the board changes.
  • An opaque disc emits its colour. In clear air, a glow of radius r contributes about asin(r / d) / π of that colour at distance d: about one tenth at three radii and one fortieth at twelve. Light in a plane falls as one over distance rather than distance squared.
  • Walls use opaque texels without emission and cast soft shadows. A point on a wall face lies in those dark texels. Offset its sample two texels along the normal.
  • Extinction is per texel, so finer fields make haze dim light faster. Multiply by SIDE / TEXELS to express it per world unit.
  • The field is flat. Elevated points receive the light at ground level, and points outside the square sample its nearest edge. Use ambient to fade light with height and reject samples outside the square.
  • A medium can read any simulation buffer, including towers and shots, to find emitters.
  • The kernel above tests every glow at every texel, which suits a few dozen glows. For thousands, run one thread per glow to atomicAdd fixed-point light into a u32 grid. Then run one thread per texel to convert the sums into the medium.
  • field.byteSize gives the field’s memory. The medium adds 8 bytes a texel.

field.sectors stores light from four directions. Use it to shade by normal and compute reflections from walls:

  • Read the previous run’s sectors just outside each wall face. Each sector contains one quarter of the surrounding light. Multiply by four, weight by the normal and multiply by wall albedo.
  • Add the reflected light to the medium before the next run. Each run adds one bounce.
  • Falling sand computes one bounce from walls and steam at each 30 Hz update.