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
record→encoder
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.
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:
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(
constroot:TgpuRoot
root,
constsurface: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
constprepass:passes.Passes<{
readonly depth:typeofpasses.depth;
}>
prepass=
import passes
passes.
functioncreate<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
},{
readonly depth:typeofpasses.depth;
}>(root:TgpuRoot,screen:targets.Targets<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>,writes:{
readonly depth:typeofpasses.depth;
},options?:passes.TargetOptions<{
readonly depth:{
readonly format:"depth32float";
readonly scale:0.5;
readonly sampled:true;
};
}>|undefined):passes.Passes<{
readonly depth:typeofpasses.depth;
}>(+1overload)
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
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(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
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(
constroot:TgpuRoot
root,
constview: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.
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(
constroot:TgpuRoot
root,{
ao:bindings.Part<bindings.Entries>
ao:
constocclusion: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.
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.
constat:(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.
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
Applies a configuration transform to the slots and accessors this function reads,
as root.pipe does to a pipeline's.
pipe(
constocclusion:ao.Ao
occlusion.
Ao.inject(): Inject
Fills ao.at for pipelines created from the configuration it is piped into.
inject())},
});
const
constscene:passes.Passes<{
readonly hdr:typeofpasses.color;
readonly depth:typeofpasses.depth;
}>
scene=
import passes
passes
.
functioncreate<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
readonly depth:{
readonly format:"depth24plus";
};
},{
readonly hdr:typeofpasses.color;
readonly depth:typeofpasses.depth;
}>(root:TgpuRoot,screen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
readonly depth:{
readonly format:"depth24plus";
};
}>,writes:{
readonly hdr:typeofpasses.color;
readonly depth:typeofpasses.depth;
},options?:passes.TargetOptions<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
readonly depth:{
readonly format:"depth24plus";
};
}>|undefined):passes.Passes<...>(+1overload)
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(
constroot:TgpuRoot
root,
constscreen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
readonly depth:{
readonly format:"depth24plus";
};
}>
screen,{
hdr:typeofpasses.color
hdr:
import passes
passes.
constcolor:typeofpasses.color
Marks the target that receives each renderer's colour.
color,
depth:typeofpasses.depth
depth:
import passes
passes.
constdepth:typeofpasses.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.
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(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
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(
constlight:lighting.Lighting
light.
Lighting.inject(): Inject
Fills the shading hooks and resources for pipelines created from the configuration you pipe
it into.
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.
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.
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
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.
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.
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.
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.
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,
constdrawn: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.
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 rg16floatoutput, and give the target as normal:
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
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(
constroot:TgpuRoot
root,
constearly: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.
constnormal: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.
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(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
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(
constroot:TgpuRoot
root,
constview: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.
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.
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:
constmenu={occlusion:true};
constocclusion=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.