Navigate

Search topics across all sections

GitHub
Shaders

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.

terminal
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

vertex.glslGLSL
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

fragment.glslGLSL
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

GradientPlane.tsxTSX
<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

AnimatedShader.tsxTSX
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.

1
Forgetting precision in RawShaderMaterial
RawShaderMaterial doesn't inject precision automatically
Don't do this
RawShaderPrecision.tsxTSX
<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);
    }
  `}
/>
RawShaderMaterial is a completely blank slate -- nothing is injected for you. You must declare 'precision mediump float;' at the top of both shaders. ShaderMaterial handles this automatically, which is why it's recommended for beginners.
2
Setting gl_Position with vec3 instead of vec4
gl_Position requires a 4D homogeneous coordinate
Don't do this
vertex.glslGLSL
void main() {
  // ERROR: position is vec3, gl_Position needs vec4
  gl_Position = projectionMatrix
    * modelViewMatrix * position;
}
gl_Position is a vec4 that uses homogeneous coordinates for perspective projection. The w component must be 1.0 for position vertices. Always wrap position in vec4(position, 1.0).
3
Creating new uniform objects on every render
Recreating the uniforms object causes shader recompilation
Don't do this
StableUniforms.tsxTSX
function BadShader() {
  // New object every render = recompilation!
  return (
    <shaderMaterial
      uniforms={{
        uTime: { value: 0 },
        uColor: { value: new THREE.Color('red') },
      }}
      vertexShader={vs}
      fragmentShader={fs}
    />
  )
}
When you define uniforms inline in JSX, React creates a new object reference on every render. Three.js detects this and recompiles the shader program, which is extremely expensive. Always define uniforms with useRef or useMemo, and mutate only the .value property inside useFrame.

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.