iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…