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,572 @@
---
name: threejs-shaders
description: Three.js shader programming adapted for React Three Fiber and Suaveplan R3F packages. Covers shaderMaterial in JSX, the @suaveplan/r3f-shaders library, GLSL vertex and fragment shaders with React refs, uniform animation via useFrame, the extend() pattern for custom JSX elements, and onBeforeCompile for material patching.
---
This skill covers custom shader development in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
The user needs to write custom GLSL shaders for visual effects, procedural materials, particle systems, or compute-like operations in the GPU.
## Core Concepts
### Three.js Shader Pipeline
Three.js shaders run on the GPU as GLSL programs:
- **Vertex Shader**: Transforms vertex positions (runs per vertex)
- **Fragment Shader**: Determines pixel color (runs per fragment/pixel)
- **Uniforms**: Values passed from JavaScript to GLSL (same for all vertices/fragments)
- **Attributes**: Per-vertex data (position, normal, UV)
- **Varyings**: Values interpolated from vertex to fragment shader
### R3F Shader Approach
| Three.js (Imperative) | R3F (Declarative) |
|---|---|
| `new ShaderMaterial({ uniforms, vertexShader, fragmentShader })` | `<shaderMaterial>` JSX or `extend()` |
| `material.uniforms.time.value = t` | Ref + `useFrame` |
| `new RawShaderMaterial(...)` | `<rawShaderMaterial>` |
| Custom class extending Material | `extend({ CustomMaterial })` |
### Suaveplan R3F Packages
| Package | Purpose | Import |
|---|---|---|
| `@suaveplan/r3f-shaders` | Pre-built shader materials and utilities | `import { GlassMaterial, HologramMaterial, useShaderUniforms } from '@suaveplan/r3f-shaders'` |
| `@suaveplan/r3f` | Core R3F utilities | `import { useR3F } from '@suaveplan/r3f'` |
| `@suaveplan/r3f-performance` | Shader performance monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
## ShaderMaterial in JSX
### Basic Custom Shader
```tsx
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { ShaderMaterial, Color } from 'three';
const vertexShader = /* glsl */ `
varying vec2 vUv;
varying vec3 vNormal;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = /* glsl */ `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
varying vec3 vNormal;
void main() {
float stripe = sin(vUv.y * 20.0 + uTime * 2.0) * 0.5 + 0.5;
vec3 color = mix(uColor, uColor * 0.3, step(0.5, stripe));
float fresnel = pow(1.0 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
color += fresnel * 0.3;
gl_FragColor = vec4(color, 1.0);
}
`;
export function StripedMaterial({ color = '#4ecdc4' }: { color?: string }) {
const materialRef = useRef<ShaderMaterial>(null);
const uniforms = useMemo(
() => ({
uTime: { value: 0 },
uColor: { value: new Color(color) },
}),
[]
);
useFrame(({ clock }) => {
if (materialRef.current) {
materialRef.current.uniforms.uTime.value = clock.elapsedTime;
}
});
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<shaderMaterial
ref={materialRef}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
uniforms={uniforms}
/>
</mesh>
);
}
```
### Key Rule: useMemo for Uniforms
Always wrap uniforms in `useMemo`. Without it, R3F recreates the material every render:
```tsx
// CORRECT: uniforms created once
const uniforms = useMemo(() => ({
uTime: { value: 0 },
uResolution: { value: new Vector2(window.innerWidth, window.innerHeight) },
}), []);
// WRONG: new uniforms object every render, causes material recreation
const uniforms = {
uTime: { value: 0 },
};
```
## extend(): Custom JSX Elements
For reusable shader materials, use `extend()` to register a custom Three.js class as a JSX element:
```tsx
import { extend, useFrame } from '@react-three/fiber';
import { shaderMaterial } from '@react-three/drei';
import { Color, Vector2 } from 'three';
import { useRef } from 'react';
// Create a typed shader material class
const WaveMaterial = shaderMaterial(
// Uniforms with defaults
{
uTime: 0,
uColor: new Color('#ff6b6b'),
uAmplitude: 0.3,
uFrequency: 4.0,
},
// Vertex shader
/* glsl */ `
uniform float uTime;
uniform float uAmplitude;
uniform float uFrequency;
varying vec2 vUv;
varying float vElevation;
void main() {
vUv = uv;
vec3 pos = position;
float elevation = sin(pos.x * uFrequency + uTime) * uAmplitude
+ sin(pos.z * uFrequency * 0.7 + uTime * 0.8) * uAmplitude * 0.5;
pos.y += elevation;
vElevation = elevation;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
// Fragment shader
/* glsl */ `
uniform vec3 uColor;
uniform float uAmplitude;
varying vec2 vUv;
varying float vElevation;
void main() {
float brightness = (vElevation / uAmplitude + 1.0) * 0.5;
vec3 color = mix(uColor * 0.3, uColor, brightness);
gl_FragColor = vec4(color, 1.0);
}
`
);
// Register as JSX element
extend({ WaveMaterial });
// TypeScript declaration for JSX
declare module '@react-three/fiber' {
interface ThreeElements {
waveMaterial: JSX.IntrinsicElements['shaderMaterial'] & {
uTime?: number;
uColor?: Color;
uAmplitude?: number;
uFrequency?: number;
};
}
}
// Usage component
export function WavePlane() {
const materialRef = useRef<typeof WaveMaterial.prototype>(null);
useFrame(({ clock }) => {
if (materialRef.current) {
materialRef.current.uTime = clock.elapsedTime;
}
});
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[10, 10, 128, 128]} />
<waveMaterial
ref={materialRef}
uColor={new Color('#4ecdc4')}
uAmplitude={0.3}
uFrequency={4}
/>
</mesh>
);
}
```
## GLSL Built-in Variables
### Vertex Shader
| Variable | Type | Description |
|---|---|---|
| `position` | `vec3` | Vertex position (attribute) |
| `normal` | `vec3` | Vertex normal (attribute) |
| `uv` | `vec2` | Texture coordinate (attribute) |
| `modelMatrix` | `mat4` | Object to world transform |
| `viewMatrix` | `mat4` | World to camera transform |
| `projectionMatrix` | `mat4` | Camera to clip space |
| `modelViewMatrix` | `mat4` | `viewMatrix * modelMatrix` |
| `normalMatrix` | `mat3` | Inverse transpose of modelView (for normals) |
| `cameraPosition` | `vec3` | Camera world position |
| `gl_Position` | `vec4` | **Output**: clip-space position |
### Fragment Shader
| Variable | Type | Description |
|---|---|---|
| `gl_FragCoord` | `vec4` | Pixel position in screen space |
| `gl_FrontFacing` | `bool` | Whether fragment is on front face |
| `gl_FragColor` | `vec4` | **Output**: pixel color (deprecated, use `out vec4`) |
## Common GLSL Patterns
### Fresnel Effect
```glsl
// In vertex shader
varying vec3 vViewDirection;
varying vec3 vWorldNormal;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vViewDirection = normalize(cameraPosition - worldPos.xyz);
vWorldNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
gl_Position = projectionMatrix * viewMatrix * worldPos;
}
// In fragment shader
void main() {
float fresnel = pow(1.0 - dot(vViewDirection, vWorldNormal), 3.0);
vec3 color = mix(vec3(0.1), vec3(0.0, 0.8, 1.0), fresnel);
gl_FragColor = vec4(color, 1.0);
}
```
### Noise Functions
```glsl
// Simple 2D noise (include in fragment shader)
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
// Simplex-like value noise
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
// FBM (Fractal Brownian Motion) for complex noise
float fbm(vec2 st) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 5; i++) {
value += amplitude * noise(st);
st *= 2.0;
amplitude *= 0.5;
}
return value;
}
```
### SDF (Signed Distance Functions)
Common 2D SDFs: `sdCircle(p, r)` = `length(p) - r`, `sdBox(p, b)` = `length(max(abs(p) - b, 0.0))`. Use for procedural shapes, soft shadows, and UI in shaders. Combine with `smoothstep` for anti-aliased edges.
## @suaveplan/r3f-shaders Library
Pre-built shader materials from the Suaveplan shader library:
### Available Shaders
```tsx
import {
GlassMaterial,
HologramMaterial,
NoiseMaterial,
GradientMaterial,
FresnelMaterial,
GridMaterial,
DissolveEffect,
} from '@suaveplan/r3f-shaders';
// Glass with custom refraction
<GlassMaterial
color="#88ccff"
ior={1.45}
thickness={0.3}
distortion={0.1}
chromaticAberration={0.02}
/>
// Sci-fi hologram
<HologramMaterial
color="#00ff88"
scanlineSpeed={2}
fresnelIntensity={1.5}
glitchIntensity={0.3}
wireframe
/>
// Procedural noise surface
<NoiseMaterial
color="#4a90d9"
scale={3}
speed={0.5}
displacement={0.2}
octaves={4}
/>
// Two-color gradient
<GradientMaterial
colorA="#ff6b6b"
colorB="#4ecdc4"
direction="vertical"
/>
```
### Creating Custom Shaders for the Library
Follow the `@suaveplan/r3f-shaders` pattern:
```tsx
// src/materials/my-effect/my-effect.ts
import { shaderMaterial } from '@react-three/drei';
import { Color } from 'three';
export const MyEffectMaterial = shaderMaterial(
{ uTime: 0, uColor: new Color('#ffffff'), uIntensity: 1.0 },
/* glsl */ `/* vertex shader */`,
/* glsl */ `/* fragment shader */`
);
// src/materials/my-effect/my-effect.types.ts
import type { Color } from 'three';
export interface MyEffectMaterialProps {
uTime?: number;
uColor?: Color | string;
uIntensity?: number;
}
```
## Uniform Animation Patterns
### Time-Based Animation
```tsx
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import type { ShaderMaterial } from 'three';
export function AnimatedShader() {
const matRef = useRef<ShaderMaterial>(null);
useFrame(({ clock, pointer }) => {
if (!matRef.current) return;
matRef.current.uniforms.uTime.value = clock.elapsedTime;
matRef.current.uniforms.uMouse.value.set(pointer.x, pointer.y);
});
// ... shader material with uTime and uMouse uniforms
}
```
### Reactive Uniforms (Props to Shader)
```tsx
import { useRef, useEffect } from 'react';
import { Color } from 'three';
import type { ShaderMaterial } from 'three';
interface ReactiveShaderProps {
color: string;
intensity: number;
}
export function ReactiveShader({ color, intensity }: ReactiveShaderProps) {
const matRef = useRef<ShaderMaterial>(null);
useEffect(() => {
if (!matRef.current) return;
(matRef.current.uniforms.uColor.value as Color).set(color);
matRef.current.uniforms.uIntensity.value = intensity;
}, [color, intensity]);
// ... shader material
}
```
## onBeforeCompile: Patching Built-in Materials
Modify Three.js built-in materials by injecting GLSL:
```tsx
import { useRef, useCallback } from 'react';
import { useFrame } from '@react-three/fiber';
import type { MeshStandardMaterial, Shader } from 'three';
export function PatchedMaterial() {
const matRef = useRef<MeshStandardMaterial>(null);
const shaderRef = useRef<Shader | null>(null);
const onBeforeCompile = useCallback((shader: Shader) => {
shaderRef.current = shader;
shader.uniforms.uTime = { value: 0 };
// Inject uniform declaration
shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
/* glsl */ `
#include <common>
uniform float uTime;
`
);
// Inject vertex displacement
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
/* glsl */ `
#include <begin_vertex>
transformed.y += sin(transformed.x * 4.0 + uTime * 2.0) * 0.2;
`
);
}, []);
useFrame(({ clock }) => {
if (shaderRef.current) {
shaderRef.current.uniforms.uTime.value = clock.elapsedTime;
}
});
return (
<mesh>
<planeGeometry args={[5, 5, 64, 64]} />
<meshStandardMaterial
ref={matRef}
color="steelblue"
onBeforeCompile={onBeforeCompile}
/>
</mesh>
);
}
```
## RawShaderMaterial
For full control without Three.js built-in uniforms/attributes, use `<rawShaderMaterial>`. You must manually declare all attributes (`position`, `uv`), uniforms (`projectionMatrix`, `modelViewMatrix`), and set `precision`. Supports `glslVersion="300 es"` for GLSL 3.0.
## Performance Tips
1. **Minimize texture lookups**: Each `texture2D()` call has a cost. Cache results in variables.
2. **Avoid branches**: GPU prefers math over `if/else`. Use `step()`, `smoothstep()`, `mix()` instead.
3. **Limit loop iterations**: Unroll small loops. Keep `for` loops short and bounded.
4. **Use mediump where possible**: `precision mediump float` saves bandwidth on mobile.
5. **Pre-calculate on CPU**: Move invariant math to `useFrame` and pass as uniforms.
```glsl
// SLOW: branching
if (dist < radius) { color = vec3(1.0); }
else { color = vec3(0.0); }
// FAST: step function
color = vec3(step(dist, radius));
// SLOW: multiple conditions
if (x > 0.0 && x < 1.0 && y > 0.0 && y < 1.0) { ... }
// FAST: step math
float inside = step(0.0, x) * step(x, 1.0) * step(0.0, y) * step(y, 1.0);
```
## Testing Shaders
### Vitest Tests
```tsx
import { describe, it, expect } from 'vitest';
import { ShaderMaterial, Color } from 'three';
describe('Custom shader material', () => {
it('creates material with correct uniforms', () => {
const material = new ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new Color('#ff0000') },
},
vertexShader: 'void main() { gl_Position = vec4(0.0); }',
fragmentShader: 'void main() { gl_FragColor = vec4(1.0); }',
});
expect(material.uniforms.uTime.value).toBe(0);
expect(material.uniforms.uColor.value).toBeInstanceOf(Color);
});
it('updates uniform values', () => {
const uniforms = { uTime: { value: 0 } };
uniforms.uTime.value = 1.5;
expect(uniforms.uTime.value).toBe(1.5);
});
});
```
### Storybook Stories
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import { WavePlane } from './wave-plane';
const meta: Meta<typeof WavePlane> = {
title: 'R3F/Shaders/WavePlane',
component: WavePlane,
decorators: [
(Story) => (
<Canvas camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
<ambientLight intensity={0.5} />
<Story />
<OrbitControls />
</Canvas>
),
],
};
export default meta;
export const Default: StoryObj = {};
```
## Checklist
When writing shaders in R3F:
1. Wrap GLSL strings with `/* glsl */` template literal tag for IDE syntax highlighting
2. Always `useMemo` for uniform objects to prevent material recreation
3. Use `extend()` + `shaderMaterial` from drei for reusable shader elements
4. Animate uniforms via `useFrame` and refs (not React state)
5. Declare TypeScript JSX types when using `extend()`
6. Use `@suaveplan/r3f-shaders` pre-built materials before writing custom GLSL
7. Prefer `step`/`smoothstep`/`mix` over `if/else` in GLSL
8. Pre-allocate all uniform objects (Color, Vector2, Vector3) in `useMemo`
9. Use `onBeforeCompile` to patch built-in materials rather than rewriting from scratch
10. Keep shader component files under 250 lines (split GLSL into separate strings or files)