Selective Post-Processing in Three.js Using Mask Pass

Applying post-processing effects to specific objects in a Three.js scene—rather than the entire viewport—is a common requirement for creating advanced visual highlights, glowing elements, or localized shaders. This article explains how to achieve selective post-processing using layers, the EffectComposer, and the MaskPass (accompanied by ClearMaskPass) to isolate rendering effects to a designated subset of your 3D objects.

The Core Concept

To apply an effect to only specific objects, you must define which pixels on the screen those objects occupy. The MaskPass achieves this by writing the stencil buffer based on the geometry of your selected objects. Any subsequent post-processing passes will only render within the boundaries of that stencil mask.

To implement this, you will use Three.js Layers to separate your standard objects from your masked objects, allowing a single camera to view different sets of objects during different rendering passes.


Step-by-Step Implementation

1. Set Up the Layers

Three.js objects have a .layers property. By default, all objects and cameras belong to Layer 0. Allocate a separate layer (e.g., Layer 1) for the objects that should receive the selective post-processing effect.

const DEFAULT_LAYER = 0;
const SELECTIVE_LAYER = 1;

// Create standard object
const backgroundBox = new THREE.Mesh(geometry, standardMaterial);
backgroundBox.layers.set(DEFAULT_LAYER);
scene.add(backgroundBox);

// Create the object that will receive the effect
const glowingSphere = new THREE.Mesh(geometry, effectMaterial);
glowingSphere.layers.enable(SELECTIVE_LAYER); // Object now exists on both 0 and 1
scene.add(glowingSphere);

2. Configure the EffectComposer and Passes

Import the necessary post-processing files. You will need EffectComposer, RenderPass, MaskPass, ClearMaskPass, and the shader/effect pass you wish to apply (e.g., ShaderPass or GlitchPass).

Ensure your WebGLRenderer has stencil enabled, as the MaskPass relies on the stencil buffer to mask the pixels.

const renderer = new THREE.WebGLRenderer({ stencil: true });
const composer = new THREE.EffectComposer(renderer);

3. Build the Pass Stack

The rendering pipeline must be structured so that the main scene renders first, followed by the mask definition, the localized effect, and finally the removal of the mask.

// 1. Render the entire scene normally
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

// 2. Create the Mask Pass
const maskPass = new THREE.MaskPass(scene, camera);
composer.addPass(maskPass);

// 3. Create the effect pass (e.g., a custom vignette, color-shift, or blur)
const effectPass = new THREE.ShaderPass(MyCustomShader);
composer.addPass(effectPass);

// 4. Create the Clear Mask Pass to stop masking subsequent passes
const clearMaskPass = new THREE.ClearMaskPass();
composer.addPass(clearMaskPass);

4. Direct the Camera via Render Hooks

By default, the MaskPass will render the entire scene into the stencil buffer. To restrict the mask to your specific layer, temporarily alter the camera’s layer mask before the MaskPass executes, and restore it afterward.

You can hook into the rendering cycle of the passes:

// Before the mask pass renders, force the camera to only see the selective layer
maskPass.onBeforeRender = () => {
    renderer.autoClear = false;
    camera.layers.set(SELECTIVE_LAYER);
};

// After the mask pass renders, restore the camera to see the default layer
maskPass.onAfterRender = () => {
    camera.layers.set(DEFAULT_LAYER);
};

5. Update the Render Loop

Replace your standard renderer.render(scene, camera) call in your animation loop with the composer’s update method.

function animate() {
    requestAnimationFrame(animate);

    // Update animations or controls here
    
    // Render through the composer
    composer.render();
}
animate();

Performance Considerations