Skip to content

@typegpu/kitcameras

Cameras

Orbit, flat, custom and layered cameras with shader accessors, eased motion and last-frame views.

kit.cameras

module

cameras.custom(root) → CustomCamera

cameras.layered(root, count) → LayeredCamera

cameras.turn(goal, dx, dy, options?) → G

cameras.eyeOf(pose, out)

and cameras.orbit, cameras.flat, cameras.hitHeight

rootborrows
the root
CustomCamerareturns
a camera you write and destroy
rootborrows
the root that owns the uniform
counttakes
the number of layers, the layers of the targets the description draws into
LayeredCamerareturns
views you write and destroy
goaltakes
an orbit camera's goal, whose yaw and pitch it writes
dxtakes
the drag to the right, in pixels or points
dytakes
the drag down, in pixels or points
Greturns
goal
posetakes
an orbit's yaw, pitch, distance and target
outtakes
where the eye goes
GPU functions
forwardOflensOfdistanceOfrayOftowardssees
accessors
current
schemas
CameraRay
types
Out3CpuViewCameraBaseCustomCameraOrbitOrbitCameraFlatCameraLayeredCamera
TurnOptionsturn · 1 field
sensitivity?

number | undefined= 0.008

Sets the radians a dragged pixel or point turns.

OrbitOptionsorbit · 13 fields
size

SizeSource

Supplies the size used for the aspect ratio, such as the canvas surface.

target

readonly [number, number, number]

Sets the initial point that the camera orbits and looks at.

distance

number

Sets the initial distance from the target to the eye, in world units.

yaw?

number= 0

Sets the initial yaw in radians around +y.

pitch?

number= 0.6

Sets the initial pitch in radians above the horizon.

fov?

number= 0.8

Sets the initial vertical field of view in radians.

near?

number= 0.1

Sets the initial distance from the eye to the near clipping plane, in world units.

far?

number= 500

Sets the initial distance from the eye to the far clipping plane, in world units.

pitchRange?

readonly [number, number]= -1.5 to 1.5

Limits the goal pitch to this range, in radians.

distanceRange?

readonly [number, number]= near * 2 to far / 2, using the option values

Limits the goal distance to this range, in world units.

floor?

(x: number, z: number) => number= no floor

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.

reach?

(target: ArrayLike<number>, yaw: number, pitch: number) => number= no limit

Returns how far the eye may go from target along a yaw and pitch, such as the distance to a room's wall less a margin.

damping?

number= 14

Controls how fast the view approaches the goal.

FlatOptionsflat · 6 fields
size

SizeSource

Supplies the size used for the aspect ratio, such as the canvas surface or a low-resolution target.

height?

number= the height fit gives

Sets the initial view height in world units, required without fit.

fit?

{ readonly width: number; readonly height: number; readonly mode: 'cover' | 'contain' }= none

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.

import {
import cameras
cameras
} from '@typegpu/kit';
const
const view: cameras.OrbitCamera
view
=
import cameras
cameras
.
function orbit(root: TgpuRoot, options: cameras.OrbitOptions): cameras.OrbitCamera

Creates a perspective camera that orbits a target and eases towards a goal set by input.

@returns ― a camera you update every frame and destroy

orbit
(
const root: TgpuRoot
root
, {
OrbitOptions.size: SizeSource

Supplies the size used for the aspect ratio, such as the canvas surface.

size
:
const surface: web.Surface
surface
,
OrbitOptions.target: readonly [number, number, number]

Sets the initial point that the camera orbits and looks at. Change it through goal.target.

target
: [0, 1, 0],
OrbitOptions.distance: number

Sets the initial distance from the target to the eye, in world units. Change it through goal.distance.

distance
: 12 });
import web
web
.
function orbitControls(orbit: {
readonly goal: web.OrbitGoal;
}, element: HTMLElement, options?: web.OrbitControlsOptions): web.OrbitControls

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
(
const view: cameras.OrbitCamera
view
,
const canvas: HTMLCanvasElement
canvas
); // drag writes view.goal
const view: 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.

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 view: cameras.OrbitCamera
view
.
OrbitCamera.update(dt: number): void

Eases towards the goal by dt seconds and writes the uniform if the view changed. Call once per frame.

update
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
); // eases, and writes the uniform if the view moved
// ..
});
  • 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. */
function
function pan(dx: number, dy: number, height: number): void

Moves the target by a drag of dx, dy pixels on a view height pixels tall.

pan
(
dx: number
dx
: number,
dy: number
dy
: number,
height: number
height
: number) {
const {
const yaw: number
yaw
,
const distance: number
distance
,
const fov: number
fov
} =
const view: cameras.OrbitCamera
view
.
OrbitCamera.pose: {
readonly yaw: number;
readonly pitch: number;
readonly distance: number;
readonly fov: number;
readonly target: readonly [number, number, number];
}

Reports the eased pose, the view as drawn while it approaches the goal.

pose
;
const
const scale: number
scale
= (2 *
const distance: number
distance
*
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.tan(x: number): number

Returns the tangent of a number.

@param ― x A numeric expression that contains an angle measured in radians.

tan
(
const fov: number
fov
/ 2)) /
height: number
height
;
const
const cos: number
cos
=
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.cos(x: number): number

Returns the cosine of a number.

@param ― x A numeric expression that contains an angle measured in radians.

cos
(
const yaw: number
yaw
);
const
const sin: number
sin
=
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.sin(x: number): number

Returns the sine of a number.

@param ― x A numeric expression that contains an angle measured in radians.

sin
(
const yaw: number
yaw
);
const view: cameras.OrbitCamera
view
.
OrbitCamera.goal: cameras.Orbit

Sets the desired camera pose. update eases the view towards it.

goal
.
Orbit.target: [number, number, number]

Sets the point that the camera orbits and looks at.

target
[0] -= (
const cos: number
cos
*
dx: number
dx
+
const sin: number
sin
*
dy: number
dy
) *
const scale: number
scale
;
const view: cameras.OrbitCamera
view
.
OrbitCamera.goal: cameras.Orbit

Sets the desired camera pose. update eases the view towards it.

goal
.
Orbit.target: [number, number, number]

Sets the point that the camera orbits and looks at.

target
[2] -= (
const cos: number
cos
*
dy: number
dy
-
const sin: number
sin
*
dx: number
dx
) *
const scale: number
scale
;
}

The stampede keeps the eye 1.2 m above its hills, however low input turns it:

const
const view: cameras.OrbitCamera
view
=
import cameras
cameras
.
function orbit(root: TgpuRoot, options: cameras.OrbitOptions): cameras.OrbitCamera

Creates a perspective camera that orbits a target and eases towards a goal set by input.

@returns ― a camera you update every frame and destroy

orbit
(
const root: TgpuRoot
root
, {
OrbitOptions.size: SizeSource

Supplies the size used for the aspect ratio, such as the canvas surface.

size
: {
SizeSource.width: number
width
: 8,
SizeSource.height: number
height
: 6 },
OrbitOptions.target: readonly [number, number, number]

Sets the initial point that the camera orbits and looks at. Change it through goal.target.

target
: [0, 2, 0],
OrbitOptions.distance: number

Sets the initial distance from the target to the eye, in world units. Change it through goal.distance.

distance
: 40,
OrbitOptions.floor?: ((x: number, z: number) => number) | undefined

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
) =>
function heightAt(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
const ground: () => d.v3f

Where the pointer's ray meets the ground, y = 0.

ground
= () => {
'use gpu';
const
const ray: {
origin: d.v3f;
direction: d.v3f;
}
ray
=
import cameras
cameras
.
const rayOf: (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
.
const current: 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.

current
.
TgpuAccessor<WgslStruct<{ viewProjection: Mat4x4f; position: Vec3f; right: Vec3f; up: Vec3f; }>>.$: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}
$
,
const pointer: TgpuUniform<d.WgslStruct<{
ndc: d.Vec2f;
}>>
pointer
.
TgpuUniform<WgslStruct<{ ndc: Vec2f; }>>.$: {
ndc: d.v2f;
}
$
.
ndc: d.v2f
ndc
);
return
const ray: {
origin: d.v3f;
direction: d.v3f;
}
ray
.
origin: d.v3f
origin
+
const ray: {
origin: d.v3f;
direction: d.v3f;
}
ray
.
direction: d.v3f

Gives the unit ray direction.

direction
* (-
const ray: {
origin: d.v3f;
direction: d.v3f;
}
ray
.
origin: d.v3f
origin
.
v3f.y: number
y
/
const ray: {
origin: d.v3f;
direction: d.v3f;
}
ray
.
direction: d.v3f

Gives the unit ray direction.

direction
.
v3f.y: number
y
);
};

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:

// Balls, each a centre and a radius in a vec4f
const
const balls: TgpuReadonly<d.WgslArray<d.Vec4f>>
balls
=
const root: TgpuRoot
root
.
TgpuRoot.createReadonly<d.WgslArray<d.Vec4f>>(typeSchema: d.WgslArray<d.Vec4f>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.Vec4f>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.Vec4f>>> | undefined): TgpuReadonly<d.WgslArray<d.Vec4f>> (+1 overload)

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)

createReadonly
(
import d
d
.
arrayOf<d.Vec4f>(elementType: d.Vec4f, elementCount: number): d.WgslArray<d.Vec4f> (+2 overloads)
export arrayOf

@location ― . Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).

arrayOf
(
import d
d
.
const vec4f: d.Vec4f
vec4f
, 256));
const
const seen: liveLists.LiveLists
seen
=
import liveLists
liveLists
.
function create(root: TgpuRoot, options: liveLists.Options): liveLists.LiveLists

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
(
const root: TgpuRoot
root
, {
ListOptions.capacity: number

Sets the number of slots, including dead slots. Indices range from 0 to capacity - 1.

capacity
: 256,
ListOptions.draws?: readonly liveLists.IndirectDraw[] | undefined

Provides one draw per group. Requires 1 to 256 draws. Default an empty draw for each of groups, for lists that only dispatch.

draws
: [{
vertexCount: number
vertexCount
: 36 }],
alive: (i: number) => boolean

Tests whether slot i holds a live row in a shader. A table supplies this through live(). Default every slot is live.

alive
:
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: <(i: number) => boolean>(inner: (i: number) => boolean) => TgpuGenericFn<(i: number) => boolean> (+2 overloads)
fn
((
i: number
i
: number) => {
'use gpu';
const
const ball: d.v4f
ball
=
const balls: TgpuReadonly<d.WgslArray<d.Vec4f>>
balls
.
TgpuReadonly<WgslArray<Vec4f>>.$: d.v4f[]
$
[
i: number
i
]!;
return
import cameras
cameras
.
const sees: (view: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}, center: d.v3f, radius: number) => boolean

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
.
const current: 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.

current
.
TgpuAccessor<WgslStruct<{ viewProjection: Mat4x4f; position: Vec3f; right: Vec3f; up: Vec3f; }>>.$: {
viewProjection: d.m4x4f;
position: d.v3f;
right: d.v3f;
up: d.v3f;
}
$
,
const ball: d.v4f
ball
.
xyz: d.v3f
xyz
,
const ball: d.v4f
ball
.
v4f.w: number
w
);
})
.
TgpuGenericFn<(i: number) => boolean>.pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<(i: number) => boolean>

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

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

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

inject
()),
});

To find what is under a pointer among things that move on the GPU, use picker, which projects each candidate with this camera.

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:

const clipOf = (world: d.v3f) => {
'use gpu';
return cameras.current.$.viewProjection * d.vec4f(world, 1);
};
const clipBefore = history.previously(clipOf);

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
const at: number[]
at
= [0, 0, 0];
const
const ray: {
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
if (
const view: cameras.OrbitCamera
view
.
CpuView.projectAt(point: ArrayLike<number>, out: cameras.Out3): boolean

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.

projectAt
([0, 2, 0],
const at: number[]
at
)) {
const label: HTMLElement
label
.
ElementCSSInlineStyle.style: CSSStyleDeclaration
style
.
CSSStyleProperties.translate: string

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.

MDN Reference

translate
= `${
const at: number[]
at
[0]! * 800}px ${
const at: number[]
at
[1]! * 600}px`;
}
// The ground under the pointer, y = 0; a miss leaves `aim` where it was
const
const aim: number[]
aim
= [0, 0, 0];
const
const pointing: {
move(x: number, y: number): void;
}
pointing
= {
function move(x: number, y: number): void
move
(
x: number
x
: number,
y: number
y
: number) {
const view: 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.

rayAt
(
x: number
x
,
y: number
y
,
const ray: {
origin: number[];
direction: number[];
}
ray
);
import cameras
cameras
.
function hitHeight(ray: Ray, height: number | ((x: number, z: number) => number), out?: cameras.Out3, options?: cameras.HitOptions): number | undefined

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
(
const ray: {
origin: number[];
direction: number[];
}
ray
, 0,
const aim: 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:

const orbit = Gesture.Pan()
.runOnJS(true)
.onChange((event) => {
cameras.turn(view.goal, event.changeX, event.changeY);
});
const zoom = Gesture.Pinch()
.runOnJS(true)
.onChange((event) => {
view.goal.distance /= event.scaleChange;
});

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.

import {
import cameras
cameras
} from '@typegpu/kit';
import {
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
} from 'wgpu-matrix';
const
const chase: cameras.CustomCamera
chase
=
import cameras
cameras
.
function custom(root: TgpuRoot): cameras.CustomCamera

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.

@returns ― a camera you write and destroy

custom
(
const root: TgpuRoot
root
);
const
const eye: Float32Array<ArrayBuffer>
eye
= new
var Float32Array: Float32ArrayConstructor
new (length: number) => Float32Array<ArrayBuffer> (+6 overloads)
Float32Array
(3);
const
const up: number[]
up
= [0, 1, 0];
const
const view: Float32Array<ArrayBufferLike>
view
=
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
create: (v0?: number, v1?: number, v2?: number, v3?: number, v4?: number, v5?: number, v6?: number, v7?: number, v8?: number, v9?: number, v10?: number, v11?: number, v12?: number, v13?: number, v14?: number, v15?: number) => Float32Array<ArrayBufferLike>
create
();
const
const projection: Float32Array<ArrayBufferLike>
projection
=
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
perspective: <Float32Array<ArrayBufferLike>>(fieldOfViewYInRadians: number, aspect: number, zNear: number, zFar: number, dst?: Float32Array<ArrayBufferLike> | undefined) => Float32Array<ArrayBufferLike>
perspective
(0.8,
const aspect: number
aspect
, 0.1, 500);
// after the hero moves
const eye: Float32Array<ArrayBuffer>
eye
[0] =
const hero: Float32Array<ArrayBufferLike>
hero
[0]!;
const eye: Float32Array<ArrayBuffer>
eye
[1] =
const hero: Float32Array<ArrayBufferLike>
hero
[1]! + 3;
const eye: Float32Array<ArrayBuffer>
eye
[2] =
const hero: Float32Array<ArrayBufferLike>
hero
[2]! + 6;
const chase: cameras.CustomCamera
chase
.
CustomCamera.write(view: Mat4Arg, projection: Mat4Arg): void

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).

write
(
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
lookAt: <Float32Array<ArrayBufferLike>>(eye: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: Float32Array<ArrayBufferLike> | undefined) => Float32Array<ArrayBufferLike>
lookAt
(
const eye: Float32Array<ArrayBuffer>
eye
,
const hero: Float32Array<ArrayBufferLike>
hero
,
const up: number[]
up
,
const view: Float32Array<ArrayBufferLike>
view
),
const projection: Float32Array<ArrayBufferLike>
projection
);
  • 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:

import {
import cameras
cameras
} from '@typegpu/kit';
import {
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
} from 'wgpu-matrix';
const
const views: cameras.LayeredCamera
views
=
import cameras
cameras
.
function layered(root: TgpuRoot, count: number): cameras.LayeredCamera

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
(
const root: TgpuRoot
root
, 2);
const
const casters: passes.Passes<{
readonly depth: typeof passes.depth;
}>
casters
=
import passes
passes
.
function create<{
depth: {
format: "depth16unorm";
layers: 2;
};
}, {
readonly depth: typeof passes.depth;
}>(root: TgpuRoot, screen: Targets<{
depth: {
format: "depth16unorm";
layers: 2;
};
}>, writes: {
readonly depth: typeof passes.depth;
}, options?: passes.TargetOptions<{
depth: {
format: "depth16unorm";
layers: 2;
};
}> | 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 maps: Targets<{
depth: {
format: "depth16unorm";
layers: 2;
};
}>
maps
, {
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 views: 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.

inject
());
const
const sun: Float32Array<ArrayBufferLike>
sun
=
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
lookAt: <Float32Array<ArrayBufferLike>>(eye: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: Float32Array<ArrayBufferLike> | undefined) => Float32Array<ArrayBufferLike>
lookAt
([20, 40, 20], [0, 0, 0], [0, 1, 0]);
const views: cameras.LayeredCamera
views
.
LayeredCamera.write(layer: number, view: Mat4Arg, projection: Mat4Arg): void

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
(0,
const sun: Float32Array<ArrayBufferLike>
sun
,
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
ortho: <BaseArgType>(left: number, right: number, bottom: number, top: number, near: number, far: number, dst?: BaseArgType | undefined) => BaseArgType
ortho
(-10, 10, -10, 10, 0, 200)); // near
const views: cameras.LayeredCamera
views
.
LayeredCamera.write(layer: number, view: Mat4Arg, projection: Mat4Arg): void

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
(1,
const sun: Float32Array<ArrayBufferLike>
sun
,
const mat4: {
add: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(a: Mat4Arg, b: Mat4Arg, dst?: T | undefined) => T;
aim: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(position: Vec3Arg, target: Vec3Arg, up: Vec3Arg, dst?: T | undefined) => T;
axisRotate: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(m: Mat4Arg, axis: Vec3Arg, angleInRadians: number, dst?: T | undefined) => T;
... 44 more ...;
uniformScaling: <T extends Mat4Arg = Float32Array<ArrayBufferLike>>(s: number, dst?: T | undefined) => T;
}

4x4 Matrix functions that default to returning Float32Array

@namespace

mat4
.
ortho: <BaseArgType>(left: number, right: number, bottom: number, top: number, near: number, far: number, dst?: BaseArgType | undefined) => BaseArgType
ortho
(-40, 40, -40, 40, 0, 200)); // far
  • 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.
  • It has no CPU project, ray or rayAt.
  • 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.