Post-processing
Post-processing runs after the scene renders, applying effects in screen space: bloom for highlights, depth-of-field for cinematic focus, FXAA/SMAA for antialiasing, vignette for mood. Three.js ships an EffectComposer that chains passes; each pass reads the previous render target and writes the next, finishing on the canvas.
Bloom + vignette + FXAA via EffectComposer
EXAMPLE
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
import { VignetteShader } from 'three/examples/jsm/shaders/VignetteShader.js';
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
// Something to glow
const geo = new THREE.IcosahedronGeometry(1, 4);
const mat = new THREE.MeshStandardMaterial({ color: 0x224488, emissive: 0x223388, emissiveIntensity: 1.2 });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
scene.add(new THREE.PointLight(0xffffff, 50, 100));
// ---- the post-processing pipeline ----
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
0.9, // strength
0.4, // radius
0.85, // threshold (only pixels above this luminance bloom)
);
composer.addPass(bloom);
const vignette = new ShaderPass(VignetteShader);
vignette.uniforms.darkness.value = 1.2;
vignette.uniforms.offset.value = 1.0;
composer.addPass(vignette);
const fxaa = new ShaderPass(FXAAShader);
fxaa.uniforms.resolution.value.set(
1 / (innerWidth * renderer.getPixelRatio()),
1 / (innerHeight * renderer.getPixelRatio()),
);
composer.addPass(fxaa);
// Resize handling — composer needs the same size as the renderer
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
composer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
fxaa.uniforms.resolution.value.set(
1 / (innerWidth * renderer.getPixelRatio()),
1 / (innerHeight * renderer.getPixelRatio()),
);
});
renderer.setAnimationLoop(() => {
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.007;
composer.render();
});
Why it matters
Bloom is the effect that most often gets over-applied — keep threshold high (0.8+) and strength under 1, otherwise everything blooms and nothing reads as bright. On mobile, drop the bloom resolution or skip it entirely; it is the dominant cost in this pipeline.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
Try it Yourself »
Discussion
Loading…