Skip to content

@typegpu/three

A banner image showcasing a snippet of code using @typegpu/three

TSL (Three.js Shading Language) is a node-based shader composition system for Three.js. Shader logic and control flow is built up by composing special functions, with a focus on composability, intuitive sharing of logic across modules and customizability. TypeGPU fits naturally into this system thanks to the @typegpu/three package. You can choose to write your TSL building blocks in TypeGPU, which has a few benefits:

  • Control-flow like if statements and for loops makes use of familiar JavaScript syntax instead of special functions.
  • The code you write is semantically valid JavaScript, with types flowing through each expression.
  • Unit-testability, since you can call these functions on the CPU

You can see a direct comparison between TSL and TypeGPU here.

Refer to TypeGPU’s installation guide for setting up TypeGPU if you haven’t already. After that, install Three.js and @typegpu/three using the package manager of your choice.

npm install three @typegpu/three

Calling t3.toTSL with a TypeGPU function will return a TSL node, which can then be plugged into material properties, or used by other nodes.

import * as
import THREE
THREE
from 'three/webgpu';
import * as
import t3
t3
from '@typegpu/three';
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.colorNode: THREE.Node | null

The diffuse color of node materials is by default inferred from the color and map properties. This node property allows to overwrite the default and define the diffuse color with a node instead.

material.colorNode = color( 0xff0000 ); // define red color

If you don't want to overwrite the diffuse color but modify the existing values instead, use

materialColor

.

material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint

@defaultnull

colorNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
return
import d
d
.
const vec4f: d.Vec4f
(x: number, y: number, z: number, w: number) => d.v4f (+9 overloads)
vec4f
(1, 0, 0, 1); // just red
});
A red cube

We can use other TSL nodes within TypeGPU functions with t3.fromTSL:

import * as THREE from 'three/webgpu';
import * as TSL from 'three/tsl';
import * as t3 from '@typegpu/three';
const material = new THREE.MeshBasicNodeMaterial();
const albedo = TSL.color('magenta').mul(0.5);
const albedoAccess = t3.fromTSL(albedo, d.vec3f);
material.colorNode = t3.toTSL(() => {
'use gpu';
return d.vec4f(albedoAccess.$, 1);
});
A magenta cube

For geometry attributes, t3.attribute is a typed shorthand that creates the TSL attribute node and makes it available to TypeGPU using the supplied schema:

import * as
import THREE
THREE
from 'three/webgpu';
import * as
import t3
t3
from '@typegpu/three';
const
const position: t3.TSLAccessor<d.Vec3f, THREE.AttributeNode>
position
=
import t3
t3
.
attribute<d.Vec3f>(name: string, dataType: d.Vec3f): t3.TSLAccessor<d.Vec3f, THREE.AttributeNode>
export attribute

Shorthand for t3.fromTSL(attribute(...), ...).

@example

const position = t3.attribute('position', d.vec3f);
// Equivalent to:
// const position = t3.fromTSL(
// attribute('position', 'vec3'),
// d.vec3f,
// );

attribute
('position',
import d
d
.
const vec3f: d.Vec3f
vec3f
);
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.positionNode: THREE.Node | null

The local vertex positions are computed based on multiple factors like the attribute data, morphing or skinning. This node property allows to overwrite the default and define local vertex positions with nodes instead.

If you don't want to overwrite the vertex positions but modify the existing values instead, use

positionLocal

.

material.positionNode = positionLocal.add( displace );

@defaultnull

positionNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
return
const position: t3.TSLAccessor<d.Vec3f, THREE.AttributeNode>
position
.
TSLAccessor<Vec3f, AttributeNode>.$: d.v3f
$
* 2;
});

This is equivalent to t3.fromTSL(TSL.attribute('position', 'vec3'), d.vec3f), with the corresponding TSL node type inferred from the TypeGPU schema.

Similarly, t3.uniform creates a Three.js uniform and exposes it as a typed TypeGPU accessor. Its value can be updated through the underlying uniform node:

import * as
import THREE
THREE
from 'three/webgpu';
import * as
import t3
t3
from '@typegpu/three';
const
const tint: t3.TSLAccessor<d.Vec3f, THREE.UniformNode<THREE.Color>>
tint
=
import t3
t3
.
uniform<THREE.Color, d.Vec3f>(value: THREE.Color | InputNode<THREE.Color>, dataType: d.Vec3f): t3.TSLAccessor<d.Vec3f, THREE.UniformNode<THREE.Color>>
export uniform

Shorthand for t3.fromTSL(uniform(...), ...)

@example

const attractorsLength = t3.uniform(attractorsPositions.array.length, d.u32);
// Equivalent to:
// const attractorsLength = t3.fromTSL(
// uniform(attractorsPositions.array.length, 'uint'),
// d.u32,
// );

uniform
(new
import THREE
THREE
.
constructor Color(color?: THREE.ColorRepresentation): THREE.Color (+1 overload)
Color
('hotpink'),
import d
d
.
const vec3f: d.Vec3f
vec3f
);
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.colorNode: THREE.Node | null

The diffuse color of node materials is by default inferred from the color and map properties. This node property allows to overwrite the default and define the diffuse color with a node instead.

material.colorNode = color( 0xff0000 ); // define red color

If you don't want to overwrite the diffuse color but modify the existing values instead, use

materialColor

.

material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint

@defaultnull

colorNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
return
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const tint: t3.TSLAccessor<d.Vec3f, THREE.UniformNode<THREE.Color>>
tint
.
TSLAccessor<Vec3f, UniformNode<Color>>.$: d.v3f
$
, 1);
});
// Later, update the uniform without rebuilding the material.
const tint: t3.TSLAccessor<d.Vec3f, THREE.UniformNode<THREE.Color>>
tint
.
TSLAccessor<Vec3f, UniformNode<Color>>.node: THREE.UniformNode<THREE.Color>
node
.
InputNode<Color>.value: THREE.Color
value
.
Color.set(...args: [color: THREE.ColorRepresentation] | [r: number, g: number, b: number]): THREE.Color
set
('skyblue');

This is equivalent to t3.fromTSL(TSL.uniform(value, nodeType), schema). For uniform arrays, use t3.uniformArray(values, elementSchema) and access individual elements through uniforms.$[index] inside TypeGPU code.

Three.js textures can also be imported as typed texture handles. Pass a texture directly to fromTSL, then use the regular TypeGPU texture functions to load or sample it:

import * as
import THREE
THREE
from 'three/webgpu';
import * as
import TSL
TSL
from 'three/tsl';
import * as
import t3
t3
from '@typegpu/three';
const
const texture: THREE.DataTexture
texture
= new
import THREE
THREE
.
constructor DataTexture(data?: THREE.TypedArray | null, width?: number, height?: number, format?: THREE.PixelFormat, type?: THREE.TextureDataType, mapping?: THREE.Mapping, wrapS?: THREE.Wrapping, wrapT?: THREE.Wrapping, magFilter?: THREE.MagnificationTextureFilter, minFilter?: THREE.MinificationTextureFilter, anisotropy?: number, colorSpace?: string): THREE.DataTexture

Creates a texture directly from raw data, width and height.

@paramdata https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView ArrayBufferView of the texture. Default null.

@paramwidth Width of the texture. Default 1.

@paramheight Height of the texture. Default 1.

@paramformat See Texture.format.format. Default THREE.RGBAFormat

@paramtype See Texture.type.type. Default THREE.UnsignedByteType

@parammapping See Texture.mapping.mapping. Default THREE.Texture.DEFAULT_MAPPING

@paramwrapS See Texture.wrapS.wrapS. Default THREE.ClampToEdgeWrapping

@paramwrapT See Texture.wrapT.wrapT. Default THREE.ClampToEdgeWrapping

@parammagFilter See Texture.magFilter.magFilter. Default THREE.NearestFilter

@paramminFilter See Texture.minFilter.minFilter. Default THREE.NearestFilter

@paramanisotropy See Texture.anisotropy.anisotropy. Default THREE.Texture.DEFAULT_ANISOTROPY

@paramcolorSpace See Texture.colorSpace.colorSpace. Default NoColorSpace

DataTexture
(new
var Uint8Array: Uint8ArrayConstructor
new (length: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(4), 1, 1);
const texture: THREE.DataTexture
texture
.
Texture<DataTextureImageData>.needsUpdate: boolean

Set this to true to trigger an update next time the texture is used. Particularly important for setting the wrap mode.

needsUpdate
= true;
const
const textureAccess: t3.TSLAccessor<d.WgslTexture2d<d.F32>, THREE.Node>
textureAccess
=
import t3
t3
.
fromTSL<d.WgslTexture2d<d.F32>>(texture: THREE.Texture, type: d.WgslTexture2d<d.F32>): t3.TSLAccessor<d.WgslTexture2d<d.F32>, THREE.Node> (+2 overloads)
export fromTSL
fromTSL
(
const texture: THREE.DataTexture
texture
,
import d
d
.
function texture2d(): d.WgslTexture2d<d.F32> (+1 overload)
export texture2d
texture2d
());
const
const samplerAccess: t3.TSLAccessor<d.WgslSampler, THREE.Node>
samplerAccess
=
import t3
t3
.
fromTSL<d.WgslSampler, THREE.Node>(node: THREE.Node, type: d.WgslSampler): t3.TSLAccessor<d.WgslSampler, THREE.Node> (+2 overloads)
export fromTSL
fromTSL
(
import TSL
TSL
.
const sampler: (value: THREE.Texture | THREE.TextureNode) => THREE.Node
sampler
(
const texture: THREE.DataTexture
texture
),
import d
d
.
function sampler(): d.WgslSampler
export sampler
sampler
());
const
const uvAccess: t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uvAccess
=
import t3
t3
.
const uv: (index?: number) => t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uv
();
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.colorNode: THREE.Node | null

The diffuse color of node materials is by default inferred from the color and map properties. This node property allows to overwrite the default and define the diffuse color with a node instead.

material.colorNode = color( 0xff0000 ); // define red color

If you don't want to overwrite the diffuse color but modify the existing values instead, use

materialColor

.

material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint

@defaultnull

colorNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
return
import std
std
.
textureSample<d.texture2d<d.F32>>(texture: d.texture2d<d.F32>, sampler: d.sampler, coords: d.v2f, offset?: d.v2i): d.v4f (+8 overloads)
export textureSample
textureSample
(
const textureAccess: t3.TSLAccessor<d.WgslTexture2d<d.F32>, THREE.Node>
textureAccess
.
TSLAccessor<WgslTexture2d<F32>, Node>.$: d.texture2d<d.F32>
$
,
const samplerAccess: t3.TSLAccessor<d.WgslSampler, THREE.Node>
samplerAccess
.
TSLAccessor<WgslSampler, Node>.$: d.sampler
$
,
const uvAccess: t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uvAccess
.
TSLAccessor<Vec2f, AttributeNode>.$: d.v2f
$
);
});

See the interactive texture access example for a material that combines filtered textureSample colors with crisp textureLoad accents.

There are a handful of builtin TSL node accessors in the t3 namespace:

import * as
import THREE
THREE
from 'three/webgpu';
import * as
import t3
t3
from '@typegpu/three';
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.colorNode: THREE.Node | null

The diffuse color of node materials is by default inferred from the color and map properties. This node property allows to overwrite the default and define the diffuse color with a node instead.

material.colorNode = color( 0xff0000 ); // define red color

If you don't want to overwrite the diffuse color but modify the existing values instead, use

materialColor

.

material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint

@defaultnull

colorNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
const
const uv: d.v2f
uv
=
import t3
t3
.
const uv: (index?: number) => t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uv
().
TSLAccessor<Vec2f, AttributeNode>.$: d.v2f
$
;
return
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec2Instance, z: number, w: number) => d.v4f (+9 overloads)
vec4f
(
const uv: d.v2f
uv
, 0, 1);
});
A UV cube

Other TypeGPU functions (user-defined or from libraries) can be called to achieve more complex effects.

import {
import perlin3d
perlin3d
} from '@typegpu/noise';
import * as
import THREE
THREE
from 'three/webgpu';
import * as
import t3
t3
from '@typegpu/three';
const
const material: THREE.MeshBasicNodeMaterial
material
= new
import THREE
THREE
.
new MeshBasicNodeMaterial(parameters?: THREE.MeshBasicNodeMaterialParameters): THREE.MeshBasicNodeMaterial
export MeshBasicNodeMaterial

Constructs a new mesh basic node material.

@paramparameters - The configuration parameter.

MeshBasicNodeMaterial
();
const material: THREE.MeshBasicNodeMaterial
material
.
NodeMaterialNodeProperties.colorNode: THREE.Node | null

The diffuse color of node materials is by default inferred from the color and map properties. This node property allows to overwrite the default and define the diffuse color with a node instead.

material.colorNode = color( 0xff0000 ); // define red color

If you don't want to overwrite the diffuse color but modify the existing values instead, use

materialColor

.

material.colorNode = materialColor.mul( color( 0xff0000 ) ); // give diffuse colors a red tint

@defaultnull

colorNode
=
import t3
t3
.
function toTSL(fn: () => unknown): THREE.TSL.NodeObject<THREE.Node>
export toTSL
toTSL
(() => {
'use gpu';
const
const coords: d.v2f
coords
=
import t3
t3
.
const uv: (index?: number) => t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uv
().
TSLAccessor<Vec2f, AttributeNode>.$: d.v2f
$
* 2;
const
const pattern: number
pattern
=
import perlin3d
perlin3d
.
function sample(pos: d.v3f): number
export sample
sample
(
import d
d
.
const vec3f: d.Vec3f
(v0: AnyNumericVec2Instance, z: number) => d.v3f (+5 overloads)
vec3f
(
const coords: d.v2f
coords
,
import t3
t3
.
const time: t3.TSLAccessor<d.F32, THREE.Node>
time
.
TSLAccessor<F32, Node>.$: number
$
* 0.2));
return
import d
d
.
const vec4f: d.Vec4f
(x: number, y: number, z: number, w: number) => d.v4f (+9 overloads)
vec4f
(
import std
std
.
function tanh(value: number): number (+1 overload)
export tanh
tanh
(
const pattern: number
pattern
* 5), 0.2, 0.4, 1);
});
A funky cube

The code and interactive preview of this example can be found here.

Below are a select few cases comparing TSL and TypeGPU:

TSL:

const simulate = Fn(() => {
//
// ... TSL code ...
//
});

TypeGPU:

const
const simulate: Node
simulate
=
import t3
t3
.
function toTSL(fn: () => unknown): NodeObject<Node>
export toTSL
toTSL
(() => {
'use gpu';
//
// ... TypeGPU code ...
//
});

TSL:

const oscSine = Fn(([t = time]) => {
return t.add(0.75).mul(Math.PI * 2).sin().mul(0.5).add(0.5);
});

TypeGPU:

const
const oscSine: (t: number) => number
oscSine
= (
t: number
t
: number) => {
'use gpu';
return
import std
std
.
function sin(value: number): number (+1 overload)
export sin
sin
((
t: number
t
+ 0.75) *
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.PI: number

Pi. This is the ratio of the circumference of a circle to its diameter.

PI
* 2) * 0.5 + 0.5;
};

TSL:

If(instanceIndex.greaterThanEqual(uint(vertexCount)), () => {
Return();
});

TypeGPU:

if (
import t3
t3
.
const instanceIndex: t3.TSLAccessor<U32, IndexNode>
instanceIndex
.
TSLAccessor<U32, IndexNode>.$: number
$
>=
const vertexCount: number
vertexCount
) {
return;
}

TSL:

Loop({ start: ptrStart, end: ptrEnd, type: 'uint', condition: '<' }, ({ i }) => {
const springId = springListBuffer.element( i ).toVar( 'springId' );
const springForce = springForceBuffer.element( springId );
const springVertexIds = springVertexIdBuffer.element( springId );
const factor = select( springVertexIds.x.equal( instanceIndex ), 1.0, - 1.0 );
force.addAssign( springForce.mul( factor ) );
});

TypeGPU:

for (let
let i: number
i
=
const ptrStart: number
ptrStart
;
let i: number
i
<
const ptrEnd: number
ptrEnd
;
let i: number
i
++) {
const
const springId: number
springId
=
const springListBuffer: TSLStorageAccessor<d.WgslArray<d.U32>>
springListBuffer
.
TSLAccessor<WgslArray<U32>, StorageBufferNode>.$: number[]
$
[
let i: number
i
];
const
const springForce: d.v3f
springForce
=
const springForceBuffer: TSLStorageAccessor<d.WgslArray<d.Vec3f>>
springForceBuffer
.
TSLAccessor<WgslArray<Vec3f>, StorageBufferNode>.$: d.v3f[]
$
[
const springId: number
springId
];
const
const springVertexIds: d.v2u
springVertexIds
=
const springVertexIdBuffer: TSLStorageAccessor<d.WgslArray<d.Vec2u>>
springVertexIdBuffer
.
TSLAccessor<WgslArray<Vec2u>, StorageBufferNode>.$: d.v2u[]
$
[
const springId: number
springId
];
const
const factor: number
factor
=
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(-1, 1,
const springVertexIds: d.v2u
springVertexIds
.
v2u.x: number
x
===
const idx: number
idx
);
let force: d.v3f
force
+=
const springForce: d.v3f
springForce
*
const factor: number
factor
;
}