You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

231 lines
18 KiB

<!DOCTYPE html><html lang="ko"><head>
<meta charset="utf-8">
<title>후처리</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@threejs">
<meta name="twitter:title" content="Three.js – 후처리">
<meta property="og:image" content="https://threejs.org/files/share.png">
<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="/manual/resources/lesson.css">
<link rel="stylesheet" href="/manual/resources/lang.css">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js"
}
}
</script>
<link rel="stylesheet" href="/manual/ko/lang.css">
</head>
<body>
<div class="container">
<div class="lesson-title">
<h1>후처리</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p><em>후처리(post processing)</em>란 보통 2D 이미지에 어떤 효과나 필터를 넣는 것을 의미합니다. Three.js는 다양한 mesh로 이루어진 장면을 2D 이미지로 렌더링하죠. 일반적으로 이 이미지는 바로 캔버스를 통해 브라우저 화면에 렌더링됩니다. 하지만 대신 이 이미지를 <a href="rendertargets.html">렌더 타겟에 렌더링하고</a> 캔버스에 보내기 전 임의의 <em>후처리</em> 효과를 줄 수 있습니다.</p>
<p>인스타그램 필터, 포토샵 필터 등이 후처리의 좋은 예이죠.</p>
<p>Three.js에는 후처리를 순차적으로 처리해주는 모범 클래스가 있습니다. 일단 <code class="notranslate" translate="no">EffectComposer</code>의 인스턴스를 만들고 여기에 <code class="notranslate" translate="no">Pass</code> 객체(효과, 필터)들을 추가합니다. 그리고 <code class="notranslate" translate="no">EffectComposer.render</code> 메서드를 호출하면 현재 장면을 <a href="rendertargets.html">렌더 타겟</a>에 렌더링한 뒤 각 pass*를 순서대로 적용합니다.</p>
<p>※ 편의상 <code class="notranslate" translate="no">Pass</code> 인스턴스를 pass로 번역합니다.</p>
<p>이 pass는 비넷(vignette), 흐림(blur), 블룸(bloom), 필름 그레인(film grain) 효과 또는 hue, 채도(saturation), 대비(contrast) 조정 등의 후처리 효과로, 이 효과를 모두 적용한 결과물을 최종적으로 캔버스에 렌더링합니다.</p>
<p>여기서 어느 정도 <code class="notranslate" translate="no">EffectComposer</code>의 원리를 이해할 필요가 있습니다. <code class="notranslate" translate="no">EffectComposer</code>는 두 개의 <a href="rendertargets.html">렌더 타겟</a>을 사용합니다. 편의상 이 둘을 <strong>rtA</strong>, <strong>rtB</strong>라고 부르도록 하죠.</p>
<p><code class="notranslate" translate="no">EffectComposer.addPass</code>를 각 pass를 적용할 순서대로 호출하고 <code class="notranslate" translate="no">EffectComposer.render</code>를 호출하면 pass*는 아래 그림과 같은 순서로 적용됩니다.</p>
<div class="threejs_center"><img src="../resources/images/threejs-postprocessing.svg" style="width: 600px"></div>
<p>먼저 <code class="notranslate" translate="no">RenderPass</code>에 넘긴 장면을 <strong>rtA</strong>에 렌더링합니다. 그리고 <strong>rtA</strong>를 다음 pass에 넘겨주면 해당 pass는 <strong>rtA</strong>에 pass를 적용한 결과를 <strong>rtB</strong>에 렌더링합니다. 그런 다음 <strong>rtB</strong>를 다음 pass로 넘겨 적용한 결과를 <strong>rtA</strong>에, <strong>rtA</strong>에 pass를 적용한 결과를 다시 <strong>rtB</strong>에, 이런 식으로 모든 pass가 끝날 때까지 계속 반복합니다.</p>
<p><code class="notranslate" translate="no">Pass</code>에는 공통적으로 4가지 옵션이 있습니다.</p>
<h2 id="-enabled-"><code class="notranslate" translate="no">enabled</code></h2>
<p>이 pass를 사용할지의 여부입니다.</p>
<h2 id="-needsswap-"><code class="notranslate" translate="no">needsSwap</code></h2>
<p>이 pass를 적용한 후 <code class="notranslate" translate="no">rtA</code><code class="notranslate" translate="no">rtB</code>를 바꿀지의 여부입니다.</p>
<h2 id="-clear-"><code class="notranslate" translate="no">clear</code></h2>
<p>이 pass를 적용하기 전에 화면을 초기화할지의 여부입니다.</p>
<h2 id="-rendertoscreen-"><code class="notranslate" translate="no">renderToScreen</code></h2>
<p>지정한 렌더 타겟이 아닌 캔버스에 렌더링할지의 여부입니다. 보통 <code class="notranslate" translate="no">EffectComposer</code>에 추가하는 마지막 pass에 이 옵션을 true로 설정합니다.</p>
<p>간단한 예제를 만들어봅시다. <a href="responsive.html">반응형 디자인에 관한 글</a>에서 썼던 예제를 가져오겠습니다.</p>
<p>추가로 먼저 <code class="notranslate" translate="no">EffectComposer</code> 인스턴스를 생성합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const composer = new EffectComposer(renderer);
</pre>
<p>다음으로 <code class="notranslate" translate="no">RenderPass</code>를 첫 pass로 추가합니다. 이 pass는 넘겨 받은 장면을 첫 렌더 타겟에 렌더링할 겁니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">composer.addPass(new RenderPass(scene, camera));
</pre>
<p>다음으로 <code class="notranslate" translate="no">BloomPass</code>를 추가합니다. <code class="notranslate" translate="no">BloomPass</code>는 장면을 원래의 장면보다 작게 렌더링해 흐림(blur) 효과를 줍니다. 그리고 효과가 적용된 장면을 원래 장면에 덮어 씌우는 식으로 <em>블룸</em> 효과를 구현합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const bloomPass = new BloomPass(
1, // 강도
25, // 커널(kernel) 크기
4, // 시그마 ?
256, // 렌더 타겟의 해상도를 낮춤
);
composer.addPass(bloomPass);
</pre>
<p>마지막으로 원본 장면에 노이즈와 스캔라인(scanline)을 추가하는 <code class="notranslate" translate="no">FilmPass</code>를 추가합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const filmPass = new FilmPass(
0.35, // 노이즈 강도
0.025, // 스캔라인 강도
648, // 스캔라인 개수
false, // 흑백
);
filmPass.renderToScreen = true;
composer.addPass(filmPass);
</pre>
<p><code class="notranslate" translate="no">filmPass</code>가 마지막 pass이기에 캔버스에 결과를 바로 렌더링하도록 <code class="notranslate" translate="no">renderToScreen</code> 옵션을 true로 설정했습니다. 이 옵션을 설정하지 않으면 캔버스가 아닌 다음 렌더 타겟에 장면을 렌더링할 거예요.</p>
<p>또 이 클래스들을 사용하기 위해 여러 스크립트를 불러와야 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
</pre>
<p>대부분의 후처리에는 <code class="notranslate" translate="no">EffectComposer.js</code><code class="notranslate" translate="no">RenderPass.js</code>가 필수입니다.</p>
<p>이제 <a href="/docs/#api/ko/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> 대신 <code class="notranslate" translate="no">EffectComposer.render</code>를 사용<em>하고</em> <code class="notranslate" translate="no">EffectComposer</code>가 결과물을 캔버스의 크기에 맞추도록 해야 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function render(now) {
- time *= 0.001;
+let then = 0;
+function render(now) {
+ now *= 0.001; // 초 단위로 변환
+ const deltaTime = now - then;
+ then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
+ composer.setSize(canvas.width, canvas.height);
}
cubes.forEach((cube, ndx) =&gt; {
const speed = 1 + ndx * .1;
- const rot = time * speed;
+ const rot = now * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
- renderer.render(scene, camera);
+ composer.render(deltaTime);
requestAnimationFrame(render);
}
</pre>
<p><code class="notranslate" translate="no">EffectComposer.render</code> 메서드는 인자로 마지막 프레임을 렌더링한 이후의 시간값인 <code class="notranslate" translate="no">deltaTime</code>을 인자로 받습니다. pass에 애니메이션이 필요할 경우를 대비해 이 값을 넘겨주기 위해서이죠. 예제의 경우에는 <code class="notranslate" translate="no">FilmPass</code>에 애니메이션이 있습니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>런타임에 효과의 속성을 변경할 때는 보통 uniform의 value 값을 바꿉니다. GUI를 추가해 이 속성을 조정할 수 있게 만들어보죠. 어떤 속성을 어떻게 조작할 수 있는지는 해당 효과의 소스 코드를 열어봐야 알 수 있습니다.</p>
<p><a href="https://github.com/mrdoob/three.js/blob/master/examples/js/postprocessing/BloomPass.js"><code class="notranslate" translate="no">BloomPass.js</code></a>에서
아래 코드를 찾았습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">this.copyUniforms[ "opacity" ].value = strength;
</pre>
<p>아래처럼 하면 강도를 런타임에 바꿀 수 있겠네요.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">bloomPass.copyUniforms.opacity.value = someValue;
</pre>
<p>마찬가지로 <a href="https://github.com/mrdoob/three.js/blob/master/examples/js/postprocessing/FilmPass.js"><code class="notranslate" translate="no">FilmPass.js</code></a>에서
아래 코드를 찾았습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">if ( grayscale !== undefined ) this.uniforms.grayscale.value = grayscale;
if ( noiseIntensity !== undefined ) this.uniforms.nIntensity.value = noiseIntensity;
if ( scanlinesIntensity !== undefined ) this.uniforms.sIntensity.value = scanlinesIntensity;
if ( scanlinesCount !== undefined ) this.uniforms.sCount.value = scanlinesCount;
</pre>
<p>이제 어떻게 값을 지정해야 하는지 알았으니 이 값을 조작하는 GUI를 만들어봅시다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
</pre>
<p>일단 모듈을 로드합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
{
const folder = gui.addFolder('BloomPass');
folder.add(bloomPass.copyUniforms.opacity, 'value', 0, 2).name('strength');
folder.open();
}
{
const folder = gui.addFolder('FilmPass');
folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
folder.add(filmPass.uniforms.nIntensity, 'value', 0, 1).name('noise intensity');
folder.add(filmPass.uniforms.sIntensity, 'value', 0, 1).name('scanline intensity');
folder.add(filmPass.uniforms.sCount, 'value', 0, 1000).name('scanline count');
folder.open();
}
</pre>
<p>이제 각 설정을 조작할 수 있습니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-gui.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-gui.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>여기까지 잘 따라왔다면 이제 효과를 직접 만들어볼 수 있습니다.</p>
<p>후처리 효과는 쉐이더를 사용합니다. 쉐이더는 <a href="https://www.khronos.org/files/opengles_shading_language.pdf">GLSL (Graphics Library Shading Language)</a>이라는 언어를 사용하죠. 언어가 방대해 이 글에서 전부 다루기는 어렵습니다. 기초부터 알아보고 싶다면 <a href="https://webglfundamentals.org/webgl/lessons/ko/webgl-shaders-and-glsl.html">이 글</a><a href="https://thebookofshaders.com/">쉐이더란 무엇인가(The Book of Shaders)</a>를 읽어보기 바랍니다.</p>
<p>직접 예제를 만들어보는 게 도움이 될 테니 간단한 GLSL 후처리 쉐이더를 만들어봅시다. 이미지에 특정 색을 혼합하는 쉐이더를 만들 겁니다.</p>
<p>Three.js에는 후처리를 도와주는 <code class="notranslate" translate="no">ShaderPass</code> 헬퍼 클래스가 있습니다. 인자로 vertex 쉐이더, fragment 쉐이더, 기본값으로 이루어진 객체를 받죠. 이 클래스는 이전 pass의 결과물에서 어떤 텍스처를 읽을지, 그리고 <code class="notranslate" translate="no">EffectComposer</code>의 렌더 타겟과 캔버스 중 어디에 렌더링할지를 결정할 겁니다.</p>
<p>아래는 이전 pass의 결과물에 특정 색을 혼합하는 간단한 후처리 쉐이더입니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const colorShader = {
uniforms: {
tDiffuse: { value: null },
color: { value: new THREE.Color(0x88CCFF) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform vec3 color;
void main() {
vec4 previousPassColor = texture2D(tDiffuse, vUv);
gl_FragColor = vec4(
previousPassColor.rgb * color,
previousPassColor.a);
}
`,
};
</pre>
<p>위 코드에서 <code class="notranslate" translate="no">tDiffuse</code>는 이전 pass의 결과물을 받아오기 위한 것으로 거의 모든 경우에 필수입니다. 그리고 그 바로 밑에 <code class="notranslate" translate="no">color</code> 속성을 Three.js의 <a href="/docs/#api/ko/math/Color"><code class="notranslate" translate="no">Color</code></a>로 선언했습니다.</p>
<p>다음으로 vertex 쉐이더를 작성해야 합니다. 위 코드에서 작성한 vertex 쉐이더는 후처리에서 거의 표준처럼 사용하는 코드로, 대부분의 경우 바꿀 필요가 없습니다. 뭔가 많이 설정한 경우(아까 언급한 링크 참조)가 아니라면 <code class="notranslate" translate="no">uv</code>, <code class="notranslate" translate="no">projectionMatrix</code>, <code class="notranslate" translate="no">modelViewMatrix</code>, <code class="notranslate" translate="no">position</code> 변수는 Three.js가 알아서 넣어줍니다.</p>
<p>마지막으로 fragment 쉐이더를 생성합니다. 아래 코드로 이전 pass에서 넘겨준 결과물의 픽셀 색상값을 가져올 수 있습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">vec4 previousPassColor = texture2D(tDiffuse, vUv);
</pre>
<p>여기에 지정한 색상을 곱해 <code class="notranslate" translate="no">gl_FragColor</code>에 결과를 저장합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">gl_FragColor = vec4(
previousPassColor.rgb * color,
previousPassColor.a);
</pre>
<p>추가로 간단한 GUI를 만들어 rgb의 각 색상값을 조정할 수 있도록 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');
</pre>
<p>색을 혼합하는 간단한 후처리 쉐이더를 완성했습니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-custom.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-custom.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>언급했듯 이 글에서 GLSL의 작성법과 사용자 지정 쉐이더를 만드는 법을 모두 다루기는 무리입니다. WebGL이 어떻게 동작하는지 알고 싶다면 <a href="https://webglfundamentals.org">이 시리즈</a>를 참고하세요. <a href="https://github.com/mrdoob/three.js/tree/master/examples/js/shaders">Three.js의 후처리 쉐이더 소스 코드</a>를 분석하는 것도 좋은 방법입니다. 상대적으로 복잡한 쉐이더도 있지만 작은 것부터 차근차근 살펴본다면 언젠가 전체를 이해할 수 있을 거예요.</p>
<p>아쉽게도 Three.js의 후처리 효과 대부분은 공식 문서가 없어 <a href="https://github.com/mrdoob/three.js/tree/master/examples">예제를 참고하거나</a> <a href="https://github.com/mrdoob/three.js/tree/master/examples/js/postprocessing">후처리 효과의 소스 코드</a>를 직접 분석해야 합니다. 부디 이 글과 이 시리즈의 <a href="rendertargets.html">렌더 타겟에 관한 글</a>이 좋은 출발점을 마련해주었으면 좋겠네요.</p>
</div>
</div>
</div>
<script src="/manual/resources/prettify.js"></script>
<script src="/manual/resources/lesson.js"></script>
</body></html>