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
.
function vec4f(x: number, y: number, z: number, w: number): d.v4f (+9 overloads)
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

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

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 | undefined) => t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uv
().
TSLAccessor<Vec2f, AttributeNode>.$: d.v2f
$
;
return
import d
d
.
function vec4f(v0: AnyNumericVec2Instance, z: number, w: number): d.v4f (+9 overloads)
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

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 | undefined) => t3.TSLAccessor<d.Vec2f, THREE.AttributeNode>
uv
().
TSLAccessor<Vec2f, AttributeNode>.$: d.v2f
$
.
vecInfixNotation<v2f>.mul(other: number): d.v2f (+2 overloads)
mul
(2);
const
const pattern: number
pattern
=
import perlin3d
perlin3d
.
function sample(pos: d.v3f): number
export sample
sample
(
import d
d
.
function vec3f(v0: AnyNumericVec2Instance, z: number): d.v3f (+5 overloads)
export vec3f

Schema representing vec3f - a vector with 3 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec3f(); // (0.0, 0.0, 0.0) const vector = d.vec3f(1); // (1.0, 1.0, 1.0) const vector = d.vec3f(1, 2, 3.5); // (1.0, 2.0, 3.5)

@example const buffer = root.createBuffer(d.vec3f, d.vec3f(0, 1, 2)); // buffer holding a d.vec3f value, with an initial value of vec3f(0, 1, 2);

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
.
function vec4f(x: number, y: number, z: number, w: number): d.v4f (+9 overloads)
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

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 i = ptrStart; i < ptrEnd; i++) {
const springId = springListBuffer.$[i];
const springForce = springForceBuffer.$[springId];
const springVertexIds = springVertexIdBuffer.$[springId];
const factor = std.select(-1, 1, springVertexIds.x === idx);
force = force.add(springForce.mul(d.f32(factor)));
}