This skill should be used when writing custom shaders for Three.js, creating visual effects with GLSL or TSL (Three Shader Language) for WebGL and WebGPU, debugging shader issues, building post-processing pipelines, implementing noise functions, procedural textures, or custom materials. Covers shader workflow, TSL node system, GLSL patterns, debugging, performance optimization, and post-processing with pmndrs/postprocessing.
From bopen-toolsnpx claudepluginhub b-open-io/claude-plugins --plugin bopen-toolsThis skill uses the workspace's default tool permissions.
README.mdreferences/glsl-patterns.mdreferences/postprocessing.mdreferences/tsl-guide.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
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