
Webgpu Threejs Tsl
- 932 installs
- 1.1k repo stars
- Updated April 10, 2026
- dgreenheck/webgpu-claude-skill
webgpu-threejs-tsl is a frontend agent skill that teaches correct Three.js TSL compute shader patterns for developers building WebGPU GPU parallel workloads in JavaScript.
About
webgpu-threejs-tsl is a specialized skill from dgreenheck/webgpu-claude-skill focused on Three.js TSL compute shaders that run parallel GPU processing from JavaScript. Its core lesson is that TSL intercepts property assignments on nodes—such as `node.y = value`, `node.x.assign(value)`, and `buffer.element(i).assign(v)`—but cannot track JavaScript variable reassignment like `variable = variable.add(1)`. Developers reach for webgpu-threejs-tsl when Claude-powered agents emit broken TSL compute code that silently fails because agents confuse JS reassignment with TSL node mutation. The skill includes worked examples and a pattern compatibility table distinguishing property setters from invalid reassignment.
- Teaches the critical difference between TSL node property assignment and JavaScript variable reassignment
- Provides working patterns using property setters and .assign() calls that TSL can intercept
- Shows exact failure modes when reassigning JS variables inside compute shader logic
- Includes ready-to-use code examples for vec3 manipulation and buffer operations in compute shaders
Webgpu Threejs Tsl by the numbers
- 932 all-time installs (skills.sh)
- +34 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #411 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dgreenheck/webgpu-claude-skill --skill webgpu-threejs-tslAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 932 |
|---|---|
| repo stars | ★ 1.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 10, 2026 |
| Repository | dgreenheck/webgpu-claude-skill ↗ |
How do you write Three.js TSL compute shaders correctly?
Correctly write GPU compute shaders with Three.js TSL inside Claude-powered agents.
Who is it for?
Frontend developers building WebGPU compute pipelines in Three.js TSL who hit silent failures from JS reassignment anti-patterns.
Skip if: Teams using WebGL-only renderers, non-Three.js GPU frameworks, or backend compute without browser TSL nodes.
When should I use this skill?
A Three.js TSL or WebGPU compute task shows broken node updates, .assign() confusion, or JS variable reassignment in shader code.
What you get
Working TSL compute shader snippets, node assignment patterns, and GPU buffer update code compatible with WebGPU.
- TSL compute shader snippet
- Node assignment pattern reference
- GPU buffer update code
By the numbers
- Documents 4 TSL mutation patterns in its compatibility table
Files
WebGPU Three.js with TSL
TSL (Three.js Shading Language) is a node-based shader abstraction that lets you write GPU shaders in JavaScript instead of GLSL/WGSL strings.
Quick Start
import * as THREE from 'three/webgpu';
import { color, time, oscSine } from 'three/tsl';
const renderer = new THREE.WebGPURenderer();
await renderer.init();
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = color(0xff0000).mul(oscSine(time));Skill Contents
Documentation
docs/core-concepts.md- Types, operators, uniforms, control flowdocs/materials.md- Node materials and all propertiesdocs/compute-shaders.md- GPU compute with instanced arraysdocs/post-processing.md- Built-in and custom effectsdocs/wgsl-integration.md- Custom WGSL functionsdocs/device-loss.md- Handling GPU device loss and recoverydocs/limits-and-features.md- WebGPU device limits and optional features
Examples
examples/basic-setup.js- Minimal WebGPU projectexamples/custom-material.js- Custom shader materialexamples/particle-system.js- GPU compute particlesexamples/post-processing.js- Effect pipelineexamples/earth-shader.js- Complete Earth with atmosphere
Templates
templates/webgpu-project.js- Starter project templatetemplates/compute-shader.js- Compute shader template
Reference
REFERENCE.md- Quick reference cheatsheet
Key Concepts
Import Pattern
// Always use the WebGPU entry point
import * as THREE from 'three/webgpu';
import { /* TSL functions */ } from 'three/tsl';Node Materials
Replace standard material properties with TSL nodes:
material.colorNode = texture(map); // instead of material.map
material.roughnessNode = float(0.5); // instead of material.roughness
material.positionNode = displaced; // vertex displacementMethod Chaining
TSL uses method chaining for operations:
// Instead of: sin(time * 2.0 + offset) * 0.5 + 0.5
time.mul(2.0).add(offset).sin().mul(0.5).add(0.5)Custom Functions
Use Fn() for reusable shader logic:
const fresnel = Fn(([power = 2.0]) => {
const nDotV = normalWorld.dot(viewDir).saturate();
return float(1.0).sub(nDotV).pow(power);
});When to Use This Skill
- Setting up Three.js with WebGPU renderer
- Creating custom shader materials with TSL
- Writing GPU compute shaders
- Building post-processing pipelines
- Migrating from GLSL to TSL
- Implementing visual effects (particles, water, terrain, etc.)
Resources
- Three.js TSL Wiki
- WebGPU Examples (files prefixed with
webgpu_)
TSL Compute Shaders
Compute shaders run on the GPU for parallel processing of data. TSL makes them accessible through JavaScript.
CRITICAL: TSL Node Property Assignment vs JS Variable Reassignment
TSL can intercept property assignments on nodes, but NOT JavaScript variable reassignment.
What Works vs What Doesn't
| Pattern | Works? | Why |
|---|---|---|
node.y = value | ✅ | Property setter - TSL intercepts |
node.x.assign(value) | ✅ | TSL method call |
buffer.element(i).assign(v) | ✅ | TSL method call |
variable = variable.add(1) | ❌ | JS variable reassignment - TSL can't see it |
This WORKS (property assignment on vec3):
// ✅ CORRECT - Property assignment on node object
const computeShader = Fn(() => {
const result = vec3(position);
If(result.y.greaterThan(limit), () => {
result.y = limit; // TSL intercepts property setters!
});
return result;
})();This does NOT work (JS variable reassignment):
// ❌ WRONG - JavaScript variable reassignment inside If()
const computeShader = Fn(() => {
let value = buffer.element(index).toFloat(); // Scalar float - no .x/.y properties
If(condition, () => {
value = value.add(1.0); // JS reassigns variable to NEW node - TSL can't track this!
});
buffer.element(index).assign(value); // Uses ORIGINAL node, not the add result!
})().compute(count);Why it fails: value = value.add(1.0) creates a new TSL node and reassigns the JavaScript variable to point to it. But TSL can't intercept JavaScript variable assignment - it can only intercept property setters and method calls on TSL node objects. Since value is a scalar float (no .x/.y properties), you can't use property assignment.
Solution 1: Use select() for Conditional Values
// ✅ CORRECT - Use select() for inline conditionals
import { select } from 'three/tsl';
const computeShader = Fn(() => {
const currentValue = buffer.element(index).toFloat();
// select(condition, valueIfTrue, valueIfFalse)
const newValue = select(
condition,
currentValue.add(1.0), // If true
currentValue // If false
);
buffer.element(index).assign(newValue);
})().compute(count);Solution 2: Use .assign() Directly on Buffer Elements Inside If()
// ✅ CORRECT - Direct buffer assignment inside If() works
const computeShader = Fn(() => {
const element = buffer.element(index);
If(condition, () => {
// Direct assignment to buffer element works!
element.assign(element.add(1.0));
});
})().compute(count);Solution 3: Use .toVar() for Mutable Variables
// ✅ CORRECT - Use .toVar() for variables that need mutation
const computeShader = Fn(() => {
// .toVar() creates a proper GPU variable that can be reassigned
const value = buffer.element(index).toFloat().toVar();
If(condition, () => {
value.assign(value.add(1.0)); // This works with .toVar()!
});
buffer.element(index).assign(value);
})().compute(count);Quick Reference: When to Use What
| Pattern | Use Case |
|---|---|
select(cond, a, b) | Simple conditional value selection |
element.assign() inside If() | Direct buffer writes |
.toVar() + assign() | Complex logic with multiple conditionals |
Regular If() with direct assigns | Multiple buffer element updates |
Example: Correct Stamp/Fade Pattern
// ✅ CORRECT implementation of conditional stamping
const computeShader = Fn(() => {
const currentFoam = foamBuffer.element(index).toFloat();
// Calculate distance
const dist = worldPos.distance(stampPos);
const radius = float(50.0);
// Calculate falloff (will be 0 outside radius due to select)
const falloff = float(1.0).sub(dist.div(radius));
// Use select() - returns falloff if inside radius, 0 if outside
const foamToAdd = select(dist.lessThan(radius), falloff, float(0.0));
// Combine and write
const newFoam = max(currentFoam, foamToAdd);
foamBuffer.element(index).assign(clamp(newFoam, 0.0, 1.0));
})().compute(bufferSize);---
Basic Setup
import * as THREE from 'three/webgpu';
import { Fn, instancedArray, instanceIndex, vec3 } from 'three/tsl';
// Create storage buffer
const count = 100000;
const positions = instancedArray(count, 'vec3');
// Create compute shader
const computeShader = Fn(() => {
const position = positions.element(instanceIndex);
position.x.addAssign(0.01);
})().compute(count);
// Initialize renderer first, then use synchronous compute
await renderer.init();
renderer.compute(computeShader);Read-Only Storage Buffers
import { attributeArray } from 'three/tsl';
// attributeArray() creates read-only storage buffers (vs instancedArray for read-write)
const lookupTable = attributeArray(data, 'float');
// Use in compute or materials - data is read-only on GPU
const value = lookupTable.element(index);Storage Buffers
Instanced Arrays
import { instancedArray } from 'three/tsl';
// Create typed storage buffers
const positions = instancedArray(count, 'vec3');
const velocities = instancedArray(count, 'vec3');
const colors = instancedArray(count, 'vec4');
const indices = instancedArray(count, 'uint');
const values = instancedArray(count, 'float');Accessing Elements
const computeShader = Fn(() => {
// Get element at current index
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// Read values
const x = position.x;
const speed = velocity.length();
// Write values
position.assign(vec3(0, 0, 0));
position.x.assign(1.0);
position.addAssign(velocity);
})().compute(count);Accessing Other Elements
const computeShader = Fn(() => {
const myIndex = instanceIndex;
const neighborIndex = myIndex.add(1).mod(count);
const myPos = positions.element(myIndex);
const neighborPos = positions.element(neighborIndex);
// Calculate distance to neighbor
const dist = myPos.distance(neighborPos);
})().compute(count);Compute Shader Patterns
Initialize Particles
const computeInit = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// Random positions using hash
position.x.assign(hash(instanceIndex).mul(10).sub(5));
position.y.assign(hash(instanceIndex.add(1)).mul(10).sub(5));
position.z.assign(hash(instanceIndex.add(2)).mul(10).sub(5));
// Zero velocity
velocity.assign(vec3(0));
})().compute(count);
// Run once at startup (after await renderer.init())
renderer.compute(computeInit);Physics Update
const gravity = uniform(-9.8);
const deltaTimeUniform = uniform(0);
const groundY = uniform(0);
const computeUpdate = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
const dt = deltaTimeUniform;
// Apply gravity
velocity.y.addAssign(gravity.mul(dt));
// Update position
position.addAssign(velocity.mul(dt));
// Ground collision
If(position.y.lessThan(groundY), () => {
position.y.assign(groundY);
velocity.y.assign(velocity.y.negate().mul(0.8)); // Bounce
velocity.xz.mulAssign(0.95); // Friction
});
})().compute(count);
// In animation loop
function animate() {
deltaTimeUniform.value = clock.getDelta();
renderer.compute(computeUpdate);
renderer.render(scene, camera);
}Attraction to Point
const attractorPos = uniform(new THREE.Vector3(0, 0, 0));
const attractorStrength = uniform(1.0);
const computeAttract = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// Direction to attractor
const toAttractor = attractorPos.sub(position);
const distance = toAttractor.length();
const direction = toAttractor.normalize();
// Apply force (inverse square falloff)
const force = direction.mul(attractorStrength).div(distance.mul(distance).add(0.1));
velocity.addAssign(force.mul(deltaTimeUniform));
})().compute(count);Neighbor Interaction (Boids-like)
const computeBoids = Fn(() => {
const myPos = positions.element(instanceIndex);
const myVel = velocities.element(instanceIndex);
const separation = vec3(0).toVar();
const alignment = vec3(0).toVar();
const cohesion = vec3(0).toVar();
const neighborCount = int(0).toVar();
// Check nearby particles
Loop(count, ({ i }) => {
If(i.notEqual(instanceIndex), () => {
const otherPos = positions.element(i);
const otherVel = velocities.element(i);
const dist = myPos.distance(otherPos);
If(dist.lessThan(2.0), () => {
// Separation
const diff = myPos.sub(otherPos).normalize().div(dist);
separation.addAssign(diff);
// Alignment
alignment.addAssign(otherVel);
// Cohesion
cohesion.addAssign(otherPos);
neighborCount.addAssign(1);
});
});
});
If(neighborCount.greaterThan(0), () => {
const n = neighborCount.toFloat();
alignment.divAssign(n);
cohesion.divAssign(n);
cohesion.assign(cohesion.sub(myPos));
myVel.addAssign(separation.mul(0.05));
myVel.addAssign(alignment.sub(myVel).mul(0.05));
myVel.addAssign(cohesion.mul(0.05));
});
// Limit speed
const speed = myVel.length();
If(speed.greaterThan(2.0), () => {
myVel.assign(myVel.normalize().mul(2.0));
});
myPos.addAssign(myVel.mul(deltaTimeUniform));
})().compute(count);Workgroups and Synchronization
Workgroup Size
// Default workgroup size is typically 64 or 256
// Pass workgroup size as an array
const computeShader = Fn(() => {
// shader code
})().compute(count, [64]);
// 2D workgroup
const compute2D = Fn(() => {
// shader code
})().compute(width * height, [8, 8]);Compute Builtins
import {
globalId, localId, workgroupId, numWorkgroups, subgroupSize,
invocationLocalIndex, invocationSubgroupIndex, subgroupIndex
} from 'three/tsl';
const computeShader = Fn(() => {
// Global invocation ID across all workgroups
const gid = globalId;
// Local invocation ID within the workgroup
const lid = localId;
// Workgroup ID
const wid = workgroupId;
// Total number of workgroups
const nwg = numWorkgroups;
})().compute(count, [64]);Barriers
import { workgroupBarrier, storageBarrier, textureBarrier } from 'three/tsl';
const computeShader = Fn(() => {
// Write data
sharedData.element(localIndex).assign(value);
// Ensure all workgroup threads reach this point
workgroupBarrier();
// Now safe to read data written by other threads
const neighborValue = sharedData.element(localIndex.add(1));
})().compute(count);Atomic Operations
For thread-safe read-modify-write operations:
import { atomicAdd, atomicSub, atomicMax, atomicMin, atomicAnd, atomicOr, atomicXor } from 'three/tsl';
const counter = instancedArray(1, 'uint');
const computeShader = Fn(() => {
// Atomically increment counter
atomicAdd(counter.element(0), 1);
// Atomic max
atomicMax(maxValue.element(0), localValue);
})().compute(count);Using Compute Results in Materials
Instanced Mesh with Computed Positions
// Create instanced mesh
const geometry = new THREE.SphereGeometry(0.1, 16, 16);
const material = new THREE.MeshStandardNodeMaterial();
// Use computed positions
material.positionNode = positions.element(instanceIndex);
// Optionally use computed colors
material.colorNode = colors.element(instanceIndex);
const mesh = new THREE.InstancedMesh(geometry, material, count);
scene.add(mesh);Points with Computed Positions
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(count * 3), 3));
const material = new THREE.PointsNodeMaterial();
material.positionNode = positions.element(instanceIndex);
material.colorNode = colors.element(instanceIndex);
material.sizeNode = float(5.0);
const points = new THREE.Points(geometry, material);
scene.add(points);Execution Methods
// IMPORTANT: Always initialize the renderer first
await renderer.init();
// Synchronous compute (preferred since r181)
renderer.compute(computeShader);
// Multiple computes
renderer.compute(computeInit);
renderer.compute(computePhysics);
renderer.compute(computeCollisions);
// Note: computeAsync() is deprecated since r181.
// Use await renderer.init() at startup, then renderer.compute() synchronously.Reading Back Data (GPU to CPU)
// Create buffer for readback
const readBuffer = new Float32Array(count * 3);
// Read data back from GPU
await renderer.readRenderTargetPixelsAsync(
computeTexture,
0, 0, width, height,
readBuffer
);Complete Example: Particle System
import * as THREE from 'three/webgpu';
import {
Fn, If, instancedArray, instanceIndex, uniform,
vec3, float, hash, time
} from 'three/tsl';
// Setup
const count = 50000;
const positions = instancedArray(count, 'vec3');
const velocities = instancedArray(count, 'vec3');
const lifetimes = instancedArray(count, 'float');
// Uniforms
const emitterPos = uniform(new THREE.Vector3(0, 0, 0));
const gravity = uniform(-2.0);
const dt = uniform(0);
// Initialize
const computeInit = Fn(() => {
const pos = positions.element(instanceIndex);
const vel = velocities.element(instanceIndex);
const life = lifetimes.element(instanceIndex);
pos.assign(emitterPos);
// Random velocity in cone
const angle = hash(instanceIndex).mul(Math.PI * 2);
const speed = hash(instanceIndex.add(1)).mul(2).add(1);
vel.x.assign(angle.cos().mul(speed).mul(0.3));
vel.y.assign(speed);
vel.z.assign(angle.sin().mul(speed).mul(0.3));
// Random lifetime
life.assign(hash(instanceIndex.add(2)).mul(2).add(1));
})().compute(count);
// Update
const computeUpdate = Fn(() => {
const pos = positions.element(instanceIndex);
const vel = velocities.element(instanceIndex);
const life = lifetimes.element(instanceIndex);
// Apply gravity
vel.y.addAssign(gravity.mul(dt));
// Update position
pos.addAssign(vel.mul(dt));
// Decrease lifetime
life.subAssign(dt);
// Respawn dead particles
If(life.lessThan(0), () => {
pos.assign(emitterPos);
const angle = hash(instanceIndex.add(time.mul(1000))).mul(Math.PI * 2);
const speed = hash(instanceIndex.add(time.mul(1000)).add(1)).mul(2).add(1);
vel.x.assign(angle.cos().mul(speed).mul(0.3));
vel.y.assign(speed);
vel.z.assign(angle.sin().mul(speed).mul(0.3));
life.assign(hash(instanceIndex.add(time.mul(1000)).add(2)).mul(2).add(1));
});
})().compute(count);
// Material
const material = new THREE.PointsNodeMaterial();
material.positionNode = positions.element(instanceIndex);
material.sizeNode = float(3.0);
material.colorNode = vec3(1, 0.5, 0.2);
// Geometry (dummy positions)
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(count * 3), 3));
const points = new THREE.Points(geometry, material);
scene.add(points);
// Init (after await renderer.init())
renderer.compute(computeInit);
// Animation loop
function animate() {
dt.value = Math.min(clock.getDelta(), 0.1);
renderer.compute(computeUpdate);
renderer.render(scene, camera);
}TSL Core Concepts
Types and Constructors
Scalar Types
import { float, int, uint, bool } from 'three/tsl';
const f = float(1.0);
const i = int(42);
const u = uint(100);
const b = bool(true);Vector Types
import { vec2, vec3, vec4, color } from 'three/tsl';
const v2 = vec2(1.0, 2.0);
const v3 = vec3(1.0, 2.0, 3.0);
const v4 = vec4(1.0, 2.0, 3.0, 1.0);
// Color (RGB, accepts hex or components)
const c = color(0xff0000); // Red
const c2 = color(1, 0.5, 0); // OrangeMatrix Types
import { mat2, mat3, mat4 } from 'three/tsl';
const m3 = mat3();
const m4 = mat4();Type Conversion
const v = vec3(1, 2, 3);
const v4 = v.toVec4(1.0); // vec4(1, 2, 3, 1)
const f = int(42).toFloat(); // 42.0
const c = v.toColor(); // Convert to colorVector Swizzling
Access and reorder vector components using standard notation:
const v = vec3(1.0, 2.0, 3.0);
// Single component access
v.x // 1.0
v.y // 2.0
v.z // 3.0
// Multiple components
v.xy // vec2(1.0, 2.0)
v.xyz // vec3(1.0, 2.0, 3.0)
// Reorder components
v.zyx // vec3(3.0, 2.0, 1.0)
v.xxy // vec3(1.0, 1.0, 2.0)
v.rrr // vec3(1.0, 1.0, 1.0) - same as xxx
// Alternative accessors (all equivalent)
v.xyz // position
v.rgb // color
v.stp // texture coordinatesUniforms
Uniforms pass values from JavaScript to shaders:
import { uniform } from 'three/tsl';
import * as THREE from 'three/webgpu';
// Create uniforms
const myColor = uniform(new THREE.Color(0x0066ff));
const myFloat = uniform(0.5);
const myVec3 = uniform(new THREE.Vector3(1, 2, 3));
// Update at runtime
myColor.value.set(0xff0000);
myFloat.value = 0.8;
myVec3.value.set(4, 5, 6);
// Use in material
material.colorNode = myColor;Auto-Updating Uniforms
// Update every frame
const animatedValue = uniform(0).onFrameUpdate((frame) => {
return Math.sin(frame.time);
});
// Update per object render
const perObjectValue = uniform(0).onObjectUpdate((object) => {
return object.userData.customValue;
});
// Update once per render cycle
const renderValue = uniform(0).onRenderUpdate((state) => {
return state.delta;
});Operators
Arithmetic
// Method chaining (preferred)
const result = a.add(b).mul(c).sub(d).div(e);
// Individual operations
a.add(b) // a + b
a.sub(b) // a - b
a.mul(b) // a * b
a.div(b) // a / b
a.mod(b) // a % b
a.negate() // -aComparison
a.equal(b) // a == b
a.notEqual(b) // a != b
a.lessThan(b) // a < b
a.greaterThan(b) // a > b
a.lessThanEqual(b) // a <= b
a.greaterThanEqual(b) // a >= bLogical
a.and(b) // a && b
a.or(b) // a || b
a.not() // !a
a.xor(b) // a ^ bBitwise
a.bitAnd(b) // a & b
a.bitOr(b) // a | b
a.bitXor(b) // a ^ b
a.bitNot() // ~a
a.shiftLeft(n) // a << n
a.shiftRight(n) // a >> nAssignment (for variables)
const v = vec3(0).toVar(); // Create mutable variable
v.assign(vec3(1, 2, 3)); // v = vec3(1, 2, 3)
v.addAssign(vec3(1)); // v += vec3(1)
v.subAssign(vec3(1)); // v -= vec3(1)
v.mulAssign(2.0); // v *= 2.0
v.divAssign(2.0); // v /= 2.0Variables
Mutable Variables
// Create mutable variable with toVar()
const myVar = vec3(1, 0, 0).toVar();
myVar.assign(vec3(0, 1, 0));
myVar.addAssign(vec3(0, 0, 1));
// Name the variable (useful for debugging)
const named = vec3(0).toVar('myPosition');Constants
// Create compile-time constant
const PI_HALF = float(Math.PI / 2).toConst();Properties (named values for shader stages)
import { property } from 'three/tsl';
// Create named property
const myProp = property('vec3', 'customColor');
myProp.assign(vec3(1, 0, 0));Control Flow
⚠️ CRITICAL: Property Assignment vs Variable Reassignment
TSL intercepts property assignments on nodes, but NOT JavaScript variable reassignment.
| Pattern | Works? | Why |
|---|---|---|
node.y = value | ✅ | Property setter - TSL intercepts |
node.x.assign(value) | ✅ | TSL method call |
variable = variable.add(1) | ❌ | JS variable reassignment |
This WORKS (vec3 property assignment):
const result = vec3(position);
If(result.y.greaterThan(limit), () => {
result.y = limit; // ✅ Property assignment - TSL intercepts!
});This DOES NOT work (scalar variable reassignment):
let value = buffer.element(index).toFloat(); // Scalar - no .x/.y properties
If(condition, () => {
value = value.add(1.0); // ❌ JS variable reassignment - TSL can't see this!
});
return value; // Returns ORIGINAL node!Solutions for scalars:
// ✅ Use select() for conditional values
const result = select(condition, valueIfTrue, valueIfFalse);
// ✅ Use .toVar() for mutable variables
const value = buffer.element(index).toVar();
If(condition, () => {
value.assign(value.add(1.0)); // Works with .toVar()!
});
// ✅ Use direct .assign() on buffer elements
If(condition, () => {
element.assign(element.add(1.0)); // Direct buffer writes work!
});Conditionals
import { If, select } from 'three/tsl';
// If-ElseIf-Else (use with .toVar() or direct .assign())
const result = vec3(0).toVar();
If(value.greaterThan(0.5), () => {
result.assign(vec3(1, 0, 0)); // Red
}).ElseIf(value.greaterThan(0.25), () => {
result.assign(vec3(0, 1, 0)); // Green
}).Else(() => {
result.assign(vec3(0, 0, 1)); // Blue
});
// Ternary operator (select) - PREFERRED for simple conditionals
const color = select(
condition, // if true
vec3(1, 0, 0), // return this
vec3(0, 0, 1) // else return this
);Switch-Case
import { Switch } from 'three/tsl';
const col = vec3(0).toVar();
Switch(intValue)
.Case(0, () => { col.assign(color(1, 0, 0)); })
.Case(1, () => { col.assign(color(0, 1, 0)); })
.Case(2, () => { col.assign(color(0, 0, 1)); })
.Default(() => { col.assign(color(1, 1, 1)); });Loops
import { Loop, Break, Continue } from 'three/tsl';
// Simple loop (0 to 9)
const sum = float(0).toVar();
Loop(10, ({ i }) => {
sum.addAssign(float(i));
});
// Ranged loop with options
Loop({ start: int(0), end: int(count), type: 'int' }, ({ i }) => {
// Loop body
});
// Nested loops
Loop(width, height, ({ i, j }) => {
// i = outer loop index
// j = inner loop index
});
// Loop control
Loop(100, ({ i }) => {
If(shouldStop, () => {
Break(); // Exit loop
});
If(shouldSkip, () => {
Continue(); // Skip to next iteration
});
});Flow Control
import { Discard, Return } from 'three/tsl';
// Discard fragment (make transparent)
If(alpha.lessThan(0.5), () => {
Discard();
});
// Return from function
const myFn = Fn(() => {
If(condition, () => {
Return(vec3(1, 0, 0));
});
return vec3(0, 0, 1);
});Custom Functions with Fn()
Basic Function
import { Fn } from 'three/tsl';
const addVectors = Fn(([a, b]) => {
return a.add(b);
});
// Usage
const result = addVectors(vec3(1, 0, 0), vec3(0, 1, 0));Default Parameters
const oscillate = Fn(([frequency = 1.0, amplitude = 1.0]) => {
return time.mul(frequency).sin().mul(amplitude);
});
// Call variations
oscillate(); // Uses defaults
oscillate(2.0); // frequency = 2.0
oscillate(2.0, 0.5); // frequency = 2.0, amplitude = 0.5Named Parameters (Object Style)
const createGradient = Fn(({ colorA = vec3(0), colorB = vec3(1), t = 0.5 }) => {
return mix(colorA, colorB, t);
});
// Call with named parameters
createGradient({ colorA: vec3(1, 0, 0), t: uv().x });Function with Context
// Access shader context
const customShader = Fn(({ material, geometry, object }) => {
if (material.userData.customColor) {
return uniform(material.userData.customColor);
}
return vec3(1);
});Time and Animation
import { time, deltaTime } from 'three/tsl';
// time - seconds since start
const rotation = time.mul(0.5); // Half rotation per second
// deltaTime - time since last frame
const velocity = speed.mul(deltaTime);Oscillators
import { oscSine, oscSquare, oscTriangle, oscSawtooth } from 'three/tsl';
// All oscillators return 0-1 range
oscSine(time) // Smooth sine wave
oscSquare(time) // Square wave (0 or 1)
oscTriangle(time) // Triangle wave
oscSawtooth(time) // Sawtooth wave
// Custom frequency
oscSine(time.mul(2.0)) // 2Hz oscillationMath Functions
Basic Math
import { abs, sign, floor, ceil, fract, mod, min, max, clamp } from 'three/tsl';
abs(x) // Absolute value
sign(x) // -1, 0, or 1
floor(x) // Round down
ceil(x) // Round up
fract(x) // Fractional part (x - floor(x))
mod(x, y) // Modulo
min(x, y) // Minimum
max(x, y) // Maximum
clamp(x, 0, 1) // Clamp to rangeTrigonometry
import { sin, cos, tan, asin, acos, atan, atan2 } from 'three/tsl';
sin(x)
cos(x)
tan(x)
asin(x)
acos(x)
atan(x)
atan2(y, x)Exponential
import { pow, exp, log, sqrt, inverseSqrt } from 'three/tsl';
pow(x, 2.0) // x^2
exp(x) // e^x
log(x) // Natural log
sqrt(x) // Square root
inverseSqrt(x) // 1 / sqrt(x)Interpolation
import { mix, step, smoothstep } from 'three/tsl';
mix(a, b, 0.5) // Linear interpolation
step(0.5, x) // 0 if x < 0.5, else 1
smoothstep(0.0, 1.0, x) // Smooth 0-1 transitionVector Math
import { length, distance, dot, cross, normalize, reflect, refract } from 'three/tsl';
length(v) // Vector length
distance(a, b) // Distance between points
dot(a, b) // Dot product
cross(a, b) // Cross product (vec3 only)
normalize(v) // Unit vector
reflect(incident, normal)
refract(incident, normal, eta)Constants
import { PI, TWO_PI, HALF_PI, EPSILON } from 'three/tsl';
PI // 3.14159...
TWO_PI // 6.28318...
HALF_PI // 1.57079...
EPSILON // Very small numberUtility Functions
import { hash, checker, remap, range, rotate } from 'three/tsl';
// Pseudo-random hash
hash(seed) // Returns 0-1
// Checkerboard pattern
checker(uv()) // Returns 0 or 1
// Remap value from one range to another
remap(x, 0, 1, -1, 1) // Map 0-1 to -1 to 1
// Generate value in range
range(min, max) // Random in range (per instance)
// Rotate 2D vector
rotate(vec2(1, 0), angle)WebGPU Device Loss Handling
What Is Device Loss?
Device loss occurs when the GPU driver cannot continue processing commands. Causes include:
- Driver crashes
- Extreme resource pressure
- Long-running shaders (GPU watchdog triggers after ~10 seconds)
- Driver updates
- Significant device configuration changes
When a device is lost, the GPUDevice object and all objects created with it become unusable. All buffers, textures, pipelines, and GPU memory are discarded.
Listening for Device Loss
Detect loss by attaching a callback to the device's lost promise:
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();
device.lost.then((info) => {
console.error('WebGPU device lost:', info.message);
// Handle recovery
});Important: Don't await this promise directly - it will block indefinitely if loss never occurs.
Device Loss Information
The GPUDeviceLostInfo object provides:
| Property | Description |
|---|---|
reason | 'destroyed' (intentional via destroy()) or 'unknown' (unexpected) |
message | Human-readable debugging info (don't parse programmatically) |
device.lost.then((info) => {
if (info.reason === 'unknown') {
// Unexpected loss - attempt recovery
handleUnexpectedDeviceLoss();
} else {
// Intentional destruction - expected behavior
}
});Devices Starting Lost
adapter.requestDevice() always returns a GPUDevice, but it may already be lost if creation failed. This occurs when the adapter was "consumed" (used previously) or "expired."
Best practice: Always get a new adapter right before requesting a device.
Recovery Strategies
Minimal Recovery (Page Reload)
For simple applications:
device.lost.then((info) => {
if (info.reason === 'unknown') {
// Warn user before reload
alert('Graphics error occurred. The page will reload.');
location.reload();
}
});Restart GPU Content Only (Recommended for Three.js)
Recreate the device and reconfigure the canvas without full page reload:
import * as THREE from 'three/webgpu';
let renderer;
let scene, camera;
async function initWebGPU() {
renderer = new THREE.WebGPURenderer();
await renderer.init();
// Access the underlying WebGPU device
const device = renderer.backend.device;
device.lost.then((info) => {
console.error('Device lost:', info.message);
if (info.reason === 'unknown') {
// Dispose current renderer
renderer.dispose();
// Reinitialize
initWebGPU();
}
});
// Configure canvas
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Recreate scene content
setupScene();
}
function setupScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// ... add meshes, lights, etc.
}
initWebGPU();Restore with Application State
For applications with user progress or configuration:
let appState = {
cameraPosition: { x: 0, y: 5, z: 10 },
settings: {},
// Don't save transient data like particle positions
};
// Save state periodically
function saveState() {
appState.cameraPosition = {
x: camera.position.x,
y: camera.position.y,
z: camera.position.z
};
localStorage.setItem('appState', JSON.stringify(appState));
}
// Restore on recovery
async function initWebGPU() {
renderer = new THREE.WebGPURenderer();
await renderer.init();
const savedState = localStorage.getItem('appState');
if (savedState) {
appState = JSON.parse(savedState);
}
setupScene();
// Restore camera position
camera.position.set(
appState.cameraPosition.x,
appState.cameraPosition.y,
appState.cameraPosition.z
);
renderer.backend.device.lost.then((info) => {
if (info.reason === 'unknown') {
saveState();
renderer.dispose();
initWebGPU();
}
});
}When Recovery Fails
If requestAdapter() returns null after device loss, the OS or browser has blocked GPU access:
async function initWebGPU() {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
// Check if this is initial failure or post-loss failure
if (hadPreviousDevice) {
showMessage('GPU access lost. Please restart your browser.');
} else {
showMessage('WebGPU is not supported on this device.');
}
return;
}
// Continue with device creation...
}Testing Device Loss
Using destroy()
Call device.destroy() to simulate loss:
let simulatedLoss = false;
function simulateDeviceLoss() {
simulatedLoss = true;
renderer.backend.device.destroy();
}
// In your device.lost handler:
device.lost.then((info) => {
if (info.reason === 'unknown' || simulatedLoss) {
simulatedLoss = false;
// Treat as unexpected loss for testing
handleDeviceLoss();
}
});
// Add debug keybinding
window.addEventListener('keydown', (e) => {
if (e.key === 'L' && e.ctrlKey && e.shiftKey) {
simulateDeviceLoss();
}
});Limitations of destroy():
- Unmaps buffers immediately (real loss doesn't)
- Always allows device recovery (real loss may not)
Chrome GPU Process Crash Testing
Navigate to about:gpucrash in a separate tab to crash the GPU process.
Chrome enforces escalating restrictions:
| Crash | Effect |
|---|---|
| 1st | New adapters allowed |
| 2nd within 2 min | Adapter requests fail (resets on page refresh) |
| 3rd within 2 min | All pages blocked (reset after 2 min or browser restart) |
| 3-6 within 5 min | GPU process stops restarting; browser restart required |
Chrome Testing Flags
Bypass crash limits for development:
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--disable-domain-blocking-for-3d-apis \
--disable-gpu-process-crash-limit
# Windows
chrome.exe --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit
# Linux
google-chrome --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limitComplete Example
import * as THREE from 'three/webgpu';
import { color, time, oscSine } from 'three/tsl';
let renderer, scene, camera, mesh;
let hadPreviousDevice = false;
async function init() {
// Check WebGPU support
if (!navigator.gpu) {
showError('WebGPU not supported');
return;
}
// Create renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
try {
await renderer.init();
} catch (e) {
if (hadPreviousDevice) {
showError('GPU recovery failed. Please restart browser.');
} else {
showError('Failed to initialize WebGPU.');
}
return;
}
hadPreviousDevice = true;
// Setup device loss handler
const device = renderer.backend.device;
device.lost.then(handleDeviceLoss);
// Setup scene
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = color(0x00ff00).mul(oscSine(time));
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));
animate();
}
function handleDeviceLoss(info) {
console.error('Device lost:', info.reason, info.message);
if (info.reason === 'unknown') {
// Cleanup
if (renderer) {
renderer.domElement.remove();
renderer.dispose();
}
// Attempt recovery after short delay
setTimeout(() => {
init();
}, 100);
}
}
function animate() {
if (!renderer) return;
requestAnimationFrame(animate);
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
function showError(message) {
const div = document.createElement('div');
div.textContent = message;
div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:#f44;color:#fff;border-radius:8px;';
document.body.appendChild(div);
}
init();Best Practices
1. Always listen for device loss - Even if you just show an error message 2. Get a fresh adapter before each device request - The GPU hardware may have changed 3. Don't parse the message field - It's implementation-specific and changes between browsers 4. Save critical application state - Restore user progress after recovery 5. Don't save transient state - Particle positions, physics state can be reset 6. Test your recovery path - Use destroy() and Chrome's about:gpucrash 7. Handle adapter failure gracefully - Distinguish between initial failure and post-loss failure 8. Add a short delay before recovery - Give the system time to stabilize
WebGPU Limits and Features
Why This Matters
WebGPU devices have default limits (guaranteed minimums) that may be lower than what your application needs. For example, the default maxBufferSize is 256 MiB — if you create a large compute buffer, you'll silently get errors unless you request a higher limit. Similarly, optional features like float32-filterable must be explicitly enabled.
Limits
Limits define numeric constraints on resources. Every WebGPU implementation guarantees a set of minimum values, but most GPUs support much higher limits.
Common limits you may need to increase:
| Limit | Default | When to Increase |
|---|---|---|
maxBufferSize | 268435456 (256 MiB) | Large storage/vertex buffers |
maxStorageBufferBindingSize | 134217728 (128 MiB) | Large compute storage buffers |
maxStorageBuffersPerShaderStage | 8 | Many storage buffers in one shader |
maxComputeWorkgroupSizeX | 128 | Large workgroup dimensions |
maxComputeInvocationsPerWorkgroup | 128 | Dense compute workgroups |
maxColorAttachments | 8 | Many render targets |
Querying Adapter Limits
const adapter = await navigator.gpu?.requestAdapter();
console.log(adapter.limits.maxBufferSize);
console.log(adapter.limits.maxStorageBufferBindingSize);Requesting Increased Limits
You must request higher limits when creating the device — otherwise you get the defaults, not the adapter's maximums.
Raw WebGPU:
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter.requestDevice({
requiredLimits: {
maxBufferSize: 1024 * 1024 * 1024, // 1 GiB
maxStorageBufferBindingSize: 1024 * 1024 * 512, // 512 MiB
},
});Three.js WebGPURenderer:
Three.js accepts requiredLimits as a renderer constructor option, which gets passed through to requestDevice():
const renderer = new THREE.WebGPURenderer({
requiredLimits: {
maxBufferSize: 1024 * 1024 * 1024, // 1 GiB
maxStorageBufferBindingSize: 1024 * 1024 * 512, // 512 MiB
},
});
await renderer.init();If the adapter doesn't support the requested limit, requestDevice() (or renderer.init()) will fail.
Safe Pattern: Check Before Requesting
const adapter = await navigator.gpu?.requestAdapter();
const desiredBufferSize = 1024 * 1024 * 1024; // 1 GiB
const requiredLimits = {};
if (adapter.limits.maxBufferSize >= desiredBufferSize) {
requiredLimits.maxBufferSize = desiredBufferSize;
} else {
console.warn('Adapter does not support 1 GiB buffers, using default');
}
const renderer = new THREE.WebGPURenderer({ requiredLimits });
await renderer.init();Features
Features are optional capabilities that vary by GPU. Unlike limits, features are either present or absent — there's no numeric value to adjust.
How Three.js Handles Features
Three.js automatically requests all features supported by the adapter. You generally don't need to manage features manually when using Three.js.
Querying Available Features (Raw WebGPU)
const adapter = await navigator.gpu?.requestAdapter();
// adapter.features is a Set
console.log(adapter.features.has('float32-filterable'));
console.log(adapter.features.has('shader-f16'));Common Optional Features
| Feature | Purpose |
|---|---|
float32-filterable | Linear filtering on float32 textures |
float32-blendable | Blending on float32 render targets |
shader-f16 | 16-bit floats in shaders |
texture-compression-bc | BC (desktop) texture compression |
texture-compression-etc2 | ETC2 (mobile) texture compression |
texture-compression-astc | ASTC (mobile) texture compression |
timestamp-query | GPU timing measurements |
depth-clip-control | Disable depth clipping |
dual-source-blending | Two blend sources from one shader |
subgroups | Subgroup operations in compute |
clip-distances | Custom clip planes in vertex shader |
Best Practices
1. Only request limits you actually need — requesting maximums hides portability issues where your app works on your GPU but fails on weaker ones 2. Check adapter limits before requesting — gracefully degrade when limits aren't available 3. Don't forget storage buffer binding size — maxStorageBufferBindingSize is often the bottleneck, not maxBufferSize 4. Use [webgpureport.org](https://webgpureport.org) to check what limits/features different GPUs support
Debugging
If you're hitting buffer size errors or validation failures:
// Log all adapter limits
const adapter = await navigator.gpu?.requestAdapter();
for (const [key, value] of Object.entries(Object.getPrototypeOf(adapter.limits))) {
if (typeof value !== 'function') {
console.log(`${key}: ${adapter.limits[key]}`);
}
}Check Chrome DevTools console for WebGPU validation errors — they often mention which limit was exceeded.
TSL Node Materials
Available Material Types
| Material | Description |
|---|---|
MeshBasicNodeMaterial | Unlit, no lighting calculations |
MeshStandardNodeMaterial | PBR material with metalness/roughness |
MeshPhysicalNodeMaterial | Advanced PBR with clearcoat, transmission, etc. |
MeshPhongNodeMaterial | Classic Phong shading |
MeshToonNodeMaterial | Cel/toon shading |
MeshLambertNodeMaterial | Diffuse-only lighting |
MeshNormalNodeMaterial | Visualize normals |
MeshMatcapNodeMaterial | Matcap texture shading |
PointsNodeMaterial | For point clouds |
LineBasicNodeMaterial | For lines |
LineDashedNodeMaterial | For dashed lines |
SpriteNodeMaterial | For sprites/billboards |
Creating Node Materials
import * as THREE from 'three/webgpu';
// Standard PBR material
const material = new THREE.MeshStandardNodeMaterial();
// Physical material with advanced features
const physicalMat = new THREE.MeshPhysicalNodeMaterial();
// Unlit material
const basicMat = new THREE.MeshBasicNodeMaterial();Material Properties
Color and Opacity
import { texture, color, float } from 'three/tsl';
// Color from texture
material.colorNode = texture(diffuseMap);
// Solid color
material.colorNode = color(0xff0000);
// Computed color
material.colorNode = positionLocal.normalize();
// Opacity (requires material.transparent = true)
material.opacityNode = float(0.8);
material.transparent = true;
// Alpha test threshold
material.alphaTestNode = float(0.5);PBR Properties (MeshStandardNodeMaterial)
import { texture, float, color } from 'three/tsl';
// Metalness (0 = dielectric, 1 = metal)
material.metalnessNode = texture(metalMap).r;
material.metalnessNode = float(0.0);
// Roughness (0 = smooth/mirror, 1 = rough)
material.roughnessNode = texture(roughnessMap).r;
material.roughnessNode = float(0.5);
// Emissive (self-illumination)
material.emissiveNode = color(0xff0000).mul(2.0);
material.emissiveNode = texture(emissiveMap);Normal Mapping
import { texture, normalMap, bumpMap } from 'three/tsl';
// Normal map
material.normalNode = normalMap(texture(normalMapTexture));
// Normal map with strength
material.normalNode = normalMap(texture(normalMapTexture), float(0.5));
// Bump map (height to normal)
material.normalNode = bumpMap(texture(heightMap), 0.05);Physical Properties (MeshPhysicalNodeMaterial)
const material = new THREE.MeshPhysicalNodeMaterial();
// Clearcoat (car paint effect)
material.clearcoatNode = float(1.0);
material.clearcoatRoughnessNode = float(0.1);
material.clearcoatNormalNode = normalMap(texture(clearcoatNormalMap));
// Transmission (glass/translucency)
material.transmissionNode = float(0.9);
material.thicknessNode = float(0.5);
material.attenuationDistanceNode = float(1.0);
material.attenuationColorNode = color(0xffffff);
// Iridescence (soap bubble effect)
material.iridescenceNode = float(1.0);
material.iridescenceIORNode = float(1.3);
material.iridescenceThicknessNode = float(400);
// Sheen (fabric effect)
material.sheenNode = float(1.0);
material.sheenRoughnessNode = float(0.5);
material.sheenColorNode = color(0xffffff);
// Anisotropy (brushed metal)
material.anisotropyNode = float(1.0);
material.anisotropyRotationNode = float(0);
// Specular
material.specularIntensityNode = float(1.0);
material.specularColorNode = color(0xffffff);
// Index of Refraction
material.iorNode = float(1.5);
// Dispersion (rainbow effect in glass)
material.dispersionNode = float(0.0);Environment and Lighting
import { cubeTexture, envMap } from 'three/tsl';
// Environment map reflection
material.envMapNode = cubeTexture(envCubeMap);
// Custom lights
material.lightsNode = lights();Vertex Manipulation
Position Displacement
import { positionLocal, normalLocal, texture } from 'three/tsl';
// Displace vertices along normals
const displacement = texture(heightMap).r.mul(0.1);
material.positionNode = positionLocal.add(normalLocal.mul(displacement));
// Wave displacement
const wave = positionLocal.x.add(time).sin().mul(0.1);
material.positionNode = positionLocal.add(vec3(0, wave, 0));Custom Vertex Shader
// Complete vertex position override
material.vertexNode = customVertexPosition;Fragment Override
// Complete fragment output override
material.fragmentNode = vec4(finalColor, 1.0);
// Output node (respects lighting)
material.outputNode = outputStruct;Geometry Attributes
Position Nodes
import {
positionGeometry, // Original mesh position
positionLocal, // Position in model space
positionWorld, // Position in world space
positionView // Position in camera space
} from 'three/tsl';Normal Nodes
import {
normalGeometry, // Original mesh normal
normalLocal, // Normal in model space
normalWorld, // Normal in world space (use for lighting)
normalView // Normal in camera space
} from 'three/tsl';Tangent/Bitangent
import {
tangentLocal, tangentWorld, tangentView,
bitangentLocal, bitangentWorld, bitangentView
} from 'three/tsl';UV Coordinates
import { uv } from 'three/tsl';
uv() // Primary UV set (UV0)
uv(1) // Secondary UV set (UV1)
uv(2) // Tertiary UV set (UV2)Other Attributes
import { vertexColor, instanceIndex, vertexIndex } from 'three/tsl';
vertexColor() // Vertex colors (if present)
instanceIndex // Index for instanced meshes
vertexIndex // Current vertex indexCamera Nodes
import {
cameraPosition, // Camera world position
cameraNear, // Near plane distance
cameraFar, // Far plane distance
cameraViewMatrix, // View matrix
cameraProjectionMatrix, // Projection matrix
cameraWorldMatrix // Camera world matrix
} from 'three/tsl';Screen Space Nodes
import {
screenUV, // Screen UV (0-1)
screenCoordinate, // Pixel coordinates
screenSize, // Screen dimensions
viewportUV, // Viewport UV
viewport, // Viewport dimensions
depth // Fragment depth
} from 'three/tsl';Examples
Animated Color Material
import * as THREE from 'three/webgpu';
import { color, time, oscSine, mix } from 'three/tsl';
const material = new THREE.MeshStandardNodeMaterial();
const colorA = color(0xff0000);
const colorB = color(0x0000ff);
const t = oscSine(time.mul(0.5));
material.colorNode = mix(colorA, colorB, t);
material.roughnessNode = float(0.5);
material.metalnessNode = float(0.0);Triplanar Mapping Material
import * as THREE from 'three/webgpu';
import { texture, triplanarTexture, float } from 'three/tsl';
const material = new THREE.MeshStandardNodeMaterial();
// Apply texture from all three axes
material.colorNode = triplanarTexture(
texture(diffuseMap),
null, // Y-axis texture (optional)
null, // Z-axis texture (optional)
float(0.1) // Blend sharpness
);Glass Material
import * as THREE from 'three/webgpu';
import { float, color } from 'three/tsl';
const material = new THREE.MeshPhysicalNodeMaterial();
material.colorNode = color(0xffffff);
material.transmissionNode = float(0.95);
material.roughnessNode = float(0.0);
material.metalnessNode = float(0.0);
material.iorNode = float(1.5);
material.thicknessNode = float(0.5);Fresnel Rim Material
import * as THREE from 'three/webgpu';
import {
color, float, normalWorld, positionWorld,
cameraPosition, Fn
} from 'three/tsl';
const fresnel = Fn(() => {
const viewDir = cameraPosition.sub(positionWorld).normalize();
const nDotV = normalWorld.dot(viewDir).saturate();
return float(1.0).sub(nDotV).pow(3.0);
});
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = color(0x222222);
material.emissiveNode = color(0x00ffff).mul(fresnel());Dissolve Effect Material
import * as THREE from 'three/webgpu';
import {
color, float, hash, positionLocal, uniform,
If, Discard, smoothstep
} from 'three/tsl';
const threshold = uniform(0.5);
const material = new THREE.MeshStandardNodeMaterial();
const noise = hash(positionLocal.mul(50));
// Discard fragments below threshold
If(noise.lessThan(threshold), () => {
Discard();
});
// Edge glow
const edge = smoothstep(threshold, threshold.add(0.1), noise);
material.colorNode = color(0x333333);
material.emissiveNode = color(0xff5500).mul(float(1.0).sub(edge));TSL Post-Processing
Post-processing applies effects to the rendered image. TSL provides both built-in effects and the ability to create custom effects.
Note:PostProcessingwas renamed toRenderPipelinein r183.PostProcessingstill works as a compatibility wrapper but is deprecated.
Basic Setup
import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
// Create renderer
const renderer = new THREE.WebGPURenderer();
await renderer.init();
// Create render pipeline (formerly PostProcessing, renamed in r183)
const renderPipeline = new THREE.RenderPipeline(renderer);
// Create scene pass
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Output (passthrough)
renderPipeline.outputNode = scenePassColor;
// Render with post-processing
function animate() {
renderPipeline.render(); // Not renderer.render()
}Built-in Effects
Bloom
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Add bloom
const bloomPass = bloom(scenePassColor);
// Configure
bloomPass.threshold.value = 0.5; // Brightness threshold
bloomPass.strength.value = 1.0; // Bloom intensity
bloomPass.radius.value = 0.5; // Blur radius
// Combine original + bloom
renderPipeline.outputNode = scenePassColor.add(bloomPass);Gaussian Blur
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
const blurred = gaussianBlur(scenePassColor, vec2(2.0)); // Blur strength (sigma)
renderPipeline.outputNode = blurred;Note (r177): Sigma values were rescaled — double previous sigma values to get the same blur strength. Also,resolutionwas renamed toresolutionScale(now a scalar, not a Vector2) in r180.
FXAA (Anti-aliasing)
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
renderPipeline.outputNode = fxaa(scenePassColor);SMAA (Anti-aliasing)
import { smaa } from 'three/addons/tsl/display/SMAANode.js';
renderPipeline.outputNode = smaa(scenePassColor);Depth of Field
import { dof } from 'three/addons/tsl/display/DepthOfFieldNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const viewZNode = scenePass.getViewZNode();
// dof(colorNode, viewZNode, focusDistance, focalLength, bokehScale)
const dofPass = dof(colorNode, viewZNode, 5.0, 25.0, 1.0);
renderPipeline.outputNode = dofPass;Note: The DOF API was completely reimplemented in r181. The old dof(color, depth, { focus, aperture, maxblur }) options-object signature no longer works.Motion Blur
import { motionBlur } from 'three/addons/tsl/display/MotionBlurNode.js';
const scenePass = pass(scene, camera);
const velocityPass = scenePass.getTextureNode('velocity');
const motionBlurPass = motionBlur(scenePassColor, velocityPass);
renderPipeline.outputNode = motionBlurPass;Screen Space Reflections (SSR)
import { ssr } from 'three/addons/tsl/display/SSRNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const ssrPass = ssr(colorNode, depthNode, normalNode, camera);
renderPipeline.outputNode = ssrPass;Ambient Occlusion (SSAO)
import { ao } from 'three/addons/tsl/display/AmbientOcclusionNode.js';
const scenePass = pass(scene, camera);
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const aoPass = ao(depthNode, normalNode, camera);
renderPipeline.outputNode = scenePassColor.mul(aoPass);Film Grain
import { film } from 'three/addons/tsl/display/FilmNode.js';
const filmPass = film(scenePassColor, {
intensity: 0.5,
grayscale: false
});
renderPipeline.outputNode = filmPass;Outline
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
const outlinePass = outline(scene, camera, selectedObjects, {
edgeStrength: 3.0,
edgeGlow: 0.0,
edgeThickness: 1.0,
visibleEdgeColor: new THREE.Color(0xffffff),
hiddenEdgeColor: new THREE.Color(0x190a05)
});
renderPipeline.outputNode = scenePassColor.add(outlinePass);Chromatic Aberration
import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';
const caPass = chromaticAberration(scenePassColor, {
offset: vec2(0.002, 0.002)
});
renderPipeline.outputNode = caPass;Color Adjustments
Grayscale
import { grayscale } from 'three/tsl';
renderPipeline.outputNode = grayscale(scenePassColor);Saturation
import { saturation } from 'three/tsl';
// 0 = grayscale, 1 = normal, 2 = oversaturated
renderPipeline.outputNode = saturation(scenePassColor, 1.5);Hue Shift
import { hue } from 'three/tsl';
// Shift hue by radians
renderPipeline.outputNode = hue(scenePassColor, time.mul(0.5));Vibrance
import { vibrance } from 'three/tsl';
renderPipeline.outputNode = vibrance(scenePassColor, 0.5);Posterize
import { posterize } from 'three/tsl';
// Reduce color levels
renderPipeline.outputNode = posterize(scenePassColor, 8);Sepia
import { sepia } from 'three/addons/tsl/display/SepiaNode.js';
renderPipeline.outputNode = sepia(scenePassColor);3D LUT
import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js';
const lutTexture = new THREE.Data3DTexture(lutData, size, size, size);
renderPipeline.outputNode = lut3D(scenePassColor, lutTexture, size);Custom Post-Processing
Basic Custom Effect
import { Fn, screenUV, float, vec4 } from 'three/tsl';
const customEffect = Fn(() => {
const color = scenePassColor.toVar();
// Invert colors
color.rgb.assign(float(1.0).sub(color.rgb));
return color;
});
renderPipeline.outputNode = customEffect();Vignette Effect
const vignette = Fn(() => {
const color = scenePassColor.toVar();
// Distance from center
const uv = screenUV;
const dist = uv.sub(0.5).length();
// Vignette falloff
const vignette = float(1.0).sub(dist.mul(1.5)).clamp(0, 1);
color.rgb.mulAssign(vignette);
return color;
});
renderPipeline.outputNode = vignette();CRT/Scanline Effect
import { viewportSharedTexture } from 'three/tsl';
const crtEffect = Fn(() => {
const uv = screenUV;
// Sample scene at offset UVs for RGB separation (chromatic aberration)
const uvR = uv.add(vec2(0.002, 0));
const uvG = uv;
const uvB = uv.sub(vec2(0.002, 0));
// Use viewportSharedTexture to sample at different UV coordinates
const r = viewportSharedTexture(uvR).r;
const g = viewportSharedTexture(uvG).g;
const b = viewportSharedTexture(uvB).b;
const color = vec4(r, g, b, 1.0).toVar();
// Scanlines
const scanline = uv.y.mul(screenSize.y).mul(0.5).sin().mul(0.1).add(0.9);
color.rgb.mulAssign(scanline);
// Vignette
const dist = uv.sub(0.5).length();
color.rgb.mulAssign(float(1.0).sub(dist.mul(0.5)));
return color;
});
// Note: For this effect, apply after scene rendering
renderPipeline.outputNode = crtEffect();Pixelate Effect
const pixelSize = uniform(8.0);
const pixelate = Fn(() => {
const uv = screenUV;
const pixelUV = uv.mul(screenSize).div(pixelSize).floor().mul(pixelSize).div(screenSize);
return texture(scenePassColor, pixelUV);
});
renderPipeline.outputNode = pixelate();Edge Detection (Sobel)
const sobelEdge = Fn(() => {
const uv = screenUV;
const texelSize = vec2(1.0).div(screenSize);
// Sample 3x3 kernel
const tl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, -1)))));
const tc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, -1)))));
const tr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, -1)))));
const ml = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 0)))));
const mr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 0)))));
const bl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 1)))));
const bc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, 1)))));
const br = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 1)))));
// Sobel operators
const gx = tl.add(ml.mul(2)).add(bl).sub(tr).sub(mr.mul(2)).sub(br);
const gy = tl.add(tc.mul(2)).add(tr).sub(bl).sub(bc.mul(2)).sub(br);
const edge = sqrt(gx.mul(gx).add(gy.mul(gy)));
return vec4(vec3(edge), 1.0);
});
renderPipeline.outputNode = sobelEdge();Multiple Render Targets (MRT)
Access multiple buffers from the scene pass:
import { mrt, output } from 'three/tsl';
const scenePass = pass(scene, camera);
// Set up MRT
scenePass.setMRT(mrt({
output: output, // Color output
normal: normalView, // View-space normals
depth: depth // Depth buffer
}));
// Access individual targets
const colorTexture = scenePass.getTextureNode('output');
const normalTexture = scenePass.getTextureNode('normal');
const depthTexture = scenePass.getTextureNode('depth');Selective Bloom with MRT
Bloom only emissive objects by rendering emissive to a separate target:
import { pass, mrt, output, emissive } from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
// Render both color and emissive to separate targets
scenePass.setMRT(mrt({
output: output,
emissive: emissive
}));
// Get the texture nodes
const colorTexture = scenePass.getTextureNode('output');
const emissiveTexture = scenePass.getTextureNode('emissive');
// Apply bloom only to emissive
const bloomPass = bloom(emissiveTexture);
bloomPass.threshold.value = 0.0; // Bloom all emissive
bloomPass.strength.value = 1.5;
bloomPass.radius.value = 0.5;
// Combine: original color + bloomed emissive
renderPipeline.outputNode = colorTexture.add(bloomPass);This approach prevents non-emissive bright areas (like white surfaces) from blooming.
Chaining Effects
const scenePass = pass(scene, camera);
const color = scenePass.getTextureNode('output');
// Chain multiple effects
let output = color;
// 1. Apply bloom
const bloomPass = bloom(output);
output = output.add(bloomPass.mul(0.5));
// 2. Apply color grading
output = saturation(output, 1.2);
// 3. Apply vignette
const dist = screenUV.sub(0.5).length();
const vignette = float(1.0).sub(dist.mul(0.5));
output = output.mul(vignette);
// 4. Apply FXAA
output = fxaa(output);
renderPipeline.outputNode = output;Conditional Effects
const effectEnabled = uniform(true);
const conditionalEffect = Fn(() => {
const color = scenePassColor;
return select(effectEnabled, grayscale(color), color);
});
renderPipeline.outputNode = conditionalEffect();
// Toggle at runtime
effectEnabled.value = false;Transitions
import { transition } from 'three/addons/tsl/display/TransitionNode.js';
const scenePassA = pass(sceneA, camera);
const scenePassB = pass(sceneB, camera);
const transitionProgress = uniform(0);
const transitionPass = transition(
scenePassA.getTextureNode('output'),
scenePassB.getTextureNode('output'),
transitionProgress,
texture(transitionTexture) // Optional transition texture
);
renderPipeline.outputNode = transitionPass;
// Animate transition
function animate() {
transitionProgress.value = Math.sin(time) * 0.5 + 0.5;
renderPipeline.render();
}Additional Effects (r182+)
These effects were added in recent Three.js releases:
// Volumetric god rays (r183)
import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
const godraysPass = godrays(scenePassColor, depthNode, camera, lightPosition);
// Retro/CRT effect (r183)
import { retroPass } from 'three/addons/tsl/display/RetroNode.js';
// Anamorphic lens flare
import { anamorphic } from 'three/addons/tsl/display/AnamorphicNode.js';
const anamorphicPass = anamorphic(scenePassColor);
// Lens flare
import { lensflare } from 'three/addons/tsl/display/LensflareNode.js';
// Denoising
import { denoise } from 'three/addons/tsl/display/DenoiseNode.js';
// Screen-space global illumination
import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
// Temporal anti-aliasing (replaces FXAA/SMAA for better quality)
import { traa } from 'three/addons/tsl/display/TRAANode.js';
// Alternative blur modes
import { boxBlur } from 'three/addons/tsl/display/BoxBlurNode.js'; // Mobile-friendly
import { hashBlur } from 'three/addons/tsl/display/HashBlurNode.js'; // Single-pass
import { bilateralBlur } from 'three/addons/tsl/display/BilateralBlurNode.js'; // Edge-preserving
// 3D texture sampling (r182)
import { texture3DLoad, texture3DLevel } from 'three/tsl';WGSL Integration
TSL allows embedding raw WGSL (WebGPU Shading Language) code when you need direct GPU control.
wgslFn - Custom WGSL Functions
Basic Usage
import { wgslFn, float, vec3 } from 'three/tsl';
// Define WGSL function
const gammaCorrect = wgslFn(`
fn gammaCorrect(color: vec3<f32>, gamma: f32) -> vec3<f32> {
return pow(color, vec3<f32>(1.0 / gamma));
}
`);
// Use in TSL
material.colorNode = gammaCorrect(inputColor, float(2.2));Function with Multiple Parameters
const blendColors = wgslFn(`
fn blendColors(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
return mix(a, b, t);
}
`);
material.colorNode = blendColors(colorA, colorB, blendFactor);Advanced Math Functions
const fresnelSchlick = wgslFn(`
fn fresnelSchlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
return F0 + (vec3<f32>(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
}
`);
const GGX = wgslFn(`
fn distributionGGX(N: vec3<f32>, H: vec3<f32>, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let NdotH = max(dot(N, H), 0.0);
let NdotH2 = NdotH * NdotH;
let num = a2;
let denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = 3.14159265359 * denom * denom;
return num / denom;
}
`);Noise Functions
const simplexNoise = wgslFn(`
fn mod289(x: vec3<f32>) -> vec3<f32> {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
fn permute(x: vec3<f32>) -> vec3<f32> {
return mod289(((x * 34.0) + 1.0) * x);
}
fn snoise(v: vec2<f32>) -> f32 {
let C = vec4<f32>(
0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439
);
var i = floor(v + dot(v, C.yy));
let x0 = v - i + dot(i, C.xx);
var i1: vec2<f32>;
if (x0.x > x0.y) {
i1 = vec2<f32>(1.0, 0.0);
} else {
i1 = vec2<f32>(0.0, 1.0);
}
var x12 = x0.xyxy + C.xxzz;
x12 = vec4<f32>(x12.xy - i1, x12.zw);
i = mod289(vec3<f32>(i, 0.0)).xy;
let p = permute(permute(i.y + vec3<f32>(0.0, i1.y, 1.0)) + i.x + vec3<f32>(0.0, i1.x, 1.0));
var m = max(vec3<f32>(0.5) - vec3<f32>(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), vec3<f32>(0.0));
m = m * m;
m = m * m;
let x = 2.0 * fract(p * C.www) - 1.0;
let h = abs(x) - 0.5;
let ox = floor(x + 0.5);
let a0 = x - ox;
m = m * (1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h));
let g = vec3<f32>(
a0.x * x0.x + h.x * x0.y,
a0.y * x12.x + h.y * x12.y,
a0.z * x12.z + h.z * x12.w
);
return 130.0 * dot(m, g);
}
`);
// Use noise
const noiseValue = simplexNoise(uv().mul(10.0));FBM (Fractal Brownian Motion)
const fbm = wgslFn(`
fn fbm(p: vec2<f32>, octaves: i32) -> f32 {
var value = 0.0;
var amplitude = 0.5;
var frequency = 1.0;
var pos = p;
for (var i = 0; i < octaves; i = i + 1) {
value = value + amplitude * snoise(pos * frequency);
amplitude = amplitude * 0.5;
frequency = frequency * 2.0;
}
return value;
}
`);WGSL Types Reference
Scalar Types
bool // Boolean
i32 // 32-bit signed integer
u32 // 32-bit unsigned integer
f32 // 32-bit float
f16 // 16-bit float (if enabled)Vector Types
vec2<f32> // 2D float vector
vec3<f32> // 3D float vector
vec4<f32> // 4D float vector
vec2<i32> // 2D integer vector
vec2<u32> // 2D unsigned integer vectorMatrix Types
mat2x2<f32> // 2x2 matrix
mat3x3<f32> // 3x3 matrix
mat4x4<f32> // 4x4 matrix
mat2x3<f32> // 2 columns, 3 rowsTexture Types
texture_2d<f32>
texture_3d<f32>
texture_cube<f32>
texture_storage_2d<rgba8unorm, write>WGSL Syntax Reference
Variables
let x = 1.0; // Immutable
var y = 2.0; // Mutable
const PI = 3.14159; // Compile-time constantControl Flow
// If-else
if (condition) {
// ...
} else if (other) {
// ...
} else {
// ...
}
// For loop
for (var i = 0; i < 10; i = i + 1) {
// ...
}
// While loop
while (condition) {
// ...
}
// Switch
switch (value) {
case 0: { /* ... */ }
case 1, 2: { /* ... */ }
default: { /* ... */ }
}Built-in Functions
// Math
abs(x), sign(x), floor(x), ceil(x), round(x)
fract(x), trunc(x)
min(a, b), max(a, b), clamp(x, lo, hi)
mix(a, b, t), step(edge, x), smoothstep(lo, hi, x)
sin(x), cos(x), tan(x), asin(x), acos(x), atan(x), atan2(y, x)
pow(x, y), exp(x), log(x), exp2(x), log2(x)
sqrt(x), inverseSqrt(x)
// Vector
length(v), distance(a, b)
dot(a, b), cross(a, b)
normalize(v), faceForward(n, i, nref)
reflect(i, n), refract(i, n, eta)
// Matrix
transpose(m), determinant(m)
// Texture
textureSample(t, s, coord)
textureLoad(t, coord, level)
textureStore(t, coord, value)
textureDimensions(t)Combining TSL and WGSL
TSL Wrapper for WGSL
import { Fn, wgslFn, float, vec2, vec3 } from 'three/tsl';
// WGSL implementation
const wgslNoise = wgslFn(`
fn noise2d(p: vec2<f32>) -> f32 {
return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}
`);
// TSL wrapper with nice API
const noise = Fn(([position, scale = 1.0]) => {
return wgslNoise(position.xy.mul(scale));
});
// Use
material.colorNode = vec3(noise(positionWorld, 10.0));Hybrid Approach
// Complex math in WGSL
const complexMath = wgslFn(`
fn complexOperation(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
let blended = mix(a, b, t);
let rotated = vec3<f32>(
blended.x * cos(t) - blended.y * sin(t),
blended.x * sin(t) + blended.y * cos(t),
blended.z
);
return normalize(rotated);
}
`);
// Simple logic in TSL
const finalColor = Fn(() => {
const base = texture(diffuseMap).rgb;
const processed = complexMath(base, vec3(1, 0, 0), time);
return mix(base, processed, oscSine(time));
});
material.colorNode = finalColor();Performance Tips
Avoid Branching When Possible
// Instead of:
if (x > 0.5) {
result = a;
} else {
result = b;
}
// Use:
result = mix(b, a, step(0.5, x));Use Local Variables
fn compute(p: vec2<f32>) -> f32 {
// Cache repeated calculations
let p2 = p * p;
let p4 = p2 * p2;
return p2.x + p2.y + p4.x * p4.y;
}Minimize Texture Samples
// Sample once, use multiple times
let sample = textureSample(tex, sampler, uv);
let r = sample.r;
let g = sample.g;
let b = sample.b;/**
* Basic WebGPU Three.js Setup
*
* Minimal example showing WebGPU renderer initialization
* with a simple animated mesh using TSL.
*
* Based on Three.js examples (MIT License)
* https://github.com/mrdoob/three.js
*/
import * as THREE from 'three/webgpu';
import { color, time, oscSine, positionLocal, normalWorld } from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let camera, scene, renderer, controls;
async function init() {
// Camera
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.z = 4;
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// Create mesh with TSL material
const geometry = new THREE.TorusKnotGeometry(1, 0.3, 128, 32);
const material = new THREE.MeshStandardNodeMaterial();
// Animated color using TSL
material.colorNode = color(0x0088ff).mul(
oscSine(time.mul(0.5)).mul(0.5).add(0.5)
);
// Add slight position wobble
material.positionNode = positionLocal.add(
normalWorld.mul(oscSine(time.mul(2.0).add(positionLocal.y)).mul(0.05))
);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Initialize WebGPU
await renderer.init();
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Handle resize
window.addEventListener('resize', onWindowResize);
// Start animation loop
renderer.setAnimationLoop(animate);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
controls.update();
renderer.render(scene, camera);
}
init();
/**
* Custom TSL Material Example
*
* Demonstrates creating custom shader effects using TSL:
* - Fresnel rim lighting
* - Animated patterns
* - Dynamic displacement
*
* Based on Three.js examples (MIT License)
* https://github.com/mrdoob/three.js
*/
import * as THREE from 'three/webgpu';
import {
Fn,
color,
float,
vec2,
vec3,
uniform,
texture,
uv,
time,
mix,
smoothstep,
sin,
cos,
positionLocal,
positionWorld,
normalLocal,
normalWorld,
cameraPosition
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let camera, scene, renderer, controls;
let rimColor, patternScale, displacementStrength;
async function init() {
// Setup
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 3;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
// Uniforms for runtime control
rimColor = uniform(new THREE.Color(0x00ffff));
patternScale = uniform(5.0);
displacementStrength = uniform(0.1);
// Create custom material
const material = createCustomMaterial();
// Mesh
const geometry = new THREE.IcosahedronGeometry(1, 64);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
await renderer.init();
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Events
window.addEventListener('resize', onWindowResize);
// GUI (optional - requires lil-gui)
setupGUI();
renderer.setAnimationLoop(animate);
}
function createCustomMaterial() {
const material = new THREE.MeshStandardNodeMaterial();
// --- Fresnel Rim Effect ---
const fresnel = Fn(() => {
const viewDir = cameraPosition.sub(positionWorld).normalize();
const nDotV = normalWorld.dot(viewDir).saturate();
return float(1.0).sub(nDotV).pow(3.0);
});
// --- Animated Pattern ---
const animatedPattern = Fn(() => {
const uvCoord = uv().mul(patternScale);
const t = time.mul(0.5);
// Create animated wave pattern
const wave1 = sin(uvCoord.x.mul(10.0).add(t)).mul(0.5).add(0.5);
const wave2 = sin(uvCoord.y.mul(10.0).sub(t.mul(1.3))).mul(0.5).add(0.5);
const wave3 = sin(uvCoord.x.add(uvCoord.y).mul(7.0).add(t.mul(0.7))).mul(0.5).add(0.5);
return wave1.mul(wave2).mul(wave3);
});
// --- Displacement ---
const displacement = Fn(() => {
const pattern = animatedPattern();
return normalLocal.mul(pattern.mul(displacementStrength));
});
// Apply displacement
material.positionNode = positionLocal.add(displacement());
// --- Color ---
const baseColor = color(0x222244);
const highlightColor = color(0x4444ff);
// Mix colors based on pattern
const pattern = animatedPattern();
const surfaceColor = mix(baseColor, highlightColor, pattern);
material.colorNode = surfaceColor;
// --- Rim lighting ---
material.emissiveNode = rimColor.mul(fresnel());
// --- PBR properties ---
material.roughnessNode = float(0.3).add(pattern.mul(0.4));
material.metalnessNode = float(0.1);
return material;
}
function setupGUI() {
// Only setup if lil-gui is available
if (typeof window.GUI === 'undefined') {
console.log('Add lil-gui for interactive controls');
return;
}
const gui = new GUI();
const params = {
rimColor: '#00ffff',
patternScale: 5.0,
displacementStrength: 0.1
};
gui.addColor(params, 'rimColor').onChange((value) => {
rimColor.value.set(value);
});
gui.add(params, 'patternScale', 1, 20).onChange((value) => {
patternScale.value = value;
});
gui.add(params, 'displacementStrength', 0, 0.5).onChange((value) => {
displacementStrength.value = value;
});
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
controls.update();
renderer.render(scene, camera);
}
init();
/**
* Earth Shader Example
*
* Complete procedural Earth with:
* - Day/night texture blending
* - Atmospheric glow (fresnel)
* - Cloud layer
* - City lights at night
* - Bump mapping
*
* Based on Three.js webgpu_tsl_earth example (MIT License)
* https://github.com/mrdoob/three.js
*/
import * as THREE from 'three/webgpu';
import {
Fn,
If,
float,
vec2,
vec3,
vec4,
color,
uniform,
texture,
uv,
time,
mix,
smoothstep,
pow,
clamp,
normalize,
dot,
max,
positionWorld,
normalWorld,
normalLocal,
cameraPosition,
bumpMap
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let camera, scene, renderer, controls;
let earth, clouds, atmosphere;
// Uniforms
const sunDirection = uniform(new THREE.Vector3(1, 0.2, 0.5).normalize());
const atmosphereDayColor = uniform(new THREE.Color(0x4db2ff));
const atmosphereTwilightColor = uniform(new THREE.Color(0xbd5f1b));
const cloudSpeed = uniform(0.01);
const cityLightIntensity = uniform(1.5);
async function init() {
// Camera
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 4);
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000011);
// Load textures
const loader = new THREE.TextureLoader();
// Note: Replace with actual texture paths
const earthDayTexture = loader.load('textures/earth_day.jpg');
const earthNightTexture = loader.load('textures/earth_night.jpg');
const earthCloudsTexture = loader.load('textures/earth_clouds.jpg');
const earthBumpTexture = loader.load('textures/earth_bump.jpg');
// Set texture properties
[earthDayTexture, earthNightTexture, earthCloudsTexture, earthBumpTexture].forEach((tex) => {
tex.colorSpace = THREE.SRGBColorSpace;
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.ClampToEdgeWrapping;
});
// Create Earth
earth = createEarth(earthDayTexture, earthNightTexture, earthBumpTexture);
scene.add(earth);
// Create cloud layer
clouds = createClouds(earthCloudsTexture);
scene.add(clouds);
// Create atmosphere glow
atmosphere = createAtmosphere();
scene.add(atmosphere);
// Stars background
createStars();
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
await renderer.init();
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 2;
controls.maxDistance = 10;
// Events
window.addEventListener('resize', onWindowResize);
renderer.setAnimationLoop(animate);
}
function createEarth(dayTex, nightTex, bumpTex) {
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshStandardNodeMaterial();
// Sun illumination factor
const sunOrientation = Fn(() => {
return normalWorld.dot(sunDirection).mul(0.5).add(0.5);
});
// Day/night color mixing
material.colorNode = Fn(() => {
const dayColor = texture(dayTex, uv());
const nightColor = texture(nightTex, uv());
const orientation = sunOrientation();
const dayNight = smoothstep(0.4, 0.6, orientation);
// Add city lights on night side
const cityLights = nightColor.mul(cityLightIntensity).mul(
float(1.0).sub(dayNight)
);
const baseColor = mix(nightColor, dayColor, dayNight);
return baseColor.add(cityLights.mul(float(1.0).sub(orientation).pow(2.0)));
})();
// Bump mapping for terrain
material.normalNode = bumpMap(texture(bumpTex, uv()), 0.03);
// PBR properties vary with day/night
material.roughnessNode = Fn(() => {
const orientation = sunOrientation();
return mix(float(0.8), float(0.4), smoothstep(0.3, 0.7, orientation));
})();
material.metalnessNode = float(0.0);
// Subtle atmospheric rim on day side
material.emissiveNode = Fn(() => {
const viewDir = normalize(cameraPosition.sub(positionWorld));
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).saturate()), 4.0);
const orientation = sunOrientation();
const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, orientation);
return atmosphereColor.mul(fresnel).mul(orientation).mul(0.3);
})();
return new THREE.Mesh(geometry, material);
}
function createClouds(cloudsTex) {
const geometry = new THREE.SphereGeometry(1.01, 64, 64);
const material = new THREE.MeshStandardNodeMaterial();
// Animated UV for cloud movement
const cloudUV = Fn(() => {
const baseUV = uv();
const offset = time.mul(cloudSpeed);
return vec2(baseUV.x.add(offset), baseUV.y);
});
// Cloud color (white with transparency)
material.colorNode = color(0xffffff);
// Cloud opacity from texture
material.opacityNode = Fn(() => {
const cloudAlpha = texture(cloudsTex, cloudUV()).r;
// Fade clouds on night side
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
const dayFactor = smoothstep(0.2, 0.5, sunOrientation);
return cloudAlpha.mul(0.8).mul(dayFactor.mul(0.5).add(0.5));
})();
material.transparent = true;
material.depthWrite = false;
material.side = THREE.DoubleSide;
// Slight self-illumination
material.emissiveNode = Fn(() => {
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
return color(0xffffff).mul(sunOrientation.mul(0.1));
})();
return new THREE.Mesh(geometry, material);
}
function createAtmosphere() {
const geometry = new THREE.SphereGeometry(1.15, 64, 64);
const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = Fn(() => {
const viewDir = normalize(cameraPosition.sub(positionWorld));
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 3.0);
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, sunOrientation);
return atmosphereColor;
})();
material.opacityNode = Fn(() => {
const viewDir = normalize(cameraPosition.sub(positionWorld));
const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 2.5);
// Stronger on day side
const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5);
return fresnel.mul(sunOrientation.mul(0.5).add(0.3));
})();
material.transparent = true;
material.depthWrite = false;
material.side = THREE.BackSide;
return new THREE.Mesh(geometry, material);
}
function createStars() {
const starsGeometry = new THREE.BufferGeometry();
const starCount = 2000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
// Random position on sphere
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
const radius = 50 + Math.random() * 50;
positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = radius * Math.cos(phi);
// Slight color variation
const brightness = 0.5 + Math.random() * 0.5;
colors[i * 3] = brightness;
colors[i * 3 + 1] = brightness;
colors[i * 3 + 2] = brightness + Math.random() * 0.2;
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const starsMaterial = new THREE.PointsNodeMaterial();
starsMaterial.colorNode = Fn(() => {
return vec3(1.0);
})();
starsMaterial.sizeNode = float(2.0);
starsMaterial.vertexColors = true;
const stars = new THREE.Points(starsGeometry, starsMaterial);
scene.add(stars);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
// Rotate Earth slowly
earth.rotation.y += 0.001;
clouds.rotation.y += 0.0012;
// Animate sun direction (optional - creates day/night cycle)
// const angle = time.value * 0.1;
// sunDirection.value.set(Math.cos(angle), 0.2, Math.sin(angle)).normalize();
controls.update();
renderer.render(scene, camera);
}
init();
// Export for external control
export { sunDirection, atmosphereDayColor, atmosphereTwilightColor, cloudSpeed, cityLightIntensity };
/**
* GPU Particle System with Compute Shaders
*
* Demonstrates TSL compute shaders for particle simulation:
* - Instanced array buffers
* - Physics simulation on GPU
* - Mouse interaction
*
* Based on Three.js webgpu_compute_particles example (MIT License)
* https://github.com/mrdoob/three.js
*/
import * as THREE from 'three/webgpu';
import {
Fn,
If,
uniform,
float,
vec3,
color,
instancedArray,
instanceIndex,
hash,
time
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let camera, scene, renderer, controls;
let computeInit, computeUpdate, computeHit;
// Particle count
const PARTICLE_COUNT = 100000;
// Storage buffers
const positions = instancedArray(PARTICLE_COUNT, 'vec3');
const velocities = instancedArray(PARTICLE_COUNT, 'vec3');
// Uniforms
const gravity = uniform(-9.8);
const bounce = uniform(0.7);
const friction = uniform(0.98);
const deltaTimeUniform = uniform(0);
const clickPosition = uniform(new THREE.Vector3());
const hitStrength = uniform(5.0);
async function init() {
// Camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 5, 15);
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
// Create compute shaders
createComputeShaders();
// Create particle mesh
createParticleMesh();
// Floor
const floorGeometry = new THREE.PlaneGeometry(30, 30);
const floorMaterial = new THREE.MeshStandardNodeMaterial({
color: 0x333333
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 100);
pointLight.position.set(5, 10, 5);
scene.add(pointLight);
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
await renderer.init();
// Initialize particles (renderer already initialized above)
renderer.compute(computeInit);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 2, 0);
// Events
window.addEventListener('resize', onWindowResize);
renderer.domElement.addEventListener('click', onClick);
renderer.setAnimationLoop(animate);
}
function createComputeShaders() {
// Grid dimensions for initialization
const gridSize = Math.ceil(Math.sqrt(PARTICLE_COUNT));
const spacing = 0.15;
const offset = (gridSize * spacing) / 2;
// Initialize particles in a grid
computeInit = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// Calculate grid position
const x = instanceIndex.mod(gridSize);
const z = instanceIndex.div(gridSize);
// Set position
position.x.assign(x.toFloat().mul(spacing).sub(offset));
position.y.assign(float(5.0).add(hash(instanceIndex).mul(2.0)));
position.z.assign(z.toFloat().mul(spacing).sub(offset));
// Random initial velocity
velocity.x.assign(hash(instanceIndex.add(1)).sub(0.5).mul(2.0));
velocity.y.assign(hash(instanceIndex.add(2)).mul(-2.0));
velocity.z.assign(hash(instanceIndex.add(3)).sub(0.5).mul(2.0));
})().compute(PARTICLE_COUNT);
// Physics update
computeUpdate = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
const dt = deltaTimeUniform;
// Apply gravity
velocity.y.addAssign(gravity.mul(dt));
// Update position
position.addAssign(velocity.mul(dt));
// Apply friction
velocity.mulAssign(friction);
// Ground collision
If(position.y.lessThan(0), () => {
position.y.assign(0);
velocity.y.assign(velocity.y.abs().mul(bounce)); // Reverse and dampen
// Extra friction on ground
velocity.x.mulAssign(0.9);
velocity.z.mulAssign(0.9);
});
// Boundary walls
If(position.x.abs().greaterThan(15), () => {
position.x.assign(position.x.sign().mul(15));
velocity.x.assign(velocity.x.negate().mul(bounce));
});
If(position.z.abs().greaterThan(15), () => {
position.z.assign(position.z.sign().mul(15));
velocity.z.assign(velocity.z.negate().mul(bounce));
});
})().compute(PARTICLE_COUNT);
// Hit/explosion effect
computeHit = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// Distance to click
const toClick = position.sub(clickPosition);
const distance = toClick.length();
// Apply force within radius
If(distance.lessThan(3.0), () => {
const direction = toClick.normalize();
const force = float(3.0).sub(distance).div(3.0).mul(hitStrength);
// Add randomness
const randomForce = force.mul(hash(instanceIndex.add(time.mul(1000))).mul(0.5).add(0.75));
velocity.addAssign(direction.mul(randomForce));
velocity.y.addAssign(randomForce.mul(0.5));
});
})().compute(PARTICLE_COUNT);
}
function createParticleMesh() {
// Simple sphere geometry for each particle
const geometry = new THREE.SphereGeometry(0.08, 8, 8);
// Material using computed positions
const material = new THREE.MeshStandardNodeMaterial();
// Position from compute buffer
material.positionNode = positions.element(instanceIndex);
// Color based on velocity
material.colorNode = Fn(() => {
const velocity = velocities.element(instanceIndex);
const speed = velocity.length();
// Color gradient: blue (slow) -> orange (fast)
const t = speed.div(10.0).saturate();
return color(0x0066ff).mix(color(0xff6600), t);
})();
material.roughnessNode = float(0.5);
material.metalnessNode = float(0.2);
// Create instanced mesh
const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT);
scene.add(mesh);
}
function onClick(event) {
// Raycast to find click position on floor
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1
);
raycaster.setFromCamera(mouse, camera);
// Intersect with floor plane (y = 0)
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersection);
if (intersection) {
// Raise click position slightly
intersection.y = 0.5;
clickPosition.value.copy(intersection);
// Run hit compute shader
renderer.compute(computeHit);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
const clock = new THREE.Clock();
function animate() {
// Update delta time
deltaTimeUniform.value = Math.min(clock.getDelta(), 0.1);
// Run physics compute
renderer.compute(computeUpdate);
controls.update();
renderer.render(scene, camera);
}
init();
/**
* Post-Processing Pipeline Example
*
* Demonstrates TSL post-processing:
* - Bloom effect
* - Custom vignette
* - Color grading
* - Effect chaining
*
* Based on Three.js webgpu_postprocessing examples (MIT License)
* https://github.com/mrdoob/three.js
*/
import * as THREE from 'three/webgpu';
import {
Fn,
float,
vec2,
vec3,
vec4,
color,
uniform,
pass,
screenUV,
screenSize,
time,
oscSine,
mix,
smoothstep,
texture,
grayscale,
saturation
} from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let camera, scene, renderer, controls;
let postProcessing;
// Effect uniforms
const bloomStrength = uniform(1.0);
const bloomThreshold = uniform(0.5);
const vignetteIntensity = uniform(0.5);
const saturationAmount = uniform(1.2);
const colorTint = uniform(new THREE.Color(1.0, 0.95, 0.9));
async function init() {
// Camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// Add objects with emissive materials (for bloom)
createScene();
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
await renderer.init();
// Create post-processing pipeline
setupPostProcessing();
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 1, 0);
// Events
window.addEventListener('resize', onWindowResize);
renderer.setAnimationLoop(animate);
}
function createScene() {
// Floor
const floorGeometry = new THREE.PlaneGeometry(20, 20);
const floorMaterial = new THREE.MeshStandardNodeMaterial({
color: 0x222222
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// Emissive spheres (will bloom)
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const colors = [0xff0044, 0x00ff88, 0x4488ff, 0xffaa00, 0xff00ff];
for (let i = 0; i < 5; i++) {
const material = new THREE.MeshStandardNodeMaterial();
// Base color
material.colorNode = color(colors[i]).mul(0.3);
// Animated emissive
material.emissiveNode = Fn(() => {
const pulse = oscSine(time.mul(1.0 + i * 0.2)).mul(0.5).add(0.5);
return color(colors[i]).mul(pulse.mul(2.0).add(0.5));
})();
material.roughnessNode = float(0.2);
material.metalnessNode = float(0.8);
const sphere = new THREE.Mesh(sphereGeometry, material);
sphere.position.set(
Math.cos((i / 5) * Math.PI * 2) * 3,
1 + Math.sin(i) * 0.5,
Math.sin((i / 5) * Math.PI * 2) * 3
);
scene.add(sphere);
}
// Central reflective sphere
const centerMaterial = new THREE.MeshStandardNodeMaterial();
centerMaterial.colorNode = color(0x888888);
centerMaterial.roughnessNode = float(0.1);
centerMaterial.metalnessNode = float(1.0);
const centerSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), centerMaterial);
centerSphere.position.y = 1;
scene.add(centerSphere);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 50);
pointLight.position.set(5, 10, 5);
scene.add(pointLight);
}
function setupPostProcessing() {
// Create post-processing instance
postProcessing = new THREE.RenderPipeline(renderer);
// Create scene pass
const scenePass = pass(scene, camera);
const sceneColor = scenePass.getTextureNode('output');
// --- Effect Chain ---
// 1. Bloom
const bloomPass = bloom(sceneColor);
bloomPass.threshold = bloomThreshold;
bloomPass.strength = bloomStrength;
bloomPass.radius = uniform(0.5);
// Add bloom to scene
let output = sceneColor.add(bloomPass);
// 2. Color Grading
output = saturation(output, saturationAmount);
output = output.mul(colorTint);
// 3. Vignette (custom effect)
const vignette = Fn(() => {
const uv = screenUV;
const dist = uv.sub(0.5).length();
return float(1.0).sub(dist.mul(vignetteIntensity).pow(2.0)).saturate();
});
output = output.mul(vignette());
// 4. Optional: Scanlines
const scanlines = Fn(() => {
const scanline = screenUV.y.mul(screenSize.y).mul(0.5).sin().mul(0.05).add(0.95);
return scanline;
});
// Uncomment for CRT effect:
// output = output.mul(scanlines());
// Set final output
postProcessing.outputNode = output;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
controls.update();
// Render with post-processing
postProcessing.render();
}
init();
// Export uniforms for external control (e.g., GUI)
export { bloomStrength, bloomThreshold, vignetteIntensity, saturationAmount, colorTint };
TSL Quick Reference
Imports
// WebGPU Three.js
import * as THREE from 'three/webgpu';
// Core TSL
import {
float, int, uint, bool,
vec2, vec3, vec4, color,
mat2, mat3, mat4,
uniform, texture, uv,
Fn, If, Loop, Break, Continue,
time, deltaTime
} from 'three/tsl';Types
| TSL | WGSL | Example |
|---|---|---|
float(1.0) | f32 | Scalar float |
int(1) | i32 | Signed integer |
uint(1) | u32 | Unsigned integer |
bool(true) | bool | Boolean |
vec2(x, y) | vec2<f32> | 2D vector |
vec3(x, y, z) | vec3<f32> | 3D vector |
vec4(x, y, z, w) | vec4<f32> | 4D vector |
color(0xff0000) | vec3<f32> | RGB color |
uniform(value) | uniform | Dynamic value |
Operators
| Operation | TSL | GLSL Equivalent |
|---|---|---|
| Add | a.add(b) | a + b |
| Subtract | a.sub(b) | a - b |
| Multiply | a.mul(b) | a * b |
| Divide | a.div(b) | a / b |
| Modulo | a.mod(b) | mod(a, b) |
| Negate | a.negate() | -a |
| Less Than | a.lessThan(b) | a < b |
| Greater Than | a.greaterThan(b) | a > b |
| Equal | a.equal(b) | a == b |
| And | a.and(b) | a && b |
| Or | a.or(b) | `a \ |
| Assign | a.assign(b) | a = b |
| Add Assign | a.addAssign(b) | a += b |
Swizzling
const v = vec3(1, 2, 3);
v.x // 1
v.xy // vec2(1, 2)
v.zyx // vec3(3, 2, 1)
v.rgb // same as xyzMath Functions
| Function | Description |
|---|---|
abs(x) | Absolute value |
sign(x) | Sign (-1, 0, 1) |
floor(x) | Round down |
ceil(x) | Round up |
fract(x) | Fractional part |
min(a, b) | Minimum |
max(a, b) | Maximum |
clamp(x, lo, hi) | Clamp to range |
mix(a, b, t) | Linear interpolation |
step(edge, x) | Step function |
smoothstep(a, b, x) | Smooth step |
sin(x), cos(x) | Trigonometry |
pow(x, y) | Power |
sqrt(x) | Square root |
length(v) | Vector length |
distance(a, b) | Distance |
dot(a, b) | Dot product |
cross(a, b) | Cross product |
normalize(v) | Unit vector |
reflect(i, n) | Reflection |
Geometry Nodes
| Node | Description |
|---|---|
positionLocal | Model space position |
positionWorld | World space position |
positionView | Camera space position |
normalLocal | Model space normal |
normalWorld | World space normal |
normalView | Camera space normal |
uv() | UV coordinates |
uv(1) | Secondary UVs |
tangentLocal | Tangent vector |
vertexColor() | Vertex colors |
Camera Nodes
| Node | Description |
|---|---|
cameraPosition | Camera world position |
cameraNear | Near plane |
cameraFar | Far plane |
cameraViewMatrix | View matrix |
cameraProjectionMatrix | Projection matrix |
screenUV | Screen UV (0-1) |
screenSize | Screen dimensions |
Time
| Node | Description |
|---|---|
time | Seconds since start |
deltaTime | Frame delta |
oscSine(t) | Sine wave (0-1) |
oscSquare(t) | Square wave |
oscTriangle(t) | Triangle wave |
oscSawtooth(t) | Sawtooth wave |
Material Properties
const mat = new THREE.MeshStandardNodeMaterial();
// Basic
mat.colorNode = color(0xff0000);
mat.opacityNode = float(0.8);
mat.alphaTestNode = float(0.5);
// PBR
mat.roughnessNode = float(0.5);
mat.metalnessNode = float(0.0);
mat.emissiveNode = color(0x000000);
mat.normalNode = normalMap(tex);
// Physical (MeshPhysicalNodeMaterial)
mat.clearcoatNode = float(1.0);
mat.transmissionNode = float(0.9);
mat.iridescenceNode = float(1.0);
mat.sheenNode = float(1.0);
// Vertex
mat.positionNode = displaced;Control Flow
// If-Else
If(condition, () => {
// true
}).ElseIf(other, () => {
// other true
}).Else(() => {
// false
});
// Select (ternary)
const result = select(condition, trueVal, falseVal);
// Loop
Loop(10, ({ i }) => {
// i = 0 to 9
});
// Loop control
Break();
Continue();
Discard(); // Fragment onlyCustom Functions
// Basic function
const myFn = Fn(([a, b]) => {
return a.add(b);
});
// With defaults
const myFn = Fn(([a = 1.0, b = 2.0]) => {
return a.add(b);
});
// Usage
myFn(x, y);
myFn(); // uses defaultsCompute Shaders
// Storage buffers (read-write)
const positions = instancedArray(count, 'vec3');
const values = instancedArray(count, 'float');
// Read-only storage buffers
const lookupTable = attributeArray(data, 'float');
// Compute shader
const compute = Fn(() => {
const pos = positions.element(instanceIndex);
pos.addAssign(vec3(0.01, 0, 0));
})().compute(count);
// Execute (after await renderer.init())
renderer.compute(compute);
// Workgroup size
const compute2 = Fn(() => { /* ... */ })().compute(count, [64]);Post-Processing
import { pass } from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
// Setup (RenderPipeline replaced PostProcessing in r183)
const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
const color = scenePass.getTextureNode('output');
// Apply effects
const bloomPass = bloom(color);
renderPipeline.outputNode = color.add(bloomPass);
// Render
renderPipeline.render();Common Patterns
Fresnel
const viewDir = cameraPosition.sub(positionWorld).normalize();
const fresnel = float(1).sub(normalWorld.dot(viewDir).saturate()).pow(3);Animated UV
const animUV = uv().add(vec2(time.mul(0.1), 0));Noise Hash
const noise = fract(position.dot(vec3(12.9898, 78.233, 45.543)).sin().mul(43758.5453));Dissolve
const noise = hash(positionLocal.mul(50));
If(noise.lessThan(threshold), () => Discard());Color Gradient
const gradient = mix(colorA, colorB, positionLocal.y.mul(0.5).add(0.5));Node Materials
| Material | Use Case |
|---|---|
MeshBasicNodeMaterial | Unlit |
MeshStandardNodeMaterial | PBR |
MeshPhysicalNodeMaterial | Advanced PBR |
MeshPhongNodeMaterial | Phong shading |
MeshToonNodeMaterial | Cel shading |
PointsNodeMaterial | Point clouds |
LineBasicNodeMaterial | Lines |
SpriteNodeMaterial | Sprites |
Device Loss Handling
// Listen for device loss
renderer.backend.device.lost.then((info) => {
if (info.reason === 'unknown') {
// Unexpected loss - recover
renderer.dispose();
initWebGPU(); // Reinitialize
}
});
// Simulate loss for testing
renderer.backend.device.destroy();| Loss Reason | Meaning |
|---|---|
'destroyed' | Intentional via destroy() |
'unknown' | Unexpected (driver crash, timeout, etc.) |
Recovery tips:
- Always get fresh adapter before new device
- Save/restore application state (not transient data)
- Use Chrome
about:gpucrashto test real GPU crashes
Compute Shader Built-ins
| Node | Description |
|---|---|
instanceIndex | Current instance/invocation index |
vertexIndex | Current vertex index |
drawIndex | Current draw call index |
globalId | Global invocation position (uvec3) |
localId | Local workgroup position (uvec3) |
workgroupId | Workgroup index (uvec3) |
numWorkgroups | Number of workgroups dispatched (uvec3) |
subgroupSize | Size of the subgroup |
Device Limits
WebGPU devices use default minimums unless you request higher limits. This is critical for large buffers.
// Three.js: pass requiredLimits to the renderer
const renderer = new THREE.WebGPURenderer({
requiredLimits: {
maxBufferSize: 1024 * 1024 * 1024, // 1 GiB
maxStorageBufferBindingSize: 1024 * 1024 * 512, // 512 MiB
},
});
await renderer.init();| Limit | Default | Common Need |
|---|---|---|
maxBufferSize | 256 MiB | Large vertex/storage buffers |
maxStorageBufferBindingSize | 128 MiB | Large compute buffers |
maxStorageBuffersPerShaderStage | 8 | Many storage buffers |
Check before requesting:
const adapter = await navigator.gpu?.requestAdapter();
if (adapter.limits.maxBufferSize >= desiredSize) {
// Safe to request
}See docs/limits-and-features.md for full details.
Version Notes
r178+:
PI2is deprecated → useTWO_PItransformedNormalView→ usenormalViewtransformedNormalWorld→ usenormalWorld
r171+:
- Recommended minimum version for stable TSL
- Requires separate
three/webgpuimport map entry
Resources
/**
* Compute Shader Template
*
* A template for GPU compute shaders with:
* - Storage buffer setup
* - Initialize and update shaders
* - Visualization with instanced mesh
*
* Usage:
* 1. Modify PARTICLE_COUNT and buffer types
* 2. Implement your initialization logic
* 3. Implement your update logic
* 4. Customize visualization
*/
import * as THREE from 'three/webgpu';
import {
Fn,
If,
Loop,
float,
int,
vec2,
vec3,
vec4,
color,
uniform,
instancedArray,
instanceIndex,
hash,
time,
deltaTime,
select, // Use for conditional value selection
max,
clamp
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ============================================
// CONFIGURATION
// ============================================
const PARTICLE_COUNT = 50000;
// ============================================
// STORAGE BUFFERS
// ============================================
// Define your storage buffers here
// Available types: 'float', 'vec2', 'vec3', 'vec4', 'int', 'uint'
const positions = instancedArray(PARTICLE_COUNT, 'vec3');
const velocities = instancedArray(PARTICLE_COUNT, 'vec3');
// Add more buffers as needed:
// const colors = instancedArray(PARTICLE_COUNT, 'vec3');
// const lifetimes = instancedArray(PARTICLE_COUNT, 'float');
// const states = instancedArray(PARTICLE_COUNT, 'uint');
// ============================================
// UNIFORMS
// ============================================
const dt = uniform(0);
// Add your uniforms here:
// const gravity = uniform(-9.8);
// const attractorPosition = uniform(new THREE.Vector3());
// const forceStrength = uniform(1.0);
// ============================================
// COMPUTE SHADERS
// ============================================
/**
* ⚠️ CRITICAL TSL GOTCHA - READ THIS FIRST!
*
* TSL intercepts PROPERTY ASSIGNMENTS on nodes, but NOT JS variable reassignment.
*
* // ✅ WORKS - Property assignment on vec3 node
* const result = vec3(position);
* If(result.y.greaterThan(limit), () => {
* result.y = limit; // TSL intercepts property setters!
* });
*
* // ❌ WRONG - JS variable reassignment (scalars have no .x/.y properties)
* let value = buffer.element(index).toFloat();
* If(condition, () => {
* value = value.add(1.0); // JS reassignment - TSL can't see this!
* });
* buffer.element(index).assign(value); // Uses ORIGINAL node!
*
* Solutions for scalars:
*
* // ✅ Use select() for conditional values
* const newValue = select(condition, valueIfTrue, valueIfFalse);
*
* // ✅ Use .toVar() for mutable scalars
* const value = buffer.element(index).toVar();
* If(condition, () => {
* value.assign(value.add(1.0)); // Works with .toVar()!
* });
*
* // ✅ Use direct .assign() on buffer elements
* If(condition, () => {
* element.assign(element.add(1.0));
* });
*/
/**
* Initialize particles
* Called once at startup
*/
const computeInit = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// ========================================
// IMPLEMENT YOUR INITIALIZATION HERE
// ========================================
// Example: Random positions in a cube
position.x.assign(hash(instanceIndex).sub(0.5).mul(10));
position.y.assign(hash(instanceIndex.add(1)).sub(0.5).mul(10));
position.z.assign(hash(instanceIndex.add(2)).sub(0.5).mul(10));
// Example: Zero velocity
velocity.assign(vec3(0));
})().compute(PARTICLE_COUNT);
/**
* Update particles each frame
* Called every frame in animation loop
*/
const computeUpdate = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// ========================================
// IMPLEMENT YOUR UPDATE LOGIC HERE
// ========================================
// Example: Simple gravity
velocity.y.addAssign(float(-9.8).mul(dt));
// Example: Update position
position.addAssign(velocity.mul(dt));
// Example: Ground bounce
If(position.y.lessThan(0), () => {
position.y.assign(0);
velocity.y.assign(velocity.y.negate().mul(0.8));
});
// Example: Boundary wrapping
// If(position.x.abs().greaterThan(5), () => {
// position.x.assign(position.x.negate());
// });
})().compute(PARTICLE_COUNT);
/**
* Optional: Additional compute pass (e.g., for interactions)
*/
const computeInteraction = Fn(() => {
const position = positions.element(instanceIndex);
const velocity = velocities.element(instanceIndex);
// ========================================
// IMPLEMENT INTERACTION LOGIC HERE
// ========================================
// Example: Attract to point
// const toTarget = attractorPosition.sub(position);
// const dist = toTarget.length();
// const force = toTarget.normalize().mul(forceStrength).div(dist.add(0.1));
// velocity.addAssign(force.mul(dt));
})().compute(PARTICLE_COUNT);
// ============================================
// VISUALIZATION
// ============================================
function createVisualization(scene) {
// Choose visualization type:
// - Points (fastest, simplest)
// - Instanced Mesh (more control)
// Option 1: Points
// return createPointsVisualization(scene);
// Option 2: Instanced Mesh
return createInstancedVisualization(scene);
}
function createPointsVisualization(scene) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
'position',
new THREE.Float32BufferAttribute(new Float32Array(PARTICLE_COUNT * 3), 3)
);
const material = new THREE.PointsNodeMaterial();
// Position from compute buffer
material.positionNode = positions.element(instanceIndex);
// ========================================
// CUSTOMIZE POINT APPEARANCE HERE
// ========================================
material.sizeNode = float(3.0);
material.colorNode = Fn(() => {
// Example: Color based on velocity
const velocity = velocities.element(instanceIndex);
const speed = velocity.length();
return mix(color(0x0066ff), color(0xff6600), speed.div(5).saturate());
})();
const points = new THREE.Points(geometry, material);
scene.add(points);
return points;
}
function createInstancedVisualization(scene) {
// Geometry for each instance
const geometry = new THREE.SphereGeometry(0.05, 8, 8);
// Or use simpler geometry for better performance:
// const geometry = new THREE.IcosahedronGeometry(0.05, 0);
const material = new THREE.MeshStandardNodeMaterial();
// Position from compute buffer
material.positionNode = positions.element(instanceIndex);
// ========================================
// CUSTOMIZE MESH APPEARANCE HERE
// ========================================
material.colorNode = Fn(() => {
// Example: Color based on position
const position = positions.element(instanceIndex);
return color(0x0088ff).add(position.mul(0.05));
})();
material.roughnessNode = float(0.5);
material.metalnessNode = float(0.2);
const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT);
scene.add(mesh);
return mesh;
}
// ============================================
// MAIN SETUP
// ============================================
let camera, scene, renderer, controls;
let visualization;
async function init() {
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
// Camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 5, 15);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);
// Optional: Ground plane
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardNodeMaterial({ color: 0x333333 })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
await renderer.init();
// Initialize particles (renderer already initialized above)
renderer.compute(computeInit);
// Create visualization
visualization = createVisualization(scene);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 2, 0);
// Events
window.addEventListener('resize', onWindowResize);
// Start
renderer.setAnimationLoop(animate);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
const clock = new THREE.Clock();
function animate() {
// Update delta time uniform
dt.value = Math.min(clock.getDelta(), 0.1);
// Run compute shaders
renderer.compute(computeUpdate);
// renderer.compute(computeInteraction);
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
}
init().catch(console.error);
// Export for external control
export {
positions,
velocities,
dt,
computeInit,
computeUpdate,
computeInteraction
};
/**
* WebGPU Three.js Project Template
*
* A complete starter template with:
* - WebGPU renderer setup
* - TSL material example
* - Post-processing ready
* - Responsive design
* - Animation loop
*
* Usage:
* 1. Copy this file to your project
* 2. Install Three.js: npm install three
* 3. Replace placeholder content with your scene
*/
import * as THREE from 'three/webgpu';
import {
// Types
float,
vec2,
vec3,
vec4,
color,
uniform,
// Geometry
positionLocal,
positionWorld,
normalLocal,
normalWorld,
uv,
// Camera
cameraPosition,
// Time
time,
deltaTime,
// Math
mix,
smoothstep,
clamp,
sin,
cos,
// Texture
texture,
// Functions
Fn,
If,
Loop,
// Post-processing
pass
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ============================================
// CONFIGURATION
// ============================================
const CONFIG = {
// Renderer
antialias: true,
pixelRatio: Math.min(window.devicePixelRatio, 2),
// Camera
fov: 60,
near: 0.1,
far: 1000,
position: new THREE.Vector3(0, 2, 5),
// Scene
backgroundColor: 0x111111,
// Controls
enableDamping: true,
dampingFactor: 0.05
};
// ============================================
// GLOBALS
// ============================================
let camera, scene, renderer, controls;
let clock;
// Add your uniforms here
const uniforms = {
// Example: myColor: uniform(new THREE.Color(0xff0000))
};
// ============================================
// INITIALIZATION
// ============================================
async function init() {
// Clock
clock = new THREE.Clock();
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(CONFIG.backgroundColor);
// Camera
camera = new THREE.PerspectiveCamera(
CONFIG.fov,
window.innerWidth / window.innerHeight,
CONFIG.near,
CONFIG.far
);
camera.position.copy(CONFIG.position);
// Renderer
renderer = new THREE.WebGPURenderer({ antialias: CONFIG.antialias });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(CONFIG.pixelRatio);
document.body.appendChild(renderer.domElement);
// Initialize WebGPU
await renderer.init();
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = CONFIG.enableDamping;
controls.dampingFactor = CONFIG.dampingFactor;
// Setup scene content
setupLights();
setupScene();
// Optional: Setup post-processing
// setupPostProcessing();
// Events
window.addEventListener('resize', onWindowResize);
// Start animation loop
renderer.setAnimationLoop(animate);
}
// ============================================
// SCENE SETUP
// ============================================
function setupLights() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);
// Add more lights as needed
}
function setupScene() {
// ========================================
// ADD YOUR SCENE CONTENT HERE
// ========================================
// Example: Create a mesh with TSL material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = createExampleMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Example: Add a floor
const floorGeometry = new THREE.PlaneGeometry(10, 10);
const floorMaterial = new THREE.MeshStandardNodeMaterial({
color: 0x333333
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.5;
scene.add(floor);
}
function createExampleMaterial() {
const material = new THREE.MeshStandardNodeMaterial();
// ========================================
// CUSTOMIZE YOUR MATERIAL HERE
// ========================================
// Example: Animated color
material.colorNode = Fn(() => {
const t = time.mul(0.5).sin().mul(0.5).add(0.5);
return mix(color(0x0066ff), color(0xff6600), t);
})();
// Example: PBR properties
material.roughnessNode = float(0.5);
material.metalnessNode = float(0.0);
// Example: Simple fresnel rim
material.emissiveNode = Fn(() => {
const viewDir = cameraPosition.sub(positionWorld).normalize();
const fresnel = float(1.0).sub(normalWorld.dot(viewDir).saturate()).pow(3.0);
return color(0x00ffff).mul(fresnel).mul(0.5);
})();
return material;
}
// ============================================
// POST-PROCESSING (Optional)
// ============================================
let postProcessing;
function setupPostProcessing() {
// Uncomment and customize as needed
// postProcessing = new THREE.RenderPipeline(renderer);
// const scenePass = pass(scene, camera);
// const sceneColor = scenePass.getTextureNode('output');
//
// // Add effects here
// postProcessing.outputNode = sceneColor;
}
// ============================================
// ANIMATION LOOP
// ============================================
function animate() {
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
// ========================================
// UPDATE YOUR SCENE HERE
// ========================================
// Example: Rotate mesh
const mesh = scene.children.find((child) => child.type === 'Mesh');
if (mesh) {
mesh.rotation.y += delta * 0.5;
}
// Update controls
controls.update();
// Render
if (postProcessing) {
postProcessing.render();
} else {
renderer.render(scene, camera);
}
}
// ============================================
// EVENT HANDLERS
// ============================================
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// ============================================
// START
// ============================================
init().catch(console.error);
// Export for external access if needed
export { scene, camera, renderer, uniforms };
Related skills
FAQ
Why does variable reassignment fail in Three.js TSL compute shaders?
webgpu-threejs-tsl explains TSL intercepts property assignments on nodes but cannot see JavaScript variable reassignment such as `variable = variable.add(1)`, so those updates never reach the GPU graph.
Which TSL update patterns work for compute buffers?
webgpu-threejs-tsl supports `node.y = value`, `node.x.assign(value)`, and `buffer.element(i).assign(v)` because TSL hooks property setters and explicit .assign() method calls.
Is Webgpu Threejs Tsl safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.