Help us improve
Share bugs, ideas, or general feedback.
From bopen-tools
Writes custom shaders for Three.js using GLSL or TSL for WebGL/WebGPU. Covers debugging, post-processing, noise functions, procedural textures, and performance optimization.
npx claudepluginhub b-open-io/claude-plugins --plugin bopen-toolsHow this skill is triggered — by the user, by Claude, or both
Slash command
/bopen-tools:shadersThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Custom shaders for Three.js — from simple material overrides to full WebGPU node graphs. Covers the complete workflow: choosing an approach, writing code, debugging, and shipping.
Guides Three.js shader creation with GLSL, ShaderMaterial, uniforms for custom visual effects, vertex deformation, fragment shaders, and material extensions.
Guides WebGPU renderer setup in Three.js with TSL for node shaders, compute shaders, post-processing effects, and WGSL integration.
Generates ShaderToy-compatible GLSL shaders for ray marching, SDF scenes, fluid sims, particles, procedural noise, lighting, and post-processing effects via WebGL2 HTML pages.
Share bugs, ideas, or general feedback.
Custom shaders for Three.js — from simple material overrides to full WebGPU node graphs. Covers the complete workflow: choosing an approach, writing code, debugging, and shipping.
| TSL (Three Shader Language) | GLSL (ShaderMaterial) | |
|---|---|---|
| WebGPU | Yes (compiles to WGSL automatically) | No |
| WebGL fallback | Yes (automatic) | Yes |
| Syntax | JavaScript / node graph | Raw GLSL strings |
| Built-in uniforms | Auto-inferred | Manual declarations |
| Best for | New projects, r163+, R3F | Legacy code, full manual control |
Default choice: TSL. Production-ready since r163. Use GLSL only when targeting legacy environments or when RawShaderMaterial manual control is specifically required.
Read references/tsl-guide.md for the full TSL API including all node types, built-ins, control flow, and material setup.
import { extend, useFrame } from '@react-three/fiber';
import * as THREE from 'three/webgpu';
import { color, float, time, mix, mx_noise_float, positionWorld, uniform } from 'three/tsl';
function NoiseSphere() {
const noiseUniform = uniform(0);
useFrame(({ clock }) => {
noiseUniform.value = clock.elapsedTime;
});
const noise = mx_noise_float(positionWorld.mul(1.5).add(time.mul(0.3)));
const dynamicColor = mix(color(0x1a0533), color(0x00eaff), noise);
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardNodeMaterial
colorNode={dynamicColor}
roughnessNode={float(0.4)}
/>
</mesh>
);
}
Renderer requirement: WebGPURenderer from three/webgpu — it automatically falls back to WebGL2 when WebGPU is unavailable. Import materials from three/webgpu, nodes from three/tsl.
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float n = sin(vUv.x * 10.0 + uTime) * 0.5 + 0.5;
gl_FragColor = vec4(uColor * n, 1.0);
}
`;
function ShaderMesh() {
const matRef = useRef();
const uniforms = useMemo(() => ({
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x00aaff) }
}), []);
useFrame(({ clock }) => {
if (matRef.current) matRef.current.uniforms.uTime.value = clock.elapsedTime;
});
return (
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial ref={matRef} vertexShader={vertexShader} fragmentShader={fragmentShader} uniforms={uniforms} />
</mesh>
);
}
Memoize uniforms with useMemo — without it, a new object is created every render and the material recompiles. Update .uniforms.uTime.value inside useFrame, never the object reference.
Read references/glsl-patterns.md for complete recipes: noise, Fresnel, dissolve, hologram, water, matcap, toon shading, particles, and more.
Shader errors are silent: the mesh renders black, pink, or nothing. Debug systematically:
Output intermediate values as colors to inspect them directly:
// Is my UV correct?
gl_FragColor = vec4(vUv, 0.0, 1.0);
// Is my normal correct?
gl_FragColor = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0);
// Is my noise in range?
gl_FragColor = vec4(vec3(noiseValue), 1.0);
In TSL, assign any node directly to colorNode:
material.colorNode = normalWorld.mul(0.5).add(0.5); // visualize normals
material.colorNode = positionLocal; // visualize position
| Symptom | Likely cause |
|---|---|
| Solid black | Missing lights, normals inverted, or NaN from division by zero |
| Pink / magenta | Texture not loaded, sampler2D binding missing |
| Flickering | Uniform not updated every frame, or uniformsNeedUpdate needed |
| No visible change | Wrong material reference, or effect not connected to colorNode |
| NaN propagation | Division by zero, sqrt of negative, atan of zero vector — check all math |
chrome://tracing): GPU timeline and overdraw analysisrenderer.info: draw calls, triangles, textures in flightApply these unconditionally on every shader:
if/else on GPU creates divergent execution across warps. Prefer step(), mix(), or select() (TSL).texture2D / texture() call is a memory fetch. Cache results in a variable; never sample the same texture twice.mediump where possible — in fragment shaders, declare precision mediump float; unless high precision is required. Saves bandwidth on mobile GPUs.depthWrite: true where possible.Install pmndrs/postprocessing:
bun add postprocessing # vanilla
bun add @react-three/postprocessing # R3F wrapper
Effect ordering (wrong order produces incorrect results):
1. SSAO → needs depth buffer, must come first
2. DepthOfField → needs depth, before color grading
3. Bloom → operates on HDR scene before tone mapping
4. ToneMapping → converts HDR → LDR; must follow Bloom
5. ChromaticAberration → full-screen warp, near end
6. Vignette → full-screen overlay
7. Noise → full-screen overlay, last
Performance cost at a glance:
| Effect | Cost |
|---|---|
| ChromaticAberration, Vignette, Noise, ToneMapping | Very Low |
| Bloom (no mipmapBlur), SMAA, GodRays | Low–Medium |
| SSAO, DepthOfField, Bloom (mipmapBlur) | Medium–High |
Mobile: Skip SSAO and DepthOfField entirely. Use Bloom without mipmapBlur. Keep Vignette + Noise for polish at near-zero cost.
One EffectPass merges multiple effects into one draw call — always prefer <EffectPass camera={...} effects={[bloom, smaa, toneMap]} /> over separate passes.
Read references/postprocessing.md for full R3F setup, all effect parameters, custom effect authoring, and the selective bloom pattern.
references/tsl-guide.md — Complete TSL API: node types, uniforms, built-ins, control flow, varyings, NodeMaterial setup, GLSL→TSL migrationreferences/glsl-patterns.md — Common GLSL shader recipes with complete vertex + fragment codereferences/postprocessing.md — Full pmndrs/postprocessing guide: R3F setup, all effects, custom effect authoring