Inject Custom GLSL into Three.js Materials
This article provides a practical guide on how to use the
onBeforeCompile method in Three.js to inject custom GLSL
code into built-in materials. By leveraging this method, you can modify
existing vertex and fragment shaders—such as those in
MeshStandardMaterial or MeshPhongMaterial—to
add custom animations, visual effects, or custom lighting without having
to build a ShaderMaterial from scratch.
Understanding onBeforeCompile
Three.js materials compile into GLSL shader code before being sent to
the GPU. The onBeforeCompile callback executes right before
this compilation step. It exposes the raw shader object, which contains
three key properties: * shader.vertexShader: The GLSL code
for the vertex shader. * shader.fragmentShader: The GLSL
code for the fragment shader. * shader.uniforms: The
uniforms associated with the shader.
By using JavaScript’s string replacement methods, you can target specific parts of the built-in shader code and swap them with your custom GLSL.
Step-by-Step Implementation
To modify a built-in material, instantiate the material, define the
onBeforeCompile function, inject your custom GLSL, and
handle any new uniforms.
1. Define the Material and Inject Code
In this example, we will modify a MeshStandardMaterial
to wave vertices up and down using a sine wave over time.
import * as THREE from 'three';
// Create a standard material
const material = new THREE.MeshStandardMaterial({ color: 0x00ffcc });
// Define custom uniforms holding state
const customUniforms = {
uTime: { value: 0.0 }
};
material.onBeforeCompile = (shader) => {
// 1. Merge our custom uniforms with the built-in shader uniforms
shader.uniforms.uTime = customUniforms.uTime;
// 2. Inject the uniform declaration at the top of the vertex shader
shader.vertexShader = `
uniform float uTime;
` + shader.vertexShader;
// 3. Replace a built-in chunk to apply displacement
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
// Modify the vertex position using our custom uTime uniform
transformed.y += sin(position.x * 2.0 + uTime) * 0.5;
`
);
// 4. Store a reference to the shader in userData to access it in the animation loop
material.userData.shader = shader;
};2. Update Uniforms in the Animation Loop
Because Three.js clones uniforms during compilation, directly
updating customUniforms.uTime.value is the safest way to
ensure the compiled shader receives the updated values in your render
loop.
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// Update the uniform value directly
customUniforms.uTime.value = elapsedTime;
renderer.render(scene, camera);
}
animate();Key Considerations
- Shader Chunks: Three.js organizes its shaders using
reusable blocks called “shader chunks” (e.g.,
#include <begin_vertex>). To find the best injection points, inspect the Three.js source code shaders folder on GitHub. - Shadows: If your custom vertex shader deforms the
mesh geometry, the shadow map will not match the deformed mesh unless
you also apply the same
onBeforeCompilemodification to the mesh’scustomDepthMaterial.