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.