How one module's shaders read another module's resources, and how the kit's own modules fill each other.
A particle kernel reads the camera. A fish’s steering reads the neighbour grid. Your lighting reads ambient occlusion. Each time, a shader reads resources another module owns. It reads them through an accessor, the owner fills the accessor with its inject(), and the pipeline is created from root.pipe(owner.inject()), as with the ripple in How the kit works.
A weather module keeps the wind in a uniform and offers windAt to other shaders. A snow module’s kernel calls it, so the snow takes the weather as an argument and pipes its fill:
Allocates memory on the GPU, allows passing data between host and shader.
Read-only on the GPU, optimized for small data. For a general-purpose buffer,
use
TgpuRoot.createBuffer
.
@param ― typeSchema The type of data that this buffer will hold.
@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)
Fills module accessors for pipelines created from the configuration it is piped into. Modules
return it from inject(), as TypeGPU caches do. Use root.pipe(table.inject()).
Where an accessor is made decides what a pipeline can read through it:
Kind
Made
Two in one pipeline
In the kit
Per instance
in create
both are read
particles’ emit, the grid’s walk, events’ add, live lists’ slot, text’s setLabel, the picker’s found
Definition
beside a definition
the later pipe wins
a table definition’s rows, a machine’s rows and events, the mesh pool’s vertexAt, the environment’s cube, the sun
Hook
at module scope
one, chosen on purpose
cameras.current, the lighting’s reflect, ambient, fog and sunShadow, ao.at, dof.circle, video.sample
A per-instance accessor, like the weather’s, reads one instance’s resources, so one kernel can emit into two particle systems or walk two grids.
A definition accessor exists before any instance, so a shader written against Enemy.position(i) works for whichever table of enemies it is piped with.
A hook is a piece each pipeline chooses, so two descriptions of passes can draw the same drawings through two cameras, such as a scene and a reflection.
The figure resolves a pipeline piped two instances of each kind, in either order, one or none, and shows whose resources it binds.
Choose which instances fill the pipeline's root. The diagram shows which resources it binds after TypeGPU resolves it on a recording root.
Each module’s card lists what it injects and of which kind.
A module that takes a 'use gpu' function, such as particles’ update or bloom’s bright, builds its pipelines without knowing what the function reads. Pipe the fills into the function itself:
constwarm=tgpu.fn(glowOf).pipe(weather.inject());// reads the weather
Particles’ update, born, died and place, a trail’s width and shade, and the lighting’s shading hooks all take functions this way. A callback can read cameras.current.$ without a fill, because the description of passes fills the camera.
A fill binds a fixed resource, so replacing the resource would need a new pipeline. A resource that is replaced, such as a texture that follows the screen size or each new camera frame, goes in a bindings part instead. Your program shares every part into one layout before creating the pipelines that read them, and sets that layout’s group on each pass.
A pipeline that reads an accessor nothing filled fails at first use or in initAsync, so warm-up catches it while loading. The kit’s accessors default to unfilled, which names the inject() to add, as in Nothing filled `cameras.current` . Yours can do the same:
Ambient occlusion and lighting use every way of connecting. The occlusion reaches the lighting through a callback that carries its fill, both read the camera through a hook, and the occlusion’s texture sits in a shared part because it is replaced when the depth resizes:
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())},// a callback
});
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.
inject());// definitions: the sun, the lights, the shadow