Custom Shader Passes in Three.js EffectComposer

This article provides a step-by-step guide on how to create and register custom post-processing shader passes using the EffectComposer in Three.js. You will learn how to define custom vertex and fragment shaders, structure the shader object, instantiate a ShaderPass, and integrate it into your post-processing pipeline to achieve unique visual effects.


1. Import Required Modules

To implement post-processing, you need to import the EffectComposer along with the core post-processing passes from the Three.js addons directory.

import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

2. Initialize the EffectComposer

First, set up your standard Three.js scene, camera, and renderer. Instead of rendering the scene directly through the renderer, you will route the rendering pipeline through the EffectComposer.

// Setup renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Setup composer
const composer = new EffectComposer(renderer);

// Add default RenderPass to render the basic scene
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

3. Define the Custom Shader Object

A custom shader pass requires a shader definition object containing three main components: * uniforms: Variables sent from JavaScript to the GLSL shaders. Post-processing shaders must include tDiffuse (type t), which represents the texture of the previous pass. * vertexShader: Projects the 2D screen quad geometry. * fragmentShader: Manipulates the pixels of the rendered frame.

Below is an example of a custom shader that performs color inversion:

const InvertShader = {
    name: 'InvertShader',
    uniforms: {
        'tDiffuse': { value: null }, // Required: Holds the texture of the previous pass
        'uAmount': { value: 1.0 }    // Custom uniform to control effect strength
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform float uAmount;
        varying vec2 vUv;
        void main() {
            vec4 color = texture2D(tDiffuse, vUv);
            vec3 invertedColor = 1.0 - color.rgb;
            gl_FragColor = vec4(mix(color.rgb, invertedColor, uAmount), color.a);
        }
    `
};

4. Register the Custom ShaderPass

Instantiate the ShaderPass by passing your custom shader configuration object into it, and then add this pass to your EffectComposer.

// Create the pass using the shader object
const invertPass = new ShaderPass(InvertShader);

// (Optional) Modify uniforms directly after instantiation
invertPass.uniforms['uAmount'].value = 0.8;

// Add the pass to your composer pipeline
composer.addPass(invertPass);

If this is the final pass in your composer chain, ShaderPass automatically sets renderToScreen = true so the output displays on the canvas.

5. Update the Animation Loop

To see the post-processing effects in action, replace renderer.render(scene, camera) with composer.render() inside your main animation loop.

function animate() {
    requestAnimationFrame(animate);

    // Perform any object rotations or logic here
    
    // Render the scene through the post-processing pipeline
    composer.render();
}

animate();

6. Handling Window Resizing

When the window resizes, you must update both the renderer and the EffectComposer size to prevent scaling and resolution issues.

window.addEventListener('resize', () => {
    const width = window.innerWidth;
    const height = window.innerHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    composer.setSize(width, height);
});