Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
# Compute Shaders in React Three Fiber
|
||||
|
||||
Compute shaders run arbitrary GPU programs outside the render pipeline. They are
|
||||
used for particle simulation, physics, image processing, and any massively parallel
|
||||
workload. **Compute shaders require the WebGPU backend** -- they do not fall back
|
||||
to WebGL.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```tsx
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
// Canvas MUST use WebGPURenderer for compute
|
||||
<Canvas gl={(canvas) => {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return renderer;
|
||||
}}>
|
||||
```
|
||||
|
||||
## Core API
|
||||
|
||||
### Storage Buffers
|
||||
|
||||
Storage buffers hold GPU-accessible data arrays:
|
||||
|
||||
```ts
|
||||
import { instancedArray, storage, instanceIndex } from 'three/tsl';
|
||||
|
||||
// instancedArray(count, type) -- creates a typed storage buffer
|
||||
const positions = instancedArray(1024, 'vec3'); // 1024 vec3 values
|
||||
const velocities = instancedArray(1024, 'vec3');
|
||||
const lifetimes = instancedArray(1024, 'float');
|
||||
const colors = instancedArray(1024, 'vec4');
|
||||
```
|
||||
|
||||
### Element Access
|
||||
|
||||
```ts
|
||||
// Read/write individual elements by index
|
||||
const pos = positions.element(instanceIndex); // current instance
|
||||
const vel = velocities.element(instanceIndex);
|
||||
|
||||
// Read another element (e.g., neighbor)
|
||||
const neighbor = positions.element(instanceIndex.add(1));
|
||||
```
|
||||
|
||||
### Compute Functions
|
||||
|
||||
```ts
|
||||
import { Fn, compute } from 'three/tsl';
|
||||
|
||||
// Define the kernel
|
||||
const updateKernel = Fn(() => {
|
||||
const pos = positions.element(instanceIndex);
|
||||
const vel = velocities.element(instanceIndex);
|
||||
|
||||
// Update position by velocity * dt
|
||||
pos.addAssign(vel.mul(timerDelta()));
|
||||
|
||||
// Bounce off floor
|
||||
If(pos.y.lessThan(0), () => {
|
||||
pos.y.assign(0);
|
||||
vel.y.assign(vel.y.negate().mul(0.8));
|
||||
});
|
||||
});
|
||||
|
||||
// Create compute node with workgroup count
|
||||
const computeNode = updateKernel().compute(1024);
|
||||
```
|
||||
|
||||
### Dispatching from useFrame
|
||||
|
||||
```tsx
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
function ParticleSystem() {
|
||||
const computeRef = useRef(computeNode);
|
||||
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(computeRef.current);
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Particle System Example
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame, Canvas } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
import { PointsNodeMaterial } from 'three/webgpu';
|
||||
import {
|
||||
instancedArray, instanceIndex, Fn, compute,
|
||||
float, vec3, timerDelta, timerLocal, hash,
|
||||
storage, If, sin, cos, color, mix,
|
||||
} from 'three/tsl';
|
||||
|
||||
const PARTICLE_COUNT = 10_000;
|
||||
|
||||
function GPUParticles() {
|
||||
const { computeNode, material, geometry } = useMemo(() => {
|
||||
// Storage buffers
|
||||
const positions = instancedArray(PARTICLE_COUNT, 'vec3');
|
||||
const velocities = instancedArray(PARTICLE_COUNT, 'vec3');
|
||||
const ages = instancedArray(PARTICLE_COUNT, 'float');
|
||||
|
||||
// Init kernel -- runs once to seed particles
|
||||
const initKernel = Fn(() => {
|
||||
const i = float(instanceIndex);
|
||||
const h = hash(i);
|
||||
const angle = h.mul(Math.PI * 2);
|
||||
const radius = hash(i.add(1)).mul(2);
|
||||
|
||||
positions.element(instanceIndex).assign(
|
||||
vec3(cos(angle).mul(radius), float(0), sin(angle).mul(radius))
|
||||
);
|
||||
velocities.element(instanceIndex).assign(
|
||||
vec3(cos(angle).mul(0.5), hash(i.add(2)).mul(3).add(1), sin(angle).mul(0.5))
|
||||
);
|
||||
ages.element(instanceIndex).assign(hash(i.add(3)).mul(5));
|
||||
});
|
||||
|
||||
// Update kernel -- runs each frame
|
||||
const updateKernel = Fn(() => {
|
||||
const pos = positions.element(instanceIndex);
|
||||
const vel = velocities.element(instanceIndex);
|
||||
const age = ages.element(instanceIndex);
|
||||
const dt = timerDelta();
|
||||
|
||||
// Gravity
|
||||
vel.y.subAssign(float(9.8).mul(dt));
|
||||
|
||||
// Move
|
||||
pos.addAssign(vel.mul(dt));
|
||||
|
||||
// Age
|
||||
age.addAssign(dt);
|
||||
|
||||
// Respawn when aged out
|
||||
If(age.greaterThan(5.0), () => {
|
||||
pos.assign(vec3(0, 0, 0));
|
||||
vel.y.assign(hash(float(instanceIndex).add(age)).mul(3).add(2));
|
||||
age.assign(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Compute nodes
|
||||
const initNode = initKernel().compute(PARTICLE_COUNT);
|
||||
const updateNode = updateKernel().compute(PARTICLE_COUNT);
|
||||
|
||||
// Material reads from positions buffer
|
||||
const mat = new PointsNodeMaterial();
|
||||
mat.sizeNode = float(3.0);
|
||||
mat.colorNode = mix(
|
||||
color(0xff6600),
|
||||
color(0xffcc00),
|
||||
ages.element(instanceIndex).div(5).saturate()
|
||||
);
|
||||
|
||||
// Geometry uses storage buffer as position attribute
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position',
|
||||
new THREE.BufferAttribute(new Float32Array(PARTICLE_COUNT * 3), 3)
|
||||
);
|
||||
geo.drawRange.count = PARTICLE_COUNT;
|
||||
|
||||
return {
|
||||
initNode,
|
||||
computeNode: updateNode,
|
||||
material: mat,
|
||||
geometry: geo,
|
||||
positions,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const initialized = useRef(false);
|
||||
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
|
||||
if (!initialized.current) {
|
||||
renderer.computeAsync(initNode);
|
||||
initialized.current = true;
|
||||
}
|
||||
|
||||
renderer.computeAsync(computeNode);
|
||||
});
|
||||
|
||||
return (
|
||||
<points geometry={geometry}>
|
||||
<primitive object={material} attach="material" />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Buffer Types
|
||||
|
||||
| Type String | WGSL Type | Byte Size |
|
||||
|---|---|---|
|
||||
| `'float'` | `f32` | 4 |
|
||||
| `'vec2'` | `vec2f` | 8 |
|
||||
| `'vec3'` | `vec3f` | 12 (padded to 16) |
|
||||
| `'vec4'` | `vec4f` | 16 |
|
||||
| `'int'` | `i32` | 4 |
|
||||
| `'uint'` | `u32` | 4 |
|
||||
| `'mat3'` | `mat3x3f` | 48 |
|
||||
| `'mat4'` | `mat4x4f` | 64 |
|
||||
|
||||
## Reading Back Storage Data
|
||||
|
||||
To read GPU buffer data back to the CPU (for debugging, saving state):
|
||||
|
||||
```ts
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
|
||||
// Read buffer back asynchronously
|
||||
const data = await renderer.getArrayBufferAsync(positions.value);
|
||||
const floats = new Float32Array(data);
|
||||
```
|
||||
|
||||
**Warning**: GPU readback is slow. Avoid doing it every frame. Use only for
|
||||
debugging, snapshots, or saving state.
|
||||
|
||||
## Workgroup Size
|
||||
|
||||
The `.compute(count)` argument defines the total dispatch size. Three.js
|
||||
handles workgroup subdivision internally (typically 64 invocations per workgroup).
|
||||
|
||||
```ts
|
||||
// 1024 total invocations
|
||||
const computeNode = myKernel().compute(1024);
|
||||
|
||||
// For 2D dispatches (e.g., image processing)
|
||||
const computeNode2D = myKernel().compute(width * height);
|
||||
```
|
||||
|
||||
## Atomic Operations
|
||||
|
||||
For concurrent writes (histograms, counters):
|
||||
|
||||
```ts
|
||||
import { atomicAdd, atomicStore, atomicLoad } from 'three/tsl';
|
||||
|
||||
const counter = instancedArray(1, 'uint');
|
||||
|
||||
const countKernel = Fn(() => {
|
||||
If(someCondition, () => {
|
||||
atomicAdd(counter.element(uint(0)), uint(1));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Double Buffering
|
||||
|
||||
For simulations where reading and writing the same buffer causes race conditions:
|
||||
|
||||
```ts
|
||||
const posA = instancedArray(count, 'vec3');
|
||||
const posB = instancedArray(count, 'vec3');
|
||||
|
||||
// Frame N: read A, write B
|
||||
const stepA = Fn(() => {
|
||||
const src = posA.element(instanceIndex);
|
||||
const dst = posB.element(instanceIndex);
|
||||
dst.assign(src.add(vel.mul(timerDelta())));
|
||||
}).compute(count);
|
||||
|
||||
// Frame N+1: read B, write A
|
||||
const stepB = Fn(() => {
|
||||
const src = posB.element(instanceIndex);
|
||||
const dst = posA.element(instanceIndex);
|
||||
dst.assign(src.add(vel.mul(timerDelta())));
|
||||
}).compute(count);
|
||||
|
||||
// Toggle each frame
|
||||
let frame = 0;
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(frame % 2 === 0 ? stepA : stepB);
|
||||
frame++;
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-physics-core
|
||||
|
||||
Compute shaders can implement physics simulations that integrate with the
|
||||
`@suaveplan/r3f-physics-core` abstraction layer. The physics core defines
|
||||
an interface for simulation steps:
|
||||
|
||||
```ts
|
||||
// Implement the physics step with a compute shader
|
||||
interface ComputePhysicsProvider {
|
||||
init(count: number): void;
|
||||
step(renderer: WebGPURenderer, dt: number): void;
|
||||
getPositions(): StorageBuffer;
|
||||
getVelocities(): StorageBuffer;
|
||||
}
|
||||
```
|
||||
|
||||
Use the `useFrame` priority parameter to ensure compute dispatch runs
|
||||
before the render pass:
|
||||
|
||||
```tsx
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(physicsComputeNode);
|
||||
}, -1); // Priority -1 runs before default (0) render
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Minimize readbacks**: `getArrayBufferAsync` stalls the pipeline.
|
||||
2. **Batch dispatches**: Multiple compute calls per frame are fine but each
|
||||
adds dispatch overhead.
|
||||
3. **Storage alignment**: vec3 is padded to 16 bytes in WGSL. Use vec4 if
|
||||
you need the extra channel anyway.
|
||||
4. **Workgroup locality**: Access patterns that read neighboring elements
|
||||
benefit from workgroup shared memory (not yet exposed in TSL).
|
||||
5. **Async compute**: `computeAsync` returns a Promise. In R3F useFrame,
|
||||
you typically fire-and-forget (do not await).
|
||||
@@ -0,0 +1,398 @@
|
||||
# TSL Core Concepts
|
||||
|
||||
Three Shading Language (TSL) is a graph-based shading system where you compose
|
||||
JavaScript objects ("nodes") into shader programs. The Three.js compiler then emits
|
||||
WGSL (WebGPU) or GLSL (WebGL) depending on the active backend.
|
||||
|
||||
## Node Graph Model
|
||||
|
||||
Every TSL expression produces a **node**. Nodes are connected by method calls to
|
||||
form an acyclic graph. The compiler walks the graph to generate shader source.
|
||||
|
||||
```ts
|
||||
import { float, sin, timerLocal, color, mix } from 'three/tsl';
|
||||
|
||||
// Each call returns a node; chaining builds the graph
|
||||
const time = timerLocal();
|
||||
const wave = sin(time.mul(2.0)).mul(0.5).add(0.5);
|
||||
const result = mix(color(0x000000), color(0x3b82f6), wave);
|
||||
```
|
||||
|
||||
This graph compiles to roughly:
|
||||
|
||||
```wgsl
|
||||
let wave = sin(time * 2.0) * 0.5 + 0.5;
|
||||
let result = mix(vec3(0.0), vec3(0.231, 0.510, 0.965), wave);
|
||||
```
|
||||
|
||||
## Primitive Types
|
||||
|
||||
### Scalars
|
||||
|
||||
```ts
|
||||
float(1.0) // f32
|
||||
int(5) // i32
|
||||
uint(10) // u32
|
||||
bool(true) // bool
|
||||
```
|
||||
|
||||
### Vectors
|
||||
|
||||
```ts
|
||||
vec2(1.0, 2.0) // vec2f
|
||||
vec3(1.0, 0.0, 0.0) // vec3f
|
||||
vec4(1.0, 0.0, 0.0, 1.0) // vec4f
|
||||
color(0xff6600) // vec3f from hex
|
||||
color(new THREE.Color('red')) // vec3f from Color
|
||||
```
|
||||
|
||||
### Matrices
|
||||
|
||||
```ts
|
||||
mat2(1, 0, 0, 1) // 2x2 identity
|
||||
mat3(...) // 3x3
|
||||
mat4(...) // 4x4
|
||||
```
|
||||
|
||||
### Type Coercion
|
||||
|
||||
TSL auto-promotes scalars in arithmetic:
|
||||
|
||||
```ts
|
||||
vec3(1, 0, 0).mul(0.5) // float(0.5) auto-wrapped
|
||||
float(1).add(2) // int 2 auto-wrapped to float
|
||||
```
|
||||
|
||||
## Swizzling
|
||||
|
||||
All vector nodes support GLSL-style swizzle access:
|
||||
|
||||
```ts
|
||||
const v = vec4(1, 2, 3, 4);
|
||||
v.x // float -- component access
|
||||
v.xy // vec2
|
||||
v.xyz // vec3
|
||||
v.zyx // vec3 -- reversed
|
||||
v.rrr // vec3 -- RGBA aliases work too
|
||||
v.stpq // vec4 -- texture coordinate aliases
|
||||
```
|
||||
|
||||
Assignment swizzles are not supported in TSL. To set individual components,
|
||||
construct a new vector:
|
||||
|
||||
```ts
|
||||
// Instead of v.x = 5, do:
|
||||
const modified = vec4(float(5), v.y, v.z, v.w);
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
TSL uses method calls rather than JavaScript operators because JS does not support
|
||||
operator overloading.
|
||||
|
||||
### Arithmetic
|
||||
|
||||
```ts
|
||||
a.add(b) // +
|
||||
a.sub(b) // -
|
||||
a.mul(b) // *
|
||||
a.div(b) // /
|
||||
a.mod(b) // %
|
||||
a.negate() // unary -
|
||||
```
|
||||
|
||||
### Comparison
|
||||
|
||||
```ts
|
||||
a.equal(b) // ==
|
||||
a.notEqual(b) // !=
|
||||
a.lessThan(b) // <
|
||||
a.greaterThan(b) // >
|
||||
a.lessThanEqual(b) // <=
|
||||
a.greaterThanEqual(b) // >=
|
||||
```
|
||||
|
||||
### Logical
|
||||
|
||||
```ts
|
||||
a.and(b) // &&
|
||||
a.or(b) // ||
|
||||
a.not() // !
|
||||
```
|
||||
|
||||
### Assignment (Compute Only)
|
||||
|
||||
In compute shaders, storage elements support in-place assignment:
|
||||
|
||||
```ts
|
||||
element.assign(newValue) // =
|
||||
element.addAssign(delta) // +=
|
||||
element.subAssign(delta) // -=
|
||||
element.mulAssign(factor) // *=
|
||||
```
|
||||
|
||||
## Uniforms
|
||||
|
||||
Uniforms are the bridge between JavaScript (React state, useFrame) and GPU code.
|
||||
|
||||
### Creating Uniforms
|
||||
|
||||
```ts
|
||||
import { uniform } from 'three/tsl';
|
||||
|
||||
const uFloat = uniform(0.0); // float uniform
|
||||
const uVec3 = uniform(new THREE.Vector3()); // vec3 uniform
|
||||
const uColor = uniform(new THREE.Color()); // vec3 uniform
|
||||
const uMat4 = uniform(new THREE.Matrix4()); // mat4 uniform
|
||||
const uInt = uniform(int(0)); // explicit int uniform
|
||||
```
|
||||
|
||||
### Updating Uniforms in R3F
|
||||
|
||||
```tsx
|
||||
function AnimatedMesh() {
|
||||
const uProgress = useMemo(() => uniform(0.0), []);
|
||||
const uMouse = useMemo(() => uniform(new THREE.Vector2()), []);
|
||||
|
||||
useFrame((state) => {
|
||||
uProgress.value = state.clock.elapsedTime;
|
||||
uMouse.value.set(
|
||||
state.pointer.x * 0.5 + 0.5,
|
||||
state.pointer.y * 0.5 + 0.5
|
||||
);
|
||||
});
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = mix(color(0x000000), color(0xffffff), uProgress.fract());
|
||||
return mat;
|
||||
}, [uProgress]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Uniform Value Types
|
||||
|
||||
| JS Value | TSL Type | Notes |
|
||||
|---|---|---|
|
||||
| `number` | `float` | Default for bare numbers |
|
||||
| `THREE.Vector2` | `vec2` | |
|
||||
| `THREE.Vector3` | `vec3` | |
|
||||
| `THREE.Vector4` | `vec4` | |
|
||||
| `THREE.Color` | `vec3` | RGB, no alpha |
|
||||
| `THREE.Matrix3` | `mat3` | |
|
||||
| `THREE.Matrix4` | `mat4` | |
|
||||
| `int(n)` | `int` | Explicit int type |
|
||||
| `uint(n)` | `uint` | Explicit uint type |
|
||||
|
||||
## Time and Animation
|
||||
|
||||
TSL has built-in time nodes that auto-increment each frame:
|
||||
|
||||
```ts
|
||||
timerLocal() // Seconds since this material was first rendered
|
||||
timerGlobal() // Seconds since renderer was created
|
||||
timerDelta() // Seconds elapsed since previous frame
|
||||
frameId // Integer frame counter (uint)
|
||||
```
|
||||
|
||||
### R3F Integration -- timerLocal vs useFrame
|
||||
|
||||
For pure-GPU animation, `timerLocal()` is simpler and avoids JS-to-GPU
|
||||
round trips:
|
||||
|
||||
```ts
|
||||
// Pure GPU -- no useFrame needed
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = color(0x3b82f6).mul(sin(timerLocal().mul(3.0)).mul(0.5).add(0.5));
|
||||
```
|
||||
|
||||
Use `uniform()` + `useFrame()` only when animation depends on React state,
|
||||
pointer position, scroll, or other JS-side data.
|
||||
|
||||
## Attributes and Varyings
|
||||
|
||||
### Built-in Attributes
|
||||
|
||||
```ts
|
||||
positionLocal // Vertex position in object space
|
||||
normalLocal // Vertex normal in object space
|
||||
uv() // UV channel 0
|
||||
uv(1) // UV channel 1
|
||||
vertexColor // Vertex color attribute
|
||||
tangentLocal // Tangent vector
|
||||
```
|
||||
|
||||
### Custom Attributes
|
||||
|
||||
```ts
|
||||
import { attribute } from 'three/tsl';
|
||||
|
||||
// Reads from geometry.setAttribute('aCustom', ...)
|
||||
const custom = attribute('aCustom', 'vec3');
|
||||
```
|
||||
|
||||
### Varyings
|
||||
|
||||
Varyings pass data from vertex to fragment shader:
|
||||
|
||||
```ts
|
||||
import { varying, varyingProperty } from 'three/tsl';
|
||||
|
||||
// Declare a varying
|
||||
const vWorldPos = varying(positionWorld, 'v_worldPos');
|
||||
|
||||
// Or use varyingProperty for named access
|
||||
varyingProperty('vec3', 'v_myData').assign(someNode);
|
||||
```
|
||||
|
||||
## Control Flow
|
||||
|
||||
### Branching (If/Else)
|
||||
|
||||
```ts
|
||||
import { If } from 'three/tsl';
|
||||
|
||||
If(condition, () => {
|
||||
// true branch
|
||||
}).ElseIf(otherCondition, () => {
|
||||
// else-if
|
||||
}).Else(() => {
|
||||
// false branch
|
||||
});
|
||||
```
|
||||
|
||||
### Select (Ternary)
|
||||
|
||||
```ts
|
||||
import { select } from 'three/tsl';
|
||||
|
||||
// select(condition, ifTrue, ifFalse)
|
||||
const result = select(uv().x.greaterThan(0.5), color(0xff0000), color(0x0000ff));
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
```ts
|
||||
import { Loop } from 'three/tsl';
|
||||
|
||||
// Simple counted loop
|
||||
Loop(10, ({ i }) => {
|
||||
// i is an int node, 0..9
|
||||
});
|
||||
|
||||
// Explicit range
|
||||
Loop({ start: int(0), end: int(100), type: 'int' }, ({ i }) => {
|
||||
// body
|
||||
});
|
||||
|
||||
// Break out early
|
||||
Loop(100, ({ i, Break }) => {
|
||||
If(someCondition, () => {
|
||||
Break();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Functions (Fn)
|
||||
|
||||
`Fn` creates reusable TSL functions. They compile to WGSL/GLSL functions.
|
||||
|
||||
```ts
|
||||
import { Fn } from 'three/tsl';
|
||||
|
||||
const fbm = Fn(([pos, octaves]) => {
|
||||
const value = float(0.0).toVar();
|
||||
const amplitude = float(0.5).toVar();
|
||||
const frequency = float(1.0).toVar();
|
||||
|
||||
Loop(octaves, () => {
|
||||
value.addAssign(mx_noise_float(pos.mul(frequency)).mul(amplitude));
|
||||
amplitude.mulAssign(0.5);
|
||||
frequency.mulAssign(2.0);
|
||||
});
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
// Use in material
|
||||
mat.colorNode = vec3(fbm(positionWorld, int(6)));
|
||||
```
|
||||
|
||||
### Important: `.toVar()`
|
||||
|
||||
Inside `Fn`, mutable local variables require `.toVar()`:
|
||||
|
||||
```ts
|
||||
const myValue = float(0.0).toVar(); // mutable local
|
||||
myValue.addAssign(1.0); // modify in place
|
||||
```
|
||||
|
||||
Without `.toVar()`, the node is an immutable expression and `addAssign` will fail.
|
||||
|
||||
## Math Functions
|
||||
|
||||
All standard math is available as both standalone functions and methods:
|
||||
|
||||
```ts
|
||||
// Standalone
|
||||
sin(x), cos(x), tan(x)
|
||||
asin(x), acos(x), atan(x), atan2(y, x)
|
||||
pow(x, y), exp(x), log(x), sqrt(x)
|
||||
abs(x), sign(x), floor(x), ceil(x), fract(x), round(x)
|
||||
min(a, b), max(a, b), clamp(x, lo, hi)
|
||||
mix(a, b, t), step(edge, x), smoothstep(lo, hi, x)
|
||||
length(v), distance(a, b), dot(a, b), cross(a, b), normalize(v)
|
||||
reflect(v, n), refract(v, n, eta)
|
||||
|
||||
// As methods
|
||||
x.sin(), x.cos(), x.abs(), x.floor()
|
||||
v.length(), v.normalize(), v.dot(other)
|
||||
a.mix(b, t), x.clamp(0, 1), x.smoothstep(0, 1)
|
||||
x.saturate() // clamp(x, 0, 1)
|
||||
```
|
||||
|
||||
## Textures
|
||||
|
||||
```ts
|
||||
import { texture } from 'three/tsl';
|
||||
|
||||
const tex = texture(myTexture); // Sample at default UV
|
||||
const texCustom = texture(myTexture, customUV); // Custom UV
|
||||
tex.uv(customUV) // Alternative UV syntax
|
||||
tex.level(float(2)) // Specific mip level
|
||||
tex.bias(float(-1)) // LOD bias
|
||||
|
||||
// Texture properties
|
||||
tex.r, tex.g, tex.b, tex.a // Channel access
|
||||
```
|
||||
|
||||
### R3F Texture Loading
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { texture } from 'three/tsl';
|
||||
|
||||
function TexturedMesh() {
|
||||
const map = useTexture('/textures/brick.jpg');
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = texture(map);
|
||||
return mat;
|
||||
}, [map]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,422 @@
|
||||
# GPU Device Loss and Recovery
|
||||
|
||||
GPU device loss occurs when the browser's WebGPU device becomes unavailable.
|
||||
Common causes: driver crash, GPU hang, system sleep/wake, GPU reset by OS,
|
||||
resource exhaustion, or the user switching GPUs. Your application must handle
|
||||
this gracefully.
|
||||
|
||||
## How Device Loss Manifests
|
||||
|
||||
When the GPU device is lost:
|
||||
|
||||
1. All GPU resources (buffers, textures, pipelines) become invalid.
|
||||
2. Subsequent GPU operations silently fail or throw.
|
||||
3. The canvas may go black or freeze on the last rendered frame.
|
||||
4. `GPUDevice.lost` promise resolves with a `GPUDeviceLostInfo` object.
|
||||
|
||||
## Detection
|
||||
|
||||
### WebGPU Native API
|
||||
|
||||
```ts
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
const device = await adapter.requestDevice();
|
||||
|
||||
device.lost.then((info) => {
|
||||
console.error(`GPU device lost: ${info.reason}`, info.message);
|
||||
if (info.reason === 'destroyed') {
|
||||
// Intentional -- app called device.destroy()
|
||||
} else {
|
||||
// Unexpected loss -- attempt recovery
|
||||
recoverDevice();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Three.js WebGPURenderer
|
||||
|
||||
Three.js wraps the device internally. You can detect loss by listening on
|
||||
the renderer's underlying device:
|
||||
|
||||
```ts
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
async function setupRenderer(canvas: HTMLCanvasElement) {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
|
||||
// Access the underlying GPUDevice
|
||||
const backend = renderer.backend;
|
||||
const device = backend.device;
|
||||
|
||||
device.lost.then((info) => {
|
||||
console.error('WebGPU device lost:', info.reason, info.message);
|
||||
handleDeviceLoss(renderer);
|
||||
});
|
||||
|
||||
return renderer;
|
||||
}
|
||||
```
|
||||
|
||||
## React Error Boundary for Device Loss
|
||||
|
||||
Wrap your WebGPU canvas in an error boundary that catches device-loss errors
|
||||
and offers recovery.
|
||||
|
||||
### DeviceLossErrorBoundary Component
|
||||
|
||||
```tsx
|
||||
import { Component, type ReactNode } from 'react';
|
||||
|
||||
interface DeviceLossState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
recovering: boolean;
|
||||
}
|
||||
|
||||
interface DeviceLossProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onDeviceLost?: (error: Error) => void;
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
export class DeviceLossErrorBoundary extends Component<
|
||||
DeviceLossProps,
|
||||
DeviceLossState
|
||||
> {
|
||||
private retryCount = 0;
|
||||
|
||||
constructor(props: DeviceLossProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, recovering: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<DeviceLossState> {
|
||||
// Check if the error is GPU-related
|
||||
const isGPUError =
|
||||
error.message.includes('device') ||
|
||||
error.message.includes('GPU') ||
|
||||
error.message.includes('context lost') ||
|
||||
error.message.includes('destroyed') ||
|
||||
error.name === 'GPUDeviceLostError';
|
||||
|
||||
if (isGPUError) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
// Re-throw non-GPU errors
|
||||
throw error;
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
this.props.onDeviceLost?.(error);
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
const maxRetries = this.props.maxRetries ?? 3;
|
||||
|
||||
if (this.retryCount >= maxRetries) {
|
||||
return; // Give up after max retries
|
||||
}
|
||||
|
||||
this.retryCount++;
|
||||
this.setState({ hasError: false, error: null, recovering: true });
|
||||
|
||||
// Small delay to let GPU recover
|
||||
setTimeout(() => {
|
||||
this.setState({ recovering: false });
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center' }}>
|
||||
<h3>GPU Connection Lost</h3>
|
||||
<p>The graphics device became unavailable.</p>
|
||||
<button onClick={this.handleRetry}>
|
||||
Retry ({this.retryCount}/{this.props.maxRetries ?? 3})
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.recovering) {
|
||||
return <div style={{ padding: 20 }}>Reconnecting to GPU...</div>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in R3F
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
return (
|
||||
<DeviceLossErrorBoundary
|
||||
maxRetries={3}
|
||||
onDeviceLost={(err) => {
|
||||
analytics.track('gpu_device_lost', { message: err.message });
|
||||
}}
|
||||
fallback={
|
||||
<div>
|
||||
<p>3D rendering is unavailable.</p>
|
||||
<p>Try refreshing the page or switching to a different browser.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
### Strategy 1: Full Remount (Simple, Reliable)
|
||||
|
||||
Unmount and remount the entire Canvas. This destroys all Three.js state
|
||||
and recreates it from scratch.
|
||||
|
||||
```tsx
|
||||
function RecoverableScene() {
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const handleDeviceLoss = useCallback(() => {
|
||||
// Increment key to force full remount
|
||||
setTimeout(() => setKey((k) => k + 1), 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DeviceLossErrorBoundary
|
||||
key={key}
|
||||
onDeviceLost={handleDeviceLoss}
|
||||
>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 2: Renderer Recreation (Preserves Scene Graph)
|
||||
|
||||
More sophisticated -- recreate only the renderer while keeping the
|
||||
scene graph intact. This requires accessing Three.js internals.
|
||||
|
||||
```tsx
|
||||
function useDeviceLossRecovery() {
|
||||
const { gl, set } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
const backend = renderer.backend;
|
||||
|
||||
if (!backend?.device) return;
|
||||
|
||||
backend.device.lost.then(async (info) => {
|
||||
console.warn('GPU device lost, attempting recovery...');
|
||||
|
||||
// Create a new renderer on the same canvas
|
||||
const canvas = renderer.domElement;
|
||||
const newRenderer = new WebGPURenderer({
|
||||
canvas,
|
||||
antialias: true,
|
||||
});
|
||||
await newRenderer.init();
|
||||
|
||||
// Replace the renderer in R3F state
|
||||
set({ gl: newRenderer as unknown as THREE.WebGLRenderer });
|
||||
});
|
||||
}, [gl, set]);
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 3: Graceful Degradation to WebGL
|
||||
|
||||
Fall back to WebGL when WebGPU fails:
|
||||
|
||||
```tsx
|
||||
async function createRendererWithFallback(canvas: HTMLCanvasElement) {
|
||||
// Try WebGPU first
|
||||
if (navigator.gpu) {
|
||||
try {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return { renderer, backend: 'webgpu' as const };
|
||||
} catch {
|
||||
console.warn('WebGPU init failed, falling back to WebGL');
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL fallback
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
return { renderer, backend: 'webgl' as const };
|
||||
}
|
||||
```
|
||||
|
||||
## State Preservation
|
||||
|
||||
When recovering from device loss, you may want to preserve application state
|
||||
(camera position, animation progress, user selections). Store critical state
|
||||
outside the Three.js scene graph.
|
||||
|
||||
```tsx
|
||||
interface SceneState {
|
||||
cameraPosition: [number, number, number];
|
||||
cameraTarget: [number, number, number];
|
||||
animationProgress: number;
|
||||
selectedObjectId: string | null;
|
||||
}
|
||||
|
||||
function useSceneStateBackup() {
|
||||
const stateRef = useRef<SceneState>({
|
||||
cameraPosition: [0, 5, 10],
|
||||
cameraTarget: [0, 0, 0],
|
||||
animationProgress: 0,
|
||||
selectedObjectId: null,
|
||||
});
|
||||
|
||||
// Periodically snapshot camera state
|
||||
useFrame(({ camera }) => {
|
||||
stateRef.current.cameraPosition = camera.position.toArray();
|
||||
});
|
||||
|
||||
return stateRef;
|
||||
}
|
||||
```
|
||||
|
||||
After recovery, restore from the backup:
|
||||
|
||||
```tsx
|
||||
function Scene({ initialState }: { initialState?: SceneState }) {
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera>(null!);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
cameraRef.current.position.fromArray(initialState.cameraPosition);
|
||||
}
|
||||
}, [initialState]);
|
||||
|
||||
return (
|
||||
<perspectiveCamera ref={cameraRef} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Compute Shader Buffer Recovery
|
||||
|
||||
Storage buffers for compute shaders are lost along with the device. If the
|
||||
simulation state is important, periodically snapshot it:
|
||||
|
||||
```tsx
|
||||
function useComputeStateBackup(
|
||||
renderer: WebGPURenderer,
|
||||
storageBuffer: StorageBuffer,
|
||||
intervalMs = 5000
|
||||
) {
|
||||
const backupRef = useRef<Float32Array | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const data = await renderer.getArrayBufferAsync(storageBuffer.value);
|
||||
backupRef.current = new Float32Array(data);
|
||||
} catch {
|
||||
// Device may already be lost
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [renderer, storageBuffer, intervalMs]);
|
||||
|
||||
return backupRef;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Device Loss
|
||||
|
||||
### Simulating Device Loss in Tests
|
||||
|
||||
```ts
|
||||
// In a Vitest test
|
||||
describe('device loss recovery', () => {
|
||||
it('should remount canvas on device loss', async () => {
|
||||
const onDeviceLost = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<DeviceLossErrorBoundary onDeviceLost={onDeviceLost}>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
|
||||
// Simulate device loss by triggering error boundary
|
||||
// In a real scenario, the GPU device.lost promise resolves
|
||||
// For testing, throw a simulated GPU error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Chrome DevTools
|
||||
|
||||
1. Open Chrome DevTools.
|
||||
2. Go to `chrome://gpu` to see GPU status.
|
||||
3. Use `chrome://flags/#force-gpu-rasterization` for GPU stress testing.
|
||||
4. The WebGPU spec provides `device.destroy()` for testing intentional loss.
|
||||
|
||||
### Programmatic Device Destruction
|
||||
|
||||
```ts
|
||||
// Intentionally destroy device for testing recovery
|
||||
function testDeviceLoss(renderer: WebGPURenderer) {
|
||||
const device = renderer.backend.device;
|
||||
device.destroy(); // Triggers device.lost promise
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-performance
|
||||
|
||||
The `@suaveplan/r3f-performance` package monitors frame timing and GPU
|
||||
utilization. On device loss, the performance monitor should:
|
||||
|
||||
1. Detect the anomaly (sudden drop to 0 FPS or error events).
|
||||
2. Emit a `'device-lost'` event via the performance event bus.
|
||||
3. Pause metric collection during recovery.
|
||||
4. Resume collection after the new device is initialized.
|
||||
5. Log a recovery marker in the performance timeline.
|
||||
|
||||
```ts
|
||||
// Performance monitor integration
|
||||
performanceMonitor.on('device-lost', () => {
|
||||
performanceMonitor.pause();
|
||||
});
|
||||
|
||||
performanceMonitor.on('device-recovered', () => {
|
||||
performanceMonitor.resume();
|
||||
performanceMonitor.addMarker('gpu-recovery');
|
||||
});
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When implementing WebGPU in a Suaveplan R3F component:
|
||||
|
||||
- [ ] Wrap the Canvas in `DeviceLossErrorBoundary`
|
||||
- [ ] Provide meaningful fallback UI
|
||||
- [ ] Preserve important scene state outside the GPU
|
||||
- [ ] Test with `device.destroy()` during development
|
||||
- [ ] Log device loss events to analytics
|
||||
- [ ] Consider WebGL fallback for critical features
|
||||
- [ ] Handle async renderer init errors
|
||||
- [ ] Clean up GPU resources in React effect cleanup functions
|
||||
@@ -0,0 +1,370 @@
|
||||
# TSL Node Materials in React Three Fiber
|
||||
|
||||
Node materials are the primary way to use TSL. Each material type extends a
|
||||
standard Three.js material with node slots that accept TSL expressions.
|
||||
|
||||
## Material Types
|
||||
|
||||
| Material | Use Case |
|
||||
|---|---|
|
||||
| `MeshStandardNodeMaterial` | PBR with metalness/roughness workflow |
|
||||
| `MeshPhysicalNodeMaterial` | Extended PBR (clearcoat, transmission, sheen) |
|
||||
| `MeshBasicNodeMaterial` | Unlit, flat shading |
|
||||
| `MeshLambertNodeMaterial` | Non-PBR diffuse lighting |
|
||||
| `MeshPhongNodeMaterial` | Non-PBR specular lighting |
|
||||
| `SpriteNodeMaterial` | Billboard sprites |
|
||||
| `LineBasicNodeMaterial` | Lines |
|
||||
| `PointsNodeMaterial` | Point clouds |
|
||||
| `MeshSSSNodeMaterial` | Subsurface scattering |
|
||||
| `VolumeNodeMaterial` | Volumetric rendering |
|
||||
|
||||
All are imported from `'three/webgpu'`.
|
||||
|
||||
## Node Slots
|
||||
|
||||
### MeshStandardNodeMaterial Slots
|
||||
|
||||
```ts
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
|
||||
// Color & appearance
|
||||
mat.colorNode // vec3 -- base color (replaces .color + .map)
|
||||
mat.opacityNode // float -- alpha (replaces .opacity + .alphaMap)
|
||||
mat.emissiveNode // vec3 -- emissive light
|
||||
mat.metalnessNode // float -- 0 = dielectric, 1 = metal
|
||||
mat.roughnessNode // float -- 0 = mirror, 1 = diffuse
|
||||
mat.aoNode // float -- ambient occlusion
|
||||
|
||||
// Normals
|
||||
mat.normalNode // vec3 -- replaces normalMap
|
||||
|
||||
// Vertex
|
||||
mat.positionNode // vec3 -- vertex displacement (object space)
|
||||
|
||||
// Output (advanced)
|
||||
mat.fragmentNode // vec4 -- replaces entire fragment (no lighting)
|
||||
mat.outputNode // vec4 -- replaces final output (after lighting)
|
||||
mat.shadowNode // vec4 -- custom shadow contribution
|
||||
```
|
||||
|
||||
### MeshPhysicalNodeMaterial Additional Slots
|
||||
|
||||
```ts
|
||||
const mat = new MeshPhysicalNodeMaterial();
|
||||
|
||||
// All MeshStandardNodeMaterial slots plus:
|
||||
mat.clearcoatNode // float -- clearcoat intensity
|
||||
mat.clearcoatRoughnessNode // float -- clearcoat roughness
|
||||
mat.clearcoatNormalNode // vec3 -- clearcoat normal
|
||||
mat.sheenNode // vec3 -- sheen color
|
||||
mat.sheenRoughnessNode // float -- sheen roughness
|
||||
mat.iorNode // float -- index of refraction
|
||||
mat.transmissionNode // float -- transmission (glass)
|
||||
mat.thicknessNode // float -- volume thickness
|
||||
mat.attenuationColorNode // vec3 -- volume color
|
||||
mat.attenuationDistanceNode // float -- volume distance
|
||||
mat.specularColorNode // vec3 -- specular tint
|
||||
mat.specularIntensityNode // float -- specular intensity
|
||||
mat.iridescenceNode // float -- iridescence intensity
|
||||
mat.iridescenceIORNode // float -- iridescence IOR
|
||||
mat.anisotropyNode // float -- anisotropy strength
|
||||
```
|
||||
|
||||
## R3F Declarative Patterns
|
||||
|
||||
### Pattern 1: useMemo + primitive (Recommended)
|
||||
|
||||
The standard approach. Create the material in `useMemo`, attach via `<primitive>`.
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
import { color, sin, timerLocal, mix } from 'three/tsl';
|
||||
|
||||
function GlowingSphere() {
|
||||
const material = useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = color(0x3b82f6);
|
||||
mat.emissiveNode = color(0x60a5fa).mul(sin(time.mul(2)).mul(0.5).add(0.5));
|
||||
mat.roughnessNode = float(0.3);
|
||||
mat.metalnessNode = float(0.8);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: extend() for JSX Syntax
|
||||
|
||||
Register the material so R3F recognizes it as a JSX element. Note that node
|
||||
properties (colorNode, etc.) cannot be set as JSX props -- only standard
|
||||
Three.js properties (color, roughness, etc.) work declaratively.
|
||||
|
||||
```tsx
|
||||
import { extend } from '@react-three/fiber';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
|
||||
extend({ MeshStandardNodeMaterial });
|
||||
|
||||
// JSX usage (standard props only -- no node props)
|
||||
<meshStandardNodeMaterial color="hotpink" roughness={0.5} />
|
||||
```
|
||||
|
||||
This is useful for simple materials where you do not need TSL nodes.
|
||||
|
||||
### Pattern 3: Custom Material Hook
|
||||
|
||||
For materials used across multiple components, create a hook:
|
||||
|
||||
```tsx
|
||||
function useHologramMaterial(baseColor: number = 0x00ff88) {
|
||||
return useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.transparent = true;
|
||||
mat.side = THREE.DoubleSide;
|
||||
|
||||
const scanline = sin(positionLocal.y.mul(50).add(time.mul(5)))
|
||||
.mul(0.5).add(0.5);
|
||||
const fresnel = float(1).sub(normalView.z.abs()).pow(2);
|
||||
|
||||
mat.colorNode = color(baseColor);
|
||||
mat.opacityNode = scanline.mul(0.5).add(fresnel.mul(0.5));
|
||||
mat.emissiveNode = color(baseColor).mul(fresnel);
|
||||
|
||||
return mat;
|
||||
}, [baseColor]);
|
||||
}
|
||||
|
||||
function HologramMesh() {
|
||||
const material = useHologramMaterial(0x00ff88);
|
||||
return (
|
||||
<mesh>
|
||||
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Vertex Displacement
|
||||
|
||||
Use `positionNode` to displace vertices on the GPU:
|
||||
|
||||
```tsx
|
||||
function DisplacedPlane() {
|
||||
const uAmplitude = useMemo(() => uniform(0.5), []);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
|
||||
const displacement = mx_noise_float(
|
||||
positionLocal.xz.mul(2.0).add(time.mul(0.5))
|
||||
).mul(uAmplitude);
|
||||
|
||||
mat.positionNode = positionLocal.add(
|
||||
normalLocal.mul(displacement)
|
||||
);
|
||||
mat.colorNode = mix(
|
||||
color(0x1e3a5f),
|
||||
color(0x7dd3fc),
|
||||
displacement.div(uAmplitude).mul(0.5).add(0.5)
|
||||
);
|
||||
|
||||
return mat;
|
||||
}, [uAmplitude]);
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10, 256, 256]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Glass / Transmission Material
|
||||
|
||||
```tsx
|
||||
function GlassSphere() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshPhysicalNodeMaterial();
|
||||
mat.colorNode = color(0xffffff);
|
||||
mat.roughnessNode = float(0.05);
|
||||
mat.metalnessNode = float(0.0);
|
||||
mat.transmissionNode = float(0.95);
|
||||
mat.thicknessNode = float(0.5);
|
||||
mat.iorNode = float(1.5);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Texture-Based Materials
|
||||
|
||||
Combine texture sampling with procedural effects:
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { texture, mix, mx_noise_float } from 'three/tsl';
|
||||
|
||||
function WeatheredWall() {
|
||||
const [diffuse, normal, roughnessMap] = useTexture([
|
||||
'/textures/brick_diffuse.jpg',
|
||||
'/textures/brick_normal.jpg',
|
||||
'/textures/brick_roughness.jpg',
|
||||
]);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const time = timerLocal();
|
||||
|
||||
const baseTex = texture(diffuse);
|
||||
const noise = mx_noise_float(uv().mul(10).add(time.mul(0.1)));
|
||||
|
||||
// Blend texture with procedural dirt
|
||||
mat.colorNode = mix(baseTex, color(0x3d2b1f), noise.mul(0.3));
|
||||
mat.normalNode = texture(normal);
|
||||
mat.roughnessNode = texture(roughnessMap).r.add(noise.mul(0.1));
|
||||
|
||||
return mat;
|
||||
}, [diffuse, normal, roughnessMap]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[5, 5]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Sprite and Points Materials
|
||||
|
||||
### SpriteNodeMaterial
|
||||
|
||||
```tsx
|
||||
function CustomSprite() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new SpriteNodeMaterial();
|
||||
mat.colorNode = color(0xffaa00);
|
||||
mat.opacityNode = float(1).sub(length(uv().sub(0.5)).mul(2)).saturate();
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<sprite>
|
||||
<primitive object={material} attach="material" />
|
||||
</sprite>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### PointsNodeMaterial
|
||||
|
||||
```tsx
|
||||
function ParticleField() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new PointsNodeMaterial();
|
||||
mat.sizeNode = float(5.0);
|
||||
mat.colorNode = mix(color(0xff0000), color(0x0000ff), uv().x);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<points>
|
||||
<bufferGeometry>
|
||||
{/* attach position attribute */}
|
||||
</bufferGeometry>
|
||||
<primitive object={material} attach="material" />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-shaders
|
||||
|
||||
The existing package at `packages/threejs/r3f/r3f-shaders` stores GLSL shaders as
|
||||
string exports. TSL materials should be added under a new `src/tsl/` directory:
|
||||
|
||||
```text
|
||||
packages/threejs/r3f/r3f-shaders/src/
|
||||
shaders/ # Existing GLSL string shaders
|
||||
background/
|
||||
parallax/
|
||||
tsl/ # New TSL node material factories
|
||||
hologram/
|
||||
hologram.ts
|
||||
hologram.test.ts
|
||||
hologram.types.ts
|
||||
hologram.md
|
||||
glass/
|
||||
glass.ts
|
||||
glass.test.ts
|
||||
glass.types.ts
|
||||
glass.md
|
||||
```
|
||||
|
||||
### TSL Material Factory Convention
|
||||
|
||||
Export factory functions that return configured node materials:
|
||||
|
||||
```ts
|
||||
// src/tsl/hologram/hologram.ts
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
import { color, sin, timerLocal, float } from 'three/tsl';
|
||||
import type { HologramOptions } from './hologram.types.js';
|
||||
|
||||
export function createHologramMaterial(options: HologramOptions = {}) {
|
||||
const { baseColor = 0x00ff88, scanlineFreq = 50, speed = 5 } = options;
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.transparent = true;
|
||||
|
||||
const scanline = sin(positionLocal.y.mul(scanlineFreq).add(time.mul(speed)))
|
||||
.mul(0.5).add(0.5);
|
||||
|
||||
mat.colorNode = color(baseColor);
|
||||
mat.opacityNode = scanline;
|
||||
mat.emissiveNode = color(baseColor);
|
||||
|
||||
return mat;
|
||||
}
|
||||
```
|
||||
|
||||
## Material Disposal
|
||||
|
||||
R3F automatically disposes materials attached via `<primitive>` when the
|
||||
component unmounts. For materials stored in refs or external state:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
material.dispose();
|
||||
};
|
||||
}, [material]);
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Node materials compile shaders on first render. Expect a one-frame stall.
|
||||
- Reuse materials across meshes when possible (same instance, different meshes).
|
||||
- `timerLocal()` is cheaper than uniform + useFrame for time-based animation.
|
||||
- Vertex displacement via `positionNode` is entirely GPU-side -- no JS overhead.
|
||||
- For `@suaveplan/r3f-performance`, node materials work with the existing
|
||||
LOD and quality tier plugins without modification.
|
||||
@@ -0,0 +1,400 @@
|
||||
# Post-Processing with TSL in React Three Fiber
|
||||
|
||||
Three.js provides a node-based post-processing pipeline for the WebGPU backend.
|
||||
Effects are composed as TSL node chains, replacing the older EffectComposer/Pass
|
||||
pattern.
|
||||
|
||||
## PostProcessing Class
|
||||
|
||||
The core class that manages the effect chain:
|
||||
|
||||
```ts
|
||||
import { PostProcessing } from 'three/addons/renderers/common/PostProcessing.js';
|
||||
```
|
||||
|
||||
This class takes a renderer and an `outputNode` -- a TSL node graph that
|
||||
processes the rendered scene into the final framebuffer.
|
||||
|
||||
## R3F Integration Pattern
|
||||
|
||||
Since R3F manages the render loop, you need to hook into it at the right level.
|
||||
The PostProcessing class replaces R3F's default render call.
|
||||
|
||||
### Pattern 1: Manual Render with useFrame
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo, useEffect } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { PostProcessing } from 'three/addons/renderers/common/PostProcessing.js';
|
||||
import { pass, bloom, fxaa } from 'three/tsl';
|
||||
|
||||
function WebGPUPostProcessing() {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const postProcessing = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
postProcessing.outputNode = scenePass
|
||||
.pipe(bloom({ strength: 0.5, radius: 0.4, threshold: 0.85 }))
|
||||
.pipe(fxaa());
|
||||
|
||||
postRef.current = postProcessing;
|
||||
|
||||
return () => {
|
||||
// Cleanup if needed
|
||||
postRef.current = null;
|
||||
};
|
||||
}, [gl, scene, camera]);
|
||||
|
||||
useFrame(() => {
|
||||
if (postRef.current) {
|
||||
postRef.current.render();
|
||||
}
|
||||
}, 1); // Priority 1 to run after default scene updates
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Disable R3F Auto-Render
|
||||
|
||||
For full control over rendering:
|
||||
|
||||
```tsx
|
||||
<Canvas
|
||||
gl={(canvas) => createWebGPURenderer(canvas)}
|
||||
frameloop="never" // Disable auto-render
|
||||
>
|
||||
<Scene />
|
||||
<ManualRenderLoop />
|
||||
</Canvas>
|
||||
|
||||
function ManualRenderLoop() {
|
||||
const { gl, scene, camera, invalidate } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
post.outputNode = scenePass.pipe(bloom({ strength: 1.0 }));
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Effects
|
||||
|
||||
### pass() -- Scene Render Pass
|
||||
|
||||
The entry point for all post-processing chains:
|
||||
|
||||
```ts
|
||||
import { pass } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
// Access rendered textures
|
||||
const beautyTexture = scenePass.getTextureNode(); // Color
|
||||
const depthTexture = scenePass.getDepthNode(); // Depth
|
||||
const normalTexture = scenePass.getNormalNode(); // View-space normals (if enabled)
|
||||
```
|
||||
|
||||
### bloom()
|
||||
|
||||
HDR bloom effect:
|
||||
|
||||
```ts
|
||||
import { bloom } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(bloom({
|
||||
strength: 0.5, // Bloom intensity (0..2+)
|
||||
radius: 0.4, // Blur radius (0..1)
|
||||
threshold: 0.85, // Luminance threshold (0..1)
|
||||
}));
|
||||
```
|
||||
|
||||
### fxaa()
|
||||
|
||||
Fast approximate anti-aliasing:
|
||||
|
||||
```ts
|
||||
import { fxaa } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(fxaa());
|
||||
```
|
||||
|
||||
### gaussianBlur()
|
||||
|
||||
Gaussian blur with configurable kernel:
|
||||
|
||||
```ts
|
||||
import { gaussianBlur } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(gaussianBlur({
|
||||
sigma: 2.0, // Blur sigma
|
||||
direction: vec2(1, 0), // Horizontal pass
|
||||
}));
|
||||
```
|
||||
|
||||
### dotScreen()
|
||||
|
||||
Halftone dot pattern:
|
||||
|
||||
```ts
|
||||
import { dotScreen } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(dotScreen({
|
||||
scale: 4.0,
|
||||
angle: 0.5,
|
||||
}));
|
||||
```
|
||||
|
||||
### film()
|
||||
|
||||
Film grain effect:
|
||||
|
||||
```ts
|
||||
import { film } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(film({
|
||||
intensity: 0.35,
|
||||
grayscale: false,
|
||||
}));
|
||||
```
|
||||
|
||||
### sobel()
|
||||
|
||||
Edge detection:
|
||||
|
||||
```ts
|
||||
import { sobel } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(sobel());
|
||||
```
|
||||
|
||||
## Depth-Based Effects
|
||||
|
||||
### SSAO (Screen-Space Ambient Occlusion)
|
||||
|
||||
```ts
|
||||
import { ao } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
const depthNode = scenePass.getDepthNode();
|
||||
const normalNode = scenePass.getNormalNode();
|
||||
|
||||
const ssao = ao({
|
||||
depthNode,
|
||||
normalNode,
|
||||
radius: 0.5,
|
||||
intensity: 1.0,
|
||||
samples: 16,
|
||||
});
|
||||
|
||||
// Multiply AO into the beauty pass
|
||||
postProcessing.outputNode = scenePass.getTextureNode().mul(ssao);
|
||||
```
|
||||
|
||||
### Depth of Field
|
||||
|
||||
```ts
|
||||
import { dof } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
postProcessing.outputNode = scenePass.pipe(dof({
|
||||
focus: 5.0, // Focus distance
|
||||
aperture: 0.025, // Aperture size
|
||||
maxblur: 0.01, // Maximum blur
|
||||
}));
|
||||
```
|
||||
|
||||
## Custom Post-Processing Effects
|
||||
|
||||
### Simple Full-Screen Effect
|
||||
|
||||
```ts
|
||||
import { Fn, screenCoordinate, screenSize, texture } from 'three/tsl';
|
||||
|
||||
const vignetteEffect = Fn(([inputTexture]) => {
|
||||
const uv = screenCoordinate.div(screenSize);
|
||||
const center = vec2(0.5, 0.5);
|
||||
const dist = distance(uv, center);
|
||||
const vignette = smoothstep(float(0.8), float(0.4), dist);
|
||||
|
||||
return inputTexture.mul(vignette);
|
||||
});
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
postProcessing.outputNode = vignetteEffect(scenePass.getTextureNode());
|
||||
```
|
||||
|
||||
### Color Grading
|
||||
|
||||
```ts
|
||||
const colorGrade = Fn(([inputTexture]) => {
|
||||
const col = inputTexture.toVar();
|
||||
|
||||
// Lift / Gamma / Gain
|
||||
const lift = vec3(0.0, 0.0, 0.05); // Shadow tint
|
||||
const gamma = vec3(1.0, 0.95, 0.9); // Midtone tint
|
||||
const gain = vec3(1.1, 1.05, 1.0); // Highlight tint
|
||||
|
||||
col.assign(col.mul(gain).add(lift));
|
||||
col.assign(pow(col, vec3(1.0).div(gamma)));
|
||||
|
||||
// Contrast
|
||||
col.assign(col.sub(0.5).mul(1.2).add(0.5));
|
||||
|
||||
// Saturation
|
||||
const lum = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
||||
col.assign(mix(vec3(lum), col, float(1.3)));
|
||||
|
||||
return vec4(col, float(1.0));
|
||||
});
|
||||
```
|
||||
|
||||
### Chromatic Aberration
|
||||
|
||||
```ts
|
||||
const chromaticAberration = Fn(([inputTexture]) => {
|
||||
const uv = screenCoordinate.div(screenSize);
|
||||
const center = vec2(0.5, 0.5);
|
||||
const dir = uv.sub(center);
|
||||
const offset = float(0.003);
|
||||
|
||||
const r = texture(inputTexture, uv.add(dir.mul(offset))).r;
|
||||
const g = texture(inputTexture, uv).g;
|
||||
const b = texture(inputTexture, uv.sub(dir.mul(offset))).b;
|
||||
|
||||
return vec4(r, g, b, float(1.0));
|
||||
});
|
||||
```
|
||||
|
||||
## Effect Chaining
|
||||
|
||||
Effects can be chained with `.pipe()`:
|
||||
|
||||
```ts
|
||||
postProcessing.outputNode = scenePass
|
||||
.pipe(bloom({ strength: 0.5, threshold: 0.8 }))
|
||||
.pipe(fxaa())
|
||||
.pipe(film({ intensity: 0.15 }));
|
||||
```
|
||||
|
||||
Or composed manually:
|
||||
|
||||
```ts
|
||||
const beauty = scenePass.getTextureNode();
|
||||
const bloomed = bloom({ strength: 0.5 })(beauty);
|
||||
const graded = colorGrade(bloomed);
|
||||
const final = vignetteEffect(graded);
|
||||
|
||||
postProcessing.outputNode = final;
|
||||
```
|
||||
|
||||
## Uniform-Driven Effects (React State)
|
||||
|
||||
```tsx
|
||||
function DynamicPostFX({ bloomStrength = 0.5 }) {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const uBloom = useMemo(() => uniform(bloomStrength), []);
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
// Sync React prop to GPU uniform
|
||||
useEffect(() => {
|
||||
uBloom.value = bloomStrength;
|
||||
}, [bloomStrength, uBloom]);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
post.outputNode = scenePass.pipe(bloom({
|
||||
strength: uBloom,
|
||||
radius: 0.4,
|
||||
threshold: 0.85,
|
||||
}));
|
||||
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera, uBloom]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
}, 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## R3F Post-Processing Component Pattern
|
||||
|
||||
Encapsulate post-processing into a reusable component:
|
||||
|
||||
```tsx
|
||||
interface PostFXProps {
|
||||
bloomStrength?: number;
|
||||
bloomThreshold?: number;
|
||||
enableFXAA?: boolean;
|
||||
enableFilm?: boolean;
|
||||
filmIntensity?: number;
|
||||
}
|
||||
|
||||
function PostFX({
|
||||
bloomStrength = 0.5,
|
||||
bloomThreshold = 0.85,
|
||||
enableFXAA = true,
|
||||
enableFilm = false,
|
||||
filmIntensity = 0.1,
|
||||
}: PostFXProps) {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
let output = scenePass.pipe(bloom({
|
||||
strength: bloomStrength,
|
||||
threshold: bloomThreshold,
|
||||
}));
|
||||
|
||||
if (enableFXAA) output = output.pipe(fxaa());
|
||||
if (enableFilm) output = output.pipe(film({ intensity: filmIntensity }));
|
||||
|
||||
post.outputNode = output;
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera, bloomStrength, bloomThreshold, enableFXAA, enableFilm, filmIntensity]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
}, 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usage in scene
|
||||
<Canvas gl={(canvas) => createWebGPURenderer(canvas)}>
|
||||
<Scene />
|
||||
<PostFX bloomStrength={0.8} enableFilm />
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
1. Each `.pipe()` adds a full-screen pass. Minimize the chain length.
|
||||
2. SSAO and DOF are the most expensive built-in effects.
|
||||
3. `fxaa()` is cheap -- prefer it over MSAA for WebGPU post-processing.
|
||||
4. Custom effects that only read `screenCoordinate` / `screenSize` are
|
||||
essentially free (single texture fetch per pixel).
|
||||
5. For `@suaveplan/r3f-performance`, post-processing quality can be
|
||||
adjusted dynamically by updating uniforms based on the current
|
||||
performance tier (e.g., reduce bloom samples on low-end devices).
|
||||
@@ -0,0 +1,328 @@
|
||||
# WGSL Integration with TSL
|
||||
|
||||
When TSL's declarative node API is insufficient -- raw GPU intrinsics, shared
|
||||
workgroup memory, complex control flow -- you can drop to raw WGSL using
|
||||
`wgslFn()` and integrate the result back into the TSL graph.
|
||||
|
||||
## wgslFn() Basics
|
||||
|
||||
```ts
|
||||
import { wgslFn, float, vec2, vec3, vec4 } from 'three/tsl';
|
||||
|
||||
const myShaderFn = wgslFn(`
|
||||
fn myFunction(uv: vec2f, time: f32) -> vec4f {
|
||||
let col = vec3f(uv, sin(time) * 0.5 + 0.5);
|
||||
return vec4f(col, 1.0);
|
||||
}
|
||||
`);
|
||||
|
||||
// Call it like a TSL function
|
||||
const result = myShaderFn({ uv: uv(), time: timerLocal() });
|
||||
```
|
||||
|
||||
### Parameter Passing
|
||||
|
||||
Parameters are passed as a named object. The names must match the WGSL
|
||||
function parameter names:
|
||||
|
||||
```ts
|
||||
const sdfShape = wgslFn(`
|
||||
fn sdfShape(pos: vec3f, radius: f32, smoothness: f32) -> f32 {
|
||||
let d = length(pos) - radius;
|
||||
return smoothstep(-smoothness, smoothness, -d);
|
||||
}
|
||||
`);
|
||||
|
||||
// TSL call site
|
||||
const dist = sdfShape({
|
||||
pos: positionLocal,
|
||||
radius: float(1.0),
|
||||
smoothness: float(0.02),
|
||||
});
|
||||
```
|
||||
|
||||
## WGSL Type Reference
|
||||
|
||||
### Scalar Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `f32` | 32-bit float | `float()` |
|
||||
| `i32` | 32-bit signed int | `int()` |
|
||||
| `u32` | 32-bit unsigned int | `uint()` |
|
||||
| `bool` | Boolean | `bool()` |
|
||||
| `f16` | 16-bit float (requires extension) | -- |
|
||||
|
||||
### Vector Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `vec2f` | 2-component f32 vector | `vec2()` |
|
||||
| `vec3f` | 3-component f32 vector | `vec3()` |
|
||||
| `vec4f` | 4-component f32 vector | `vec4()` |
|
||||
| `vec2i` | 2-component i32 vector | `ivec2()` |
|
||||
| `vec3i` | 3-component i32 vector | `ivec3()` |
|
||||
| `vec4i` | 4-component i32 vector | `ivec4()` |
|
||||
| `vec2u` | 2-component u32 vector | `uvec2()` |
|
||||
| `vec3u` | 3-component u32 vector | `uvec3()` |
|
||||
| `vec4u` | 4-component u32 vector | `uvec4()` |
|
||||
|
||||
### Matrix Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `mat2x2f` | 2x2 float matrix | `mat2()` |
|
||||
| `mat3x3f` | 3x3 float matrix | `mat3()` |
|
||||
| `mat4x4f` | 4x4 float matrix | `mat4()` |
|
||||
|
||||
### Array Types
|
||||
|
||||
```wgsl
|
||||
array<f32, 16> // Fixed-size array of 16 floats
|
||||
array<vec4f, 256> // Fixed-size array of 256 vec4
|
||||
```
|
||||
|
||||
### Struct Types
|
||||
|
||||
```wgsl
|
||||
struct Particle {
|
||||
position: vec3f,
|
||||
velocity: vec3f,
|
||||
life: f32,
|
||||
pad: f32, // Padding for 16-byte alignment
|
||||
}
|
||||
```
|
||||
|
||||
## Hybrid TSL + WGSL Patterns
|
||||
|
||||
### Pattern 1: WGSL Function in TSL Material
|
||||
|
||||
```tsx
|
||||
import { wgslFn, uv, timerLocal, float } from 'three/tsl';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
|
||||
const voronoiNoise = wgslFn(`
|
||||
fn hash2(p: vec2f) -> vec2f {
|
||||
let k = vec2f(0.3183099, 0.3678794);
|
||||
let x = p * k + k.yx;
|
||||
return fract(16.0 * k * fract(x.x * x.y * (x.x + x.y))) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
fn voronoi(uv: vec2f, time: f32) -> f32 {
|
||||
let cell = floor(uv);
|
||||
let frac = fract(uv);
|
||||
var minDist: f32 = 1.0;
|
||||
|
||||
for (var y: i32 = -1; y <= 1; y++) {
|
||||
for (var x: i32 = -1; x <= 1; x++) {
|
||||
let neighbor = vec2f(f32(x), f32(y));
|
||||
let point = hash2(cell + neighbor);
|
||||
let animPoint = 0.5 + 0.5 * sin(time + 6.2831 * point);
|
||||
let diff = neighbor + animPoint - frac;
|
||||
let dist = length(diff);
|
||||
minDist = min(minDist, dist);
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
`);
|
||||
|
||||
function VoronoiMesh() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const noise = voronoiNoise({
|
||||
uv: uv().mul(5),
|
||||
time: timerLocal(),
|
||||
});
|
||||
mat.colorNode = mix(color(0x1e293b), color(0x38bdf8), noise);
|
||||
mat.roughnessNode = noise.oneMinus().mul(0.8).add(0.1);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: WGSL Compute with TSL Dispatch
|
||||
|
||||
```ts
|
||||
const nbodyCompute = wgslFn(`
|
||||
fn nbodyStep(
|
||||
idx: u32,
|
||||
count: u32,
|
||||
positions: ptr<storage, array<vec4f>, read_write>,
|
||||
velocities: ptr<storage, array<vec4f>, read_write>,
|
||||
dt: f32
|
||||
) {
|
||||
let pos = (*positions)[idx].xyz;
|
||||
var acc = vec3f(0.0);
|
||||
|
||||
for (var i: u32 = 0u; i < count; i++) {
|
||||
if (i == idx) { continue; }
|
||||
let other = (*positions)[i].xyz;
|
||||
let diff = other - pos;
|
||||
let dist2 = dot(diff, diff) + 0.01;
|
||||
acc += normalize(diff) * ((*positions)[i].w / dist2);
|
||||
}
|
||||
|
||||
(*velocities)[idx] = vec4f((*velocities)[idx].xyz + acc * dt, 0.0);
|
||||
(*positions)[idx] = vec4f(pos + (*velocities)[idx].xyz * dt, (*positions)[idx].w);
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
### Pattern 3: WGSL Utility Functions Mixed with TSL
|
||||
|
||||
```ts
|
||||
// Define a WGSL helper
|
||||
const rotateY = wgslFn(`
|
||||
fn rotateY(v: vec3f, angle: f32) -> vec3f {
|
||||
let c = cos(angle);
|
||||
let s = sin(angle);
|
||||
return vec3f(
|
||||
v.x * c + v.z * s,
|
||||
v.y,
|
||||
-v.x * s + v.z * c
|
||||
);
|
||||
}
|
||||
`);
|
||||
|
||||
// Use in TSL material
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.positionNode = rotateY({
|
||||
v: positionLocal,
|
||||
angle: timerLocal(),
|
||||
});
|
||||
```
|
||||
|
||||
## WGSL Built-in Functions
|
||||
|
||||
### Math Functions
|
||||
|
||||
```wgsl
|
||||
abs(x), acos(x), asin(x), atan(x), atan2(y, x)
|
||||
ceil(x), clamp(x, lo, hi), cos(x), cosh(x)
|
||||
cross(a, b), degrees(x), determinant(m)
|
||||
distance(a, b), dot(a, b), exp(x), exp2(x)
|
||||
floor(x), fma(a, b, c), fract(x)
|
||||
inverseSqrt(x), length(v), log(x), log2(x)
|
||||
max(a, b), min(a, b), mix(a, b, t)
|
||||
normalize(v), pow(x, y), radians(x)
|
||||
reflect(v, n), refract(v, n, eta), round(x)
|
||||
saturate(x), sign(x), sin(x), sinh(x)
|
||||
smoothstep(lo, hi, x), sqrt(x), step(edge, x)
|
||||
tan(x), tanh(x), transpose(m), trunc(x)
|
||||
```
|
||||
|
||||
### Texture Functions
|
||||
|
||||
```wgsl
|
||||
textureSample(tex, sampler, uv)
|
||||
textureSampleLevel(tex, sampler, uv, level)
|
||||
textureSampleGrad(tex, sampler, uv, ddx, ddy)
|
||||
textureLoad(tex, coords, level)
|
||||
textureStore(tex, coords, value)
|
||||
textureDimensions(tex)
|
||||
```
|
||||
|
||||
### Atomic Functions (Compute)
|
||||
|
||||
```wgsl
|
||||
atomicLoad(ptr)
|
||||
atomicStore(ptr, value)
|
||||
atomicAdd(ptr, value)
|
||||
atomicSub(ptr, value)
|
||||
atomicMax(ptr, value)
|
||||
atomicMin(ptr, value)
|
||||
atomicAnd(ptr, value)
|
||||
atomicOr(ptr, value)
|
||||
atomicXor(ptr, value)
|
||||
atomicCompareExchangeWeak(ptr, compare, value)
|
||||
```
|
||||
|
||||
### Synchronization (Compute)
|
||||
|
||||
```wgsl
|
||||
workgroupBarrier() // Sync all invocations in workgroup
|
||||
storageBarrier() // Sync storage memory access
|
||||
textureBarrier() // Sync texture memory access
|
||||
```
|
||||
|
||||
## Memory Layout and Alignment
|
||||
|
||||
WGSL has strict alignment rules. Key points:
|
||||
|
||||
| Type | Size | Alignment |
|
||||
|---|---|---|
|
||||
| `f32` | 4 | 4 |
|
||||
| `vec2f` | 8 | 8 |
|
||||
| `vec3f` | 12 | 16 |
|
||||
| `vec4f` | 16 | 16 |
|
||||
| `mat4x4f` | 64 | 16 |
|
||||
| `array<T, N>` | N * stride(T) | align(T) |
|
||||
|
||||
**Important**: `vec3f` has alignment 16 (not 12). Add padding or use `vec4f`
|
||||
in storage buffers to avoid alignment issues.
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Prefer TSL when possible**: The compiler optimizes better with full
|
||||
graph visibility. Use `wgslFn` only when TSL lacks the feature.
|
||||
|
||||
2. **Minimize branching**: GPU wavefronts execute both branches. Use
|
||||
`select()` / `mix()` for simple conditionals.
|
||||
|
||||
3. **Shared memory for compute**: WGSL `var<workgroup>` gives fast
|
||||
workgroup-local memory. This is the main reason to drop to WGSL
|
||||
for compute kernels.
|
||||
|
||||
```wgsl
|
||||
var<workgroup> shared_data: array<vec4f, 64>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
||||
shared_data[lid.x] = /* load from global */;
|
||||
workgroupBarrier();
|
||||
// Now all threads can read shared_data
|
||||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
4. **Avoid register pressure**: Keep live variables minimal. WGSL
|
||||
compilers spill to memory when registers are exhausted.
|
||||
|
||||
5. **Loop unrolling**: Small fixed loops are often unrolled by the
|
||||
compiler. Avoid dynamic loop bounds when the range is known.
|
||||
|
||||
<!-- markdownlint-enable MD029 -->
|
||||
|
||||
## R3F Material Integration
|
||||
|
||||
To use a `wgslFn` result in an R3F component:
|
||||
|
||||
```tsx
|
||||
function WGSLMaterial() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const wgslResult = myWgslFunction({
|
||||
pos: positionLocal,
|
||||
time: timerLocal(),
|
||||
});
|
||||
mat.colorNode = wgslResult.xyz;
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return <primitive object={material} attach="material" />;
|
||||
}
|
||||
```
|
||||
|
||||
The `wgslFn` return type matches the WGSL function return type. If the
|
||||
function returns `vec4f`, the TSL node is a `vec4` that supports
|
||||
`.xyz`, `.x`, `.r`, etc.
|
||||
Reference in New Issue
Block a user