πŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
πŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Shaders in Three.js 3D WebGL

Learn about Shaders in this comprehensive Three.js 3D WebGL tutorial. Understand the relationship between Vertex and Fragment shaders, and how to pass Uniforms.

⚑ Total XP: 0|πŸ’» threejs XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core shader concepts.

Quick Quiz //

What language are Three.js shaders written in?


πŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
πŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Listen up. If you're building modern applications, understanding Shaders in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.

1Shaders: Programming the GPU Directly

Welcome to the most advanced topic in WebGL: Shaders. If you master shaders, you unlock infinite graphical power β€” everything Three.js's built-in materials do (MeshStandardMaterial, MeshBasicMaterial) is itself implemented as a shader under the hood.

Writing custom shaders means stepping below the abstraction Three.js normally provides, giving you full control over exactly how each vertex is positioned and each pixel is colored, at the cost of needing to handle the low-level math yourself.

βœ•
β€”
+
// πŸ§™β€β™‚οΈ Welcome to the dark arts
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

2Why GLSL Runs So Fast

Shaders are small programs written in GLSL (OpenGL Shading Language) that run directly on the GPU. They are insanely fast because they run in parallel β€” a GPU executes the same shader program simultaneously across thousands of vertices or pixels at once, unlike a CPU processing them one at a time.

This massively parallel execution model is exactly why GPUs excel at graphics: a single fragment shader might run millions of times per frame (once per pixel), and the GPU's architecture is purpose-built to handle that scale efficiently.

βœ•
β€”
+
// GLSL looks a lot like C++
void main() {
  gl_Position = vec4(1.0);
}
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

3GLSL: The Shading Language

Shaders for WebGL/Three.js are written in GLSL β€” a C-like language specifically designed for graphics programming, with built-in types for vectors (vec2, vec3, vec4) and matrices that make 3D math operations concise.

GLSL code is passed to Three.js as plain strings (often written in template literals), which get compiled by the browser's WebGL implementation at runtime β€” a syntax error in your GLSL string won't be caught by TypeScript, only surfaced as a WebGL compilation error in the browser console.

βœ•
β€”
+
const shaderCode = `... ??? code ...`;
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

4The Two-Stage Shader Pipeline

There are two types of shaders that work together: The Vertex Shader (which positions every vertex) and the Fragment Shader (which colors every pixel) β€” every custom-shaded object needs both, since one handles shape and the other handles appearance.

The vertex shader runs first, once per vertex, transforming 3D positions into screen space. The fragment shader then runs once per pixel covered by the resulting triangles, deciding the final color. This two-stage pipeline mirrors the geometry/material split you learned earlier β€” vertex shaders relate to shape, fragment shaders relate to surface appearance.

βœ•
β€”
+
// 1. Vertex Shader (Shape)
// 2. Fragment Shader (Color)
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

5Wiring GLSL into Three.js with ShaderMaterial

To use shaders in Three.js, you use ShaderMaterial. You pass your GLSL code as strings to the vertexShader and fragmentShader properties β€” this is the bridge connecting your custom GLSL programs to the normal Three.js mesh/material system.

Once wrapped in a ShaderMaterial, your custom shader behaves like any other material: attach it to a mesh's material slot, and it participates in the render pipeline exactly like MeshStandardMaterial or MeshBasicMaterial would, just with fully custom rendering logic underneath.

βœ•
β€”
+
const material = new THREE.ShaderMaterial({
  vertexShader: `...`,
  fragmentShader: `...`
});
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

6Anatomy of a Basic Fragment Shader

Let's look at a basic Fragment Shader. gl_FragColor is a built-in variable that expects a vec4 (Red, Green, Blue, Alpha) β€” every fragment shader's entire job boils down to assigning a value to this one variable for each pixel.

Color channels in GLSL run from 0.0 to 1.0 rather than the 0-255 range used in CSS β€” vec4(1.0, 0.0, 0.0, 1.0) is pure red at full opacity, which is why converting familiar hex colors to GLSL requires dividing each channel by 255 first.

βœ•
β€”
+
const frag = `
  void main() {
    // Red color! (R=1, G=0, B=0, Alpha=1)
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`;
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

7Uniforms: Passing Data from JS to GLSL

How do we pass data from JavaScript to GLSL? We use 'Uniforms'. Uniforms are variables that stay the same for every vertex/pixel in a single frame (like time or mouse position) β€” they're the only bridge for feeding dynamic JavaScript state into the otherwise self-contained GLSL program.

A uniform is declared as an object with a value property on the JavaScript side, and referenced by matching name and type inside the GLSL shader code itself β€” changing uniforms.u_time.value from JavaScript each frame is how you animate a shader without recompiling it.

βœ•
β€”
+
const uniforms = {
  u_time: { value: 0.0 }
};

<shaderMaterial uniforms={uniforms} />
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

8Uniforms vs Varyings: Knowing the Difference

The variables passed from JavaScript (CPU) to the GLSL Shaders (GPU) that remain constant across all vertices/pixels for a given frame are called Uniforms β€” the name reflects that the value is 'uniform' (identical) across every invocation of the shader in that frame.

This is distinct from a 'varying', which instead carries data from the vertex shader to the fragment shader and interpolates smoothly between vertices (used for passing UV coordinates in the earlier example) β€” uniforms don't vary per-vertex, varyings do.

βœ•
β€”
+
const ??? = { u_color: { value: new THREE.Color('red') } };
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

9A Complete Animated Shader Example

Let's see a Shader in action! Below is a plane using a custom Fragment Shader. It uses a sine wave based on the UV coordinates to create a hypnotic pattern β€” this example ties together everything covered so far: a vertex shader passing UVs as a varying, a fragment shader using those UVs plus a u_time uniform, and useFrame updating that uniform every frame.

The vUv varying carries each pixel's UV coordinate (0-1 across the plane's surface) from the vertex shader into the fragment shader, where sin(vUv.x * 10.0 + u_time) generates a shifting wave pattern that animates smoothly as u_time increases.

βœ•
β€”
+
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';

const ShaderPlane = () => {
  const materialRef = useRef();

  useFrame(({ clock }) => {
    if (materialRef.current) {
      materialRef.current.uniforms.u_time.value = clock.getElapsedTime();
    }
  });

  const vertexShader = `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `;

  const fragmentShader = `
    uniform float u_time;
    varying vec2 vUv;
    void main() {
      float r = abs(sin(vUv.x * 10.0 + u_time));
      float g = abs(sin(vUv.y * 10.0 + u_time));
      gl_FragColor = vec4(r, g, 1.0, 1.0);
    }
  `;

  return (
    <mesh>
      <planeGeometry args={[4, 4]} />
      <shaderMaterial 
        ref={materialRef}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
        uniforms={{ u_time: { value: 0 } }}
      />
    </mesh>
  );
};

const App = () => (
  <Canvas camera={{ position: [0, 0, 3] }}>
    <ShaderPlane />
  </Canvas>
);

render(<App />);
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

10Driving Shader Animation with useFrame

Notice the useFrame hook? We are constantly updating the u_time uniform with the clock's elapsed time. The GPU then uses that time to animate the sine wave instantly β€” this is the standard pattern for any time-based shader animation in React Three Fiber.

Accessing materialRef.current.uniforms directly (rather than through React state) matters for performance here: updating a uniform value doesn't trigger a React re-render, letting the animation run at full frame rate without the overhead of React's reconciliation on every frame.

βœ•
β€”
+
useFrame(({ clock }) => {
  matRef.current.uniforms.u_time.value = clock.getElapsedTime();
});
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

11Where Shader Mastery Leads

Shaders are extremely complex, requiring strong math skills (Trigonometry, Vectors). But they are the secret to mind-blowing Awwwards websites β€” the fluid, generative visual effects on award-winning portfolio and product sites are almost always hand-written GLSL, not off-the-shelf materials.

Most developers don't need to write shaders from scratch for everyday work β€” libraries like drei and shader-collections provide pre-built effects β€” but understanding uniforms, varyings, and the vertex/fragment split makes it possible to customize or debug those pre-built shaders when the need arises.

βœ•
β€”
+
// πŸ§™β€β™‚οΈ You are now a WebGL Wizard
localhost:3000
Browser Preview
WebGL Output
3D Scene rendered. Objects: 4, Draw Calls: Optimized.

12Step-by-Step Breakdown

Welcome to the most advanced topic in WebGL: Shaders. If you master shaders, you unlock infinite graphical power.

Shaders are small programs written in GLSL (OpenGL Shading Language) that run directly on the GPU. They are insanely fast because they run in parallel.

What language are Shaders written in for WebGL/Three.js?

  • β†’JavaScript
  • β†’GLSL
  • β†’Python

There are two types of shaders that work together: The Vertex Shader (which positions every vertex) and the Fragment Shader (which colors every pixel).

To use shaders in Three.js, you use ShaderMaterial. You pass your GLSL code as strings to the vertexShader and fragmentShader properties.

Which of the two shaders is responsible for determining the final COLOR of every single pixel on the screen?

  • β†’Vertex Shader
  • β†’Fragment Shader
  • β†’Color Shader

Let's look at a basic Fragment Shader. gl_FragColor is a built-in variable that expects a vec4 (Red, Green, Blue, Alpha).

How do we pass data from JavaScript to GLSL? We use 'Uniforms'. Uniforms are variables that stay the same for every vertex/pixel in a single frame (like time or mouse position).

What do we call variables that are passed from JavaScript (CPU) to the GLSL Shaders (GPU) that remain constant across all vertices/pixels for a given frame?

  • β†’Varyings
  • β†’Attributes
  • β†’Uniforms

Let's see a Shader in action! Below is a plane using a custom Fragment Shader. It uses a sine wave based on the UV coordinates to create a hypnotic pattern.

Notice the useFrame hook? We are constantly updating the u_time uniform with the clock's elapsed time. The GPU then uses that time to animate the sine wave instantly.

Shaders are extremely complex, requiring strong math skills (Trigonometry, Vectors). But they are the secret to mind-blowing Awwwards websites.

Level Up πŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Custom Shader Effects Should Never Be the Only Carrier of Meaning

A hypnotic animated shader pattern is purely decorative to assistive technology β€” if a shader effect signals something functionally important (like a 'processing' or 'error' state), pair it with a text or ARIA-live announcement rather than relying on the visual effect alone.

<div aria-live="polite">{status === 'processing' ? 'Processing…' : ''}</div>

SEO Implications

  • 1

    GLSL Source Code Is Invisible to Search Crawlers

    Shader strings execute entirely inside the WebGL context and are never part of the rendered DOM or accessibility tree β€” this page's SEO value comes from explaining vertex/fragment shader concepts and uniforms in prose, not from any specific GLSL snippet.

Best Practices

Update Uniforms Directly via Refs Instead of React State

Driving per-frame uniform updates (like u_time) through React state and re-renders adds unnecessary overhead. Access materialRef.current.uniforms directly inside useFrame so animation runs at full frame rate without triggering React's reconciliation.

Keep Expensive Math Out of the Fragment Shader When Possible

Fragment shaders run once per pixel β€” potentially millions of times per frame β€” so expensive calculations there cost far more than the same calculation done once in the vertex shader (which runs only once per vertex) or precomputed on the CPU and passed as a uniform.

Frequent Bugs

THE BUG

A ShaderMaterial renders as a black or blank mesh with no visible GLSL compilation error surfaced in the app.

THE FIX

GLSL syntax errors are only reported in the browser's developer console (Sources/Console tab), not as a JavaScript exception TypeScript or React would catch β€” always check the browser console first when a custom shader material renders unexpectedly blank.

Real-World Examples

Animated Hero Backgrounds on Award-Winning Sites

Agency and product landing pages that feature flowing, generative gradient or noise backgrounds are almost always a single full-screen plane with a custom ShaderMaterial, animated via a u_time uniform updated in useFrame β€” computationally far cheaper than an equivalent video or animated GIF background.

const fragmentShader = `
  uniform float u_time;
  varying vec2 vUv;
  void main() {
    float noise = sin(vUv.x * 8.0 + u_time) * cos(vUv.y * 8.0 + u_time);
    gl_FragColor = vec4(vec3(noise * 0.5 + 0.5), 1.0);
  }
`;

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Not reading error messages carefully

Uncaught TypeError: Cannot read properties of undefined (reading 'length') // Solution: Ensure the variable you are calling .length on is initialized as a string or an array, not undefined.

The Solution //

Most of the time, the compiler or interpreter tells you exactly what line caused the crash and why. Read stack traces from the top down to identify the root cause.

The Error //

Hardcoding sensitive credentials

// Wrong const API_KEY = 'sk-123456789'; // Correct const API_KEY = process.env.API_KEY;

The Solution //

Never hardcode API keys, passwords, or secrets in your source code. Use environment variables (.env files) to keep them secure and out of version control.

Continue Learning