Navigate

Search topics across all sections

GitHub
Shaders

Uniforms & Varyings

Think of uniforms and varyings like a radio broadcast. A uniform is the radio station frequency -- the same signal goes out to every listener (every vertex and pixel gets the same value, like time or a color). A varying is like each listener's personal volume dial -- it is different for every pixel, smoothly interpolated across the surface of each triangle.

You add a uTime uniform to animate your shader. It works for about 2 seconds, then the frame rate drops to 5fps. The animation looks correct, but the entire page becomes unusable. Your fan starts spinning.

terminal
Performance collapse: 60fps -> 5fps. Shader program recompiling on every frame due to new uniform object reference being created via useState + inline uniforms.

Real-world

Picture a radio station broadcasting to a city. The station transmits one signal at one frequency -- every radio in the city receives the same broadcast. That is a uniform. When you send uTime = 3.5 to your shader, every single vertex and every single pixel receives that exact same value of 3.5. One value, shared by everyone.

Now think about the volume dial on each radio. Every listener adjusts it differently. One person has it at 80%, another at 30%. And if you walked smoothly between them, the volume would blend smoothly too. That is a varying. The vertex shader sets a value at each corner of a triangle, and the GPU smoothly blends those values across every pixel inside the triangle.

JavaScript

Set uniform values

Vertex Shader

Reads uniforms, writes varyings

GPU Interpolation

Blends varyings per-pixel

Fragment Shader

Reads interpolated varyings

Pixel Color!

Final output

Tuning Into the Broadcast

Let's set up uniforms and varyings step by step. We will send a time value from JavaScript, pass UV coordinates from vertex to fragment, and use both to create an animated gradient.

Step 1: Define uniforms with useRef

UniformSetup.tsxTSX
const uniforms = useRef({
  uTime: { value: 0 },
  uColor: { value: new THREE.Color('#ff6b6b') },
})

Uniforms are the radio frequency. Define them once with useRef so the object reference stays stable. If you used useState or inline objects, React would create a new reference every render, causing Three.js to recompile the shader -- a frame rate killer.

Step 2: Update uniform values in useFrame

UniformUpdate.tsxTSX
useFrame(({ clock }) => {
  uniforms.current.uTime.value = clock.elapsedTime
})

Mutate only the .value property, never replace the object. This is like changing what song the radio station plays without changing the frequency. The shader receives the new value on the next frame without any recompilation.

Step 3: Declare varyings in both shaders

VaryingBridge.glslGLSL
// vertex shader
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix
    * modelViewMatrix * vec4(position, 1.0);
}

// fragment shader
varying vec2 vUv;
uniform float uTime;
void main() {
  vec3 color = mix(
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 0.0, 1.0),
    vUv.x + sin(uTime) * 0.5
  );
  gl_FragColor = vec4(color, 1.0);
}

The varying vUv must be declared identically in both shaders -- same name, same type. The vertex shader writes a value at each vertex corner. The GPU then smoothly interpolates those values for every pixel between the corners. That is the volume dial: each pixel gets its own unique blended value.

Step 4: Wire it all together

Complete.tsxTSX
<mesh>
  <planeGeometry args={[4, 4]} />
  <shaderMaterial
    uniforms={uniforms.current}
    vertexShader={vs}
    fragmentShader={fs}
  />
</mesh>

You now have an animated gradient. The uniform uTime drives the animation (same for all pixels), while the varying vUv gives each pixel a unique position-based value (different for each pixel). Uniform = broadcast, varying = personal dial.

What you just learned

Uniforms are the radio frequency: one value broadcast to every vertex and pixel (time, color, mouse position).

Varyings are the volume dial: a per-vertex value that the GPU smoothly interpolates across each triangle for the fragment shader.

Always define uniforms with useRef and mutate only .value -- never replace the object or use useState.

Varyings must be declared with the exact same name and type in both vertex and fragment shaders.

Use Three.js types (Vector2, Vector3, Color) for uniform values, not plain arrays or objects.

Question

If the GPU interpolates varyings smoothly across each triangle, what would happen if a triangle had one red corner, one green corner, and one blue corner? What color would the center pixel be?

Think about it...

You declare 'varying vec2 vUv' in your vertex shader and 'varying vec2 vUV' in your fragment shader. The mesh renders but the gradient is missing -- it's just a solid color. Why?

Hint: GLSL treats uppercase and lowercase letters as completely different...

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 speed to 0 — frozen shader

Try This!

Beginner

Max fresnelPower — thin glow edge

Try This!

Beginner

Toggle wireframe — see the geometry

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Creating new uniform objects every frame
Mutating .value is correct; replacing the object is not
Don't do this
UniformUpdate.tsxTSX
function BadShader() {
  const [time, setTime] = useState(0)

  useFrame(({ clock }) => {
    setTime(clock.elapsedTime) // Re-render!
  })

  return (
    <shaderMaterial
      uniforms={{ uTime: { value: time } }}
      vertexShader={vs}
      fragmentShader={fs}
    />
  )
}
Uniforms should be updated by mutating the .value property, never by replacing the entire object. Using useState forces a React re-render and creates a new uniforms reference, which triggers expensive shader recompilation. Use useRef and mutate .value inside useFrame.
2
Mismatched varying names between shaders
Varyings must have identical name and type in both shaders
Don't do this
VaryingMatch.glslGLSL
// Vertex declares vUv
const vs = `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix
      * modelViewMatrix * vec4(position, 1.0);
  }
`

// Fragment declares vUV (capital V) — MISMATCH!
const fs = `
  varying vec2 vUV;
  void main() {
    gl_FragColor = vec4(vUV, 0.0, 1.0);
  }
`
GLSL is case-sensitive. 'vUv' and 'vUV' are different variables. If the varying names don't match between vertex and fragment shaders, the linker will report an error or the varying will be zeroed. Convention: prefix with lowercase 'v' and use camelCase (vUv, vNormal).
3
Using wrong JavaScript type for uniform values
GLSL types must match their JavaScript counterparts
Don't do this
UniformTypes.tsxTSX
const uniforms = useRef({
  // vec3 but passing an array — won't work!
  uColor: { value: [1.0, 0.0, 0.0] },
  // vec2 but passing a plain object
  uMouse: { value: { x: 0, y: 0 } },
})
Three.js expects specific JavaScript types for each GLSL uniform type. Plain arrays and objects don't work. Use the corresponding Three.js class (Vector2, Vector3, Color, Matrix4). Type mismatches fail silently, producing zeroed values in the shader.

Best Practices

useRef for Uniforms, Always

Wrap uniform definitions in useRef to keep the reference stable. Mutate only .value inside useFrame. Never use useState for values that update every frame.

Update Only When Changed

For uniforms like uColor that change on user interaction (not every frame), update them in event handlers or useEffect, not inside useFrame. No need to set the same value 60 times per second.

Prefix Naming Convention

Use consistent naming: prefix uniforms with 'u' (uTime, uColor), varyings with 'v' (vUv, vNormal), and custom attributes with 'a' (aOffset). This makes shader code self-documenting at a glance.

Match Types Exactly

Use THREE.Vector2 for vec2, THREE.Vector3 or THREE.Color for vec3, THREE.Matrix4 for mat4. Plain arrays and objects fail silently, producing zeroed values that are very hard to debug.