Skip to content

@typegpu/kitao

Ambient occlusion

Darker sky light in creases and contacts, read from a depth prepass.

kit.ao

module

ao.create(root, view, { depth, normal?, region?, … }) → Ao

rootborrows
the root
viewneeds
the camera that drew depth, which shading's cameras.current must match
depthtakes
the depth drawn before shading, read at every record
normal?takes
normals from ao.normal drawn with depth, read at every record
region?takes
the part of the depth that holds the picture, read at every record
Aoreturns
occlusion you record every frame and destroy
GPU functions
at
outputs
normal
schemas
Settings
constants
defaults
types
NormalOutputAo
Optionscreate · 7 fields
depth

() => Depth

Returns a sampled depth texture drawn before shading, such as a half-resolution prepass.

normal?

() => Normal= none, which takes normals from depth

Returns the texture written by the ao.normal output in the same pass as depth.

temporal?

boolean= false, the same directions every frame

Turns the sample directions each frame, for TAA to average.

slices?

1 | 2 | 3= 2

Sets the directions per pixel, 1 to 3, with 3 steps each way.

region?

SizeSource= the whole depth

Selects the depth region from its top-left corner, such as regions.depth when drawing below full resolution.

settings?

Partial<d.InferInput<typeof Settings>>= defaults, with radius 0.5 and power 2.2

Sets initial values over defaults.

enabled?

() => boolean= always

Controls whether record traces in the current frame.

records
  • recordencoder

record after the pass that draws depth, before the passes that shade; with temporal, a copy advances the frame.

owns

targets ·2 or 3 buffers ·2 pipelines ·2 bind groups

The raw and the denoised occlusion; the settings, the region and frame, and with temporal the frame indices; the trace and the denoise, with a group each.

allocates

on resize

Two textures for a new depth size and the bind groups of its passes; so does a new depth or normal texture.

lifecycle

initAsync()destroy()[Symbol.dispose]

injects
  • hook

    The texture and region ao.at reads, from the part.

binds
  • 1 uniform

    ao.at, the region and frame, beside the texture.

needs
  • cameras

    As view; fills a hook in its pipelines.

dominoes1 474 dominoes in late sun, shaded by ambient occlusion where they crowd; tap one and the wave runs.
Switch the view to occlusion to see what it darkens: the floor under each domino and between the close ones in the spirals. Turn it off and the dominoes float a little above the floor.

Sky light reaches a point from the whole open half of the sky. shading.hemisphere gives every point all of it, so the floor under a domino gets as much as open floor and the domino looks like it hovers. Ambient occlusion measures how much of that half each pixel sees past nearby surfaces and dims the sky light by it.

ao.create reads a depth texture you draw before shading, often at half resolution. Each frame, a trace pass walks the depth around every pixel to find the horizon on each side, using ground-truth ambient occlusion (GTAO). A denoise pass then blurs the result over neighbours on the same surface. Shading reads it at a world position with ao.at:

import {
import bindings
bindings
,
import lighting
lighting
,
import ao
ao
,
import passes
passes
,
import shading
shading
} from '@typegpu/kit';
const
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
=
import targets
targets
.
function create<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>(root: TgpuRoot, source: targets.SizeSource, specs: {
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, options?: targets.Options): targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>

Creates render targets that follow a size. Replaces the whole set when the size changes. Put targets needed only sometimes in their own set and create that set when needed. Throws for transient targets with sampling, storage or mip levels, for multisampled targets with storage or mip levels, and for layered targets that are multisampled, mipped or have more layers than the device allows.

@param ― source the size it follows, such as a surface

@param ― specs a format and options per target, by name

@param ― options.resolution the fraction of each side drawn, which changes without allocating

@returns ― a set of targets you fit every frame and destroy

create
(
const root: TgpuRoot
root
,
const surface: web.Surface
surface
, {
depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}
depth
: {
format: "depth32float"
format
: 'depth32float',
scale: 0.5
scale
: 0.5,
sampled: true
sampled
: true },
});
const
const prepass: passes.Passes<{
readonly depth: typeof passes.depth;
}>
prepass
=
import passes
passes
.
function create<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, {
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>, writes: {
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}> | undefined): passes.Passes<{
readonly depth: typeof passes.depth;
}> (+1 overload)

Describes the targets and outputs for scene passes. With a surface such as the canvas, writes only each renderer's colour. With a target set, writes colour into the target marked color, depth into the target marked depth, and other outputs into their named targets. Multisampled targets resolve according to resolve. Targets with layers draw one pass per layer, and every target the description draws into needs the same layers. add builds a renderer's drawing. begin starts a pass. pipe supplies fills such as a camera.

@param ― screen the surface, or the targets, which give the formats; each one the description names is drawn into

@param ― writes what fills each target, by its key in screen

@returns ― a description you destroy, which holds the pipelines it builds

create
(
const root: TgpuRoot
root
,
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
, {
depth: typeof passes.depth
depth
:
import passes
passes
.
const depth: typeof passes.depth

Marks the depth target.

depth
}).
Passes<{ readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly depth: typeof passes.depth;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const view: cameras.OrbitCamera
view
.
CameraBase.inject(): Inject

Fills cameras.current for the pipelines created from the configuration it is piped into.

inject
());
const
const occlusion: ao.Ao
occlusion
=
import ao
ao
.
function create(root: TgpuRoot, view: cameras.CameraBase, options: ao.Options): ao.Ao

Creates ground-truth ambient occlusion (GTAO) from a depth texture drawn before shading. As Jimenez and others describe it in 2016, a trace walks slices directions per pixel with 3 steps each way and XeGTAO's falloff, and a denoise blurs 3 × 3 pixels of the same surface. Shading reads the result through ao.at, usually in an ambient hook: (material, point) => shading.hemisphere(material, point) * ao.at(point.position), piped with ao.inject().

@injects ― hook the texture and region ao.at reads, from the part

@binds ― 0 storage, 1 uniform: ao.at, the region and frame, beside the texture

@allocates ― on resize: two textures for a new depth size and the bind groups of its passes; so does a new depth or normal texture

@owns ― targets, 2 or 3 buffers, 2 pipelines, 2 bind groups: the raw and the denoised occlusion; the settings, the region and frame, and with temporal the frame indices; the trace and the denoise, with a group each

@records ― record after the pass that draws depth, before the passes that shade; with temporal, a copy advances the frame.

@param ― view the camera that drew depth, which shading's cameras.current must match

@param ― options.depth the depth drawn before shading, read at every record

@param ― options.normal normals from ao.normal drawn with depth, read at every record

@param ― options.region the part of the depth that holds the picture, read at every record

@returns ― occlusion you record every frame and destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options.depth: () => targets.Depth

Returns a sampled depth texture drawn before shading, such as a half-resolution prepass. record reads it on every call. Its size sets the size of the occlusion.

depth
: () =>
const early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
.
Targets<{ readonly depth: { readonly format: "depth32float"; readonly scale: 0.5; readonly sampled: true; }; }>.textures: {
readonly depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>;
}

Provides the current textures. A size change replaces them. Throws after destroy.

textures
.
depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>
depth
});
const
const shared: bindings.Shared
shared
=
import bindings
bindings
.
function share(root: TgpuRoot, parts: Record<string, bindings.Part>): bindings.Shared

Combines parts from modules or your own code in one layout. Call it before creating pipelines. Set its group on each pass with pass.setBindGroup(shared.layout, shared.group()), or give it to passes.create as shared, which sets it in every pass it begins. Every pipeline in that pass that reads a part then uses the shared group.

@param ― parts parts from any modules and your own, by key

@returns ― a layout you set on each pass, with the group for the parts' resources

share
(
const root: TgpuRoot
root
, {
ao: bindings.Part<bindings.Entries>
ao
:
const occlusion: ao.Ao
occlusion
.
Ao.part: bindings.Part<bindings.Entries>

Provides the texture and its region for ao.at. Share it before creating the pipelines that shade, bindings.share(root, { ao: occlusion.part }), so a new depth size replaces the texture without replacing pipelines. A pipeline that reads ao.at without the share throws as it builds.

part
}); // before the pipelines that shade
const
const sky: (material: shading.Material, point: shading.Point) => d.v3f
sky
= (
material: {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}
material
:
import shading
shading
.
type Material = {
color: d.v3f;
roughness: number;
emission: d.v3f;
metallic: number;
occlusion: number;
}

This schema describes a surface with the glTF metallic-roughness model.

Material
,
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
:
import shading
shading
.
type Point = {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}

This schema describes a point being shaded in world space.

Point
) => {
'use gpu';
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
) *
import ao
ao
.
const at: (position: d.v3f) => number

Returns the visibility of sky light at a world position, from 0 in a closed crease to 1 in the open. It projects the position through cameras.current and blends the 4 nearest texels whose eye distance matches. Where none matches, such as on a translucent surface in front of the opaque one, it returns 1. Call it from shading in a pipeline piped with ao.inject(), such as an ambient hook.

at
(
point: {
position: d.v3f;
normal: d.v3f;
view: d.v3f;
uv: d.v2f;
uv1: d.v2f;
}
point
.
position: d.v3f
position
);
};
const
const light: lighting.Lighting
light
=
import lighting
lighting
.
function create<{
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>;
};
}>(root: TgpuRoot, options?: ({
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>;
};
} & lighting.Options & {
readonly shading: {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>;
};
}) | 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: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>;
} & lighting.Shading & {
readonly ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>;
}

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

shading
: {
ambient: TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f> & shading.AmbientHook

Computes ambient light. Default shading.hemisphere.

ambient
:
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
.
fn: <(material: shading.Material, point: shading.Point) => d.v3f>(inner: (material: shading.Material, point: shading.Point) => d.v3f) => TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f> (+2 overloads)
fn
(
const sky: (material: shading.Material, point: shading.Point) => d.v3f
sky
).
TgpuGenericFn<(material: shading.Material, point: shading.Point) => v3f>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(material: shading.Material, point: shading.Point) => d.v3f>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const occlusion: ao.Ao
occlusion
.
Ao.inject(): Inject

Fills ao.at for pipelines created from the configuration it is piped into.

inject
()) },
});
const
const scene: passes.Passes<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>
scene
=
import passes
passes
.
function create<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly depth: {
readonly format: "depth24plus";
};
}, {
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly depth: {
readonly format: "depth24plus";
};
}>, writes: {
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly depth: {
readonly format: "depth24plus";
};
}> | undefined): passes.Passes<...> (+1 overload)

Describes the targets and outputs for scene passes. With a surface such as the canvas, writes only each renderer's colour. With a target set, writes colour into the target marked color, depth into the target marked depth, and other outputs into their named targets. Multisampled targets resolve according to resolve. Targets with layers draw one pass per layer, and every target the description draws into needs the same layers. add builds a renderer's drawing. begin starts a pass. pipe supplies fills such as a camera.

@param ― screen the surface, or the targets, which give the formats; each one the description names is drawn into

@param ― writes what fills each target, by its key in screen

@returns ― a description you destroy, which holds the pipelines it builds

create
(
const root: TgpuRoot
root
,
const screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly depth: {
readonly format: "depth24plus";
};
}>
screen
, {
hdr: typeof passes.color
hdr
:
import passes
passes
.
const color: typeof passes.color

Marks the target that receives each renderer's colour.

color
,
depth: typeof passes.depth
depth
:
import passes
passes
.
const depth: typeof passes.depth

Marks the depth target.

depth
}, {
Options.shared?: bindings.Shared | undefined

Binds a layout from bindings.share in every pass begin and record start, and fills its parts in every pipeline the description builds. Call bindings.share before passes.create and before any other pipeline that reads its parts. Default none.

shared
})
.
Passes<{ readonly hdr: typeof color; readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const view: cameras.OrbitCamera
view
.
CameraBase.inject(): Inject

Fills cameras.current for the pipelines created from the configuration it is piped into.

inject
())
.
Passes<{ readonly hdr: typeof color; readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const light: lighting.Lighting
light
.
Lighting.inject(): Inject

Fills the shading hooks and resources for pipelines created from the configuration you pipe it into.

inject
());
const
const shapes: passes.Drawn[]
shapes
= [
const prepass: passes.Passes<{
readonly depth: typeof passes.depth;
}>
prepass
.
Passes<{ readonly depth: typeof depth; }>.add<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>(this: passes.Passes<passes.Fed<{
color: d.v4f;
}, {
position: d.v4f;
normal: d.v3f;
}>>, drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>): passes.Drawn (+1 overload)

Builds a pipeline for drawing that writes every described output and returns its draw methods. If the drawing cannot supply the fields an output needs, type checking fails with that output's name. Use without to leave it out or instead to replace it.

add
(
const floor: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>
floor
)];
const
const drawn: passes.Drawn[]
drawn
= [
const scene: passes.Passes<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>
scene
.
Passes<{ readonly hdr: typeof color; readonly depth: typeof depth; }>.add<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>(this: passes.Passes<passes.Fed<{
color: d.v4f;
}, {
position: d.v4f;
normal: d.v3f;
}>>, drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>): passes.Drawn (+1 overload)

Builds a pipeline for drawing that writes every described output and returns its draw methods. If the drawing cannot supply the fields an output needs, type checking fails with that output's name. Use without to leave it out or instead to replace it.

add
(
const floor: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
normal: d.Vec3f;
}>, {
color: d.v4f;
}>
floor
)];
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 early: targets.Targets<{
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
.
Targets<{ readonly depth: { readonly format: "depth32float"; readonly scale: 0.5; readonly sampled: true; }; }>.fit(): boolean

Rebuilds all textures if the source size changed, destroying the old textures. Updates regions for the current resolution. Returns true after a rebuild so callers can update bind groups that hold old textures. Call before recording passes that use the targets. Throws after destroy.

fit
();
const screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
readonly depth: {
readonly format: "depth24plus";
};
}>
screen
.
Targets<{ readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; readonly depth: { readonly format: "depth24plus"; }; }>.fit(): boolean

Rebuilds all textures if the source size changed, destroying the old textures. Updates regions for the current resolution. Returns true after a rebuild so callers can update bind groups that hold old textures. Call before recording passes that use the targets. Throws after destroy.

fit
();
const prepass: passes.Passes<{
readonly depth: typeof passes.depth;
}>
prepass
.
Passes<{ readonly depth: typeof depth; }>.record(frame: frames.Frame | TgpuCommandEncoder, drawn: passes.Drawn | readonly passes.Drawn[], options?: passes.BeginOptions<{
readonly depth: typeof passes.depth;
}> | undefined): void

Begins a pass, draws drawn or each drawing in a list in order, then ends the pass. Reuse the list to avoid allocating each frame. Call begin yourself when you need to choose drawings per frame. Records nothing while enabled is false.

record
(
frame: frames.Frame
frame
,
const shapes: passes.Drawn[]
shapes
);
const occlusion: ao.Ao
occlusion
.
Ao.record(target: frames.Frame | TgpuCommandEncoder, options?: Timed): void

Records the trace and the denoise after the pass that draws depth, before the passes that shade, into a frame's encoder, read at the call, or into an encoder. Optional timer writes measure both passes. When disabled, it writes no timestamps, so use a profiler timer around it only while enabled is true.

record
(
frame: frames.Frame
frame
);
const scene: passes.Passes<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}>
scene
.
Passes<{ readonly hdr: typeof color; readonly depth: typeof depth; }>.record(frame: frames.Frame | TgpuCommandEncoder, drawn: passes.Drawn | readonly passes.Drawn[], options?: passes.BeginOptions<{
readonly hdr: typeof passes.color;
readonly depth: typeof passes.depth;
}> | undefined): void

Begins a pass, draws drawn or each drawing in a list in order, then ends the pass. Reuse the list to avoid allocating each frame. Call begin yourself when you need to choose drawings per frame. Records nothing while enabled is false.

record
(
frame: frames.Frame
frame
,
const drawn: passes.Drawn[]
drawn
);
}),
);
  • Record in this order: the prepass, then occlusion.record, then the passes that shade.
  • Draw only opaque drawings in the prepass. A translucent surface in front of the opaque one finds no texel at its distance, so ao.at returns 1 there.
  • The occlusion only dims what the ambient hook returns. Sun and point light keep their own shadows.

A finished look for model scenes records it beside sun shadows, TAA and bloom for a scene of glTF models.

  • Use depth32float. Distance from depth16unorm steps in bands far from the eye, which the trace sees as creases.
  • Half resolution, scale: 0.5, costs a quarter of the trace and still shows contact shadows. The occlusion takes the size of its depth and ao.at blends its 4 nearest texels.
  • A multisampled depth can’t be read. Draw a single-sampled prepass, as the domino run does under its MSAA scene.
  • Give region when the depth is drawn into part of its texture, as with a resolution scale.

By default the trace rebuilds each pixel’s normal from neighbouring depth, choosing on each axis the neighbour nearer in depth so edges stay sharp. Thin geometry and normal maps lose detail this way. Write the vertex normal in the prepass with ao.normal, an rg16float output, and give the target as normal:

const
const early: targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
=
import targets
targets
.
function create<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>(root: TgpuRoot, source: targets.SizeSource, specs: {
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, options?: targets.Options): targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>

Creates render targets that follow a size. Replaces the whole set when the size changes. Put targets needed only sometimes in their own set and create that set when needed. Throws for transient targets with sampling, storage or mip levels, for multisampled targets with storage or mip levels, and for layered targets that are multisampled, mipped or have more layers than the device allows.

@param ― source the size it follows, such as a surface

@param ― specs a format and options per target, by name

@param ― options.resolution the fraction of each side drawn, which changes without allocating

@returns ― a set of targets you fit every frame and destroy

create
(
const root: TgpuRoot
root
,
const surface: web.Surface
surface
, {
normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
}
normal
: {
format: "rg16float"
format
: 'rg16float',
scale: 0.5
scale
: 0.5,
sampled: true
sampled
: true },
depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}
depth
: {
format: "depth32float"
format
: 'depth32float',
scale: 0.5
scale
: 0.5,
sampled: true
sampled
: true },
});
const
const prepass: passes.Passes<{
readonly normal: passes.Output<unknown, {
readonly normal: v3f;
}>;
readonly depth: typeof passes.depth;
}>
prepass
=
import passes
passes
.
function create<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}, {
readonly normal: passes.Output<unknown, {
readonly normal: v3f;
}>;
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>, writes: {
readonly normal: passes.Output<unknown, {
readonly normal: v3f;
}>;
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<...> | undefined): passes.Passes<...> (+1 overload)

Describes the targets and outputs for scene passes. With a surface such as the canvas, writes only each renderer's colour. With a target set, writes colour into the target marked color, depth into the target marked depth, and other outputs into their named targets. Multisampled targets resolve according to resolve. Targets with layers draw one pass per layer, and every target the description draws into needs the same layers. add builds a renderer's drawing. begin starts a pass. pipe supplies fills such as a camera.

@param ― screen the surface, or the targets, which give the formats; each one the description names is drawn into

@param ― writes what fills each target, by its key in screen

@returns ― a description you destroy, which holds the pipelines it builds

create
(
const root: TgpuRoot
root
,
const early: targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
, {
normal: passes.Output<unknown, {
readonly normal: v3f;
}>
normal
:
import ao
ao
.
const normal: passes.Output<unknown, {
readonly normal: v3f;
}>

This output writes the vertex normal, as instances.Corner carries it, into an rg16float target for ao.create's normal. It stores the world normal folded onto an octahedron. Where nothing drew, occlusion takes normals from depth.

normal
,
depth: typeof passes.depth
depth
:
import passes
passes
.
const depth: typeof passes.depth

Marks the depth target.

depth
})
.
Passes<{ readonly normal: Output<unknown, { readonly normal: v3f; }>; readonly depth: typeof depth; }>.pipe(fill: Inject): passes.Passes<{
readonly normal: passes.Output<unknown, {
readonly normal: v3f;
}>;
readonly depth: typeof passes.depth;
}>

Adds fill after existing fills and returns a description of the same passes. Piped fills take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses the camera from view.

pipe
(
const view: cameras.OrbitCamera
view
.
CameraBase.inject(): Inject

Fills cameras.current for the pipelines created from the configuration it is piped into.

inject
());
const
const occlusion: ao.Ao
occlusion
=
import ao
ao
.
function create(root: TgpuRoot, view: cameras.CameraBase, options: ao.Options): ao.Ao

Creates ground-truth ambient occlusion (GTAO) from a depth texture drawn before shading. As Jimenez and others describe it in 2016, a trace walks slices directions per pixel with 3 steps each way and XeGTAO's falloff, and a denoise blurs 3 × 3 pixels of the same surface. Shading reads the result through ao.at, usually in an ambient hook: (material, point) => shading.hemisphere(material, point) * ao.at(point.position), piped with ao.inject().

@injects ― hook the texture and region ao.at reads, from the part

@binds ― 0 storage, 1 uniform: ao.at, the region and frame, beside the texture

@allocates ― on resize: two textures for a new depth size and the bind groups of its passes; so does a new depth or normal texture

@owns ― targets, 2 or 3 buffers, 2 pipelines, 2 bind groups: the raw and the denoised occlusion; the settings, the region and frame, and with temporal the frame indices; the trace and the denoise, with a group each

@records ― record after the pass that draws depth, before the passes that shade; with temporal, a copy advances the frame.

@param ― view the camera that drew depth, which shading's cameras.current must match

@param ― options.depth the depth drawn before shading, read at every record

@param ― options.normal normals from ao.normal drawn with depth, read at every record

@param ― options.region the part of the depth that holds the picture, read at every record

@returns ― occlusion you record every frame and destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options.depth: () => targets.Depth

Returns a sampled depth texture drawn before shading, such as a half-resolution prepass. record reads it on every call. Its size sets the size of the occlusion.

depth
: () =>
const early: targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
.
Targets<{ readonly normal: { readonly format: "rg16float"; readonly scale: 0.5; readonly sampled: true; }; readonly depth: { readonly format: "depth32float"; readonly scale: 0.5; readonly sampled: true; }; }>.textures: {
readonly normal: targets.Texture<{
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
}>;
readonly depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>;
}

Provides the current textures. A size change replaces them. Throws after destroy.

textures
.
depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>
depth
,
Options.normal?: (() => ao.Normal) | undefined

Returns the texture written by the ao.normal output in the same pass as depth. record reads it on every call. Default none, which takes normals from depth.

normal
: () =>
const early: targets.Targets<{
readonly normal: {
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
};
readonly depth: {
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
};
}>
early
.
Targets<{ readonly normal: { readonly format: "rg16float"; readonly scale: 0.5; readonly sampled: true; }; readonly depth: { readonly format: "depth32float"; readonly scale: 0.5; readonly sampled: true; }; }>.textures: {
readonly normal: targets.Texture<{
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
}>;
readonly depth: targets.Texture<{
readonly format: "depth32float";
readonly scale: 0.5;
readonly sampled: true;
}>;
}

Provides the current textures. A size change replaces them. Throws after destroy.

textures
.
normal: targets.Texture<{
readonly format: "rg16float";
readonly scale: 0.5;
readonly sampled: true;
}>
normal
,
});

Drawings in that prepass need a normal in what their vertex code returns, as instances and the geometry meshes have. Pixels the prepass leaves unwritten fall back to normals from depth.

occlusion.settings is a uniform. Its initial values combine ao.defaults with the settings option.

Field Default
radius 0.5 How far occluders count, in world units.
power 2.2 Raises visibility to this power. Higher values darken creases.
  • Set radius near the size of the gaps you want darkened. The domino run uses 2.5 cm, a little under the gap between dominoes. A radius smaller than about one texel on screen leaves the pixel open.
  • slices sets how many directions each pixel walks, 1 to 3, with 3 steps on each side. Each 3 × 3 block of pixels turns its directions through 9 angles, and the denoise averages them.
  • With temporal: true, the directions turn further every frame, over 16 frames, for TAA to average. A GPU copy advances the frame without a CPU write.

enabled is a function that record calls each frame. While it returns false, the first record clears the output to full visibility and later ones draw nothing. A timer passed in timestampWrites is written from an empty compute pass, as for shadows, so a profiler timer can stay on record. Shading calls ao.at and reads 1. Skip the prepass yourself while it is off:

const menu = { occlusion: true };
const occlusion = ao.create(root, view, {
depth: () => early.textures.depth,
enabled: () => menu.occlusion,
});
// in the frame
if (menu.occlusion) prepass.record(frame, shapes);
occlusion.record(frame);

A resize while it is off clears the new output once.

When the depth changes size, the next record replaces both of the occlusion’s textures. Shading pipelines keep working only when they read occlusion.part through a shared group, so call bindings.share before creating them. A pipeline that reads ao.at without the share throws as it builds and names the call to add.

  • Each pipeline piped with occlusion.inject() binds a texture and a uniform holding the region, both in the shared group, besides the camera it already has.
  • The trace binds the depth, the normal if given, the camera, the settings and the region. The denoise binds the raw occlusion, the settings and the region.
  • occlusion.output is the rg16float occlusion, visibility in red and eye distance in green, with occlusion.region as its drawn part. Read it each frame, since a resize replaces it. Another target shows how to view it.
  • The camera can be perspective or orthographic, such as cameras.flat. A projection off its centre, such as an off-axis frustum, places the points it walks wrongly.
  • ao.at projects through the camera of the pipeline that calls it. Under a second camera, such as a reflection’s, it reads the first camera’s occlusion at the wrong texels and mostly returns 1.
  • Only what the prepass drew occludes. Anything off screen or hidden behind a nearer surface doesn’t.
  • With few steps, a closed corner stays lighter than it would with a full search.
  • Each pixel searches the depth up to radius around it, so a large radius close to the eye covers many pixels with the same 3 steps and misses thin occluders.