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.

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