How Three.js Uses ShaderChunks for Shaders

In Three.js, shaders are essential for rendering 3D graphics, but writing them from scratch for every material is highly repetitive. This article explains what ShaderChunks are—reusable snippets of GLSL (OpenGL Shading Language) code—and details how Three.js dynamically assembles them to build complex material shaders, allowing developers to customize rendering without rewriting boilerplate graphics code.

What are ShaderChunks?

ShaderChunks are modular, reusable blocks of GLSL code stored in the Three.js library under the THREE.ShaderChunk object. Instead of writing massive, monolithic shader programs for every material (such as MeshBasicMaterial or MeshPhysicalMaterial), Three.js breaks the rendering pipeline down into smaller, logical pieces.

Each ShaderChunk handles a specific graphics task. For example: * fog_vertex and fog_fragment: Manage fog calculations. * lights_pars_begin: Defines uniforms and varying variables for lighting. * shadowmap_pars_vertex: Handles shadow mapping calculations in the vertex shader. * common: Contains mathematical constants and utility functions used across multiple shaders.

By organizing GLSL code into these chunks, Three.js maintains a highly organized, DRY (Don’t Repeat Yourself) codebase for WebGL rendering.

How Three.js Assembles Materials Using ShaderChunks

When you instantiate a built-in material, Three.js does not use a single, static shader file. Instead, it dynamically assembles the vertex and fragment shaders using a pre-processing system.

1. The Template Shaders

Every built-in material has a corresponding shader template (found in the Three.js source code under src/renderers/shaders/ShaderLib/). These templates consist of basic GLSL structures interspersed with custom #include directives.

For example, a simplified vertex shader template might look like this:

void main() {
    #include <uv_vertex>
    #include <color_vertex>
    #include <begin_vertex>
    #include <project_vertex>
}

2. The Pre-processing Step

Before compiling the shader on the GPU, the Three.js WebGLRenderer parses these template strings. It searches for any line containing #include <chunk_name>.

The renderer then looks up the corresponding GLSL code in THREE.ShaderChunk and replaces the #include directive with the actual code snippet. For instance, #include <begin_vertex> is replaced with:

vec3 transformed = vec3( position );

This replacement happens recursively until all #include statements are resolved, resulting in a complete, valid GLSL program ready for WebGL compilation.

3. Conditional Assembly

Three.js only includes the chunks that are actually needed for the material’s current configuration. If a material has no map, fog, or lights enabled, the renderer will skip the corresponding chunks. This keeps the final compiled shader as lightweight and performant as possible.

How Developers Can Use ShaderChunks

Understanding ShaderChunks allows developers to customize built-in Three.js materials without writing entirely custom shaders from scratch.

Modifying Materials with onBeforeCompile

The most common way to leverage ShaderChunks is through the onBeforeCompile callback available on all Three.js materials. This callback gives you access to the assembled shader source code before it is sent to the GPU, allowing you to swap out default chunks with custom code.

Here is an example of modifying a standard material to inject custom displacement logic:

const material = new THREE.MeshStandardMaterial();

material.onBeforeCompile = (shader) => {
  // Replace the default begin_vertex chunk with custom movement code
  shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
    vec3 transformed = vec3( position );
    transformed.x += sin(position.y + time) * 0.5;
    `
  );
};

Building Custom ShaderMaterial

If you are writing a completely custom ShaderMaterial but still want to use Three.js features like fog, lighting, or shadow maps, you can reference THREE.ShaderChunk directly inside your custom GLSL code.

const customMaterial = new THREE.ShaderMaterial({
  vertexShader: `
    varying vec2 vUv;
    #include <common>
    #include <fog_pars_vertex>
    
    void main() {
      vUv = uv;
      #include <begin_vertex>
      #include <project_vertex>
      #include <fog_vertex>
    }
  `,
  fragmentShader: `
    varying vec2 vUv;
    #include <common>
    #include <fog_pars_fragment>
    
    void main() {
      vec4 color = vec4(vUv, 1.0, 1.0);
      gl_FragColor = color;
      #include <fog_fragment>
    }
  `,
  fog: true
});

By utilizing ShaderChunks, you avoid writing complex WebGL boilerplate for systems like fog and matrices, allowing you to focus purely on your custom visual effects.