Files
skills/plugins/suaveplan-webgpu/skills/webgpu-threejs-tsl/docs/core-concepts.md
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 01:10:00 +01:00

8.8 KiB

TSL Core Concepts

Three Shading Language (TSL) is a graph-based shading system where you compose JavaScript objects ("nodes") into shader programs. The Three.js compiler then emits WGSL (WebGPU) or GLSL (WebGL) depending on the active backend.

Node Graph Model

Every TSL expression produces a node. Nodes are connected by method calls to form an acyclic graph. The compiler walks the graph to generate shader source.

import { float, sin, timerLocal, color, mix } from 'three/tsl';

// Each call returns a node; chaining builds the graph
const time = timerLocal();
const wave = sin(time.mul(2.0)).mul(0.5).add(0.5);
const result = mix(color(0x000000), color(0x3b82f6), wave);

This graph compiles to roughly:

let wave = sin(time * 2.0) * 0.5 + 0.5;
let result = mix(vec3(0.0), vec3(0.231, 0.510, 0.965), wave);

Primitive Types

Scalars

float(1.0)    // f32
int(5)        // i32
uint(10)      // u32
bool(true)    // bool

Vectors

vec2(1.0, 2.0)              // vec2f
vec3(1.0, 0.0, 0.0)         // vec3f
vec4(1.0, 0.0, 0.0, 1.0)    // vec4f
color(0xff6600)              // vec3f from hex
color(new THREE.Color('red')) // vec3f from Color

Matrices

mat2(1, 0, 0, 1)  // 2x2 identity
mat3(...)          // 3x3
mat4(...)          // 4x4

Type Coercion

TSL auto-promotes scalars in arithmetic:

vec3(1, 0, 0).mul(0.5)  // float(0.5) auto-wrapped
float(1).add(2)          // int 2 auto-wrapped to float

Swizzling

All vector nodes support GLSL-style swizzle access:

const v = vec4(1, 2, 3, 4);
v.x            // float -- component access
v.xy           // vec2
v.xyz          // vec3
v.zyx          // vec3 -- reversed
v.rrr          // vec3 -- RGBA aliases work too
v.stpq         // vec4 -- texture coordinate aliases

Assignment swizzles are not supported in TSL. To set individual components, construct a new vector:

// Instead of v.x = 5, do:
const modified = vec4(float(5), v.y, v.z, v.w);

Operators

TSL uses method calls rather than JavaScript operators because JS does not support operator overloading.

Arithmetic

a.add(b)     // +
a.sub(b)     // -
a.mul(b)     // *
a.div(b)     // /
a.mod(b)     // %
a.negate()   // unary -

Comparison

a.equal(b)           // ==
a.notEqual(b)        // !=
a.lessThan(b)        // <
a.greaterThan(b)     // >
a.lessThanEqual(b)   // <=
a.greaterThanEqual(b) // >=

Logical

a.and(b)   // &&
a.or(b)    // ||
a.not()    // !

Assignment (Compute Only)

In compute shaders, storage elements support in-place assignment:

element.assign(newValue)       // =
element.addAssign(delta)       // +=
element.subAssign(delta)       // -=
element.mulAssign(factor)      // *=

Uniforms

Uniforms are the bridge between JavaScript (React state, useFrame) and GPU code.

Creating Uniforms

import { uniform } from 'three/tsl';

const uFloat = uniform(0.0);           // float uniform
const uVec3 = uniform(new THREE.Vector3()); // vec3 uniform
const uColor = uniform(new THREE.Color()); // vec3 uniform
const uMat4 = uniform(new THREE.Matrix4()); // mat4 uniform
const uInt = uniform(int(0));           // explicit int uniform

Updating Uniforms in R3F

function AnimatedMesh() {
  const uProgress = useMemo(() => uniform(0.0), []);
  const uMouse = useMemo(() => uniform(new THREE.Vector2()), []);

  useFrame((state) => {
    uProgress.value = state.clock.elapsedTime;
    uMouse.value.set(
      state.pointer.x * 0.5 + 0.5,
      state.pointer.y * 0.5 + 0.5
    );
  });

  const material = useMemo(() => {
    const mat = new MeshStandardNodeMaterial();
    mat.colorNode = mix(color(0x000000), color(0xffffff), uProgress.fract());
    return mat;
  }, [uProgress]);

  return (
    <mesh>
      <boxGeometry />
      <primitive object={material} attach="material" />
    </mesh>
  );
}

Uniform Value Types

JS Value TSL Type Notes
number float Default for bare numbers
THREE.Vector2 vec2
THREE.Vector3 vec3
THREE.Vector4 vec4
THREE.Color vec3 RGB, no alpha
THREE.Matrix3 mat3
THREE.Matrix4 mat4
int(n) int Explicit int type
uint(n) uint Explicit uint type

Time and Animation

TSL has built-in time nodes that auto-increment each frame:

timerLocal()    // Seconds since this material was first rendered
timerGlobal()   // Seconds since renderer was created
timerDelta()    // Seconds elapsed since previous frame
frameId         // Integer frame counter (uint)

R3F Integration -- timerLocal vs useFrame

For pure-GPU animation, timerLocal() is simpler and avoids JS-to-GPU round trips:

// Pure GPU -- no useFrame needed
const mat = new MeshStandardNodeMaterial();
mat.colorNode = color(0x3b82f6).mul(sin(timerLocal().mul(3.0)).mul(0.5).add(0.5));

Use uniform() + useFrame() only when animation depends on React state, pointer position, scroll, or other JS-side data.

Attributes and Varyings

Built-in Attributes

positionLocal     // Vertex position in object space
normalLocal       // Vertex normal in object space
uv()              // UV channel 0
uv(1)             // UV channel 1
vertexColor       // Vertex color attribute
tangentLocal      // Tangent vector

Custom Attributes

import { attribute } from 'three/tsl';

// Reads from geometry.setAttribute('aCustom', ...)
const custom = attribute('aCustom', 'vec3');

Varyings

Varyings pass data from vertex to fragment shader:

import { varying, varyingProperty } from 'three/tsl';

// Declare a varying
const vWorldPos = varying(positionWorld, 'v_worldPos');

// Or use varyingProperty for named access
varyingProperty('vec3', 'v_myData').assign(someNode);

Control Flow

Branching (If/Else)

import { If } from 'three/tsl';

If(condition, () => {
  // true branch
}).ElseIf(otherCondition, () => {
  // else-if
}).Else(() => {
  // false branch
});

Select (Ternary)

import { select } from 'three/tsl';

// select(condition, ifTrue, ifFalse)
const result = select(uv().x.greaterThan(0.5), color(0xff0000), color(0x0000ff));

Loops

import { Loop } from 'three/tsl';

// Simple counted loop
Loop(10, ({ i }) => {
  // i is an int node, 0..9
});

// Explicit range
Loop({ start: int(0), end: int(100), type: 'int' }, ({ i }) => {
  // body
});

// Break out early
Loop(100, ({ i, Break }) => {
  If(someCondition, () => {
    Break();
  });
});

Custom Functions (Fn)

Fn creates reusable TSL functions. They compile to WGSL/GLSL functions.

import { Fn } from 'three/tsl';

const fbm = Fn(([pos, octaves]) => {
  const value = float(0.0).toVar();
  const amplitude = float(0.5).toVar();
  const frequency = float(1.0).toVar();

  Loop(octaves, () => {
    value.addAssign(mx_noise_float(pos.mul(frequency)).mul(amplitude));
    amplitude.mulAssign(0.5);
    frequency.mulAssign(2.0);
  });

  return value;
});

// Use in material
mat.colorNode = vec3(fbm(positionWorld, int(6)));

Important: .toVar()

Inside Fn, mutable local variables require .toVar():

const myValue = float(0.0).toVar();  // mutable local
myValue.addAssign(1.0);              // modify in place

Without .toVar(), the node is an immutable expression and addAssign will fail.

Math Functions

All standard math is available as both standalone functions and methods:

// Standalone
sin(x), cos(x), tan(x)
asin(x), acos(x), atan(x), atan2(y, x)
pow(x, y), exp(x), log(x), sqrt(x)
abs(x), sign(x), floor(x), ceil(x), fract(x), round(x)
min(a, b), max(a, b), clamp(x, lo, hi)
mix(a, b, t), step(edge, x), smoothstep(lo, hi, x)
length(v), distance(a, b), dot(a, b), cross(a, b), normalize(v)
reflect(v, n), refract(v, n, eta)

// As methods
x.sin(), x.cos(), x.abs(), x.floor()
v.length(), v.normalize(), v.dot(other)
a.mix(b, t), x.clamp(0, 1), x.smoothstep(0, 1)
x.saturate()  // clamp(x, 0, 1)

Textures

import { texture } from 'three/tsl';

const tex = texture(myTexture);           // Sample at default UV
const texCustom = texture(myTexture, customUV); // Custom UV
tex.uv(customUV)                          // Alternative UV syntax
tex.level(float(2))                       // Specific mip level
tex.bias(float(-1))                       // LOD bias

// Texture properties
tex.r, tex.g, tex.b, tex.a               // Channel access

R3F Texture Loading

import { useTexture } from '@react-three/drei';
import { texture } from 'three/tsl';

function TexturedMesh() {
  const map = useTexture('/textures/brick.jpg');

  const material = useMemo(() => {
    const mat = new MeshStandardNodeMaterial();
    mat.colorNode = texture(map);
    return mat;
  }, [map]);

  return (
    <mesh>
      <boxGeometry />
      <primitive object={material} attach="material" />
    </mesh>
  );
}