Custom Three.js ShaderPass to Invert Screen Colors
In Three.js, post-processing allows you to apply full-screen visual
effects to your rendered scene by manipulating the final output pixels.
This article provides a step-by-step guide on how to write a custom
fragment shader for a ShaderPass to invert the screen
colors of your canvas. You will learn how to define the shader, set up
the EffectComposer, and integrate the pass into your render
loop.
1. Import the Required Modules
To implement post-processing, you need to import the
EffectComposer, RenderPass, and
ShaderPass modules alongside Three.js.
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';2. Define the Custom Inversion Shader
A Three.js ShaderPass requires a shader definition
object containing uniforms, a vertexShader,
and a fragmentShader.
- Uniforms: The
tDiffuseuniform is reserved by Three.js to automatically pass the texture representation of the rendered scene. - Vertex Shader: This passes the texture coordinates
(
vUv) to the fragment shader. - Fragment Shader: This samples the color at the
current coordinate and subtracts the RGB values from
1.0to invert them, while keeping the alpha channel intact.
const InvertShader = {
uniforms: {
tDiffuse: { value: null }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
// Invert only the RGB channels, preserve alpha
gl_FragColor = vec4(1.0 - texel.rgb, texel.a);
}
`
};3. Set Up the EffectComposer and ShaderPass
Once the shader is defined, set up the EffectComposer
pipeline. First, render the base scene using a RenderPass,
then apply the inversion effect using ShaderPass.
// Assuming 'renderer', 'scene', and 'camera' are already initialized
// 1. Initialize the Composer
const composer = new EffectComposer(renderer);
// 2. Add the RenderPass (renders the default scene)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 3. Create and add the Custom ShaderPass
const invertPass = new ShaderPass(InvertShader);
composer.addPass(invertPass);4. Update the Animation Loop
To display the post-processing effects, replace the standard
renderer.render(scene, camera) call in your animation loop
with composer.render().
function animate() {
requestAnimationFrame(animate);
// Perform any object rotations or physics updates here
// Render the scene through the post-processing pipeline
composer.render();
}
animate();5. Handle Window Resizing
Because post-processing operates on a pixel-by-pixel level, you must update the size of both the renderer and the composer when the browser window is resized to prevent pixelation or stretching.
```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); }); ```