| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 이진탐색
- binary search
- Trie
- Stored Procedure
- union find
- Brute Force
- 그래프
- Hash
- Two Points
- DP
- two pointer
- MYSQL
- Dijkstra
- SQL
- 스토어드 프로시저
- 다익스트라
- String
- Today
- Total
codingfarm
PostProcessing EffectComposer 본문
1. 소개
three.js 에서 post-process를 구현하기 위해 주로 EffectComposer를 사용해왔다.
간단한 효과의 경우, ShaderMaterial에 간단히 쉐이더 코드를 작성하지만,
복잡한 효과는 Pass를 상속받는 클래스를 구현한다.
r3f의 후처리는 주로 @react-three/postprocessing의 EffectComposer를 사용하며, 이는 Three.js addons의 EffectComposer 가 아니라, postprocessing 라이브러리의 Composer 를 감싼것이다.
즉, addons와 postprocessing의 EffectComposer는 이름만 같을 뿐 완전히 다른 구현을 가진, 독립적인 대상이다.
2. addons의 EffectComposer
Three.js의 공식 example이며, three.js 자체 렌더 파이프라인 확장용으로 사용된다
3. postprocessing의 EffectComposer
https://github.com/pmndrs/postprocessing?tab=readme-ov-file
이 포스팅에서 주로 소개할 대상이다.
addons와는 완전히 별개로 동작하는 라이브러리로, EffectComposer 또한 이름만 같을뿐 동작방식이 완전 다르며 호환도 불가능하다.
최적의 효율을 위해, Renderer는 기본적으로 아래와같이 설정하기를 권장한다.
import { WebGLRenderer } from "three";
const renderer = new WebGLRenderer({
powerPreference: "high-performance",
antialias: false,
stencil: false,
depth: false
});
pass의 설정은 addons EffectComposer와 마찬가지로 첫번째 pass로 renderPass를 넣고, 이후에 원하는 EffectPass를 포함시키는 방식이다.
import { BloomEffect, EffectComposer, EffectPass, RenderPass } from "postprocessing";
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new EffectPass(camera, new BloomEffect()));
requestAnimationFrame(function render() {
requestAnimationFrame(render);
composer.render();
});
4. Effect와 Pass
postprocessing EffectComposer의 후처리 효과는 주로 Effect와 Pass에 의해 이루어진다.
Effect는 단일 효과로직을 나타내며, Pass는 각 렌더링 단계를 묶는다.
EffectComposer의 효과 또한 결국 Pass들에 의해 누적되는 방식으로 이루어지기 때문에 Effect들 또한 EffectPass로 한데묶어 관리되며, 전체구조는 아래와 같이 요약될 수 있다.
- EffectComposer
- RenderPass (기본 scene을 그려냄)
- EffectPass (Effect들을 한데 묶어 그림)
- 기타 패스들
4-1. 왜 나뉘었는가?
addons의 postprocess와 달리 Effect라는 요소가 생기고, 이를 묶어내는 EffectPass도 별도로 존재하는데,
처음에는 굳이 나눈 이유가 이해되지 않았다.
하지만 pass마다 별도의 렌더링을 수행해야하는 방식과 달리, 별도의 Effect로 나누고 EffectPass로 한데 묶는 방식은, 모든 Effect들을 단일 렌더링으로 처리할 수 있으므로 성능상 이점을 낳게된다.
가령 pass 방식을 고수해서 후처리를 구현할 경우,
import {
EffectComposer,
RenderPass,
EffectPass,
BloomEffect,
VignetteEffect,
NoiseEffect
} from "postprocessing";
const composer = new EffectComposer(renderer);
// 1) 씬 렌더
composer.addPass(new RenderPass(scene, camera));
// 2) Bloom만 따로
composer.addPass(new EffectPass(camera, new BloomEffect()));
// 3) Vignette만 따로
composer.addPass(new EffectPass(camera, new VignetteEffect()));
// 4) Noise만 따로
composer.addPass(new EffectPass(camera, new NoiseEffect()));
function animate() {
requestAnimationFrame(animate);
composer.render();
}
animate();
EffectPass 가 3개 있으며, 각 과정마다 Fullscreen Draw가 따로 발생하게된다.
또한 각 단계마다 read/write buffer 를 통해 읽고 쓰는 과정이 빈번하게 발생함으로 중간 버퍼 읽기/쓰기 (render buffer ping-pong)이 빈번하게 발생하게 되며, VRAM이 낭비된다.
대신 단계별 디버깅을 보다 직관적으로 수행할 수 있게 된다.
그리고 여러 Effect를 단일 Pass로 묶어내는 코드는 아래와 같다.
import {
EffectComposer,
RenderPass,
EffectPass,
BloomEffect,
VignetteEffect,
NoiseEffect
} from "postprocessing";
const composer = new EffectComposer(renderer);
// 1) 씬 렌더
composer.addPass(new RenderPass(scene, camera));
// 2) 여러 Effect를 한 번에
const bloom = new BloomEffect({ intensity: 1.2 });
const vignette = new VignetteEffect({ eskil: false, offset: 0.2, darkness: 0.8 });
const noise = new NoiseEffect({ premultiply: true });
composer.addPass(new EffectPass(camera, bloom, vignette, noise));
function animate() {
requestAnimationFrame(animate);
composer.render();
}
animate();
위 방식을 쓸 경우, 단일 FullScreen Draw를 1회로 묶어낼 수 있게 되며, 내부 구현 방식에 따라 훨씬 더 효율적으로 횟수가 줄어들게된다.
4-2. 사용예
화면 한가운데에 흰색의 동그라미를 덧그리는 간단한 effect를 구현하고 이를 r3f 환경에서 렌더링하도록 실습해보겠다.
1. Effect 구현
import { Effect, BlendFunction } from "postprocessing";
import { Uniform } from "three";
import { wrapEffect } from "@react-three/postprocessing";
export type CenterCircleEffectOptions = {
radius?: number; // 0 ~ 0.5 (uv 기준)
feather?: number; // 가장자리 부드러움
opacity?: number; // 0 ~ 1
blendFunction?: BlendFunction;
};
const fragment = `
uniform float uRadius; // circle radius in UV space
uniform float uFeather; // edge softness
uniform float uOpacity; // circle opacity
float circleMask(vec2 uv, float radius, float feather) {
vec2 p = uv - vec2(0.5);
float d = length(p);
return 1.0 - smoothstep(radius, radius + feather, d);
}
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
float m = circleMask(uv, uRadius, uFeather);
vec3 circle = vec3(1.0); // white
vec3 color = mix(inputColor.rgb, circle, m * uOpacity);
outputColor = vec4(color, inputColor.a);
}
`;
export class CenterCircleEffect extends Effect {
declare uniforms: Map<string, Uniform>;
constructor({
radius = 0.08,
feather = 0.005,
opacity = 1.0,
blendFunction = BlendFunction.NORMAL,
}: CenterCircleEffectOptions = {}) {
super("CenterCircleEffect", fragment, {
blendFunction,
uniforms: new Map<string, Uniform>([
["uRadius", new Uniform(radius)],
["uFeather", new Uniform(feather)],
["uOpacity", new Uniform(opacity)],
]),
});
}
get radius(): number {
return this.uniforms.get("uRadius")!.value as number;
}
set radius(v: number) {
this.uniforms.get("uRadius")!.value = v;
}
get feather(): number {
return this.uniforms.get("uFeather")!.value as number;
}
set feather(v: number) {
this.uniforms.get("uFeather")!.value = v;
}
get opacity(): number {
return this.uniforms.get("uOpacity")!.value as number;
}
set opacity(v: number) {
this.uniforms.get("uOpacity")!.value = v;
}
}
export const CenterCircle = wrapEffect(CenterCircleEffect, {
radius: 0.08,
feather: 0.006,
opacity: 1.0,
});
마지막에 @react-three/processing의 wrapEffect를 통해, 컴포넌트로 사용 가능하도록 래핑하였다.
위 effect를 아래와 같이 사용하면 된다.
import * as THREE from "three";
import { OrbitControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { EffectComposer } from "@react-three/postprocessing";
import { CenterCircle } from "./center-circle-effect";
function SceneRoot() {
return (
<>
<mesh position={[1, 0, 1]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={"red"} />
</mesh>
<mesh position={[1, 0, -1]}>
<sphereGeometry args={[0.75, 16, 16]} />
<meshStandardMaterial color={"blue"} />
</mesh>
<mesh position={[-1, 0, 1]}>
<coneGeometry args={[0.75, 1.75, 16]} />
<meshStandardMaterial color={"yellow"} />
</mesh>
<mesh position={[-1, 0, -1]}>
<capsuleGeometry args={[0.75, 1, 16]} />
<meshStandardMaterial color={"green"} />
</mesh>
</>
);
}
export default function PostProcessTab() {
return (
<>
<div className="relative block w-full h-full">
<Canvas className="relative block w-full h-full">
<color attach="background" args={[new THREE.Color("black")]} />
<ambientLight intensity={0.5} />
<SceneRoot />
<OrbitControls />
<directionalLight
intensity={1.5}
position={[1, 1, 0.5]}
lookAt={[0, 0, 0]}
/>
<EffectComposer>
<CenterCircle radius={0.09} feather={0.008} opacity={1.0} />
</EffectComposer>
</Canvas>
</div>
</>
);
}
적용 결과

화면 한가운데에 흰색 동그라미가 덧그려짐을 확인할 수 있다.
'computer graphics > Three.js' 카테고리의 다른 글
| R3F의 3D 컴포넌트 (0) | 2026.01.01 |
|---|---|
| Post Process (0) | 2025.12.27 |
