Three.js From Zero · Article s13-09
R3F Custom Shaders
R3F Custom Shaders is Article s13-09 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 13 · Article 09 · R3F Mastery
Drei's shaderMaterial helper turns a GLSL pair + a uniform object into a proper JSX component with typed props. The cleanest way to write reusable shader materials in R3F.
Code-walkthrough article
Pair with your favorite shader from Season 4 (graphics deep dive) — port it from raw ShaderMaterial to drei's shaderMaterial and watch the code shrink.
The raw approach (verbose)
function Mesh() {
const ref = useRef();
useFrame((s) => { ref.current.uniforms.uTime.value = s.clock.elapsedTime; });
return (
<mesh>
<sphereGeometry />
<shaderMaterial ref={ref} uniforms={{ uTime: { value: 0 } }}
vertexShader={vert} fragmentShader={frag} />
</mesh>
);
}
Works, but you're writing the uniforms structure by hand, updating refs manually, and prop types are any. Drei's shaderMaterial generates all of this.
The drei approach
import { shaderMaterial } from '@react-three/drei';
import { extend } from '@react-three/fiber';
const RippleMaterial = shaderMaterial(
{ uTime: 0, uColor: new THREE.Color('hotpink') }, // uniforms
/*glsl*/`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
/*glsl*/`
varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
void main() {
float ring = sin(length(vUv - 0.5) * 30.0 - uTime * 2.0);
gl_FragColor = vec4(uColor * ring, 1.0);
}
`,
);
extend({ RippleMaterial }); // register as a JSX element
Now it's a first-class JSX component:
function Mesh() {
const ref = useRef();
useFrame((s, dt) => { ref.current.uTime += dt; });
return (
<mesh>
<planeGeometry args={[3, 3]} />
<rippleMaterial ref={ref} uColor="cyan" />
</mesh>
);
}
Uniforms become props. The ref auto-points to the material with uniform names as direct properties (no .uniforms.uTime.value — just .uTime).
TypeScript
declare module '@react-three/fiber' {
interface ThreeElements {
rippleMaterial: ThreeElement<typeof RippleMaterial>;
}
}
One declare block makes <rippleMaterial /> typecheck. Props are inferred from the uniform definitions — uTime is a number, uColor accepts a Color or hex string. Real type safety in shader props.
Hot reload
shaderMaterial compiles the GLSL on first render. To hot-reload during dev, recreate the material when the source string changes:
const RippleMaterial = useMemo(() => shaderMaterial({ ... }, vert, frag), [vert, frag]);
Hooked up to Vite's HMR for the shader source files, you get instant shader iteration.
Why "extend"?
R3F has a registry of every Three.js class it can render as JSX. Custom classes need to be registered via extend so R3F knows to handle <rippleMaterial />. One-time call per session.
Common first-time pitfalls
ref.current.uniforms.uTime the raw way. With shaderMaterial, it's just ref.current.uTime = ... — no .uniforms, no .value.{...{ rippleMaterial: {} } as any} as a temporary escape hatch.Exercises
- Convert Season 0's hologram. Take the S0-06 hologram shader; rebuild it as a drei shaderMaterial. Compare line counts.
- Reactive shader. Pass game state into shader uniforms via zustand:
const score = useGame(s => s.score);ref.current.uScore = scorein useFrame. Material reacts to state. - Build a library. Three or four reusable shader materials in a single file. Import the JSX components into any project.
UP NEXT
S13-10 — R3F Game Capstone → Build a 3D infinite runner end-to-end.