ShaderMaterial Defines for GLSL Macros in Three.js

This article explains how to use the defines object in Three.js ShaderMaterial to control GLSL preprocessor macros. You will learn how to inject defines into your shaders, write conditional GLSL code based on these defines, and update them dynamically at runtime to optimize shader performance and code reusability.

Understanding the Defines Object

In Three.js, ShaderMaterial allows you to pass a defines object containing key-value pairs. During shader compilation, Three.js automatically prepends these pairs as #define KEY VALUE statements at the very top of both your vertex and fragment shaders. This is highly useful for toggling features, setting constants, or conditionally compiling blocks of code, which helps avoid runtime branch performance penalties in WebGL.

Here is a basic example of declaring defines when instantiating a ShaderMaterial:

const material = new THREE.ShaderMaterial({
    defines: {
        USE_COLOR: true,
        MAX_LIGHTS: 4,
        GRID_SIZE: '10.0'
    },
    vertexShader: vertexShaderSource,
    fragmentShader: fragmentShaderSource
});

Three.js converts this object into the following GLSL preprocessor commands at the top of your shader code:

#define USE_COLOR true
#define MAX_LIGHTS 4
#define GRID_SIZE 10.0

Using Defines in GLSL Shaders

Once declared, you can use standard GLSL preprocessor directives like #ifdef, #ifndef, #if, and #endif to conditionally compile your shader code.

// Fragment Shader Example
uniform vec3 uBaseColor;
varying vec3 vCustomColor;

void main() {
    vec3 finalColor = uBaseColor;

    #ifdef USE_COLOR
        finalColor *= vCustomColor;
    #endif

    gl_FragColor = vec4(finalColor, 1.0);
}

If USE_COLOR is defined and truthy, the compiler includes the multiplication step. If it is undefined or set to false, the compiler completely ignores that block of code, saving GPU cycles.

Updating Defines at Runtime

You can modify the defines object after the material has been created. However, because preprocessor macros are resolved at compile time, you must instruct Three.js to recompile the shader program by setting the material’s needsUpdate property to true.

// Toggle the feature off
material.defines.USE_COLOR = false;

// Trigger a shader recompilation
material.needsUpdate = true;

To completely remove a define, use the delete keyword:

delete material.defines.USE_COLOR;
material.needsUpdate = true;

Best Practices and Performance