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.

← Three.js From ZeroS13-09 · R3F Mastery

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

"<rippleMaterial /> is undefined." Forgot extend(). Or extend was called after the component tried to render.
"Uniforms don't update." You're setting ref.current.uniforms.uTime the raw way. With shaderMaterial, it's just ref.current.uTime = ... — no .uniforms, no .value.
"TS complains about props." Add the declare module block. Or, for ad-hoc usage, use {...{ rippleMaterial: {} } as any} as a temporary escape hatch.

Exercises

  1. Convert Season 0's hologram. Take the S0-06 hologram shader; rebuild it as a drei shaderMaterial. Compare line counts.
  2. Reactive shader. Pass game state into shader uniforms via zustand: const score = useGame(s => s.score); ref.current.uScore = score in useFrame. Material reacts to state.
  3. Build a library. Three or four reusable shader materials in a single file. Import the JSX components into any project.