Navigate
Search topics across all sections
Shader Material
Built-in materials like MeshStandardMaterial are like buying pre-mixed paint at the store -- convenient, but limited to the colors on the shelf. ShaderMaterial is like mixing your own custom paint formula. You control every ingredient: the exact color of every pixel, how vertices move, how light interacts. Total creative freedom, but you need to understand the ingredients.
You write your first custom shader with a vertex and fragment shader. The mesh shows up, but it's solid pink (or black). No errors in your JavaScript code, but something is clearly wrong with the shader.
THREE.WebGLShader: gl.getShaderInfoLog() fragment: ERROR: 0:2: '' : No precision specified for (float)
Real-world
Imagine you are a paint chemist instead of a painter. A normal painter picks a can of “Ocean Blue” off the shelf and rolls it on. But you? You write the formula: 40% blue pigment, 10% white, a dash of green, and a metallic flake that shifts color depending on the viewing angle.
That is what ShaderMaterial lets you do. Instead of choosing MeshStandardMaterial and setting a color prop, you write two tiny programs (shaders) that run on the GPU. The vertex shader decides where each corner of the geometry goes. The fragment shader decides what color each pixel gets. Together, they can create effects that no pre-built material can match.
Vertices
position, normal, uv
Vertex Shader
Where each point goes
Rasterization
GPU fills triangles
Fragment Shader
Color of each pixel
Screen!
Pixels on display
Mixing Your First Formula
A shader material needs exactly two GLSL programs: a vertex shader that positions your geometry, and a fragment shader that colors each pixel. Let's write them step by step.
Step 1: The vertex shader -- where things go
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix
* modelViewMatrix * vec4(position, 1.0);
}
`This is the simplest vertex shader: it takes each vertex position, transforms it from 3D world space to 2D screen space, and passes the UV coordinates along to the fragment shader. The position, uv, projectionMatrix, and modelViewMatrix are all provided automatically by ShaderMaterial.
Step 2: The fragment shader -- what color each pixel gets
fragmentShader: `
varying vec2 vUv;
void main() {
vec3 color = mix(
vec3(1.0, 0.0, 0.0),
vec3(0.0, 0.0, 1.0),
vUv.x
);
gl_FragColor = vec4(color, 1.0);
}
`This fragment shader creates a gradient from red to blue across the surface. mix() blends between two colors based on vUv.x, which goes from 0 on the left to 1 on the right. Every pixel gets a unique color based on its UV position.
Step 3: Wire it up in R3F
<mesh>
<planeGeometry args={[4, 4]} />
<shaderMaterial
vertexShader={vertexShader}
fragmentShader={fragmentShader}
/>
</mesh>Pass your GLSL strings to the shaderMaterial component and attach it to any geometry. That is it -- you have written your first custom shader. Every pixel is now colored by your formula instead of a pre-built material.
Step 4: Add time-based animation
const uniforms = useRef({
uTime: { value: 0 }
})
useFrame(({ clock }) => {
uniforms.current.uTime.value = clock.elapsedTime
})
<shaderMaterial
uniforms={uniforms.current}
vertexShader={vs}
fragmentShader={fs}
/>Pass a time value as a uniform and update it every frame with useFrame. Inside your GLSL code, use uTime to animate colors, wave patterns, or vertex positions. The key: define uniforms with useRef so the object reference stays stable.
What you just learned
ShaderMaterial is like mixing your own paint formula -- total control over how every pixel is colored.
Every shader needs two programs: a vertex shader (where things go) and a fragment shader (what color they are).
ShaderMaterial auto-injects built-in uniforms (projectionMatrix, modelViewMatrix) and attributes (position, uv, normal).
Always define uniforms with useRef, never inline. Inline objects cause expensive shader recompilation.
RawShaderMaterial is the blank-slate version where nothing is injected automatically.
Question
If ShaderMaterial auto-injects built-in uniforms and attributes, when would you ever want to use RawShaderMaterial where you have to declare everything yourself?
Think about it...
Your shader works perfectly on desktop but shows a solid pink mesh on mobile. What is most likely the issue?
Hint: Think about what ShaderMaterial gives you automatically that RawShaderMaterial does not...
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set amplitude to 0 — smooth sphere!
Try This!
Beginner
Max frequency — spiky ball!
Try This!
Beginner
Change colorA and colorB — new palette
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
<rawShaderMaterial
vertexShader={`
attribute vec3 position;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
void main() {
gl_Position = projectionMatrix
* modelViewMatrix * vec4(position, 1.0);
}
`}
fragmentShader={`
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`}
/>void main() {
// ERROR: position is vec3, gl_Position needs vec4
gl_Position = projectionMatrix
* modelViewMatrix * position;
}function BadShader() {
// New object every render = recompilation!
return (
<shaderMaterial
uniforms={{
uTime: { value: 0 },
uColor: { value: new THREE.Color('red') },
}}
vertexShader={vs}
fragmentShader={fs}
/>
)
}Best Practices
Start with ShaderMaterial
Unless you need full control over the GLSL preamble, use ShaderMaterial. It auto-injects uniforms, attributes, and precision. Switch to RawShaderMaterial only when needed.
Stabilize Uniform References
Define uniforms with useRef or useMemo, never inline in JSX. Mutate only the .value property inside useFrame. A new object reference triggers expensive shader recompilation every frame.
Use High-Segment Geometry
Vertex displacement effects need enough vertices to look smooth. Use at least 64x64 segments for spheres and 32x32 for planes when doing displacement in the vertex shader.
Check the Browser Console
GLSL compilation errors appear in the browser console with line numbers. If your shader produces a pink or black mesh, open the console first -- the error message will point you to the exact line in your GLSL code.