Write a module
Write a module that moves 65 536 particles through noise, use it from your scene and test what it records.
Build the flow field example: 65 536 particles that drift through Perlin noise. You write the particles as a module, createFlowField, the way the kit writes its own. A module is a function, its factory, that borrows your root, allocates its GPU resources and returns methods that record work into passes you open. Your scene makes the module and records each frame. Modules is the one-page reference for what you write here.
The GPU runs the same small function for every particle at once. You write those functions in TypeScript, and TypeGPU turns them into shaders, the programs the GPU runs. Slime mould explains them at a slower pace.
-
Put the particles in a buffer, and scatter them.
A buffer is GPU memory that holds many values of one shape. Each
Particleholds a position and the direction it last moved in. Positions are in clip space, where the canvas spans −1 to 1 on both axes.Create
flow-field.ts. The factory takes the root and an options object, and allocates the buffer once, when the scene makes the module. Nothing allocates while a frame records.flow-field.ts import {owner, typefunction owner(message?: string): OwnerCollects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its
destroyas your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such ascapture,takeor a camera'supdate, should return early ondestroyedinstead of callingusable.const own = owner('The swarm was destroyed.');const screen = own(targets.create(root, surface, { hdr }));const settings = own(root.createUniform(Settings));own(() => {for (const buffer of reads) buffer.destroy();});return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };SizeSource } from '@typegpu/kit';(alias) interface SizeSourceimport SizeSourceSupplies a size in pixels, such as a canvas surface. Read at each
fit.import {randf } from '@typegpu/noise';const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}import {d,import dtgpu, typeconst tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}TgpuComputePass, type(alias) interface TgpuComputePassimport TgpuComputePassA compute pass recording into a
TgpuCommandEncoder
.
Dispatch either by binding TypeGPU pipelines to it (
pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style viapass.setPipeline(pipeline)followed bypass.dispatchWorkgroups(...).Call
end()when done recording.TgpuRoot } from 'typegpu';(alias) interface TgpuRootimport TgpuRootconstWORKGROUP = 64;const WORKGROUP: 64constParticle =const Particle: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>d.import dstruct({struct<{position: d.Vec2f;direction: d.Vec2f;}>(props: {position: d.Vec2f;direction: d.Vec2f;}): d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>export structCreates a struct schema that can be used to construct GPU buffers. Ensures proper alignment and padding of properties (as opposed to a
d.unstructschema). The order of members matches the passed in properties object.position:position: d.Vec2fd.import dvec2f,const vec2f: d.Vec2fdirection:direction: d.Vec2fd.import dvec2f });const vec2f: d.Vec2fexport interfaceFlowFieldOptions {interface FlowFieldOptionsreadonlycount: number;FlowFieldOptions.count: number/** Seconds per `step`. */readonlystep: number;FlowFieldOptions.step: numberSeconds per
step.readonlyformat:FlowFieldOptions.format: GPUTextureFormatGPUTextureFormat;type GPUTextureFormat = "astc-10x10-unorm" | "astc-10x10-unorm-srgb" | "astc-10x5-unorm" | "astc-10x5-unorm-srgb" | "astc-10x6-unorm" | "astc-10x6-unorm-srgb" | "astc-10x8-unorm" | "astc-10x8-unorm-srgb" | "astc-12x10-unorm" | "astc-12x10-unorm-srgb" | "astc-12x12-unorm" | "astc-12x12-unorm-srgb" | "astc-4x4-unorm" | "astc-4x4-unorm-srgb" | "astc-5x4-unorm" | "astc-5x4-unorm-srgb" | "astc-5x5-unorm" | "astc-5x5-unorm-srgb" | "astc-6x5-unorm" | "astc-6x5-unorm-srgb" | "astc-6x6-unorm" | "astc-6x6-unorm-srgb" | "astc-8x5-unorm" | "astc-8x5-unorm-srgb" | ... 76 more ... | "stencil8"/** What the particles are drawn into, such as the canvas surface. */readonlysize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
SizeSource;(alias) interface SizeSourceimport SizeSourceSupplies a size in pixels, such as a canvas surface. Read at each
fit.}export functioncreateFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {}root:root: TgpuRootTgpuRoot,(alias) interface TgpuRootimport TgpuRootoptions:options: FlowFieldOptionsFlowFieldOptions) {interface FlowFieldOptionsconst {count,const count: numberstep,const step: numberSeconds per
step.format,const format: GPUTextureFormatsize } =const size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
options;options: FlowFieldOptionsconstown =const own: Ownerowner('The flow field was destroyed.');function owner(message?: string): OwnerCollects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its
destroyas your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such ascapture,takeor a camera'supdate, should return early ondestroyedinstead of callingusable.const own = owner('The swarm was destroyed.');const screen = own(targets.create(root, surface, { hdr }));const settings = own(root.createUniform(Settings));own(() => {for (const buffer of reads) buffer.destroy();});return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };constparticles =const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagown(const own: Owner<TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlag>(item: TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlag) => TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagTakes
itemto free atdestroyand returns it. Passesundefinedthrough. Frees a fixed resource, such as one fromroot.createUniform, through itsbuffer, and runs a cleanup function. Afterdestroy, freesitemat once.root.root: TgpuRootcreateBuffer(TgpuRoot.createBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>(typeSchema: d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>> | undefined): TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader.
d.import darrayOf(arrayOf<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>(elementType: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>, elementCount: number): d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>> (+2 overloads)export arrayOfParticle,const Particle: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>count)).const count: number$usage('storage'));TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagconstwritable =const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>particles.const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagas('mutable');TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"mutable">(usage: "mutable"): TgpuMutable<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>constreadable =const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>particles.const particles: TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagas('readonly');TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"readonly">(usage: "readonly"): TgpuReadonly<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>sizeis anything with awidthandheightin pixels, such as the canvas surface. The module reads it when it draws.ownercollects what you pass throughownand frees it all atdestroy. Free the particle buffer with the module; the small uniforms below can wait for the garbage collector.writableandreadableare the same buffer, bound for writing in kernels and for reading in the vertex function. WebGPU forbids writable storage in a vertex function.
A kernel is a compute function the GPU runs many times at once. Each run is a thread, numbered by
gid.xfrom 0, and threadnplaces particlen. Threads start in workgroups of 64, so dispatch enough workgroups to cover every particle. The last workgroup can run past the end of the buffer, and the bounds check stops those threads:Change the particle count to see dispatch round up to whole workgroups of 64 threads. The bounds check if (gid.x >= count) returnstops extra threads before buffer access.Add the
seedkernel and return the module’s first recording method:flow-field.ts /** A random position across the canvas, in clip space. */constspawn = () => {const spawn: () => d.v2fA random position across the canvas, in clip space.
'use gpu';returnd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)randf.const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}sample(),sample: () => numberReturns a random f32 value in [0, 1) range.
randf.const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}sample()) * 2 - 1;sample: () => numberReturns a random f32 value in [0, 1) range.
};// Inside createFlowFieldconstseedValue =const seedValue: TgpuUniform<d.F32>root.root: TgpuRootcreateUniform(TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.F32>>) => void) | undefined): TgpuUniform<d.F32> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use
TgpuRoot.createBuffer
.
d.import df32);const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
constworkgroups =const workgroups: numberMath.var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.
ceil(Math.ceil(x: number): numberReturns the smallest integer greater than or equal to its numeric argument.
count /const count: numberWORKGROUP);const WORKGROUP: 64constseedKernel =const seedKernel: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>tgpu.const tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}computeFn({computeFn: <{gid: d.BuiltinGlobalInvocationId;}>(options: {in: {gid: d.BuiltinGlobalInvocationId;};workgroupSize: number[];}) => TgpuComputeFnShell<{gid: d.BuiltinGlobalInvocationId;}> (+1 overload)workgroupSize: [workgroupSize: number[]WORKGROUP],const WORKGROUP: 64in: {in: {gid: d.BuiltinGlobalInvocationId;}gid:gid: d.BuiltinGlobalInvocationIdd.import dbuiltin.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 builtinglobalInvocationId },globalInvocationId: d.BuiltinGlobalInvocationId})(({gid }) => {gid: d.v3u'use gpu';if (gid.gid: d.v3ux >=v3u.x: numbercount) return;const count: numberrandf.const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}seed2(seed2: (seed: d.v2f) => voidThreads do not share the generator's
State. As a result, unless you change the seed in each thread, each thread will produce the same sequence.randf.randSeed2sets the private seed of the thread.d.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)d.import df32(function f32(v?: number | boolean): numberexport f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
gid.gid: d.v3ux),v3u.x: numberseedValue.const seedValue: TgpuUniform<d.F32>$));TgpuUniform<F32>.$: numberwritable.const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>$[TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {position: d.v2f;direction: d.v2f;}[]gid.gid: d.v3ux] =v3u.x: numberParticle({const Particle: d.WgslStruct(props?: {position: d.v2f;direction: d.v2f;} | undefined) => {position: d.v2f;direction: d.v2f;}position:position: d.v2fspawn(),const spawn: () => d.v2fA random position across the canvas, in clip space.
direction:direction: d.v2fd.import dvec2f() });const vec2f: d.Vec2f() => d.v2f (+3 overloads)});constseedPipeline =const seedPipeline: TgpuComputePipelineroot.root: TgpuRootcreateComputePipeline({WithBinding.createComputePipeline<{gid: d.BuiltinGlobalInvocationId;}>(descriptor: TgpuComputePipeline.Descriptor<{gid: d.BuiltinGlobalInvocationId;}>): TgpuComputePipelinecompute:compute: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>seedKernel });const seedKernel: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>return {seed(function seed(pass: TgpuComputePass): voidpass:pass: TgpuComputePassTgpuComputePass) {(alias) interface TgpuComputePassimport TgpuComputePassA compute pass recording into a
TgpuCommandEncoder
.
Dispatch either by binding TypeGPU pipelines to it (
pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style viapass.setPipeline(pipeline)followed bypass.dispatchWorkgroups(...).Call
end()when done recording.own.const own: Ownerusable();Owner.usable(): voidThrows the owner's message if it is destroyed. Call first in methods that use GPU resources, including
inject, recording methods, writes and getters that return GPU objects.seedValue.const seedValue: TgpuUniform<d.F32>write(TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): voidMath.var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.
random());Math.random(): numberReturns a pseudorandom number between 0 and 1.
pass.pass: TgpuComputePasssetPipeline(TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): voidSets the current
TgpuComputePipeline
for subsequent dispatches
seedPipeline);const seedPipeline: TgpuComputePipelinepass.pass: TgpuComputePassdispatchWorkgroups(TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): voidworkgroups);const workgroups: number},destroy:destroy: () => voidown.const own: Ownerdestroy,Owner.destroy(): voidFrees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also
[Symbol.dispose].[Symbol.var Symbol: SymbolConstructordispose]:SymbolConstructor.dispose: typeof Symbol.disposeA method that is used to release resources held by an object. Called by the semantics of the
usingstatement.own.const own: Ownerdestroy,Owner.destroy(): voidFrees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also
[Symbol.dispose].};}randffrom@typegpu/noisegives each thread its own random numbers once you seed it. Seed it with the thread number and a value the CPU picks, so every reseed scatters the particles differently.seedValueis a uniform, a small value the CPU writes and every thread reads.seedtakes a compute pass that the caller opens, so a scene can record several modules into one pass. It records a dispatch and returns; the scene submits the frame.own.usable()throws the owner’s message once the module is destroyed, so a stale scene fails with a clear error rather than binding a freed buffer.destroyand[Symbol.dispose]are the same function, sousingfrees the module at the end of a block.
Nothing draws yet. The scene uses the module in step 3.
-
Draw them.
A draw runs two functions. The vertex function places the corners of shapes on the screen, and the fragment function colours each pixel they cover. Draw each particle as a small square of four corners, one instance per particle.
The square’s size is in pixels, so the vertex function needs the canvas size, and the point size is a setting the reader can change. Put the settings in a struct uniform at module scope, so other code can name its type, and the canvas size in a uniform of its own:
flow-field.ts export constFlowSettings =const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>d.import dstruct({struct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>(props: {speed: d.F32;scale: d.F32;pointSize: d.F32;}): d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>export structCreates a struct schema that can be used to construct GPU buffers. Ensures proper alignment and padding of properties (as opposed to a
d.unstructschema). The order of members matches the passed in properties object./** Clip-space units per second. */speed:speed: d.F32Clip-space units per second.
d.import df32,const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
/** Noise cells across the height. */scale:scale: d.F32Noise cells across the height.
d.import df32,const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
/** Particle diameter in pixels. */pointSize:pointSize: d.F32Particle diameter in pixels.
d.import df32,const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
});export constdefaults:const defaults: {speed: number;scale: number;pointSize: number;}d.import dInfer<typeoftype Infer<T> = T extends {readonly [$repr]: infer TRepr;} ? TRepr : Texport InferExtracts the inferred representation of a resource. For inferring types as seen by the GPU, see
InferGPU
FlowSettings> = {const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>speed: 0.2,speed: numberClip-space units per second.
scale: 1.5,scale: numberNoise cells across the height.
pointSize: 2 };pointSize: numberParticle diameter in pixels.
// In FlowFieldOptions/** Initial values for speed, scale and point size. Override `defaults`. */readonlysettings?:FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>> | undefinedInitial values for speed, scale and point size. Override
defaults.Partial<type Partial<T> = { [P in keyof T]?: T[P] | undefined; }Make all properties in T optional
d.import dInferInput<typeoftype InferInput<T> = d.Infer<T> | (T extends {readonly [$inRepr]: infer TRepr;} ? TRepr : never)export InferInputExtracts the inferred input (write-side) representation of a resource.
FlowSettings>>;const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>// Inside createFlowFieldconstsettings =const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>root.root: TgpuRootcreateUniform(TgpuRoot.createUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>(typeSchema: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>>) => void) | d.InferInput<NoInfer<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>> | undefined): TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use
TgpuRoot.createBuffer
.
FlowSettings, { ...const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>defaults, ...const defaults: {speed: number;scale: number;pointSize: number;}options.options: FlowFieldOptionssettings });FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>> | undefinedInitial values for speed, scale and point size. Override
defaults.constviewport =const viewport: TgpuUniform<d.Vec2f>root.root: TgpuRootcreateUniform(TgpuRoot.createUniform<d.Vec2f>(typeSchema: d.Vec2f, initial?: ((buffer: TgpuBuffer<NoInfer<d.Vec2f>>) => void) | d.InferInput<NoInfer<d.Vec2f>> | undefined): TgpuUniform<d.Vec2f> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use
TgpuRoot.createBuffer
.
d.import dvec2f,const vec2f: d.Vec2fd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)size.const size: SizeSourcewidth,SizeSource.width: numbersize.const size: SizeSourceheight));SizeSource.height: numberspeedandscalesteer the particles from step 4. Below the uniforms, write the two functions. The vertex function runs four times per particle. It reads the particle atinstanceIndex, turnsvertexIndexinto a corner of the square and colours the particle by its direction. The fragment function fades the square into a soft dot:flow-field.ts constvertex =const vertex: TgpuVertexFn<{}, {color: d.Vec3f;corner: d.Vec2f;}>tgpu.const tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}vertexFn({vertexFn: <{vertexIndex: d.BuiltinVertexIndex;instanceIndex: d.BuiltinInstanceIndex;}, {position: d.BuiltinPosition;color: d.Vec3f;corner: d.Vec2f;}>(options: {in: {vertexIndex: d.BuiltinVertexIndex;instanceIndex: d.BuiltinInstanceIndex;};out: {position: d.BuiltinPosition;color: d.Vec3f;corner: d.Vec2f;};}) => TgpuVertexFnShell<{vertexIndex: d.BuiltinVertexIndex;instanceIndex: d.BuiltinInstanceIndex;}, {position: d.BuiltinPosition;color: d.Vec3f;corner: d.Vec2f;}> (+1 overload)in: {in: {vertexIndex: d.BuiltinVertexIndex;instanceIndex: d.BuiltinInstanceIndex;}vertexIndex:vertexIndex: d.BuiltinVertexIndexd.import dbuiltin.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 builtinvertexIndex,vertexIndex: d.BuiltinVertexIndexinstanceIndex:instanceIndex: d.BuiltinInstanceIndexd.import dbuiltin.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 builtininstanceIndex },instanceIndex: d.BuiltinInstanceIndexout: {out: {position: d.BuiltinPosition;color: d.Vec3f;corner: d.Vec2f;}position:position: d.BuiltinPositiond.import dbuiltin.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 builtinposition,position: d.BuiltinPositioncolor:color: d.Vec3fd.import dvec3f,const vec3f: d.Vec3fcorner:corner: d.Vec2fd.import dvec2f },const vec2f: d.Vec2f})(({vertexIndex,vertexIndex: numberinstanceIndex }) => {instanceIndex: number'use gpu';constparticle =const particle: {position: d.v2f;direction: d.v2f;}readable.const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>$[TgpuReadonly<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {position: d.v2f;direction: d.v2f;}[]instanceIndex];instanceIndex: numberconstcorner =const corner: d.v2fd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)d.import df32(function f32(v?: number | boolean): numberexport f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
vertexIndex & 1),vertexIndex: numberd.import df32(function f32(v?: number | boolean): numberexport f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
vertexIndex >>> 1)) * 2 - 1;vertexIndex: numberconstextent = (const extent: d.v2fcorner *const corner: d.v2fsettings.const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>$.TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {speed: number;scale: number;pointSize: number;}pointSize) /pointSize: numberviewport.const viewport: TgpuUniform<d.Vec2f>$;TgpuUniform<Vec2f>.$: d.v2fconstcenter =const center: d.v2fparticle.const particle: {position: d.v2f;direction: d.v2f;}position;position: d.v2freturn {position:position: d.v4fd.import dvec4f(const vec4f: d.Vec4f(v0: AnyNumericVec2Instance, z: number, w: number) => d.v4f (+9 overloads)center +const center: d.v2fextent, 0, 1),const extent: d.v2fcolor:color: d.v3fd.import dvec3f(const vec3f: d.Vec3f(v0: AnyNumericVec2Instance, z: number) => d.v3f (+5 overloads)particle.const particle: {position: d.v2f;direction: d.v2f;}direction * 0.5 + 0.5, 0.9),direction: d.v2fcorner,corner: d.v2f};});constfragment =const fragment: TgpuFragmentFn<{color: d.Vec3f;corner: d.Vec2f;}, d.Vec4f>tgpu.const tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}fragmentFn({fragmentFn: <{color: d.Vec3f;corner: d.Vec2f;}, d.Vec4f>(options: {in: {color: d.Vec3f;corner: d.Vec2f;};out: d.Vec4f;}) => TgpuFragmentFnShell<{color: d.Vec3f;corner: d.Vec2f;}, d.Vec4f> (+1 overload)in: {in: {color: d.Vec3f;corner: d.Vec2f;}color:color: d.Vec3fd.import dvec3f,const vec3f: d.Vec3fcorner:corner: d.Vec2fd.import dvec2f },const vec2f: d.Vec2fout:out: d.Vec4fd.import dvec4f,const vec4f: d.Vec4f})(({color,color: d.v3fcorner }) => {corner: d.v2f'use gpu';constfalloff = 1 -const falloff: numberstd.import stdsmoothstep(0.4, 1,function smoothstep(edge0: number, edge1: number, x: number): number (+1 overload)export smoothstepstd.import stdlength(length<d.v2f>(value: d.v2f): number (+1 overload)export lengthcorner));corner: d.v2freturnd.import dvec4f(const vec4f: d.Vec4f(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)color * (color: d.v3ffalloff * 0.5),const falloff: numberfalloff);const falloff: number});Make the render pipeline with additive blending, so dots that overlap brighten. Return
draw, which takes a render pass, andinitAsync. Importpipelinesfrom@typegpu/kitand the typeTgpuRenderCommandsfromtypegpu:flow-field.ts constdrawPipeline =const drawPipeline: TgpuRenderPipeline<d.Vec4f>root.const root: TgpuRootcreateRenderPipeline({WithBinding.createRenderPipeline<{}, {}, {color: d.Vec3f;corner: d.Vec2f;}, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {attribs?: {} | undefined;vertex: TgpuVertexFn<{}, {color: d.Vec3f;corner: d.Vec2f;}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>);fragment: TgpuFragmentFn<{color: d.Vec3f;corner: d.Vec2f;} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<...>) => d.v4f | (AnyAutoCustoms & Partial<...>));targets?: TgpuColorTargetState;}): TgpuRenderPipeline<...> (+2 overloads)vertex,vertex: TgpuVertexFn<{}, {color: d.Vec3f;corner: d.Vec2f;}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>)fragment,fragment: TgpuFragmentFn<{color: d.Vec3f;corner: d.Vec2f;} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{color: d.Vec3f;corner: d.Vec2f;}>>) => d.v4f | (AnyAutoCustoms & Partial<InferGPURecord<{readonly $fragDepth: d.BuiltinFragDepth;readonly $sampleMask: d.BuiltinSampleMask;}>>))targets: {targets?: TgpuColorTargetStateformat,format?: GPUTextureFormat | undefinedThe
GPUTextureFormat
of this color target. The pipeline will only be compatible with
GPURenderPassEncoder
s which use a
GPUTextureView
of this format in the corresponding color attachment.
blend:blend?: GPUBlendState | undefinedThe blending behavior for this color target. If left undefined, disables blending for this color target.
pipelines.import pipelinesblend.const blend: Readonly<{add: Readonly<{color: GPUBlendComponent;alpha: GPUBlendComponent;}>;over: Readonly<{color: GPUBlendComponent;alpha: GPUBlendComponent;}>;straight: Readonly<{color: GPUBlendComponent;alpha: GPUBlendComponent;}>;}>Provides blend states for pipeline
targets.add },add: Readonly<{color: GPUBlendComponent;alpha: GPUBlendComponent;}>Adds fragment values to the target, for sparks, glow or bloom.
primitive: {TgpuRenderPipeline<in Targets = never>.DescriptorBase.primitive?: TgpuPrimitiveStateDescribes the primitive-related properties of the pipeline.
topology: 'triangle-strip' },topology: "triangle-strip"});letwidth =let width: numbersize.const size: SizeSourcewidth;SizeSource.width: numberletheight =let height: numbersize.const size: SizeSourceheight;SizeSource.height: numberreturn {settings,settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>asyncinitAsync() {function initAsync(): Promise<void>awaitPromise.var Promise: PromiseConstructorRepresents the completion of an asynchronous operation
all([PromiseConstructor.all<[Promise<void>, Promise<void>]>(values: [Promise<void>, Promise<void>]): Promise<[void, void]> (+1 overload)Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.
seedPipeline.const seedPipeline: {initAsync(): Promise<void>;}initAsync(),function initAsync(): Promise<void>drawPipeline.const drawPipeline: TgpuRenderPipeline<d.Vec4f>initAsync()]);TgpuRenderPipeline<Vec4f>.initAsync(): Promise<void>Immediately resolves the pipeline, then awaits
device.createRenderPipelineAsync(). NOTE: it is not necessary to initialize pipelines manually.},draw(function draw(pass: TgpuRenderCommands): voidpass:pass: TgpuRenderCommandsTgpuRenderCommands) {(alias) interface TgpuRenderCommandsimport TgpuRenderCommandsThe draw-recording surface shared by render passes and render bundle encoders, mirroring
GPURenderCommandsMixin
.
Draw either by binding TypeGPU pipelines to it (
pipeline.with(pass).draw(...)), or proxy-style viapass.setPipeline(pipeline)followed bypass.draw(...). Pipeline resolution is lazy: shaders compile on the first draw.own.const own: Ownerusable();Owner.usable(): voidThrows the owner's message if it is destroyed. Call first in methods that use GPU resources, including
inject, recording methods, writes and getters that return GPU objects.if (size.const size: SizeSourcewidth !==SizeSource.width: numberwidth ||let width: numbersize.const size: SizeSourceheight !==SizeSource.height: numberheight) {let height: numberwidth =let width: numbersize.const size: SizeSourcewidth;SizeSource.width: numberheight =let height: numbersize.const size: SizeSourceheight;SizeSource.height: numberviewport.const viewport: TgpuUniform<d.Vec2f>write(TgpuBufferBindingBase<Vec2f>.write(data: d.InferInput<d.Vec2f>, options?: BufferWriteOptions): voidd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)width,let width: numberheight));let height: number}pass.pass: TgpuRenderCommandssetPipeline(TgpuRenderCommands.setPipeline(pipeline: TgpuRenderPipeline): voidSets the current
TgpuRenderPipeline
for subsequent draw calls
drawPipeline);const drawPipeline: TgpuRenderPipeline<d.Vec4f>pass.pass: TgpuRenderCommandsdraw(4,TgpuRenderCommands.draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): voidcount);const count: number},// seed, destroy and [Symbol.dispose] from step 1};- A triangle strip of four corners makes the square from two triangles.
draw(4, count)draws it once for every particle. drawwritesviewportonly when the canvas size changes. It compares against the size it last wrote, so a steady frame uploads nothing.initAsynccompiles the pipelines ahead of the first frame. Without it, each pipeline compiles when it first records, and that frame stalls.settingsis returned so the scene can write it when a setting changes, in step 6.
- A triangle strip of four corners makes the square from two triangles.
-
Run the module in your scene.
Replace
scene.tsfrom Getting started. The scene makes the field and waits for its pipelines. Each frame records into an encoder, a list of GPU commands that the loop submits whenframereturns. Commands come in passes: a compute pass for the seed, then a render pass that clears the canvas and draws:scene.ts import {owner, typefunction owner(message?: string): OwnerCollects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its
destroyas your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such ascapture,takeor a camera'supdate, should return early ondestroyedinstead of callingusable.const own = owner('The swarm was destroyed.');const screen = own(targets.create(root, surface, { hdr }));const settings = own(root.createUniform(Settings));own(() => {for (const buffer of reads) buffer.destroy();});return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };Frame, type(alias) interface Frameimport FrameRepresents one frame being recorded. Each
runcreates a separate object, so modules can compare frames by identity.Surface } from '@typegpu/kit';(alias) interface Surfaceimport SurfaceProvides a destination for frames, such as a canvas from
web.surfaceornative.surface, or a texture fromoffscreenSurfacein a test. Pass the frame toviewfor code that works with every destination.import type {TgpuRoot } from 'typegpu';(alias) interface TgpuRootimport TgpuRootimport {createFlowField } from './flow-field.ts';function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
export async functioncreateScene(function createScene(root: TgpuRoot, surface: Surface): Promise<{frame(frame: Frame): void;destroy: () => void;}>root:root: TgpuRootTgpuRoot,(alias) interface TgpuRootimport TgpuRootsurface:surface: SurfaceSurface) {(alias) interface Surfaceimport SurfaceProvides a destination for frames, such as a canvas from
web.surfaceornative.surface, or a texture fromoffscreenSurfacein a test. Pass the frame toviewfor code that works with every destination.constown =const own: Ownerowner();function owner(message?: string): OwnerCollects what a scene or module makes, to free it all in one call, and guards its methods. Pass each thing through it as you make it, and return its
destroyas your own. Take a resource that is replaced during the module's life, such as a texture that follows the canvas size, as one cleanup that frees the current one. Methods that only read CPU state or do optional work, such ascapture,takeor a camera'supdate, should return early ondestroyedinstead of callingusable.const own = owner('The swarm was destroyed.');const screen = own(targets.create(root, surface, { hdr }));const settings = own(root.createUniform(Settings));own(() => {for (const buffer of reads) buffer.destroy();});return { step(pass) { own.usable(); … }, destroy: own.destroy, [Symbol.dispose]: own.destroy };constfield =const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}own(const own: Owner<{settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}>(item: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}) => {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Takes
itemto free atdestroyand returns it. Passesundefinedthrough. Frees a fixed resource, such as one fromroot.createUniform, through itsbuffer, and runs a cleanup function. Afterdestroy, freesitemat once.createFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
root, {root: TgpuRootcount: 65_536,FlowFieldOptions.count: numberstep: 1 / 60,FlowFieldOptions.step: numberSeconds per
step.format:FlowFieldOptions.format: GPUTextureFormatsurface.surface: Surfaceformat,Surface.format: GPUTextureFormatsize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
surface,surface: Surface}),);awaitfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}initAsync();function initAsync(): Promise<void>letseeding = true;let seeding: booleanreturn {frame(function frame(frame: Frame): voidframe:frame: FrameFrame) {(alias) interface Frameimport FrameRepresents one frame being recorded. Each
runcreates a separate object, so modules can compare frames by identity.constcompute =const compute: TgpuComputePassframe.frame: Frameencoder.Frame.encoder: TgpuCommandEncoderRecords 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.beginComputePass();TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePassBegins recording a compute pass
if (seeding)let seeding: booleanfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}seed(function seed(pass: TgpuComputePass): voidcompute);const compute: TgpuComputePassseeding = false;let seeding: booleancompute.const compute: TgpuComputePassend();TgpuComputePass.end(): voidCompletes the recording of this compute pass
constpass =const pass: TgpuRenderPassframe.frame: Frameencoder.Frame.encoder: TgpuCommandEncoderRecords 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.beginRenderPass({TgpuCommandEncoder.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPassBegins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw
GPUTextureView
s.
colorAttachments: {TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefinedview:ColorAttachment.view: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContextA
GPUTextureView
describing the texture subresource that will be output to for this color attachment.
surface.surface: Surfaceview(Surface.view(frame: Frame): GPUTextureViewReturns a view of the current texture, created once per texture. React Native requires the frame to present after submission.
frame),frame: FrameclearValue: [0.01, 0.015, 0.03, 1] },ColorAttachment.clearValue?: readonly [number, number, number, number] | GPUColor | undefinedIndicates the value to clear
GPURenderPassColorAttachment#view
to prior to executing the render pass. If not provided, defaults to
{r: 0, g: 0, b: 0, a: 0}. Ignored ifGPURenderPassColorAttachment#loadOp
is not
GPULoadOp# "clear"
. The components of
GPURenderPassColorAttachment#clearValue
are all double values. They are converted to a texel value of texture format matching the render attachment. If conversion fails, a validation error is generated.
});field.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}draw(function draw(pass: TgpuRenderCommands): voidpass);const pass: TgpuRenderPasspass.const pass: TgpuRenderPassend();TgpuRenderPass.end(): voidCompletes the recording of this render pass
},destroy:destroy: () => voidown.const own: Ownerdestroy,Owner.destroy(): voidFrees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also
[Symbol.dispose].};}The scene owns the field, so it passes it through an owner of its own and returns that owner’s
destroy. The host calls it when the scene closes. The root and surface are borrowed from the host, so the scene leaves them alone. The seed and the draw go into one encoder, so the GPU scatters the particles before it draws them.The particles appear, scattered and still:
flow-field65 536 particles, each steered by the Perlin noise under it.Reseed to scatter the particles again. The simulation is paused, as yours is until step 4. -
Move them through noise.
Each step, every particle samples Perlin noise at its position, turns the value into an angle and moves that way. Nearby positions get similar angles, so the particles flow in smooth streams. Import
perlin2dfrom@typegpu/noiseandstdfromtypegpu, and add thestepkernel:flow-field.ts constTAU =const TAU: numberMath.var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.
PI * 2;Math.PI: numberPi. This is the ratio of the circumference of a circle to its diameter.
/** Chance per step of respawning a particle elsewhere. Prevents particles collecting in sinks. */constRESPAWN = 1 / 240;const RESPAWN: numberChance per step of respawning a particle elsewhere. Prevents particles collecting in sinks.
// Inside createFlowFieldconststepKernel =const stepKernel: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>tgpu.const tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}computeFn({computeFn: <{gid: d.BuiltinGlobalInvocationId;}>(options: {in: {gid: d.BuiltinGlobalInvocationId;};workgroupSize: number[];}) => TgpuComputeFnShell<{gid: d.BuiltinGlobalInvocationId;}> (+1 overload)workgroupSize: [workgroupSize: number[]WORKGROUP],const WORKGROUP: 64in: {in: {gid: d.BuiltinGlobalInvocationId;}gid:gid: d.BuiltinGlobalInvocationIdd.import dbuiltin.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 builtinglobalInvocationId },globalInvocationId: d.BuiltinGlobalInvocationId})(({gid }) => {gid: d.v3u'use gpu';if (gid.gid: d.v3ux >=v3u.x: numbercount) return;const count: numberconstparticle =const particle: {position: d.v2f;direction: d.v2f;}writable.const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>$[TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {position: d.v2f;direction: d.v2f;}[]gid.gid: d.v3ux];v3u.x: numberconstaspect =const aspect: numberviewport.const viewport: TgpuUniform<d.Vec2f>$.TgpuUniform<Vec2f>.$: d.v2fx /v2f.x: numberviewport.const viewport: TgpuUniform<d.Vec2f>$.TgpuUniform<Vec2f>.$: d.v2fy;v2f.y: numberconstsample =const sample: d.v2fparticle.const particle: {position: d.v2f;direction: d.v2f;}position *position: d.v2fd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)aspect, 1) *const aspect: numbersettings.const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>$.TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {speed: number;scale: number;pointSize: number;}scale;scale: numberconstangle =const angle: numberperlin2d.(alias) namespace perlin2dimport perlin2dsample(index$1.sample(pos: d.v2f): numberexport index$1.sampleReturns value of Perlin Noise at point
possample) *const sample: d.v2fTAU;const TAU: numberconstdirection =const direction: d.v2fd.import dvec2f(const vec2f: d.Vec2f(x: number, y: number) => d.v2f (+3 overloads)std.import stdcos(function cos(value: number): number (+1 overload)export cosangle),const angle: numberstd.import stdsin(function sin(value: number): number (+1 overload)export sinangle));const angle: numberletposition =let position: d.v2fparticle.const particle: {position: d.v2f;direction: d.v2f;}position +position: d.v2fdirection * (const direction: d.v2fsettings.const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>$.TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {speed: number;scale: number;pointSize: number;}speed *speed: numberstep);const step: numberrandf.const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}seed3(seed3: (seed: d.v3f) => voidThreads do not share the generator's
State. As a result, unless you change the seed in each thread, each thread will produce the same sequence.randf.randSeed3sets the private seed of the thread. Sets the private seed of the thread.d.import dvec3f(const vec3f: d.Vec3f(v0: AnyNumericVec2Instance, z: number) => d.v3f (+5 overloads)position,let position: d.v2fd.import df32(function f32(v?: number | boolean): numberexport f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
gid.gid: d.v3ux)));v3u.x: numberif (randf.const randf: {seed: TgpuFn<(seed: d.F32) => d.Void>;seed2: TgpuFn<(seed: d.Vec2f) => d.Void>;seed3: TgpuFn<(seed: d.Vec3f) => d.Void>;seed4: TgpuFn<(seed: d.Vec4f) => d.Void>;sample: TgpuFn<() => d.F32>;sampleExclusive: TgpuFn<() => d.F32>;normal: TgpuFn<(mu: d.F32, sigma: d.F32) => d.F32>;... 10 more ...;onUnitSphere: TgpuFn<...>;}sample() <sample: () => numberReturns a random f32 value in [0, 1) range.
RESPAWN ||const RESPAWN: numberChance per step of respawning a particle elsewhere. Prevents particles collecting in sinks.
std.import stdany(function any(value: AnyBooleanVecInstance): booleanexport anyReturns
trueif any component ofvalueis true.std.import stdgt(gt<d.v2f>(lhs: d.v2f, rhs: d.v2f): d.v2bexport gtChecks component-wise whether
lhs > rhs. This function does not returnbool, for that use-case, wrap the result inall.std.import stdabs(abs<d.v2f>(value: d.v2f): d.v2f (+1 overload)export absposition),let position: d.v2fd.import dvec2f(1)))) {const vec2f: d.Vec2f(xy: number) => d.v2f (+3 overloads)position =let position: d.v2fspawn();const spawn: () => d.v2f}writable.const writable: TgpuMutable<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>$[TgpuMutable<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$: {position: d.v2f;direction: d.v2f;}[]gid.gid: d.v3ux] =v3u.x: numberParticle({const Particle: d.WgslStruct(props?: {position: d.v2f;direction: d.v2f;} | undefined) => {position: d.v2f;direction: d.v2f;}position,position: d.v2fdirection });direction: d.v2f});conststepPipeline =const stepPipeline: TgpuComputePipelineroot.const root: TgpuRootcreateComputePipeline({WithBinding.createComputePipeline<{gid: d.BuiltinGlobalInvocationId;}>(descriptor: TgpuComputePipeline.Descriptor<{gid: d.BuiltinGlobalInvocationId;}>): TgpuComputePipelinecompute:compute: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>stepKernel });const stepKernel: TgpuComputeFn<{gid: d.BuiltinGlobalInvocationId;}>- The sample position is stretched by the canvas’s aspect ratio, so the noise’s cells stay square on a wide canvas.
stepis a constant from the options. The kernel captures it, so no frame uploads the step length.- A few particles respawn at random each step, and every particle that leaves the canvas does. Without that, particles would gather where the streams meet and leave the rest of the canvas empty.
Return
stepbesideseed, recording one fixed step into the caller’s compute pass, and compile its pipeline ininitAsync:flow-field.ts asyncinitAsync() {function initAsync(): Promise<void>awaitPromise.var Promise: PromiseConstructorRepresents the completion of an asynchronous operation
all([PromiseConstructor.all<[Promise<void>, Promise<void>, Promise<void>]>(values: [Promise<void>, Promise<void>, Promise<void>]): Promise<[void, void, void]> (+1 overload)Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.
seedPipeline.const seedPipeline: {initAsync(): Promise<void>;}initAsync(),function initAsync(): Promise<void>stepPipeline.const stepPipeline: TgpuComputePipelineinitAsync(),TgpuComputePipeline.initAsync(): Promise<void>Immediately resolves the pipeline, then awaits
device.createComputePipelineAsync(). NOTE: it is not necessary to initialize pipelines manually.drawPipeline.const drawPipeline: {initAsync(): Promise<void>;}initAsync(),function initAsync(): Promise<void>]);},step(function step(pass: TgpuComputePass): voidpass:pass: TgpuComputePassTgpuComputePass) {(alias) interface TgpuComputePassimport TgpuComputePassA compute pass recording into a
TgpuCommandEncoder
.
Dispatch either by binding TypeGPU pipelines to it (
pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style viapass.setPipeline(pipeline)followed bypass.dispatchWorkgroups(...).Call
end()when done recording.own.const own: Ownerusable();Owner.usable(): voidThrows the owner's message if it is destroyed. Call first in methods that use GPU resources, including
inject, recording methods, writes and getters that return GPU objects.pass.pass: TgpuComputePasssetPipeline(TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): voidSets the current
TgpuComputePipeline
for subsequent dispatches
stepPipeline);const stepPipeline: TgpuComputePipelinepass.pass: TgpuComputePassdispatchWorkgroups(TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): voidworkgroups);const workgroups: number},Stepping once per frame would move the particles twice as fast at 120 Hz as at 60 Hz. A fixed step counts how many 1/60 s steps fit in the time since the last frame and carries the rest into the next:
Stall a frame or change the display rate. Step length stays fixed; catch-up frames run at most maxStepsand drop time beyond the cap, shown at one fifth speed.In
scene.ts, importclockfrom@typegpu/kit, make the fixed step, and step the field after the seed:scene.ts // In createSceneconstsim =const sim: clock.FixedStepclock.import clockfixedStep({function fixedStep(options: clock.FixedStepOptions): clock.FixedStepAccumulates elapsed time for fixed simulation steps. Accumulates nothing while paused. Throws unless
stepis finite and above 0 andmaxStepsis an integer above 0.step: 1 / 60 });FixedStepOptions.step: numberSets the step duration in seconds, such as 1 / 120.
constfield =const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}own(const own: Owner<{settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}>(item: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}) => {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Takes
itemto free atdestroyand returns it. Passesundefinedthrough. Frees a fixed resource, such as one fromroot.createUniform, through itsbuffer, and runs a cleanup function. Afterdestroy, freesitemat once.createFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
root, {const root: TgpuRootcount: 65_536,FlowFieldOptions.count: numberstep:FlowFieldOptions.step: numberSeconds per
step.sim.const sim: clock.FixedStepstep,FixedStep.step: numberSets the step duration in seconds. A new value applies from the next
advance: unstepped time carries over, capped atmaxStepsnew steps, andtimekeeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.format:FlowFieldOptions.format: GPUTextureFormatsurface.const surface: Surfaceformat,Surface.format: GPUTextureFormatsize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
surface,const surface: Surface}),);// In frame, after the seedfor (letsteps =let steps: numbersim.const sim: clock.FixedStepadvance(FixedStep.advance(dt: number): numberAdds
dtseconds and returns the number of steps to run. Each step lastsstepseconds, 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.frame.const frame: Framedt);Frame.dt: numberReports seconds since the previous submitted frame. The first frame has a
dtof zero.steps > 0;let steps: numbersteps--) {let steps: numberfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}step(function step(pass: TgpuComputePass): voidcompute);const compute: TgpuComputePass}The particles stream across the canvas. At 30 frames a second, each frame runs two steps in its one compute pass; at 120, half the frames run none.
-
Move on every frame.
On a display faster than 60 Hz, the particles hold still on the frames that run no step, then jump a whole step, so they stutter.
sim.aheadis the time since the last step, in seconds. Draw each particle that far along its direction, and it moves on every frame while the simulation still steps 60 times a second:Change the display rate. Ticks mark the particle after each 60 Hz step and rings mark where the last frames drew it, at one sixth speed. Drawn at its last step, the particle holds still for several frames, stacked here; drawn sim.aheadseconds along, it moves on every frame.aheadchanges every frame, so it gets a uniform of its own, which the scene owns and writes. The module reads it through an accessor, a value its shader reads whose source the pipeline decides. Filled with the scene’s uniform, the shader reads the uniform; without one, it reads 0. Import the typeTgpuUniformfromtypegpu:flow-field.ts // @filename: ../../packages/typegpu-kit-examples/src/flow-field/flow-field-tutorial-7.tsimport {pipelines } from '@typegpu/kit';import pipelinesimport {d,import dtgpu, typeconst tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}TgpuRoot, type(alias) interface TgpuRootimport TgpuRootTgpuUniform } from 'typegpu';interface TgpuUniform<out TData extends BaseData>declare constroot:const root: TgpuRootTgpuRoot,(alias) interface TgpuRootimport TgpuRootformat:const format: GPUTextureFormatGPUTextureFormat;type GPUTextureFormat = "astc-10x10-unorm" | "astc-10x10-unorm-srgb" | "astc-10x5-unorm" | "astc-10x5-unorm-srgb" | "astc-10x6-unorm" | "astc-10x6-unorm-srgb" | "astc-10x8-unorm" | "astc-10x8-unorm-srgb" | "astc-12x10-unorm" | "astc-12x10-unorm-srgb" | "astc-12x12-unorm" | "astc-12x12-unorm-srgb" | "astc-4x4-unorm" | "astc-4x4-unorm-srgb" | "astc-5x4-unorm" | "astc-5x4-unorm-srgb" | "astc-5x5-unorm" | "astc-5x5-unorm-srgb" | "astc-6x5-unorm" | "astc-6x5-unorm-srgb" | "astc-6x6-unorm" | "astc-6x6-unorm-srgb" | "astc-8x5-unorm" | "astc-8x5-unorm-srgb" | ... 76 more ... | "stencil8"constParticle =const Particle: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>d.import dstruct({struct<{position: d.Vec2f;direction: d.Vec2f;}>(props: {position: d.Vec2f;direction: d.Vec2f;}): d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>export structCreates a struct schema that can be used to construct GPU buffers. Ensures proper alignment and padding of properties (as opposed to a
d.unstructschema). The order of members matches the passed in properties object.position:position: d.Vec2fd.import dvec2f,const vec2f: d.Vec2fdirection:direction: d.Vec2fd.import dvec2f });const vec2f: d.Vec2fconstFlowSettings =const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>d.import dstruct({struct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>(props: {speed: d.F32;scale: d.F32;pointSize: d.F32;}): d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>export structCreates a struct schema that can be used to construct GPU buffers. Ensures proper alignment and padding of properties (as opposed to a
d.unstructschema). The order of members matches the passed in properties object.speed:speed: d.F32d.import df32,const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
scale:scale: d.F32d.import df32,const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
pointSize:pointSize: d.F32d.import df32 });const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
constreadable =const readable: TgpuReadonly<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>root.const root: TgpuRootcreateBuffer(TgpuRoot.createBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>(typeSchema: d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>> | undefined): TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader.
d.import darrayOf(arrayOf<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>(elementType: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>, elementCount: number): d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>> (+2 overloads)export arrayOfParticle, 4)).const Particle: d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>$usage('storage').TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.$usage<["storage"]>(usages_0: "storage"): TgpuBuffer<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>> & StorageFlagas('readonly');TgpuBuffer<WgslArray<WgslStruct<{ position: Vec2f; direction: Vec2f; }>>>.as<"readonly">(usage: "readonly"): TgpuReadonly<d.WgslArray<d.WgslStruct<{position: d.Vec2f;direction: d.Vec2f;}>>>constsettings =const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>root.const root: TgpuRootcreateUniform(TgpuRoot.createUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>(typeSchema: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>>) => void) | d.InferInput<NoInfer<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>> | undefined): TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use
TgpuRoot.createBuffer
.
FlowSettings);const FlowSettings: d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>// In FlowFieldOptions/*** Seconds since the last fixed step, written from `sim.ahead` each frame. Particles are drawn* that far along their direction, so they move at the display rate. Default 0.*/readonlyahead?:FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefinedSeconds since the last fixed step, written from
sim.aheadeach frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.TgpuUniform<interface TgpuUniform<out TData extends BaseData>d.import dF32>;export F3232-bit float schema representing a single WGSL f32 value.
// Inside createFlowFieldconstahead =const ahead: TgpuAccessor<d.F32>tgpu.const tgpu: {const: {<TDataType extends d.AnyData>(dataType: TDataType, value: d.InferGPU<TDataType>): TgpuConst<TDataType>;<TElement extends d.AnyWgslData>(dataType: (elementCount: number) => d.WgslArray<TElement>, value: d.InferGPU<d.WgslArray<TElement>>): TgpuConst<d.WgslArray<TElement>>;};fn: {<Args extends d.BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;<Args extends d.BaseData[] | [], Return extends d.BaseData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;<T extends AnyFn>(inner: T): TgpuGenericFn<T>;};... 17 more ...;'~unstable': typeof import("../../node_modules/typegpu/tgpuUnstable");}accessor(accessor: <d.F32>(schema: d.F32, defaultValue?: DataAccessorIn<NoInfer<d.F32>> | undefined) => TgpuAccessor<d.F32> (+1 overload)d.import df32);const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
// In vertex, in place of the center at the particle's positionconstcenter =const center: d.v2fparticle.const particle: {position: d.v2f;direction: d.v2f;}position +position: d.v2fparticle.const particle: {position: d.v2f;direction: d.v2f;}direction * (direction: d.v2fsettings.const settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>$.TgpuUniform<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.$: {speed: number;scale: number;pointSize: number;}speed *speed: numberahead.const ahead: TgpuAccessor<d.F32>$);TgpuAccessor<F32>.$: number// In place of root.createRenderPipelineconstdrawPipeline =const drawPipeline: TgpuRenderPipeline<d.Vec4f>root.const root: TgpuRootwith(Withable<WithBinding>.with<d.F32>(accessor: TgpuAccessor<d.F32>, value: DataAccessorIn<NoInfer<d.F32>>): WithBinding (+2 overloads)ahead,const ahead: TgpuAccessor<d.F32>options.const options: FlowFieldOptionsahead ?? 0).FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefinedSeconds since the last fixed step, written from
sim.aheadeach frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.createRenderPipeline({WithBinding.createRenderPipeline<{}, {}, {}, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {attribs?: {} | undefined;vertex: TgpuVertexFn<{}, {}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<{}>);fragment: TgpuFragmentFn<{} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{}>>) => d.v4f | (AnyAutoCustoms & Partial<...>));targets?: TgpuColorTargetState;}): TgpuRenderPipeline<...> (+2 overloads)// ...the same description as before});In
scene.ts, importdfromtypegpu, make the uniform, hand it to the field and write it after the steps:scene.ts // In createSceneconstahead =const ahead: TgpuUniform<d.F32>root.const root: TgpuRootcreateUniform(TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.F32>>) => void) | undefined): TgpuUniform<d.F32> (+1 overload)Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use
TgpuRoot.createBuffer
.
d.import df32);const f32: d.F32export f32A schema that represents a 32-bit float value. (equivalent to
f32in WGSL)Can also be called to cast a value to an f32.
constfield =const field: {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}own(const own: Owner<{settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}>(item: {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}) => {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Takes
itemto free atdestroyand returns it. Passesundefinedthrough. Frees a fixed resource, such as one fromroot.createUniform, through itsbuffer, and runs a cleanup function. Afterdestroy, freesitemat once.createFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
root, {const root: TgpuRootcount: 65_536,FlowFieldOptions.count: numberstep:FlowFieldOptions.step: numberSeconds per
step.sim.const sim: clock.FixedStepstep,FixedStep.step: numberSets the step duration in seconds. A new value applies from the next
advance: unstepped time carries over, capped atmaxStepsnew steps, andtimekeeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.ahead,FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefinedSeconds since the last fixed step, written from
sim.aheadeach frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.format:FlowFieldOptions.format: GPUTextureFormatsurface.const surface: Surfaceformat,Surface.format: GPUTextureFormatsize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
surface,const surface: Surface}),);// In frame, after the stepsahead.const ahead: TgpuUniform<d.F32>write(TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): voidsim.const sim: clock.FixedStepahead);FixedStep.ahead: numberReports the unstepped time in seconds, capped at one step. Equals
alpha * step. Use it to draw objects that far along their velocity between simulation steps.On a 120 Hz or faster display, the particles now move smoothly on every frame.
-
Change the settings.
Writing a uniform uploads all of it, so group values by how often they change. The field has four:
Uniform Holds Written settingsspeed, scale, point size by the scene, when a setting changes viewportthe canvas size by draw, when the canvas size changesaheadthe time since a step by the scene, once a frame seedValuethe random seed by seedA steady frame uploads 4 bytes, the
aheadvalue. Data on the GPU has the rule for every module. Keep the settings in a plain object, hand it to the field as its starting values, and return them with awritethat uploads them, and areseed. Importdefaultsfrom./flow-field.ts:scene.ts // In createSceneconstsettings = { ...const settings: {speed: number;scale: number;pointSize: number;}defaults };const defaults: {speed: number;scale: number;pointSize: number;}constfield =const field: {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}own(const own: Owner<{settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}>(item: {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}) => {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Takes
itemto free atdestroyand returns it. Passesundefinedthrough. Frees a fixed resource, such as one fromroot.createUniform, through itsbuffer, and runs a cleanup function. Afterdestroy, freesitemat once.createFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
root, {const root: TgpuRootcount: 65_536,FlowFieldOptions.count: numberstep:FlowFieldOptions.step: numberSeconds per
step.sim.const sim: clock.FixedStepstep,FixedStep.step: numberSets the step duration in seconds. A new value applies from the next
advance: unstepped time carries over, capped atmaxStepsnew steps, andtimekeeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.ahead,FlowFieldOptions.ahead?: TgpuUniform<d.F32> | undefinedSeconds since the last fixed step, written from
sim.aheadeach frame. Particles are drawn that far along their direction, so they move at the display rate. Default 0.format:FlowFieldOptions.format: GPUTextureFormatsurface.const surface: Surfaceformat,Surface.format: GPUTextureFormatsize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
surface,const surface: Surfacesettings,FlowFieldOptions.settings?: Partial<d.InferInput<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>> | undefinedInitial values for speed, scale and point size. Override
defaults.}),);// In the returned objectsim,sim: clock.FixedStepsettings,settings: {speed: number;scale: number;pointSize: number;}write: () =>write: () => voidfield.const field: {settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}settings.settings: TgpuUniform<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>write(TgpuBufferBindingBase<WgslStruct<{ speed: F32; scale: F32; pointSize: F32; }>>.write(data: d.InferInput<d.WgslStruct<{speed: d.F32;scale: d.F32;pointSize: d.F32;}>>, options?: BufferWriteOptions): voidsettings),const settings: {speed: number;scale: number;pointSize: number;}reseed: () => {reseed: () => voidseeding = true;let seeding: boolean},The page decides how to show the settings. Add lil-gui to the project:
npm i lil-guipnpm add lil-guiyarn add lil-guibun add lil-guiIn
index.ts, build the panel inattach, whichweb.hostcalls once the scene is made. The host disposes whatattachreturns when it tears the scene down:index.ts import * asweb from '@typegpu/kit/web';import webimportGUI from 'lil-gui';class GUIimport {createScene } from './scene.ts';function createScene(root: TgpuRoot, surface: Surface): Promise<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>web.import webhost(function host<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>(createScene: web.CreateScene<Record<never, never>, {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>, hosting: web.HostOptions<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>): web.Hosted<...> (+1 overload)Runs a scene on a canvas with a root, surface, frame loop and animation.
destroytears them down in order. Usesendto control the scene oronFrameto read it. If scene options are optional, callhost(createScene, { canvas }). Throws if bothdeviceandrootare supplied.createScene, {function createScene(root: TgpuRoot, surface: Surface): Promise<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>canvas:HostOptions<S extends Scene>.canvas: HTMLCanvasElementSupplies the canvas for the scene.
document.var document: Documentwindow.documentreturns a reference to the document contained in the window.querySelector('canvas')!,ParentNode.querySelector<"canvas">(selectors: "canvas"): HTMLCanvasElement | null (+4 overloads)Returns the first element that is a descendant of node that matches selectors.
attach({HostOptions<{ sim: FixedStep; settings: { speed: number; scale: number; pointSize: number; }; write(): void; reseed(): void; frame(frame: Frame): void; destroy(): void; }, Surface>.attach?: ((stage: Stage<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}, web.Surface>) => Disposable | void) | undefinedConnects page code, such as orbit controls, to the stage after scene creation. The host disposes its return value with the scene. Default none.
scene }) {scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}constgui = newconst gui: GUIGUI({new GUI({ parent, autoPlace, container, width, title, closeFolders, injectStyles, touchStyles }?: {autoPlace?: boolean;container?: Node;width?: number;title?: string;closeFolders?: boolean;injectStyles?: boolean;touchStyles?: number;parent?: GUI;}): GUICreates a panel that holds controllers.
title: 'flow field' });title?: string | undefinedgui.const gui: GUIadd(GUI.add<{speed: number;scale: number;pointSize: number;}>(object: {speed: number;scale: number;pointSize: number;}, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): ControllerAdds a controller to the GUI, inferring controller type using the
typeofoperator.scene.scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}settings, 'speed', 0, 1, 0.01);settings: {speed: number;scale: number;pointSize: number;}gui.const gui: GUIadd(GUI.add<{speed: number;scale: number;pointSize: number;}>(object: {speed: number;scale: number;pointSize: number;}, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): ControllerAdds a controller to the GUI, inferring controller type using the
typeofoperator.scene.scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}settings, 'scale', 0.1, 6, 0.1);settings: {speed: number;scale: number;pointSize: number;}gui.const gui: GUIadd(GUI.add<{speed: number;scale: number;pointSize: number;}>(object: {speed: number;scale: number;pointSize: number;}, property: "speed" | "scale" | "pointSize", $1?: number | object | any[], max?: number, step?: number): ControllerAdds a controller to the GUI, inferring controller type using the
typeofoperator.scene.scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}settings, 'pointSize', 0.5, 8, 0.5);settings: {speed: number;scale: number;pointSize: number;}gui.const gui: GUIonChange(GUI.onChange(callback: (arg0: {object: object;property: string;value: any;controller: Controller;}) => any): GUIPass a function to be called whenever a controller in this GUI changes.
scene.scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}write);function write(): voidgui.const gui: GUIadd(GUI.add<FixedStep>(object: FixedStep, property: keyof FixedStep, $1?: number | object | any[], max?: number, step?: number): ControllerAdds a controller to the GUI, inferring controller type using the
typeofoperator.scene.scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}sim, 'paused');sim: FixedStepgui.const gui: GUIadd(GUI.add<{sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}>(object: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}, property: "reseed" | "sim" | "settings" | "write" | "frame" | "destroy", $1?: number | object | any[], max?: number, step?: number): ControllerAdds a controller to the GUI, inferring controller type using the
typeofoperator.scene, 'reseed');scene: {sim: FixedStep;settings: {speed: number;scale: number;pointSize: number;};write(): void;reseed(): void;frame(frame: Frame): void;destroy(): void;}return { [Symbol.var Symbol: SymbolConstructordispose]: () =>SymbolConstructor.dispose: typeof Symbol.disposeA method that is used to release resources held by an object. Called by the semantics of the
usingstatement.gui.const gui: GUIdestroy() };GUI.destroy(): voidDestroys all DOM elements and event listeners associated with this GUI.
},});The panel changes the field as you drag. Pausing stops the steps and holds the particles still. The figure has the same controls, and its
settings writesreadout counts uploads ofsettings. It rises only when you change a setting:flow-field65 536 particles, each steered by the Perlin noise under it.Change the speed or point size to add one uniform write. Reseeding and waiting leave the count unchanged. -
Test the module.
A test of the module checks what it records, without a GPU.
recordingRoot()from@typegpu/kit/testingis a root on a stand-in device that logs commands and writes, andoffscreenSurfacestands in for the canvas. Add Vitest, which readsvite.config.ts, so the TypeGPU plugin transforms your module in tests too:npm i -D vitestpnpm add -D vitestyarn add -D vitestbun add -d vitestCreate
flow-field.test.tsbeside the module. Make the field on a recording root and record frames as a scene would:flow-field.test.ts import {frames } from '@typegpu/kit';import framesimport {offscreenSurface,function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurfaceCreates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device.
viewreturns one view per texture.destroydestroys the texture, and later calls toviewthrow.recordingRoot } from '@typegpu/kit/testing';function recordingRoot(options?: RecordingOptions): RecordingRootCreates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.
Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.
import {expect,const expect: ExpectStaticit } from 'vitest';const it: TestAPIDefines a test case with a given name and test function. The test function can optionally be configured with test options.
import {createFlowField } from './flow-field.ts';function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
it('records into the passes it is given, and a steady frame writes and creates nothing', async () => {it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)Defines a test case with a given name and test function. The test function can optionally be configured with test options.
const {root,const root: TgpuRootlog } =const log: RecordingLogrecordingRoot();function recordingRoot(options?: RecordingOptions): RecordingRootCreates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.
Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.
constsurface =const surface: OffscreenSurfaceoffscreenSurface(function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurfaceCreates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device.
viewreturns one view per texture.destroydestroys the texture, and later calls toviewthrow.root, {const root: TgpuRootwidth: 64,OffscreenSurfaceOptions.width: numberSets the texture width in pixels.
height: 36,OffscreenSurfaceOptions.height: numberSets the texture height in pixels.
format: 'bgra8unorm' });OffscreenSurfaceOptions.format: GPUTextureFormatSets the texture format, as a canvas preferred format would.
constfield =const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}createFlowField(function createFlowField(root: TgpuRoot, options: FlowFieldOptions): {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}Advects particles through Perlin noise. Owns the buffers and records into caller passes.
root, {const root: TgpuRootcount: 1000,FlowFieldOptions.count: numberstep: 1 / 60,FlowFieldOptions.step: numberSeconds per
step.format:FlowFieldOptions.format: GPUTextureFormatsurface.const surface: OffscreenSurfaceformat,Surface.format: GPUTextureFormatsize:FlowFieldOptions.size: SizeSourceWhat the particles are drawn into, such as the canvas surface.
surface,const surface: OffscreenSurface});awaitfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}initAsync();function initAsync(): Promise<void>expect(expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)log.const log: RecordingLogpipelines).RecordingLog.pipelines: numbertoBe(3);JestAssertion<void, number>.toBe: <number>(expected: number) => voidChecks that a value is what you expect. It calls
Object.isto compare values. Don't usetoBewith floating-point numbers.letseeding = true;let seeding: booleanconstloop =const loop: frames.Framesframes.import framescreate(function create(root: TgpuRoot, record: Recorder): frames.FramesRecords a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.
root, (const root: TgpuRootframe) => {frame: frames.Frameconstcompute =const compute: TgpuComputePassframe.frame: frames.Frameencoder.Frame.encoder: TgpuCommandEncoderRecords 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.beginComputePass();TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePassBegins recording a compute pass
if (seeding)let seeding: booleanfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}seed(function seed(pass: TgpuComputePass): voidcompute);const compute: TgpuComputePassseeding = false;let seeding: booleanfield.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}step(function step(pass: TgpuComputePass): voidcompute);const compute: TgpuComputePasscompute.const compute: TgpuComputePassend();TgpuComputePass.end(): voidCompletes the recording of this compute pass
constpass =const pass: TgpuRenderPassframe.frame: frames.Frameencoder.Frame.encoder: TgpuCommandEncoderRecords 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.beginRenderPass({TgpuCommandEncoder.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPassBegins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw
GPUTextureView
s.
colorAttachments: {TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefinedview:ColorAttachment.view: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContextA
GPUTextureView
describing the texture subresource that will be output to for this color attachment.
surface.const surface: OffscreenSurfaceview(Surface.view(frame: frames.Frame): GPUTextureViewReturns a view of the current texture, created once per texture. React Native requires the frame to present after submission.
frame) },frame: frames.Frame});field.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}draw(function draw(pass: TgpuRenderCommands): voidpass);const pass: TgpuRenderPasspass.const pass: TgpuRenderPassend();TgpuRenderPass.end(): voidCompletes the recording of this render pass
});initAsynccompiled all three pipelines, so none compiles during a frame. Run two frames, which seed and make TypeGPU’s first-use objects, then check a third:flow-field.test.ts // In the test, after the looploop.const loop: frames.Framesrun(0);Frames.run(seconds: number): booleanRecords and submits one frame with a fresh
Frameand encoder. Requires finite, nondecreasingseconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts andrunrethrows their errors.loop.const loop: frames.Framesrun(1 / 60);Frames.run(seconds: number): booleanRecords and submits one frame with a fresh
Frameand encoder. Requires finite, nondecreasingseconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts andrunrethrows their errors.constcounts =const counts: RecordingCountslog.const log: RecordingLogcounts();RecordingLog.counts(): RecordingCountsReturns counts of created objects. Steady frames should leave these counts unchanged.
log.const log: RecordingLogevents.RecordingLog.events: string[]Lists pass, draw, dispatch and submit calls in recording order.
length = 0;Array<string>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.
log.const log: RecordingLogwrites.RecordingLog.writes: {buffer: GPUBuffer;offset: number;size: number;}[]Records every
queue.writeBufferin call order, with sizes in bytes.length = 0;Array<T>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.
loop.const loop: frames.Framesrun(2 / 60);Frames.run(seconds: number): booleanRecords and submits one frame with a fresh
Frameand encoder. Requires finite, nondecreasingseconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts andrunrethrows their errors.expect(expect<string[]>(actual: string[], message?: string): Assertion<void, string[]> (+1 overload)log.const log: RecordingLogevents).RecordingLog.events: string[]Lists pass, draw, dispatch and submit calls in recording order.
toEqual([JestAssertion<void, string[]>.toEqual: <string[]>(expected: string[]) => voidUsed when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.
'begin-compute','dispatch','end-compute','begin-render','draw','end-render','submit',]);expect(expect<{buffer: GPUBuffer;offset: number;size: number;}[]>(actual: {buffer: GPUBuffer;offset: number;size: number;}[], message?: string): Assertion<void, {buffer: GPUBuffer;offset: number;size: number;}[]> (+1 overload)log.const log: RecordingLogwrites).RecordingLog.writes: {buffer: GPUBuffer;offset: number;size: number;}[]Records every
queue.writeBufferin call order, with sizes in bytes.toEqual([]);JestAssertion<void, { buffer: GPUBuffer; offset: number; size: number; }[]>.toEqual: <never[]>(expected: never[]) => voidUsed when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.
expect(expect<RecordingCounts>(actual: RecordingCounts, message?: string): Assertion<void, RecordingCounts> (+1 overload)log.const log: RecordingLogcounts()).RecordingLog.counts(): RecordingCountsReturns counts of created objects. Steady frames should leave these counts unchanged.
toEqual(JestAssertion<void, RecordingCounts>.toEqual: <RecordingCounts>(expected: RecordingCounts) => voidUsed when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.
counts);const counts: RecordingCountssurface.const surface: OffscreenSurfacesetSize({OffscreenSurface.setSize(size: FixedSize): voidReplaces the texture with one of the given size. Reports the supplied
pixelRatio, or 1 if omitted.width: 128,FixedSize.width: numberheight: 72 });FixedSize.height: numberloop.const loop: frames.Framesrun(3 / 60);Frames.run(seconds: number): booleanRecords and submits one frame with a fresh
Frameand encoder. Requires finite, nondecreasingseconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts andrunrethrows their errors.expect(expect<number[]>(actual: number[], message?: string): Assertion<void, number[]> (+1 overload)log.const log: RecordingLogwrites.RecordingLog.writes: {buffer: GPUBuffer;offset: number;size: number;}[]Records every
queue.writeBufferin call order, with sizes in bytes.map((Array<{ buffer: GPUBuffer; offset: number; size: number; }>.map<number>(callbackfn: (value: {buffer: GPUBuffer;offset: number;size: number;}, index: number, array: {buffer: GPUBuffer;offset: number;size: number;}[]) => number, thisArg?: any): number[]Calls a defined callback function on each element of an array, and returns an array that contains the results.
write) =>write: {buffer: GPUBuffer;offset: number;size: number;}write.write: {buffer: GPUBuffer;offset: number;size: number;}size)).size: numbertoEqual([8]); // the viewportJestAssertion<void, number[]>.toEqual: <number[]>(expected: number[]) => voidUsed when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.
field.const field: {settings: TgpuUniform<WgslStruct<{speed: F32;scale: F32;pointSize: F32;}>>;initAsync(): Promise<void>;seed(pass: TgpuComputePass): void;step(pass: TgpuComputePass): void;draw(pass: TgpuRenderCommands): void;destroy: () => void;[Symbol.dispose]: () => void;}destroy();destroy: () => voidFrees everything it took, the last taken first, once; later calls do nothing. When one throws, frees the rest and then rethrows. Also
[Symbol.dispose].expect(() =>expect<() => boolean>(actual: () => boolean, message?: string): Assertion<void, () => boolean> (+1 overload)loop.const loop: frames.Framesrun(4 / 60)).Frames.run(seconds: number): booleanRecords and submits one frame with a fresh
Frameand encoder. Requires finite, nondecreasingseconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts andrunrethrows their errors.toThrow('The flow field was destroyed.');JestAssertion<void, () => boolean>.toThrow: (expected?: any) => voidUsed to test that a function throws when it is called.
Also under the alias
expect.toThrowError.- The steady frame records one step and one draw into the passes it was given, submits once, writes nothing and creates no GPU object. Without a scene, nothing writes
ahead. - A new canvas size writes the 8 bytes of
viewport, once. - After
destroy, recording throws the owner’s message.
Run
pnpm vitest. Testing covers the rest of the log, and how to compile the WGSL on a real GPU. - The steady frame records one step and one draw into the passes it was given, submits once, writes nothing and creates no GPU object. Without a scene, nothing writes
What you wrote
Section titled “What you wrote”Directorysrc/
- index.ts the page:
web.hostand the settings panel - scene.ts
createScene(root, surface)with the field, the fixed step and the frame - flow-field.ts the module
- flow-field.test.ts its test on a recording root
- index.ts the page:
- Let the field drift over time. Sample
perlin3dwith the elapsed time as the third coordinate in place ofperlin2d, from a uniform the scene writes once a step. - Add finer eddies. Sample the noise again at twice the scale and add half of it to the angle.
- Give
Particlean age, count it up in the step kernel and fade each particle in after it respawns. - Package the module as the kit does, as Modules describes.
© Software Mansion 2026. All trademarks and copyrights belong to their respective owners. Read about our Privacy Policy.