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