| name | tsl |
| description | Use when writing TSL shaders, creating NodeMaterials, migrating GLSL to TSL, using compute shaders, working with three/tsl imports, or debugging shader node graphs |
TSL (Three.js Shading Language)
TSL is the CURRENT Three.js shader system (r150+). It replaces GLSL shaders, onBeforeCompile, and ShaderMaterial. All modern Three.js shader work uses TSL.
STOP — Read Before Writing Any Shader Code
Your training data contains 10+ years of GLSL patterns. None of those apply here.
- No GLSL strings. No
vertexShader:, no fragmentShader:, no #include.
- No
ShaderMaterial. Use MeshStandardNodeMaterial, MeshBasicNodeMaterial, etc.
- No
onBeforeCompile. Node materials have .colorNode, .positionNode, etc.
- No
WebGLRenderer. TSL targets WebGPURenderer via three/webgpu.
- No
three/nodes. Deprecated. Use three/tsl.
What TSL Actually Is
TSL is TypeScript/JavaScript that builds GPU shader node graphs. Code executes at two distinct times:
- Build time: TypeScript runs once, constructing a node graph
- Run time: Compiled WGSL executes per-pixel/vertex on GPU every frame
if (material.transparent) { return transparentShader }
If(value.greaterThan(0.5), () => { result.assign(1.0) })
Imports
import * as THREE from 'three/webgpu'
import { Fn, vec3, float, uniform } from 'three/tsl'
import type Node from 'three/src/nodes/core/Node.js'
import type UniformNode from 'three/src/nodes/core/UniformNode.js'
await renderer.init() before any render or compute. Skipping is silent — nothing renders.
Core Rules
Nodes are immutable — use .toVar() to modify
const pos = positionLocal.toVar()
pos.y.assign(pos.y.add(1))
Method chains for math — NOT operators
const result = a.add(b.mul(c))
a.greaterThan(b)
Capital If for GPU conditionals
If(cond, () => { ... })
select(cond, valueIfTrue, valueIfFalse)
step(edge, x) mix(a, b, t) smoothstep(e0, e1, x)
Uniforms update via .value
const speed = uniform(1.0)
speed.value = 2.0
Fn() needs trailing () to invoke
material.colorNode = Fn(() => { return color })()
Uniforms vs compile-time constants
const radius = uniform(5.0)
const SAMPLES = 16
Uniforms for tuning knobs (radius, intensity, color). Constants for structural params (sample count, number of passes).
Build-time vs run-time loops
for (let i = 0; i < samples.length; i++) { ... }
Loop(count, ({ i }) => { ... })
GLSL → TSL Transpiler
The most valuable tool for porting shaders. Run GLSL through the transpiler before guessing:
import GLSLDecoder from 'three/examples/jsm/transpiler/GLSLDecoder.js'
import TSLEncoder from 'three/examples/jsm/transpiler/TSLEncoder.js'
const decoder = new GLSLDecoder()
const encoder = new TSLEncoder()
encoder.iife = false
const ast = decoder.parse(glslString)
console.log(encoder.emit(ast))
Handles: texture2D → .sample(), math operators → method chains, uniforms, loops.
Does NOT handle: WebGL vs WebGPU depth conventions, getViewPosition/getScreenPosition helpers, TempNode orchestration.
Workflow: Transpile → adapt for WebGPU (Y flip, clip Z 0..1 not -1..1) → verify visually.
Two Execution Contexts
| Material Nodes | PostProcessing |
|---|
| Runs on | Per-fragment with geometry | Fullscreen quad after scene renders |
| Depth access | N/A (you ARE the geometry) | depthNode.sample(uv) |
| Complex effects | Just assign node | Needs TempNode + own RenderTarget |
Screen-space effects that sample neighbors (AO, blur, edge detection) need the TempNode pattern — see postprocessing.md.
Common Error Patterns
| Error | GLSL/JS Instinct | TSL Way |
|---|
| "If is not defined" | if(cond, fn) | If(cond, fn) — capital I |
| Cannot assign to node | v.x = 5 | v.x.assign(5) after .toVar() |
| Uniform not changing | u = val | u.value = val |
| Nothing renders | Render before init | await renderer.init() |
| Import not found | 'three/nodes' or 'three' | 'three/tsl' and 'three/webgpu' |
| Attribute type error | attribute('n', 'vec4') | attribute<'vec4'>('n', 'vec4') |
| Depth texture empty | depthNode.value | depthNode.sample(uv).r |
| Screen-space black/white | Inline in PostProcessing | TempNode + RenderTarget |
.add() not found | Cast to bare Node | Keep Node<'vec2'> — don't lose generic |
| mat2(a,b,c,d) | GLSL 4-arg mat2 | Manual 2D rotation — TSL mat2() only takes Matrix2 |
Reference Files
| File | Contents |
|---|
| nodes.md | Type constructors, operators, math, uniforms, Fn(), attributes, conditionals, loops, shader inputs, NodeMaterials, textures |
| postprocessing.md | pass(), MRT, depth access, TempNode pattern, WebGPU depth conventions, ping-pong, MSAA gotcha, GTAO+Denoise example |
| compute.md | Storage buffers, compute shaders, compute-to-render pipeline |
| typescript.md | tsconfig, TempNode subclassing in TS, known @types/three gaps + workarounds |
| migration.md | GLSL→TSL transpiler workflow, habit mapping tables, built-in variable mapping |
Summary — TSL Rules
'three/tsl' and 'three/webgpu'. Not 'three/nodes'. Not 'three'.
await renderer.init() before any render or compute.
.toVar() then .assign() to modify nodes. Never =.
.add(), .mul() for math. Not +, *.
- Capital
If for GPU conditionals. Lowercase if is build-time only.
uniform.value = x to update. Not uniform = x.
Fn(() => { ... })() — trailing () to invoke.
attribute<'vec4'>(...) — explicit generic for @types/three >= 0.183.
depthNode.sample(uv) for PostProcessing depth. Never .value.
- TempNode + RenderTarget for screen-space effects. Can't inline.
- Transpile GLSL first — don't guess depth conventions.
screenSize for dynamic resolution. Never hardcode window.innerWidth.