Fits a width × height world rectangle centred on the view: cover fills the view with it, cropping its longer side, and contain shows all of it, leaving bands.
center?
readonly [number, number]= the origin
Sets the initial view center in world units with +y up.
snap?
number= 0, which disables rounding
Rounds the view center to multiples of this distance in world units, such as one texel of a pixel-art target.
damping?
number= 8
Controls how fast the view approaches the goal.
HitOptionshitHeight · 3 fields
grazing?
number= 0, which misses only rays parallel to a level plane
Misses rays whose direction's y is nearer 0 than this, so a ray near the horizon finds no hit far away.
reach?
number= 1000
With a height function, the farthest distance along the ray to search.
step?
number= reach / 1000
With a height function, the first step of the search along the ray.
records
update(dt) once a frame, to ease the view towards its goal, or jump() to go there.
owns
2 buffers
The current and previous camera uniforms. Writes when the view or size changes and once more in the next frame. cameras.layered owns 1 buffer, writing a layer's view when it changes.
lifecycle
✓destroy()✓[Symbol.dispose]
injects
hook
cameras.current, for every shader that projects.
binds
1 uniform
cameras.current.$, the camera's matrices.
1 uniform
cameras.current.$ reads the previous matrices in code compiled with history.previously.
1 uniform
cameras.layered, every layer's view, beside the layer its description binds.
Drag either view to move the goal marked by the dashed ring. Each frame calls update(dt) and writes the uniform only while the camera moves.
A camera keeps a goal that your input handlers and game logic can change. Once a frame, update(dt) eases the view towards it and writes the uniform only if the view moved. Shaders read it through one cameras.current accessor, whichever kind of camera filled it.
Connects pointer input to an orbit camera's goal. Drag to orbit and use the wheel or pinch to
zoom. With pan, drag the other button to move the target. update eases the camera towards
the goal. Throws if pan is enabled but the goal has no target.
@param ― orbit an orbit camera, whose goal it writes
@param ― element what takes the drag, the wheel and the pinch
@returns ― the controls, which you destroy to remove the listeners
orbitControls(
constview:cameras.OrbitCamera
view,
constcanvas:HTMLCanvasElement
canvas);// drag writes view.goal
constview:cameras.OrbitCamera
view.goal.
Orbit.yaw: number
yaw+=
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.PI: number
Pi. This is the ratio of the circumference of a circle to its diameter.
PI/2;// or turn it from game logic
OrbitCamera.goal: cameras.Orbit
Sets the desired camera pose. update eases the view towards it.
An orbit camera’s goal holds yaw, pitch, distance, field of view and target. A flat camera’s goal holds the centre’s x and y coordinates and the view height. Write goal.target to follow something; the target option is only where it starts. A knob that writes goal.fov eases like the rest.
pose holds the view as drawn while it moves towards the goal. Read it for CPU work that has to match the frame, such as a pan.
update(dt) multiplies the gap by e−damping·dt. At the default damping of 14, half the gap closes every 50 ms at any frame rate. A gap under 1e-5 snaps shut.
jump() moves the view to the goal at once, for a camera cut or a view drawn on demand. Call it instead of update that frame. It preserves the view it replaced as last frame’s view.
clip.near and clip.far are the orbit camera’s clipping planes. The next update takes what you write there, without easing.
The uniform is written at creation, then only when the view moved or the aspect ratio changed, by a queue write that lands before the frame’s commands. A still camera costs a few comparisons and allocates nothing.
Yaw eases the short way round, so a goal from Math.atan2 that jumps from π to −π turns the camera a little, not a full turn back. A goal a whole number of turns away leaves the view where it is.
pitchRange and distanceRange clamp the orbit goal at creation and in update and jump, so input can write past them.
floor(x, z) returns the lowest height the eye may reach over a point, such as the ground there plus a margin. At the same times the goal’s pitch rises, within pitchRange, to the lowest pitch that keeps the eye at or above it.
reach(target, yaw, pitch) returns how far the eye may go from the target along a direction, such as to a room’s wall less a margin. update and jump pull the pose’s distance in to it, and goal.distance keeps the viewer’s zoom, so the eye comes back out as it turns away from the wall. Cloister keeps its eye inside the court this way.
destroy(), or [Symbol.dispose], destroys the uniform buffers.
A pan moves the target by the distance the finger moves over the ground. This handler pans with two fingers and orbits with one:
/** Moves the target by a drag of `dx`, `dy` pixels on a view `height` pixels tall. */
Returns the lowest height the eye may reach at a point x, z, in world units, such as a
terrain's height plus a margin. update and jump raise the goal pitch, within pitchRange,
until the eye is at or above it. Default no floor.
floor:(
x: number
x,
z: number
z)=>
functionheightAt(x:number,z:number):number
heightAt(
x: number
x,
z: number
z)+1.2,
});
cameras.eyeOf(pose, out) writes an orbit camera’s eye position for either goal or pose, for CPU work at the eye such as placing a sound.
root.pipe(view.inject()) fills the cameras.current accessor from @typegpu/kit with the camera’s uniform:
Field
Type
Meaning
viewProjection
mat4x4f
World to clip space.
position
vec3f
The eye, in world space.
right, up
vec3f
The view’s x and y axes in world space, for quads that face the screen.
'use gpu' functions that take cameras.current.$ derive the rest:
cameras.forwardOf(view) returns the direction the view looks in.
cameras.lensOf(view) returns the projection’s x and y scales.
cameras.rayOf(view, ndc) returns an origin and a unit direction through a screen point. ndc ranges from −1 to 1 with +y up. Orbit rays start at the eye. Flat rays are parallel and start at their screen points. To go back to the screen, multiply a point on the ray, such as origin + direction, by viewProjection and divide by w.
cameras.towards(view, point) returns the unit direction from a point to the viewer for shading. For a flat camera it is the opposite of the view direction.
cameras.distanceOf(view, depth) returns the distance along the view direction to the point a depth value holds, for orbit and flat cameras alike. Read depth from the depth target the view drew. Depth of field, ambient occlusion and the night market’s focus pull read distance this way.
forwardOf returns cross(up, right), so right, up and the view direction form a right-handed basis. A Camera you build in a shader must keep that basis. A view of a reflection must also mirror the image, as the fireworks lake does, and is read with u mirrored.
import{
import cameras
cameras}from'@typegpu/kit';
/** Where the pointer's ray meets the ground, y = 0. */
const
constground:()=>d.v3f
Where the pointer's ray meets the ground, y = 0.
ground=()=>{
'use gpu';
const
constray:{
origin:d.v3f;
direction:d.v3f;
}
ray=
import cameras
cameras.
constrayOf:(view:{
viewProjection:d.m4x4f;
position:d.v3f;
right:d.v3f;
up:d.v3f;
},ndc:d.v2f)=>{
origin:d.v3f;
direction:d.v3f;
}
Returns the ray through ndc, with coordinates from -1 to 1 and +y up. Perspective rays start
at the eye. Orthographic rays, such as those from cameras.flat, are parallel and start at
their screen points at the depth of the eye. To go back to the screen, multiply a point on the
ray by viewProjection and divide by w.
rayOf(
import cameras
cameras.
constcurrent:TgpuAccessor<d.WgslStruct<{
viewProjection:d.Mat4x4f;
position:d.Vec3f;
right:d.Vec3f;
up:d.Vec3f;
}>>
Provides the camera for a pipeline. A camera's inject() fills it with the camera uniform.
cameras.sees(view, center, radius) returns false only when a sphere lies wholly outside one of the view’s six planes. It can accept a sphere beyond a corner. Use it in the alive callback of live lists with the camera’s fill:
Allocates memory on the GPU, allows passing data between host and shader.
Read-only on the GPU, optimized for large 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)
Creates a list of live slots and an indirect draw per group. Each group reserves capacity
slots. A shader finds rows with live.slot(group, i) using a captured group index. With
firstInstance, it can use live.instanceSlot(instanceIndex) instead. Throws if the groups,
draws or workgroup size are invalid, the slot buffer exceeds maxStorageBufferBindingSize, or
firstInstance lacks its required feature.
@param ― options.alive whether a slot holds a live row, carrying the fills of what it reads, such
as a table's from rows.live(), spread into the options with its capacity
@param ― options.draws one draw per group
@returns ― live lists you run every frame and destroy
create(
constroot:TgpuRoot
root,{
ListOptions.capacity: number
Sets the number of slots, including dead slots. Indices range from 0 to capacity - 1.
Tests whether a world-space sphere may be visible. Returns false only when the whole sphere lies
outside one of the six view planes. A sphere beyond a corner can still return true. Use it for
culling, such as in live lists' alive function with the camera piped in: sees(cameras.current.$, center, radius).
sees(
import cameras
cameras.
constcurrent:TgpuAccessor<d.WgslStruct<{
viewProjection:d.Mat4x4f;
position:d.Vec3f;
right:d.Vec3f;
up:d.Vec3f;
}>>
Provides the camera for a pipeline. A camera's inject() fills it with the camera uniform.
view.previous holds last frame’s view. Code compiled with history.previously reads it through cameras.current for motion vectors and reprojection. This function finds where a world position appeared on screen last frame:
The orbit and flat cameras write previous in update and jump. When they move, they store the view they replace. On the frame after they stop, they store the current view.
Each kit camera keeps a CPU copy of the view its uniform holds, for CPU work that has to match the frame. None of these calls allocates:
view.projectAt(point, out) writes where a point lands on the surface, x and y from 0 to 1 with +y down, as pointer handlers receive them, and depth from 0 to 1 in z. It returns false for a point behind the eye.
view.rayAt(x, y, out) builds the ray through a point of the surface in the same coordinates, so a point from projectAt gives a ray through the world point. web.pointer reports pointer positions in them.
view.project(point, out) and view.ray(x, y, out) do the same with x and y from −1 to 1 and +y up, as rayOf takes them on the GPU.
view.aspect on the orbit and flat cameras reports the width divided by the height of the view they last wrote.
cameras.hitHeight(ray, height, out) writes where a ray meets the level plane at height and returns how far along the ray that is. It returns undefined, writing nothing, for a ray that runs level or points away. { grazing } also misses rays whose direction’s y is nearer 0 than it, which would hit far beyond the scene near the horizon.
height can also be a function of x and z, such as a terrain’s height. hitHeight then searches the ray in steps, the first step long and each 4% longer, up to reach, and halves the last step 16 times. It allocates nothing, so a pointer can find the hills under it every frame, as in the stampede.
const
constat:number[]
at=[0,0,0];
const
constray:{
origin:number[];
direction:number[];
}
ray={
origin: number[]
origin:[0,0,0],
direction: number[]
direction:[0,0,0]};
// A label over a point, in CSS pixels of an 800 by 600 canvas
Projects a world point to a point of the surface, as rayAt takes it. Writes x and y from 0 to
1 across the surface with +y down, as pointer handlers receive them, and depth from 0 to 1 in
z. Returns false without changing out for a point behind the eye or before the first write.
The translate CSS property allows you to specify translation transforms individually and independently of the transform property. This maps better to typical user interface usage, and saves having to remember the exact order of transform functions to specify in the transform value.
// The ground under the pointer, y = 0; a miss leaves `aim` where it was
const
constaim:number[]
aim=[0,0,0];
const
constpointing:{
move(x:number,y:number):void;
}
pointing={
functionmove(x:number,y:number):void
move(
x: number
x:number,
y: number
y:number){
constview:cameras.OrbitCamera
view.
CpuView.rayAt(x: number, y: number, out: {
readonly origin:cameras.Out3;
readonly direction: cameras.Out3;
}): void
Writes the ray through a point of the surface into out, as ray does. x and y range from
0 to 1 across the surface with +y down, as pointer handlers receive them.
Finds where a ray, such as one from view.rayAt, meets the ground at height, such as the
ground under a pointer. A height function, such as a terrain's, is searched in growing steps
up to reach, then refined by halving the last step. Writes the point into out when given.
Allocates nothing.
@param ― ray an origin and a unit direction
@param ― height the y of a level plane, or of the ground at each x and z
@param ― out where the point goes
@returns ― the distance along the ray, or undefined for a ray that misses
hitHeight(
constray:{
origin:number[];
direction:number[];
}
ray,0,
constaim:number[]
aim,{
HitOptions.grazing?: number |undefined
Misses rays whose direction's y is nearer 0 than this, so a ray near the horizon finds no hit
far away. Default 0, which misses only rays parallel to a level plane.
grazing:0.01});
},
};
Both read the view the frame draws, from the last update or write, not the goal. A custom camera projects nothing before its first write.
A module that draws with a camera takes it as an option and pipes it in, as @typegpu/kit does. Meshes shows a vertex shader that reads it.
web.orbitControls(view, element, { sensitivity, zoomSpeed, button, pan, page, onChange }) writes the goal from the pointer.
A drag with the primary button orbits, sensitivity radians per pixel (0.008), and keeps going outside the element. The wheel and a pinch zoom without scrolling the page, scaling the distance by edeltaY·zoomSpeed (0.001).
With button: 'secondary', the secondary button orbits and two fingers orbit by the move of their midpoint as they pinch. The primary button and one finger stay free for the scene, such as for strokes from web.pointer that paint. The context menu stays shut.
With pan: true, a drag with the other button, secondary or middle, or two fingers moving together, moves goal.target one distance per height of the element, and the context menu stays shut.
With page: true, vertical swipes and the wheel scroll the page. Horizontal drags turn the camera. Pinches and wheel events with Ctrl or ⌘ held zoom it. Trackpad pinches send those wheel events. Use it for a canvas inside a page that scrolls.
onChange runs after each input that wrote the goal, for a view that draws only when asked.
It sets the element’s touch-action to none, or pan-y with page. destroy(), or [Symbol.dispose], removes listeners and restores the original value. Create controls in attach on a hosted page to dispose them with the scene.
The controls leave taps to the app. Listen on the same element and count a release within a few pixels of its press as a tap, such as one that places a tower on a board the drag orbits.
On React Native, write goal from gesture handlers, here with the scene on the JS thread. cameras.turn(goal, dx, dy, { sensitivity }) turns it by a drag as the web controls do:
A handler can’t access a scene on another runtime. Write cumulative gesture values to a runtimes.cell. Each frame, have the scene move the goal by the change in those values.
cameras.flat(root, { size, height }) gives an orthographic view height world units tall, with +y up and z from −1 to 1. Its goal holds the centre and height. Lower goal.height to zoom in. snap rounds the drawn centre to a grid, such as one texel of a pixel-art target, so the art doesn’t shimmer as the view follows a hero. Pass that target as size.
fit: { width, height, mode } replaces height with a world rectangle centred on the view. 'cover' fills the view with it and crops its longer side; 'contain' shows all of it and leaves bands. The camera sets goal.height and its eased height at once, at creation and whenever the aspect changes, so a zoom still eases until the next resize. The ember mirror covers the screen with its camera feed this way.
cameras.custom(root) takes matrices you supply for a follow camera, a scripted shot or a repeatable benchmark view. Pass write(view, projection) a view matrix containing rotation and translation, such as one from mat4.lookAt, and a projection matrix. It derives the eye and screen axes and fills cameras.current. The orbit and flat cameras use this implementation.
Creates a camera from view and projection matrices that you write. Use it for a follow camera, a
scripted camera or another rig. It fills cameras.current and holds zeros until the first write.
Writes the camera from a view matrix and a projection matrix. The view matrix, such as one
from mat4.lookAt, supplies the eye position and screen axes. Moves the current view to
previous. Call once per frame, even when the view is still. Repeated writes of the same view
write nothing once previous matches it. Throws for a view that mirrors the world. Mirror the
image too, with mat4.multiply(mat4.scaling([-1, 1, 1]), view).
Call write once per frame, including while the view is still. It copies the last view into previous. Skipping calls while still leaves previous stale. Once both views match, calling write with the same matrices uploads nothing.
Until the first write the uniform holds zeros, and nothing drawn with it shows.
write throws for a view matrix that mirrors the world. For a reflection, mirror the image too with mat4.multiply(mat4.scaling([-1, 1, 1]), view). Triangles then keep their winding.
cameras.layered(root, n) holds n views in one uniform array, one for each layer of a description drawn into layers, such as a shadow cascade each. write(k, view, projection) writes layer k from the same matrices as a custom camera’s write. cameras.current.$ in a pass into layer k is view k:
Creates a view for each layer of a description drawn into layers, in one uniform array. It
keeps no previous views, so cameras.current.$ reads the current view in code compiled with
history.previously.
@param ― root the root that owns the uniform
@param ― count the number of layers, the layers of the targets the description draws into
@returns ― views you write and destroy
layered(
constroot:TgpuRoot
root,2);
const
constcasters:passes.Passes<{
readonly depth:typeofpasses.depth;
}>
casters=
import passes
passes.
functioncreate<{
depth:{
format:"depth16unorm";
layers:2;
};
},{
readonly depth:typeofpasses.depth;
}>(root:TgpuRoot,screen:Targets<{
depth:{
format:"depth16unorm";
layers:2;
};
}>,writes:{
readonly depth:typeofpasses.depth;
},options?:passes.TargetOptions<{
depth:{
format:"depth16unorm";
layers:2;
};
}>|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(
constviews:cameras.LayeredCamera
views.
LayeredCamera.inject(): Inject
Fills cameras.current with the view of the layer drawn, read through passes.layer. Pipe it into a
description of targets with as many layers; a pipeline of a description drawn into another
number of layers throws as it builds. In other pipelines cameras.current.$ is view 0.
Writes the view of layer from a view matrix and a projection matrix, as a custom camera's
write does. Writes nothing when the layer's view is unchanged. Throws for a view that mirrors
the world and for a layer out of range.
Writes the view of layer from a view matrix and a projection matrix, as a custom camera's
write does. Writes nothing when the layer's view is unchanged. Throws for a view that mirrors
the world and for a layer out of range.
write uploads one layer’s view, and nothing when it is unchanged. It throws for a mirrored view and for a layer out of range.
It keeps no last frame’s views, so code compiled with history.previously reads the current view.
A pipeline of a description drawn into another number of layers throws as it builds. In a pipeline outside a layered description, cameras.current.$ is view 0.
A gap under 1e-5 snaps shut in any unit. That suits radians and scenes a few units to a few hundred across.
goal.target is an array, so a goal copied with a spread shares its target.
lensOf, rayOf, ray and rayAt assume a projection centred on the view axis.
rayOf tells a flat camera from an orbit one by the matrix’s last row, (0, 0, 0, 1) under an orthographic projection. A custom camera’s rays follow the same test.
Camera is one fixed struct with no inverse view-projection. Put other values per view in a uniform of your own, split by how often they change.
A camera implementing CameraBase works with every module without project, ray or previous. These belong to the kit’s cameras. Without a temporal fill, history.previously reads the same camera uniform as current-frame code, so motion vectors omit camera motion.