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);
});