Post-processing

포스트 프로세싱

Reprocessing the already-rendered frame through a chain of passes to add color grading, texture, or distortion.

Also known as: EffectComposerRender pipelineScreen-space effects
···
js
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { RGBShiftShader } from 'three/addons/shaders/RGBShiftShader.js';
import { VignetteShader } from 'three/addons/shaders/VignetteShader.js';
import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x101020);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0.6, 5);

scene.add(new THREE.AmbientLight(0x445566, 1.3));
const dLight = new THREE.DirectionalLight(0xffffff, 1.1);
dLight.position.set(3, 4, 4);
scene.add(dLight);

const group = new THREE.Group();
for (let i = 0; i < 5; i++) {
  const mesh = new THREE.Mesh(
    new THREE.IcosahedronGeometry(0.55, 0),
    new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(i / 5, 0.7, 0.6), roughness: 0.35 })
  );
  mesh.position.set((i - 2) * 1.1, Math.sin(i) * 0.4, 0);
  group.add(mesh);
}
scene.add(group);

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const rgbShift = new ShaderPass(RGBShiftShader);
rgbShift.uniforms.amount.value = 0.0022;
composer.addPass(rgbShift);
composer.addPass(new FilmPass(0.35, false));
const vignette = new ShaderPass(VignetteShader);
vignette.uniforms.offset.value = 1.1;
vignette.uniforms.darkness.value = 1.3;
composer.addPass(vignette);
composer.addPass(new OutputPass());

function resize() {
  renderer.setSize(innerWidth, innerHeight);
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  composer.setSize(innerWidth, innerHeight);
  composer.setPixelRatio(Math.min(devicePixelRatio, 2));
}
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  group.rotation.y = t * 0.00025;
  group.children.forEach((m, i) => { m.rotation.x = t * 0.0006 + i; });
  composer.render();
});

EffectComposer, instead of writing the render straight to screen, captures it into a texture (a render target) and feeds that texture into the next pass as input, stacking effects like a chain. Each Pass reads the previous pass’s result (tDiffuse), processes it with its own fragment shader, and hands it to the next.

The demo stacks: RenderPass (normal render) → RGBShiftShader (nudges red and blue channels apart to fake chromatic aberration) → FilmPass (film grain and scanlines) → VignetteShader (darkens the edges) → OutputPass (final color-space correction). Individual shaders like these get wrapped in ShaderPass(shader) to slot into the chain.

No matter how many passes are stacked, you call composer.render() once instead of renderer.render(). Each added pass means one more full-screen texture sample, though, so on mobile it’s worth keeping only the effects that actually earn their cost.

When to use

Whenever the effect belongs on the "camera lens" rather than an individual object — overall color grading, a film look, screen distortion.