Three.js Post-processing
후처리는 렌더링된 이미지에 추가적인 시각 효과를 적용하는 기법으로, 영화나 게임, 고품질 시각화에 자주 사용됩니다. Three.js에서도 이 후처리를 활용할 수 있으며, 그 핵심 도구는 EffectComposer입니다.
🎯 후처리로 할 수 있는 효과들
- 피사계 심도 (Depth of Field)
- Bloom (꽃)
- God Rays (신의 광선)
- Motion Blur (모션 블러)
- Glitch (글리치 효과)
- Color Grading (색 보정)
- Anti-Aliasing (앤티앨리어싱)
- Reflection & Refraction (반사/굴절)
📦 기본 구조와 작동 원리
1. 렌더 타겟(Render Target)
Three.js에서는 후처리를 위해 실제 캔버스가 아니라 RenderTarget이라는 텍스처에 장면을 먼저 렌더링합니다.
- 이 텍스처는 이후 패스들에서 입력으로 사용됩니다.
- WebGL에서는 흔히 Framebuffer 혹은 Buffer라고도 부릅니다.
2. Ping-Pong 버퍼링
- 다중 패스(Post-Processing Pass)를 사용할 때, 하나의 버퍼로만 렌더링하면 동시에 읽고 쓸 수 없어 오류가 납니다.
- 그래서 두 개의 RenderTarget을 번갈아가며 쓰고 읽는 핑퐁(Ping-Pong) 구조가 필요합니다.
3. 최종 패스는 캔버스에 출력
- 마지막 패스는 canvas에 직접 렌더링되어 유저가 화면에서 결과를 볼 수 있도록 합니다.
🧱 EffectComposer 설정법
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
const effectComposer = new EffectComposer(renderer);
effectComposer.setSize(sizes.width, sizes.height);
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const renderPass = new RenderPass(scene, camera);
effectComposer.addPass(renderPass);
🔁 애니메이션 루프에서 사용
const tick = () => {
// effectComposer가 최종 렌더링 담당
effectComposer.render();
requestAnimationFrame(tick);
};
🔍 패스(Pass) 적용 예시
1. 🎯 DotScreenPass

import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js';
const dotScreenPass = new DotScreenPass();
effectComposer.addPass(dotScreenPass);
2. 🎯 GlitchPass (화면이 깨진 듯한 효과)


import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';
const glitchPass = new GlitchPass();
effectComposer.addPass(glitchPass);
3. 🎯 RGBShiftPass (Shader 기반)

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js';
const rgbShiftPass = new ShaderPass(RGBShiftShader);
effectComposer.addPass(rgbShiftPass);
4. 🎯 GammaCorrectionPass (색상 고정)
GammaCorrectionPass와 함께 사용 시 (색상은 고정되고 효과만 유지된다.):


import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.js';
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
effectComposer.addPass(gammaCorrectionPass);
5. 🎯 SMAA (Anti-Aliasing (AA, 앤티앨리어싱) = 그래픽에서 경계선이 매끄럽지 않고 ‘계단처럼 톱니 모양으로 보이는 현상)


// Render target (후처리를 위해 씬을 캔버스가 아닌 텍스처로 렌더링할 때 사용)
const renderTarget = new THREE.WebGLRenderTarget(800, 600, {
samples: 2, // MSAA (Multisample Anti-Aliasing)를 활성화
});
// Effect composer
const effectComposer = new EffectComposer(renderer, renderTarget);
WebGL2에서만 동작하고, WebGL1에서는 무시됨.
console.log(renderer.capabilities); // webgl 버전을 확인할 수 있다.

import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
const smaaPass = new SMAAPass();
effectComposer.addPass(smaaPass);
조건부로 추가할 수도 있음:
if (renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2) {
effectComposer.addPass(smaaPass);
}
6. 🎯 UnrealBloomPass


import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
const unrealBloomPass = new UnrealBloomPass();
effectComposer.addPass(unrealBloomPass);
조정
// Unreal bloom pass
const unrealBloomPass = new UnrealBloomPass();
unrealBloomPass.strength = 0.3;
unrealBloomPass.radius = 1;
unrealBloomPass.threshold = 0.6;
effectComposer.addPass(unrealBloomPass);
조정용 GUI
gui.add(unrealBloomPass, 'enabled');
gui.add(unrealBloomPass, 'strength').min(0).max(2).step(0.001);
gui.add(unrealBloomPass, 'radius').min(0).max(2).step(0.001);
gui.add(unrealBloomPass, 'threshold').min(0).max(1).step(0.001);
🛠 커스텀 패스 만들기
1. 🔴 TintShader - 색상 보정
const TintShader = {
uniforms: {
tDiffuse: { value: null },
uTint: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec3 uTint;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
color.rgb += uTint;
gl_FragColor = color;
}
`,
};
const tintPass = new ShaderPass(TintShader);
tintPass.material.uniforms.uTint.value = new THREE.Vector3();
effectComposer.addPass(tintPass);
gui.add(tintPass.material.uniforms.uTint.value, 'x').min(-1).max(1).step(0.001).name('red');
gui.add(tintPass.material.uniforms.uTint.value, 'y').min(-1).max(1).step(0.001).name('green');
gui.add(tintPass.material.uniforms.uTint.value, 'z').min(-1).max(1).step(0.001).name('blue');
2. 🌊 DisplacementShader - 변위


const DisplacementShader = {
uniforms: {
tDiffuse: { value: null },
uTime: { value: 0.0 },
uNormalMap: {
value: textureLoader.load('/textures/interfaceNormalMap.png'),
},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform sampler2D uNormalMap;
varying vec2 vUv;
void main() {
vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0;
vec2 newUv = vUv + normalColor.xy * 0.1;
vec4 color = texture2D(tDiffuse, newUv);
vec3 lightDirection = normalize(vec3(-1.0, 1.0, 0.0));
float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0);
color.rgb += lightness * 2.0;
gl_FragColor = color;
}
`,
};
📐 리사이즈 대응
renderer에 리사이즈 대응을 했더라도, effectComposer에 리사이즈 대응을 하지 않으면 아래처럼 픽셀이 깨지게 된다.

window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
effectComposer.setSize(sizes.width, sizes.height);
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
✅ 정리
- EffectComposer는 후처리를 위한 중심 객체입니다.
- 패스를 조합해 다양한 시각 효과를 얻을 수 있습니다.
- 커스텀 패스를 만들어 자신만의 후처리를 적용할 수도 있습니다.
- 성능을 고려해 적절한 패스만 적용하세요. (프레임 당 적용되므로 성능저하)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import {
DotScreenPass,
RenderPass,
EffectComposer,
GlitchPass,
RGBShiftShader,
ShaderPass,
GammaCorrectionShader,
SMAAPass,
UnrealBloomPass,
} from 'three/examples/jsm/Addons.js';
import GUI from 'lil-gui';
/**
* Base Setup
*/
const gui = new GUI(); // 디버깅 UI 생성
const canvas = document.querySelector('canvas.webgl'); // 렌더링 대상 캔버스
const scene = new THREE.Scene(); // 장면 생성
/**
* Loaders
*/
const gltfLoader = new GLTFLoader();
const cubeTextureLoader = new THREE.CubeTextureLoader();
const textureLoader = new THREE.TextureLoader();
/**
* Update all standard materials in the scene
*/
const updateAllMaterials = () => {
scene.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
child.material.envMapIntensity = 2.5;
child.material.needsUpdate = true;
child.castShadow = true;
child.receiveShadow = true;
}
});
};
/**
* Environment map 설정 (IBL 조명에 사용됨)
*/
const environmentMap = cubeTextureLoader.load([
'/textures/environmentMaps/0/px.jpg',
'/textures/environmentMaps/0/nx.jpg',
'/textures/environmentMaps/0/py.jpg',
'/textures/environmentMaps/0/ny.jpg',
'/textures/environmentMaps/0/pz.jpg',
'/textures/environmentMaps/0/nz.jpg',
]);
scene.background = environmentMap;
scene.environment = environmentMap;
/**
* 모델 로딩 (Damaged Helmet)
*/
gltfLoader.load('/models/DamagedHelmet/glTF/DamagedHelmet.gltf', (gltf) => {
gltf.scene.scale.set(2, 2, 2);
gltf.scene.rotation.y = Math.PI * 0.5;
scene.add(gltf.scene);
updateAllMaterials();
});
/**
* Directional Light 설정
*/
const directionalLight = new THREE.DirectionalLight('#ffffff', 3);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set(1024, 1024);
directionalLight.shadow.camera.far = 15;
directionalLight.shadow.normalBias = 0.05;
directionalLight.position.set(0.25, 3, -2.25);
scene.add(directionalLight);
/**
* 반응형 처리
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
effectComposer.setSize(sizes.width, sizes.height);
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* 카메라 및 OrbitControls
*/
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.set(4, 1, -4);
scene.add(camera);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* 렌더러 설정
*/
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 1.5;
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* 포스트 프로세싱
*/
const renderTarget = new THREE.WebGLRenderTarget(800, 600, {
// WebGL2에서는 MSAA 지원 가능
// samples: renderer.getPixelRatio() === 1 ? 2 : 0,
});
const effectComposer = new EffectComposer(renderer, renderTarget);
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
effectComposer.setSize(sizes.width, sizes.height);
// 기본 렌더링 패스
const renderPass = new RenderPass(scene, camera);
effectComposer.addPass(renderPass);
// 점 패턴 패스
const dotScreenPass = new DotScreenPass();
dotScreenPass.enabled = false;
effectComposer.addPass(dotScreenPass);
// 글리치 효과
const glitchPass = new GlitchPass();
glitchPass.goWild = false;
glitchPass.enabled = false;
effectComposer.addPass(glitchPass);
// RGB 색상 분리 효과
const rgbShiftPass = new ShaderPass(RGBShiftShader);
rgbShiftPass.enabled = false;
effectComposer.addPass(rgbShiftPass);
// 감마 보정 (색상 왜곡 방지)
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
effectComposer.addPass(gammaCorrectionPass);
// 블룸 효과 (빛 번짐 효과)
const unrealBloomPass = new UnrealBloomPass();
unrealBloomPass.strength = 0.3;
unrealBloomPass.radius = 1;
unrealBloomPass.threshold = 0.6;
effectComposer.addPass(unrealBloomPass);
gui.add(unrealBloomPass, 'enabled');
gui.add(unrealBloomPass, 'strength').min(0).max(2).step(0.001);
gui.add(unrealBloomPass, 'radius').min(0).max(2).step(0.001);
gui.add(unrealBloomPass, 'threshold').min(0).max(1).step(0.001);
// 색상 보정용 틴트 패스
const TintShader = {
uniforms: {
tDiffuse: { value: null },
uTint: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec3 uTint;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
color.rgb += uTint;
gl_FragColor = color;
}
`,
};
const tintPass = new ShaderPass(TintShader);
tintPass.material.uniforms.uTint.value = new THREE.Vector3();
effectComposer.addPass(tintPass);
gui.add(tintPass.material.uniforms.uTint.value, 'x').min(-1).max(1).step(0.001).name('red');
gui.add(tintPass.material.uniforms.uTint.value, 'y').min(-1).max(1).step(0.001).name('green');
gui.add(tintPass.material.uniforms.uTint.value, 'z').min(-1).max(1).step(0.001).name('blue');
// 노말맵 기반 UV 변형 디스플레이스 패스
const DisplacementShader = {
uniforms: {
tDiffuse: { value: null },
uNormalMap: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D uNormalMap;
varying vec2 vUv;
void main() {
vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0;
vec2 newUv = vUv + normalColor.xy * 0.1;
vec4 color = texture2D(tDiffuse, newUv);
vec3 lightDirection = normalize(vec3(-1.0, 1.0, 0.0));
float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0);
color.rgb += lightness * 2.0;
gl_FragColor = color;
}
`,
};
const displacementPass = new ShaderPass(DisplacementShader);
displacementPass.material.uniforms.uNormalMap.value = textureLoader.load('/textures/interfaceNormalMap.png');
effectComposer.addPass(displacementPass);
// SMAA 안티앨리어싱 패스 (WebGL1에서만 동작)
if (renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2) {
const smaaPass = new SMAAPass();
effectComposer.addPass(smaaPass);
console.log('Using WebGL 1');
}
/**
* 애니메이션 루프
*/
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Pass 업데이트
// displacementPass.material.uniforms.uTime.value = elapsedTime;
controls.update();
effectComposer.render();
window.requestAnimationFrame(tick);
};
tick();'Graphic > ThreeJS' 카테고리의 다른 글
| 47 로딩 스크린과 인트로(Intro and loading progress) (2) | 2025.08.06 |
|---|---|
| 46 퍼포먼스 최적화 (6) | 2025.08.06 |
| 31 MeshStandardMaterial 셰이더 확장(Modified materials) (2) | 2025.08.05 |
| 30 Animated galaxy (5) | 2025.08.05 |
| 29 Animated Water Shader (2) | 2025.08.04 |