Threejs Shaders

Automate and integrate Three.js Shaders for custom GPU-powered rendering effects

Three.js Shaders is a community skill for writing custom shaders in Three.js, covering vertex shaders, fragment shaders, uniforms, shader chunks, and GLSL techniques for custom visual effects in 3D web applications.

What Is This?

Overview

Three.js Shaders provides guidance on writing custom GLSL shaders for Three.js rendering. It covers vertex shaders that transform vertex positions for geometry deformation and animation effects, fragment shaders that compute pixel colors for custom surface appearance, uniforms that pass dynamic data from JavaScript to shader programs each frame, shader chunks that extend built-in Three.js materials with custom code injection, and GLSL techniques that implement noise, patterns, and visual effects in shader code. The skill helps developers create unique visual effects.

Who Should Use This

This skill serves creative coders building custom visual effects, graphics programmers optimizing rendering with GPU code, and web developers extending Three.js materials with GLSL.

Why Use It?

Problems It Solves

Built-in materials cannot produce every desired visual effect. Procedural textures and animations are more efficient when computed on the GPU than the CPU. Geometry deformations applied per-vertex in JavaScript are too slow for real-time rendering. Extending existing materials requires understanding the Three.js shader chunk system.

Core Highlights

Vertex processor transforms positions for deformation effects. Fragment processor computes custom pixel colors and effects. Uniform manager passes time, resolution, and data to shaders. Chunk injector extends built-in materials with custom code.

How to Use It?

Basic Usage

// Custom shader
import * as THREE from
  'three';

const WaveMaterial =
  new THREE.ShaderMaterial(
  {
    uniforms: {
      uTime: {
        value: 0 },
      uAmplitude: {
        value: 0.3 },
      uFrequency: {
        value: 4.0 },
      uColor: {
        value:
          new THREE.Color(
            0x00aaff) }
    },
    vertexShader: `
      uniform float uTime;
      uniform float
        uAmplitude;
      uniform float
        uFrequency;
      varying vec2 vUv;
      varying float
        vElevation;
      void main() {
        vUv = uv;
        vec3 pos =
          position;
        float wave =
          sin(pos.x
            * uFrequency
            + uTime)
          * uAmplitude;
        pos.z += wave;
        vElevation = wave;
        gl_Position =
          projectionMatrix
          * modelViewMatrix
          * vec4(
            pos, 1.0);
      }`,
    fragmentShader: `
      uniform vec3 uColor;
      varying vec2 vUv;
      varying float
        vElevation;
      void main() {
        float brightness =
          vElevation
          + 0.5;
        gl_FragColor =
          vec4(
            uColor
            * brightness,
            1.0);
      }`
  });

// Update in render loop:
// WaveMaterial.uniforms
//   .uTime.value =
//   clock
//   .getElapsedTime();

Real-World Examples

// Noise pattern shader
import * as THREE from
  'three';

const NoiseShader = {
  uniforms: {
    uTime: { value: 0 },
    uScale: { value: 5.0 },
    uColor1: {
      value:
        new THREE.Color(
          0x000033) },
    uColor2: {
      value:
        new THREE.Color(
          0x0066ff) }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position =
        projectionMatrix
        * modelViewMatrix
        * vec4(
          position, 1.0);
    }`,
  fragmentShader: `
    uniform float uTime;
    uniform float uScale;
    uniform vec3 uColor1;
    uniform vec3 uColor2;
    varying vec2 vUv;
    float hash(
      vec2 p
    ) {
      return fract(
        sin(dot(p,
          vec2(127.1,
            311.7)))
        * 43758.5453);
    }
    float noise(
      vec2 p
    ) {
      vec2 i = floor(p);
      vec2 f = fract(p);
      f = f * f
        * (3.0 - 2.0*f);
      return mix(
        mix(hash(i),
          hash(i
            + vec2(1,0)),
          f.x),
        mix(hash(i
            + vec2(0,1)),
          hash(i
            + vec2(1,1)),
          f.x),
        f.y);
    }
    void main() {
      float n = noise(
        vUv * uScale
        + uTime * 0.2);
      vec3 color = mix(
        uColor1,
        uColor2, n);
      gl_FragColor =
        vec4(color, 1.0);
    }`
};

const mat =
  new THREE
  .ShaderMaterial(
    NoiseShader);
// mat.uniforms
//   .uTime.value = t;

Advanced Tips

Use onBeforeCompile to inject custom code into built-in materials while keeping PBR lighting. Pass textures as uniforms for data-driven effects that combine procedural and image-based patterns. Use defines to create shader variants without runtime branching.

When to Use It?

Use Cases

Create animated water surfaces with vertex displacement and fresnel fragment effects. Build procedural textures with noise patterns that animate over time. Extend MeshStandardMaterial with custom vertex deformation for interactive experiences.

Related Topics

GLSL, Three.js, vertex shaders, fragment shaders, WebGL, custom materials, and GPU programming.

Important Notes

Requirements

Three.js with ShaderMaterial for custom GPU programs. GLSL ES knowledge for writing vertex and fragment shader code. Understanding of the Three.js uniform system for passing data to shaders from JavaScript.

Usage Recommendations

Do: keep uniforms updated each frame for time-based animations. Use varyings to pass interpolated data from vertex to fragment shaders. Test shaders on multiple devices since GPU precision varies.

Don't: use heavy branching in fragment shaders since GPUs process fragments in parallel and branches reduce throughput. Forget to set needsUpdate on materials after changing defines. Write complex math in shaders without benchmarking since GPU computation has limits.

Limitations

Custom shaders bypass Three.js lighting unless manually integrating light calculations. Shader debugging is limited to visual inspection since GLSL lacks print statements. Shader compilation errors appear at runtime and can be cryptic to diagnose.