포스트 프로세싱

Post-processing

렌더링이 끝난 화면 이미지를 여러 패스에 걸쳐 다시 처리해 색·질감·왜곡을 더하는 파이프라인.

다른 이름: 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는 렌더 결과를 화면에 바로 찍는 대신 텍스처(렌더 타겟)에 담아, 그 텍스처를 다음 패스가 입력으로 받는 식으로 여러 효과를 체인처럼 쌓습니다. 각 Pass는 이전 패스의 결과(tDiffuse)를 받아 자기만의 프래그먼트 셰이더로 처리한 뒤 다음 패스로 넘깁니다.

데모는 RenderPass(일반 렌더) → RGBShiftShader(빨강·파랑 채널을 반대 방향으로 살짝 어긋나게 해 색수차를 흉내) → FilmPass(필름 그레인과 스캔라인) → VignetteShader(가장자리를 어둡게) → OutputPass(최종 색 공간 보정) 순으로 쌓았습니다. 이런 개별 셰이더는 ShaderPass(shader)로 감싸서 체인에 끼워 넣습니다.

패스를 몇 개를 쌓든 렌더러 호출은 renderer.render()가 아니라 composer.render() 하나면 됩니다. 다만 패스가 늘수록 풀스크린 텍스처 샘플링이 그만큼 늘어나니, 모바일 등에서는 정말 필요한 효과만 남기는 게 좋습니다.

언제 쓰나

전체적인 색보정·필름 룩·화면 왜곡처럼, 개별 오브젝트가 아니라 "카메라 렌즈"에 거는 효과가 필요할 때.