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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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).

View File

@@ -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>
);
}
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.