Skip to content

@typegpu/kittrails

Trails

Ribbons behind moving things, pushed from kernels or written from the CPU.

kit.trails

module

trails.create(root, { capacity, length, width?, … }) → Trails

rootborrows
the root
width?takes
a 'use gpu' function; it and shade carry the fills of what they read, such as tgpu.fn(swell).pipe(noise.inject()). The ribbons face the camera of the pass description they are drawn in, which width and shade read too
Trailsreturns
a set of trails you destroy
schemas
SampleRingSettings
constants
defaults
types
JoinTrails
Optionscreate · 11 fields
capacity

number

Sets the number of trails to an integer of at least 1.

length

number

Sets the number of points per trail to an integer of at least 2.

label?

string= trails

Sets the drawing pipeline label.

blend?

Exclude<lines.Blend, 'opaque'>= add

Selects add for light, over for smoke or ink, translucent for the passes' translucency targets, or a custom blend state.

width?

(point: Sample) => number= settings.width at the newest point, tapering with along to 0 at the oldest point of a full trail

Returns the trail width in world units at a point.

shade?

(point: Sample, across: number) => d.v4f= settings.color, with soft edges and a fade towards the oldest point

Returns premultiplied fragment colour.

head?

(trail: number) => d.v3f= the newest point

Returns where a trail's newest end is drawn, in place of its newest point, such as where a particle system draws the particle that pushes it: tgpu.fn(sparks.drawnAt).pipe(sparks.inject()).

join?

Join= round

Selects the join at a point, such as lines.joins.miter.

startCap?

Join= round

Selects the cap at the newest end, such as lines.caps.arrow.

endCap?

Join= round

Selects the cap at the oldest end.

settings?

Partial<d.InferInput<typeof Settings>>= defaults, with white colour and width 0.1

Sets initial colour and width for the default callbacks.

records
  • drawing.drawrender
  • clearencoder

drawing, added to passes, after the passes that push; clear whenever you like.

owns

3 buffers ·a lines

The points, the rings and the settings, and the lines that draw them.

lifecycle

destroy()[Symbol.dispose]

injects
  • per-instance

    push, reset, pointAt and held, for shaders piped with it.

binds
  • 2 storage

    push or pointAt, the points and the rings.

  • 1 storage

    held or reset alone, the rings.

  • 2 storage · 2 uniform

    drawing, the points and the rings, the settings and passes.pixels.

a full trail
0.80 s
Drag to write a whole trail, keeping its newest length points, or compare the four kernel-pushed orbits. Fixed-step pushes preserve duration across displays; per-frame pushes shorten it at higher rates, and changing length, join, cap or blend rebuilds the set.

A wake, a drawn line or a comet’s path follows recent positions. Rebuilding ribbons on the CPU requires vertices for every trail each frame, with joins that can crack at sharp turns. Store each trail as a ring of recent points on the GPU. Push points from a kernel or write a whole line from the CPU. One draw turns all trails into lines with joins and caps.

import {
import trails
trails
} from '@typegpu/kit';
const
const wakes: trails.Trails
wakes
=
import trails
trails
.
function create(root: TgpuRoot, options: trails.Options): trails.Trails

Creates trails as rings of recent points, pushed by kernels or written on the CPU, and draws them as lines. Throws unless capacity is an integer of at least 1 and length is an integer of at least 2.

@injects ― per-instance push, reset, pointAt and held, for shaders piped with it

@binds ― 2 storage: push or pointAt, the points and the rings

@binds ― 1 storage: held or reset alone, the rings

@binds ― 2 storage, 2 uniform: drawing, the points and the rings, the settings and passes.pixels

@owns ― 3 buffers, a lines: the points, the rings and the settings, and the lines that draw them

@records ― drawing, added to passes, after the passes that push; clear whenever you like.

@param ― options.width a 'use gpu' function; it and shade carry the fills of what they read, such as tgpu.fn(swell).pipe(noise.inject()). The ribbons face the camera of the pass description they are drawn in, which width and shade read too

@returns ― a set of trails you destroy

create
(
const root: TgpuRoot
root
, {
Options.capacity: number

Sets the number of trails to an integer of at least 1.

capacity
: 64,
Options.length: number

Sets the number of points per trail to an integer of at least 2. Pushing beyond this count drops the oldest point.

length
: 48,
Options.settings?: Partial<d.InferInput<d.WgslStruct<{
color: d.Vec4f;
width: d.F32;
}>>> | undefined

Sets initial colour and width for the default callbacks. Default defaults, with white colour and width 0.1.

settings
: {
width: number
width
: 0.2 } });
const
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
=
import passes
passes
.
function create(root: TgpuRoot, screen: passes.Screen, options?: passes.Options): passes.Passes<{
readonly color: typeof passes.color;
}> (+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 surface: web.Surface
surface
).
Passes<{ readonly color: typeof color; }>.pipe(fill: Inject): passes.Passes<{
readonly color: typeof passes.color;
}>

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

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

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

inject
());
const
const drawn: passes.Drawn
drawn
=
const canvas: passes.Passes<{
readonly color: typeof passes.color;
}>
canvas
.
Passes<{ readonly color: typeof color; }>.add<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
normal: d.Vec3f;
color: d.Vec4f;
along: d.F32;
segment: d.Vec3f;
radii: d.Vec2f;
rim: d.Vec4f;
ends: d.U32;
line: d.U32;
}>, {
world: d.v3f;
normal: d.v3f;
color: d.v4f;
}>(this: passes.Passes<passes.Fed<{
world: d.v3f;
normal: d.v3f;
color: d.v4f;
}, {
...;
}>>, drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
normal: d.Vec3f;
color: d.Vec4f;
along: d.F32;
segment: d.Vec3f;
radii: d.Vec2f;
rim: d.Vec4f;
ends: d.U32;
line: d.U32;
}>, {
world: d.v3f;
normal: d.v3f;
color: d.v4f;
}>): passes.Drawn (+1 overload)

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

add
(
const wakes: trails.Trails
wakes
.
Trails.drawing: passes.Drawing<d.WgslStruct<{
position: d.Vec4f;
world: d.Vec3f;
normal: d.Vec3f;
color: d.Vec4f;
along: d.F32;
segment: d.Vec3f;
radii: d.Vec2f;
rim: d.Vec4f;
ends: d.U32;
line: d.U32;
}>, {
world: d.v3f;
normal: d.v3f;
color: d.v4f;
}> & {
inject(): Inject;
}

Draws trails with at least two points, newest first, as lines when added to a pass description. It tests depth without writing it.

drawing
);
const
const follow: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
follow
=
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
.
computeFn: <{
gid: d.BuiltinGlobalInvocationId;
}>(options: {
in: {
gid: d.BuiltinGlobalInvocationId;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
gid: d.BuiltinGlobalInvocationId;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [64],
in: {
gid: d.BuiltinGlobalInvocationId;
}
in
: {
gid: d.BuiltinGlobalInvocationId
gid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
globalInvocationId: d.BuiltinGlobalInvocationId
globalInvocationId
},
})(({
gid: d.v3u
gid
}) => {
'use gpu';
if (
gid: d.v3u
gid
.
v3u.x: number
x
>= 64) return;
const wakes: trails.Trails
wakes
.
Trails.push: (trail: number, position: d.v3f) => void

Pushes a point to the newest end from a shader created from root.pipe(wakes.inject()). Once the trail holds length points, it drops the oldest. Only one thread may push a given trail per dispatch; concurrent pushes race.

push
(
gid: d.v3u
gid
.
v3u.x: number
x
,
const swimmers: TgpuReadonly<d.WgslArray<d.Vec3f>>
swimmers
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
gid: d.v3u
gid
.
v3u.x: number
x
]!);
});
const
const following: TgpuComputePipeline
following
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const wakes: trails.Trails
wakes
.
Trails.inject(): Inject

Fills trail accessors for pipelines created from the configuration you pipe it into.

inject
()).
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const follow: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
follow
});
import frames
frames
.
function animate(loop: Pick<frames.Frames, "run"> & Partial<Pick<frames.Frames, "seconds">>, options?: frames.AnimateOptions): frames.Animation
export animate

Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps back, and stops while the surface is paused. Destroying the animation keeps the loop alive. Another animation on the same loop continues from its recorded time. A frame error stops the animation.

@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before

@returns ― an animation you destroy to stop the frames

animate
(
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
for (let
let steps: number
steps
=
const sim: clock.FixedStep
sim
.
FixedStep.advance(dt: number): number

Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

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

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

dt
);
let steps: number
steps
> 0;
let steps: number
steps
--) {
const school: {
step(pass: TgpuComputePass): void;
}
school
.
function step(pass: TgpuComputePass): void
step
(
const compute: TgpuComputePass
compute
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const following: TgpuComputePipeline
following
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(1);
}
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

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

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

record
(
frame: frames.Frame
frame
,
const drawn: passes.Drawn
drawn
);
}),
);
  • A push adds a point and drops the oldest once the trail holds length points. Push once per fixed step. Pushing once per frame makes trails shorter on faster displays.
  • Allow only one thread to push a given trail per dispatch. Multiple threads would race to update its ring.
  • reset(trail) clears one trail from a kernel and counts as its push for that dispatch. The same thread can push after resetting. Do this when respawning an object to avoid a streak between its old and respawn positions. clear(encoder) clears all trails.
  • canvas.add(wakes.drawing) builds the pipeline into a pass description, shaped for the camera the description injects. One indexed draw renders all trails with at least two points, using one instance per segment.
  • Ribbons blend and test depth without writing it. Draw them after opaque renderers, or pass blend: 'translucent' to draw them into the translucency targets in any order. See Passes for their target outputs.
  • The default ribbon starts at settings.width and tapers to zero at the oldest point a full trail can hold. It uses settings.color and is brightest at its centre. Change width with wakes.settings.patch({ width: 0.4 }).
  • Points land on fixed steps, while particles draw between them, ahead of the last step. head draws a trail’s newest end where its particle is drawn instead: head: tgpu.fn(sparks.drawnAt).pipe(sparks.inject()). The fireworks streaks push a point every few steps and stay on their stars this way.
  • The solar system example pushes each comet’s trail from the kernel that moves it.

write(trail, positions) replaces a trail with x, y and z values ordered oldest first. It keeps the newest length points. write(trail, []) clears it. Rewrite a player-drawn line whenever it changes:

const
const line: Float32Array<ArrayBuffer>
line
= new
var Float32Array: Float32ArrayConstructor
new (length: number) => Float32Array<ArrayBuffer> (+6 overloads)
Float32Array
(256 * 3);
function
function threadChanged(points: readonly {
x: number;
y: number;
}[]): void
threadChanged
(
points: readonly {
x: number;
y: number;
}[]
points
: readonly {
x: number
x
: number;
y: number
y
: number }[]) {
const
const kept: {
x: number;
y: number;
}[]
kept
=
points: readonly {
x: number;
y: number;
}[]
points
.
ReadonlyArray<{ x: number; y: number; }>.slice(start?: number, end?: number): {
x: number;
y: number;
}[]

Returns a section of an array.

@param ― start The beginning of the specified portion of the array.

@param ― end The end of the specified portion of the array. This is exclusive of the element at the index 'end'.

slice
(-256);
for (const [
const i: number
i
,
const point: {
x: number;
y: number;
}
point
] of
const kept: {
x: number;
y: number;
}[]
kept
.
Array<{ x: number; y: number; }>.entries(): ArrayIterator<[number, {
x: number;
y: number;
}]>

Returns an iterable of key, value pairs for every entry in the array

entries
()) {
const line: Float32Array<ArrayBuffer>
line
[
const i: number
i
* 3] =
const point: {
x: number;
y: number;
}
point
.
x: number
x
;
const line: Float32Array<ArrayBuffer>
line
[
const i: number
i
* 3 + 1] =
const point: {
x: number;
y: number;
}
point
.
y: number
y
;
}
const thread: trails.Trails
thread
.
Trails.write(trail: number, positions: ArrayLike<number>): void

Writes x, y and z coordinates from oldest to newest, keeping only the newest length points. Use write(trail, []) to empty a trail. Queue writes run before frame commands, so kernel pushes in that frame follow these points.

write
(0,
const line: Float32Array<ArrayBuffer>
line
.
Float32Array<ArrayBuffer>.subarray(begin?: number, end?: number): Float32Array<ArrayBuffer>

Gets a new Float32Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@param ― begin The index of the beginning of the array.

@param ― end The index of the end of the array.

subarray
(0,
const kept: {
x: number;
y: number;
}[]
kept
.
Array<{ x: number; y: number; }>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
* 3));
}
  • Queue writes arrive before frame commands. Kernel pushes in that frame append to the written points.
  • A closed loop repeats its first point at the end.
  • Under a flat camera, trails lie in its plane and use z as depth.

width(point) returns the width in world units. shade(point, across) returns premultiplied fragment colour, with across from -1 to 1 across the ribbon. Both receive a trails.Sample with position, trail and along. along is 0 at the newest point and 1 at the oldest point a full trail can hold.

import {
import trails
trails
} from '@typegpu/kit';
const
const hues: TgpuReadonly<d.WgslArray<d.Vec3f>>
hues
=
const root: TgpuRoot
root
.
TgpuRoot.createReadonly<d.WgslArray<d.Vec3f>>(typeSchema: d.WgslArray<d.Vec3f>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.Vec3f>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.Vec3f>>> | undefined): TgpuReadonly<d.WgslArray<d.Vec3f>> (+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.Vec3f>(elementType: d.Vec3f, elementCount: number): d.WgslArray<d.Vec3f> (+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 vec3f: d.Vec3f
vec3f
, 8));
const
const swell: (point: trails.Sample) => number
swell
= (
point: {
position: d.v3f;
trail: number;
along: number;
}
point
:
import trails
trails
.
type Sample = {
position: d.v3f;
trail: number;
along: number;
}

This schema describes a trail point passed to width and shade.

This type describes a trail point in shaders.

Sample
) => {
'use gpu';
return 0.3 *
import std
std
.
function sin(value: number): number (+1 overload)
export sin
sin
(
point: {
position: d.v3f;
trail: number;
along: number;
}
point
.
along: number

Measures distance along stored points from 0 at the newest to 1 at the oldest point of a full trail, length - 1 points behind.

along
*
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
);
};
const
const ink: (point: trails.Sample, across: number) => d.v4f
ink
= (
point: {
position: d.v3f;
trail: number;
along: number;
}
point
:
import trails
trails
.
type Sample = {
position: d.v3f;
trail: number;
along: number;
}

This schema describes a trail point passed to width and shade.

This type describes a trail point in shaders.

Sample
,
across: number
across
: number) => {
'use gpu';
const
const alpha: number
alpha
= (1 -
import std
std
.
function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)
export smoothstep
smoothstep
(0.6, 1,
import std
std
.
function abs(value: number): number (+1 overload)
export abs
abs
(
across: number
across
))) * (1 -
point: {
position: d.v3f;
trail: number;
along: number;
}
point
.
along: number

Measures distance along stored points from 0 at the newest to 1 at the oldest point of a full trail, length - 1 points behind.

along
);
return
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const hues: TgpuReadonly<d.WgslArray<d.Vec3f>>
hues
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
point: {
position: d.v3f;
trail: number;
along: number;
}
point
.
trail: number

Identifies the trail from 0 to capacity - 1.

trail
]! *
const alpha: number
alpha
,
const alpha: number
alpha
);
};
const
const strokes: trails.Trails
strokes
=
import trails
trails
.
function create(root: TgpuRoot, options: trails.Options): trails.Trails

Creates trails as rings of recent points, pushed by kernels or written on the CPU, and draws them as lines. Throws unless capacity is an integer of at least 1 and length is an integer of at least 2.

@injects ― per-instance push, reset, pointAt and held, for shaders piped with it

@binds ― 2 storage: push or pointAt, the points and the rings

@binds ― 1 storage: held or reset alone, the rings

@binds ― 2 storage, 2 uniform: drawing, the points and the rings, the settings and passes.pixels

@owns ― 3 buffers, a lines: the points, the rings and the settings, and the lines that draw them

@records ― drawing, added to passes, after the passes that push; clear whenever you like.

@param ― options.width a 'use gpu' function; it and shade carry the fills of what they read, such as tgpu.fn(swell).pipe(noise.inject()). The ribbons face the camera of the pass description they are drawn in, which width and shade read too

@returns ― a set of trails you destroy

create
(
const root: TgpuRoot
root
, {
Options.capacity: number

Sets the number of trails to an integer of at least 1.

capacity
: 8,
Options.length: number

Sets the number of points per trail to an integer of at least 2. Pushing beyond this count drops the oldest point.

length
: 64,
Options.blend?: "over" | "add" | "translucent" | GPUBlendState | undefined

Selects add for light, over for smoke or ink, translucent for the passes' translucency targets, or a custom blend state. Default add.

blend
: 'over',
Options.width?: ((point: trails.Sample) => number) | undefined

Returns the trail width in world units at a point. Default settings.width at the newest point, tapering with along to 0 at the oldest point of a full trail.

width
:
const swell: (point: trails.Sample) => number
swell
,
Options.shade?: ((point: trails.Sample, across: number) => d.v4f) | undefined

Returns premultiplied fragment colour. across ranges from -1 to 1 across the width. The drawing multiplies it by how much of the pixel the trail covers. Default settings.color, with soft edges and a fade towards the oldest point.

shade
:
const ink: (point: trails.Sample, across: number) => d.v4f
ink
,
});

Both functions can read cameras.current.$. If one reads another module, fill that dependency. For example, a width function that samples cached Perlin noise uses width: tgpu.fn(swell).pipe(noise.inject()).

Ribbons take the joins and caps of lines, round by default. Set join for the joins, startCap for the newest end and endCap for the oldest.

import {
import lines
lines
,
import trails
trails
} from '@typegpu/kit';
const
const arrows: trails.Trails
arrows
=
import trails
trails
.
function create(root: TgpuRoot, options: trails.Options): trails.Trails

Creates trails as rings of recent points, pushed by kernels or written on the CPU, and draws them as lines. Throws unless capacity is an integer of at least 1 and length is an integer of at least 2.

@injects ― per-instance push, reset, pointAt and held, for shaders piped with it

@binds ― 2 storage: push or pointAt, the points and the rings

@binds ― 1 storage: held or reset alone, the rings

@binds ― 2 storage, 2 uniform: drawing, the points and the rings, the settings and passes.pixels

@owns ― 3 buffers, a lines: the points, the rings and the settings, and the lines that draw them

@records ― drawing, added to passes, after the passes that push; clear whenever you like.

@param ― options.width a 'use gpu' function; it and shade carry the fills of what they read, such as tgpu.fn(swell).pipe(noise.inject()). The ribbons face the camera of the pass description they are drawn in, which width and shade read too

@returns ― a set of trails you destroy

create
(
const root: TgpuRoot
root
, {
Options.capacity: number

Sets the number of trails to an integer of at least 1.

capacity
: 16,
Options.length: number

Sets the number of points per trail to an integer of at least 2. Pushing beyond this count drops the oldest point.

length
: 32,
Options.join?: ((join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f) | undefined

Selects the join at a point, such as lines.joins.miter. Default round.

join
:
import lines
lines
.
const joins: {
bevel: (join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f;
miter: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
round: (join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f;
}

Joins segments with round, miter or bevel.

joins
.
miter: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f
miter
,
Options.startCap?: ((join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f) | undefined

Selects the cap at the newest end, such as lines.caps.arrow. Default round.

startCap
:
import lines
lines
.
const caps: {
arrow: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
butt: (join: JoinInput, _joinVertexIndex: number, _maxJoinCount: number) => v2f;
round: (join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f;
square: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
triangle: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
wedge: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
}
export caps
caps
.
arrow: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f
arrow
,
Options.endCap?: ((join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f) | undefined

Selects the cap at the oldest end. Default round.

endCap
:
import lines
lines
.
const caps: {
arrow: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
butt: (join: JoinInput, _joinVertexIndex: number, _maxJoinCount: number) => v2f;
round: (join: JoinInput, joinVertexIndex: number, maxJoinCount: number) => v2f;
square: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
triangle: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
wedge: (join: JoinInput, joinVertexIndex: number, _maxJoinCount: number) => v2f;
}
export caps
caps
.
butt: (join: JoinInput, _joinVertexIndex: number, _maxJoinCount: number) => v2f
butt
,
});

Apply wakes.inject() to a shader to read trail points. held(trail) returns the point count. pointAt(trail, k) reads point k counting backward from the newest, with k below held. This kernel finds trail directions for arrowheads or sprites:

const
const headings: TgpuMutable<d.WgslArray<d.Vec3f>>
headings
=
const root: TgpuRoot
root
.
TgpuRoot.createMutable<d.WgslArray<d.Vec3f>>(typeSchema: d.WgslArray<d.Vec3f>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.Vec3f>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.Vec3f>>> | undefined): TgpuMutable<d.WgslArray<d.Vec3f>> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Can be mutated in-place on the GPU. 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)

createMutable
(
import d
d
.
arrayOf<d.Vec3f>(elementType: d.Vec3f, elementCount: number): d.WgslArray<d.Vec3f> (+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 vec3f: d.Vec3f
vec3f
, 64));
const
const aim: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
aim
=
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
.
computeFn: <{
gid: d.BuiltinGlobalInvocationId;
}>(options: {
in: {
gid: d.BuiltinGlobalInvocationId;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
gid: d.BuiltinGlobalInvocationId;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [64],
in: {
gid: d.BuiltinGlobalInvocationId;
}
in
: {
gid: d.BuiltinGlobalInvocationId
gid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
globalInvocationId: d.BuiltinGlobalInvocationId
globalInvocationId
},
})(({
gid: d.v3u
gid
}) => {
'use gpu';
if (
gid: d.v3u
gid
.
v3u.x: number
x
>= 64) return;
let
let heading: d.v3f
heading
=
import d
d
.
const vec3f: d.Vec3f
() => d.v3f (+5 overloads)
vec3f
();
if (
const wakes: trails.Trails
wakes
.
Trails.held: (trail: number) => number

Returns the stored point count, up to length.

held
(
gid: d.v3u
gid
.
v3u.x: number
x
) >= 2)
let heading: d.v3f
heading
=
const wakes: trails.Trails
wakes
.
Trails.pointAt: (trail: number, k: number) => d.v3f

Returns point k, counting back from the newest. k must be below held(trail).

pointAt
(
gid: d.v3u
gid
.
v3u.x: number
x
, 0) -
const wakes: trails.Trails
wakes
.
Trails.pointAt: (trail: number, k: number) => d.v3f

Returns point k, counting back from the newest. k must be below held(trail).

pointAt
(
gid: d.v3u
gid
.
v3u.x: number
x
, 1);
const headings: TgpuMutable<d.WgslArray<d.Vec3f>>
headings
.
TgpuMutable<WgslArray<Vec3f>>.$: d.v3f[]
$
[
gid: d.v3u
gid
.
v3u.x: number
x
] =
let heading: d.v3f
heading
;
});
const
const aiming: TgpuComputePipeline
aiming
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const wakes: trails.Trails
wakes
.
Trails.inject(): Inject

Fills trail accessors for pipelines created from the configuration you pipe it into.

inject
()).
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const aim: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
aim
});
Part
points A storage buffer of capacity * length points, trail t in the length slots from t * length.
rings One trails.Ring per trail in a storage buffer. Each stores the next insertion slot and point count.
settings Colour and width for the defaults, a uniform.

Each point takes 16 bytes, so 64 trails of 48 points take 48 KiB.

  • All trails in a set have the same length. Size it for the longest trail.
  • A stopped trail keeps its points until pushes or a write replace them. To fade it, keep each trail’s stop time in a buffer of your own and read it by point.trail in shade.
  • along counts points rather than distance. Distance-based dashes and textures need your own calculation.
  • Ribbons are joined on the screen, so they always face the camera.
  • A segment that crosses the near plane is cut there with no cap.
  • A point pushed where the last one lies ends the ribbon there with endCap and starts it again with startCap. Unless both caps are round, push only points that moved.