React Three Fiber Shaders Quick Start import { Canvas, useFrame, extend } from '@react-three/fiber' import { shaderMaterial } from '@react-three/drei' import { useRef } from 'react' import * as THREE from 'three'
// Create custom shader material
const ColorShiftMaterial = shaderMaterial(
// Uniforms
{ time: 0, color: new THREE.Color(0.2, 0.0, 0.1) },
// Vertex shader
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
},
// Fragment shader
uniform float time;
uniform vec3 color;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv.x + sin(time), vUv.y + cos(time), color.b, 1.0);
}
)
// Extend so it can be used as JSX extend({ ColorShiftMaterial })
function ShaderMesh() { const materialRef = useRef()
useFrame(({ clock }) => { materialRef.current.time = clock.elapsedTime })
return (
export default function App() { return ( ) }
shaderMaterial (Drei)
The recommended way to create shader materials in R3F.
Basic Pattern import { shaderMaterial } from '@react-three/drei' import { extend } from '@react-three/fiber' import * as THREE from 'three'
// 1. Define the material const MyShaderMaterial = shaderMaterial( // Uniforms object { time: 0, color: new THREE.Color(1, 0, 0), opacity: 1, map: null, }, // Vertex shader (GLSL) ` varying vec2 vUv; varying vec3 vPosition;
void main() {
vUv = uv;
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
,
// Fragment shader (GLSL)
uniform float time;
uniform vec3 color;
uniform float opacity;
uniform sampler2D map;
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(map, vUv);
gl_FragColor = vec4(color * texColor.rgb, opacity);
}
` )
// 2. Extend R3F extend({ MyShaderMaterial })
// 3. Use in component function MyMesh() { const materialRef = useRef()
useFrame(({ clock }) => { materialRef.current.time = clock.elapsedTime })
return (
Hot Module Replacement (HMR)
The key prop on shaderMaterial enables live shader editing without page refresh:
const MyMaterial = shaderMaterial( { time: 0 }, vertexShader, fragmentShader )
extend({ MyMaterial })
// MyMaterial.key changes when shader code changes
When you edit shader code, the material automatically updates. Without key, you'd need to refresh the page to see changes.
TypeScript Support import { shaderMaterial } from '@react-three/drei' import { extend, Object3DNode } from '@react-three/fiber' import * as THREE from 'three'
// Define uniform types type WaveMaterialUniforms = { time: number amplitude: number color: THREE.Color }
const WaveMaterial = shaderMaterial(
{
time: 0,
amplitude: 0.5,
color: new THREE.Color('hotpink'),
} as WaveMaterialUniforms,
// vertex shader
...,
// fragment shader
...
)
// Extend with proper types extend({ WaveMaterial })
// Declare for TypeScript declare module '@react-three/fiber' { interface ThreeElements { waveMaterial: Object3DNode< typeof WaveMaterial & THREE.ShaderMaterial, typeof WaveMaterial > } }
Raw THREE.ShaderMaterial
For full control without Drei helper.
import { useFrame } from '@react-three/fiber' import { useMemo, useRef } from 'react' import * as THREE from 'three'
function CustomShaderMesh() { const materialRef = useRef()
const shaderMaterial = useMemo(() => {
return new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
color: { value: new THREE.Color('cyan') },
resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
},
vertexShader: varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
},
fragmentShader: `
uniform float time;
uniform vec3 color;
uniform vec2 resolution;
varying vec2 vUv;
void main() {
vec2 st = gl_FragCoord.xy / resolution;
float pattern = sin(st.x * 20.0 + time) * sin(st.y * 20.0 + time);
gl_FragColor = vec4(color * pattern, 1.0);
}
`,
side: THREE.DoubleSide,
transparent: true,
})
}, [])
useFrame(({ clock }) => { shaderMaterial.uniforms.time.value = clock.elapsedTime })
return (
Uniforms Common Uniform Types const MyMaterial = shaderMaterial( { // Numbers time: 0, intensity: 1.5,
// Vectors
resolution: new THREE.Vector2(1920, 1080),
lightPosition: new THREE.Vector3(5, 10, 5),
bounds: new THREE.Vector4(0, 0, 1, 1),
// Color (becomes vec3)
color: new THREE.Color('#ff0000'),
// Matrices
customMatrix: new THREE.Matrix4(),
// Textures
map: null, // sampler2D
cubeMap: null, // samplerCube
// Arrays
positions: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()],
}, vertexShader, fragmentShader )
GLSL Declarations // In shader code uniform float time; uniform float intensity; uniform vec2 resolution; uniform vec3 lightPosition; uniform vec3 color; // THREE.Color becomes vec3 uniform vec4 bounds; uniform mat4 customMatrix; uniform sampler2D map; uniform samplerCube cubeMap; uniform vec3 positions[3];
Updating Uniforms function AnimatedShader() { const materialRef = useRef()
useFrame(({ clock, mouse, viewport }) => { // Direct value update materialRef.current.time = clock.elapsedTime
// Vector update
materialRef.current.resolution.set(viewport.width, viewport.height)
// Color update
materialRef.current.color.setHSL((clock.elapsedTime * 0.1) % 1, 1, 0.5)
// Or via uniforms object (for THREE.ShaderMaterial)
// materialRef.current.uniforms.time.value = clock.elapsedTime
})
return (
Varyings
Pass data from vertex to fragment shader.
// Vertex shader varying vec2 vUv; varying vec3 vNormal; varying vec3 vPosition; varying vec3 vWorldPosition;
void main() { vUv = uv; vNormal = normalize(normalMatrix * normal); vPosition = position; vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
// Fragment shader varying vec2 vUv; varying vec3 vNormal; varying vec3 vPosition; varying vec3 vWorldPosition;
void main() { // Use interpolated values gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); }
Common Shader Patterns Texture Sampling import { useTexture } from '@react-three/drei'
function TexturedShaderMesh() { const texture = useTexture('/textures/color.jpg') const materialRef = useRef()
return (
// Shader
const TextureMaterial = shaderMaterial(
{ map: null },
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
},
`
uniform sampler2D map;
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(map, vUv);
gl_FragColor = texColor;
}
` )
Vertex Displacement const WaveMaterial = shaderMaterial( { time: 0, amplitude: 0.5, frequency: 2.0 }, ` uniform float time; uniform float amplitude; uniform float frequency; varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
// Wave displacement
pos.z += sin(pos.x * frequency + time) * amplitude;
pos.z += sin(pos.y * frequency + time) * amplitude;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
,
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv, 1.0, 1.0);
}
`
)
extend({ WaveMaterial })
function WavePlane() { const ref = useRef()
useFrame(({ clock }) => { ref.current.time = clock.elapsedTime })
return (
Fresnel Effect const FresnelMaterial = shaderMaterial( { fresnelColor: new THREE.Color('cyan'), baseColor: new THREE.Color('navy') }, ` varying vec3 vNormal; varying vec3 vWorldPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
,
uniform vec3 fresnelColor;
uniform vec3 baseColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
float fresnel = pow(1.0 - dot(viewDirection, vNormal), 3.0);
gl_FragColor = vec4(mix(baseColor, fresnelColor, fresnel), 1.0);
}
` )
Noise Functions // Simple random float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453); }
// Value noise float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st);
float a = random(i); float b = random(i + vec2(1.0, 0.0)); float c = random(i + vec2(0.0, 1.0)); float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; }
// FBM (Fractal Brownian Motion) float fbm(vec2 st) { float value = 0.0; float amplitude = 0.5;
for (int i = 0; i < 5; i++) { value += amplitude * noise(st); st = 2.0; amplitude = 0.5; }
return value; }
Gradient // Linear gradient vec3 gradient = mix(colorA, colorB, vUv.y);
// Radial gradient float dist = distance(vUv, vec2(0.5)); vec3 radial = mix(centerColor, edgeColor, dist * 2.0);
// Smooth gradient float t = smoothstep(0.0, 1.0, vUv.y); vec3 smooth = mix(colorA, colorB, t);
Dissolve Effect
const DissolveMaterial = shaderMaterial(
{ progress: 0, noiseScale: 10.0, edgeColor: new THREE.Color('orange') },
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
},
`
uniform float progress;
uniform float noiseScale;
uniform vec3 edgeColor;
varying vec2 vUv;
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
void main() {
float n = noise(vUv * noiseScale);
if (n < progress) {
discard;
}
float edge = smoothstep(progress, progress + 0.1, n);
vec3 baseColor = vec3(0.5);
gl_FragColor = vec4(mix(edgeColor, baseColor, edge), 1.0);
}
` )
Extending Built-in Materials onBeforeCompile
Modify existing material shaders.
import { useRef, useEffect } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three'
function ModifiedStandardMaterial() { const materialRef = useRef() const shaderRef = useRef()
useEffect(() => { if (materialRef.current) { materialRef.current.onBeforeCompile = (shader) => { // Add custom uniform shader.uniforms.time = { value: 0 } shaderRef.current = shader
// Add uniform declaration
shader.vertexShader = 'uniform float time;\n' + shader.vertexShader
// Modify vertex shader
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
transformed.y += sin(position.x * 10.0 + time) * 0.1;
`
)
}
}
}, [])
useFrame(({ clock }) => { if (shaderRef.current) { shaderRef.current.uniforms.time.value = clock.elapsedTime } })
return (
Common Injection Points
// Vertex shader chunks
'#include
// Fragment shader chunks
'#include
GLSL Built-in Functions Math Functions // Basic abs(x), sign(x), floor(x), ceil(x), fract(x) mod(x, y), min(x, y), max(x, y), clamp(x, min, max) mix(a, b, t), step(edge, x), smoothstep(edge0, edge1, x)
// Trigonometry sin(x), cos(x), tan(x) asin(x), acos(x), atan(y, x), atan(x)
// Exponential pow(x, y), exp(x), log(x), sqrt(x)
Vector Functions length(v), distance(p0, p1), dot(x, y), cross(x, y) normalize(v), reflect(I, N), refract(I, N, eta)
Instanced Shaders import { useRef, useMemo } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three'
function InstancedShaderMesh({ count = 1000 }) { const meshRef = useRef()
// Create instance attributes const { offsets, colors } = useMemo(() => { const offsets = new Float32Array(count * 3) const colors = new Float32Array(count * 3)
for (let i = 0; i < count; i++) {
offsets[i * 3] = (Math.random() - 0.5) * 20
offsets[i * 3 + 1] = (Math.random() - 0.5) * 20
offsets[i * 3 + 2] = (Math.random() - 0.5) * 20
colors[i * 3] = Math.random()
colors[i * 3 + 1] = Math.random()
colors[i * 3 + 2] = Math.random()
}
return { offsets, colors }
}, [count])
const shaderMaterial = useMemo(() => { return new THREE.ShaderMaterial({ uniforms: { time: { value: 0 } }, vertexShader: ` attribute vec3 offset; attribute vec3 instanceColor; varying vec3 vColor;
void main() {
vColor = instanceColor;
vec3 pos = position + offset;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`
})
}, [])
useFrame(({ clock }) => { shaderMaterial.uniforms.time.value = clock.elapsedTime })
return (
External Shader Files With Vite/Webpack // shaders/vertex.glsl varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
// shaders/fragment.glsl uniform float time; varying vec2 vUv; void main() { gl_FragColor = vec4(vUv, sin(time), 1.0); }
// Component.tsx import vertexShader from './shaders/vertex.glsl?raw' import fragmentShader from './shaders/fragment.glsl?raw'
const MyMaterial = shaderMaterial( { time: 0 }, vertexShader, fragmentShader )
Vite Config for GLSL // vite.config.js import glsl from 'vite-plugin-glsl'
export default { plugins: [glsl()] }
Material Properties <myShaderMaterial // Rendering transparent={true} opacity={1.0} side={THREE.DoubleSide} depthTest={true} depthWrite={true}
// Blending blending={THREE.NormalBlending} // NormalBlending, AdditiveBlending, SubtractiveBlending, MultiplyBlending
// Wireframe wireframe={false}
// Custom uniforms time={0} color="hotpink" />
Debugging Shaders function DebugShaderMesh() { const materialRef = useRef()
useEffect(() => { // Log compiled shaders if (materialRef.current) { console.log('Vertex:', materialRef.current.vertexShader) console.log('Fragment:', materialRef.current.fragmentShader) } }, [])
return (
// Debug normals (in vertex: vNormal = normal)
// gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);
}
`}
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`}
/>
</mesh>
) }
Performance Tips Minimize uniforms: Group related values into vectors Avoid conditionals: Use mix/step instead of if/else Precalculate in JS: Move static calculations out of shaders Use textures for lookup: Complex functions as texture lookups Limit overdraw: Avoid unnecessary transparent objects // Instead of: if (value > 0.5) { color = colorA; } else { color = colorB; }
// Use: color = mix(colorB, colorA, step(0.5, value));
See Also r3f-materials - Built-in material types r3f-postprocessing - Full-screen shader effects r3f-textures - Texture sampling in shaders