Shaders
Write the GPU programs yourself: move vertices and colour pixels with GLSL.
What it is
Every material is really a pair of small programs that run on the graphics card. With a ShaderMaterial you write them yourself in GLSL: the vertex shader positions each vertex, the fragment shader colours each pixel, and uniforms carry values in from JavaScript, such as the time, the sliders below, or the scroll position. drei's shaderMaterial() turns that into a JSX element with typed props.
When to use it
- Effects no built-in material can do: waves, dissolves, noise
- Animating thousands of points cheaply on the GPU
- Backgrounds and hero visuals that react to input
A wave surface driven by uniforms
Loading shader…
Each frame JavaScript only updates uTime; the roughly 26,000 vertices are moved on the GPU.
Show code · src/app/three-shaders/waveMaterial.ts
import { shaderMaterial } from "@react-three/drei";
import { extend, type ThreeElement } from "@react-three/fiber";
import { Color } from "three";
// A custom material: two small GLSL programs that run on the GPU.
// The vertex shader runs once per vertex and moves it; the fragment shader
// runs once per pixel and colours it. Uniforms are the values JavaScript
// passes in each frame.
const vertexShader = /* glsl */ `
uniform float uTime;
uniform float uAmplitude;
uniform float uFrequency;
varying float vHeight;
void main() {
vec3 p = position;
// Two crossing sine waves, drifting with time.
float h = sin(p.x * uFrequency + uTime) * cos(p.y * uFrequency + uTime * 0.8);
p.z += h * uAmplitude;
vHeight = h; // -1 … 1, handed to the fragment shader
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
`;
const fragmentShader = /* glsl */ `
uniform vec3 uColorA;
uniform vec3 uColorB;
varying float vHeight;
void main() {
// Low points get colour A, peaks colour B.
gl_FragColor = vec4(mix(uColorA, uColorB, vHeight * 0.5 + 0.5), 1.0);
#include <colorspace_fragment>
}
`;
export const WaveMaterial = shaderMaterial(
{ uTime: 0, uAmplitude: 0.35, uFrequency: 2.5, uColorA: new Color("#2b2370"), uColorB: new Color("#ff7aa2") },
vertexShader,
fragmentShader,
);
// Makes <waveMaterial /> available in JSX, with typed props.
extend({ WaveMaterial });
declare module "@react-three/fiber" {
interface ThreeElements {
waveMaterial: ThreeElement<typeof WaveMaterial>;
}
}
Show code · src/app/three-shaders/ShaderScene.tsx
"use client";
import { OrbitControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useRef, useState } from "react";
import { ThreeCanvas } from "@/components/three/ThreeCanvas";
// Imported for its side effect too: waveMaterial.ts calls extend(), which
// registers <waveMaterial>. (A type-only import would be dropped by the build.)
import { WaveMaterial } from "./waveMaterial";
import "./waveMaterial";
type Uniforms = InstanceType<typeof WaveMaterial>;
function Waves({ amplitude, frequency, colorA, colorB, animate }: { amplitude: number; frequency: number; colorA: string; colorB: string; animate: boolean }) {
const material = useRef<Uniforms>(null);
// Advance the shader's clock every frame (skipped when motion is reduced
// or the canvas is off screen, so the surface holds still).
useFrame((_, delta) => {
if (animate && material.current) material.current.uTime += delta;
});
return (
<mesh rotation-x={-Math.PI / 2.4}>
{/* 160 × 160 segments: enough vertices for the waves to look smooth. */}
<planeGeometry args={[5, 5, 160, 160]} />
<waveMaterial ref={material} uAmplitude={amplitude} uFrequency={frequency} uColorA={colorA} uColorB={colorB} />
</mesh>
);
}
export function ShaderScene() {
const [amplitude, setAmplitude] = useState(0.35);
const [frequency, setFrequency] = useState(2.5);
const [colorA, setColorA] = useState("#2b2370");
const [colorB, setColorB] = useState("#ff7aa2");
return (
<>
<ThreeCanvas camera={{ position: [0, 2.2, 4.2], fov: 45 }} label="A surface of moving waves coloured by height, drawn by a custom shader">
{(animate) => (
<>
<color attach="background" args={["#0f0e14"]} />
<Waves amplitude={amplitude} frequency={frequency} colorA={colorA} colorB={colorB} animate={animate} />
<OrbitControls minDistance={2.5} maxDistance={9} />
</>
)}
</ThreeCanvas>
<div className="mb-2 flex flex-wrap items-center gap-x-4 gap-y-2">
<label className="flex flex-1 basis-55 items-center gap-2 text-sm">
<code>uAmplitude</code>
<input className="min-w-0 flex-1" type="range" min={0} max={0.8} step={0.01} value={amplitude} onChange={(e) => setAmplitude(Number(e.target.value))} aria-label="Wave height" />
<span className="w-9 tabular-nums">{amplitude.toFixed(2)}</span>
</label>
<label className="flex flex-1 basis-55 items-center gap-2 text-sm">
<code>uFrequency</code>
<input className="min-w-0 flex-1" type="range" min={0.5} max={6} step={0.1} value={frequency} onChange={(e) => setFrequency(Number(e.target.value))} aria-label="Wave frequency" />
<span className="w-9 tabular-nums">{frequency.toFixed(1)}</span>
</label>
<label className="flex items-center gap-2 text-sm"><code>uColorA</code> <input type="color" value={colorA} onChange={(e) => setColorA(e.target.value)} /></label>
<label className="flex items-center gap-2 text-sm"><code>uColorB</code> <input type="color" value={colorB} onChange={(e) => setColorB(e.target.value)} /></label>
</div>
</>
);
}