post-processing.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>후처리</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – 후처리">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <script type="importmap">
  14. {
  15. "imports": {
  16. "three": "../../build/three.module.js"
  17. }
  18. }
  19. </script>
  20. <link rel="stylesheet" href="/manual/ko/lang.css">
  21. </head>
  22. <body>
  23. <div class="container">
  24. <div class="lesson-title">
  25. <h1>후처리</h1>
  26. </div>
  27. <div class="lesson">
  28. <div class="lesson-main">
  29. <p><em>후처리(post processing)</em>란 보통 2D 이미지에 어떤 효과나 필터를 넣는 것을 의미합니다. Three.js는 다양한 mesh로 이루어진 장면을 2D 이미지로 렌더링하죠. 일반적으로 이 이미지는 바로 캔버스를 통해 브라우저 화면에 렌더링됩니다. 하지만 대신 이 이미지를 <a href="rendertargets.html">렌더 타겟에 렌더링하고</a> 캔버스에 보내기 전 임의의 <em>후처리</em> 효과를 줄 수 있습니다.</p>
  30. <p>인스타그램 필터, 포토샵 필터 등이 후처리의 좋은 예이죠.</p>
  31. <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>
  32. <p>※ 편의상 <code class="notranslate" translate="no">Pass</code> 인스턴스를 pass로 번역합니다.</p>
  33. <p>이 pass는 비넷(vignette), 흐림(blur), 블룸(bloom), 필름 그레인(film grain) 효과 또는 hue, 채도(saturation), 대비(contrast) 조정 등의 후처리 효과로, 이 효과를 모두 적용한 결과물을 최종적으로 캔버스에 렌더링합니다.</p>
  34. <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>
  35. <p><code class="notranslate" translate="no">EffectComposer.addPass</code>를 각 pass를 적용할 순서대로 호출하고 <code class="notranslate" translate="no">EffectComposer.render</code>를 호출하면 pass*는 아래 그림과 같은 순서로 적용됩니다.</p>
  36. <div class="threejs_center"><img src="../resources/images/threejs-postprocessing.svg" style="width: 600px"></div>
  37. <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>
  38. <p><code class="notranslate" translate="no">Pass</code>에는 공통적으로 4가지 옵션이 있습니다.</p>
  39. <h2 id="-enabled-"><code class="notranslate" translate="no">enabled</code></h2>
  40. <p>이 pass를 사용할지의 여부입니다.</p>
  41. <h2 id="-needsswap-"><code class="notranslate" translate="no">needsSwap</code></h2>
  42. <p>이 pass를 적용한 후 <code class="notranslate" translate="no">rtA</code>와 <code class="notranslate" translate="no">rtB</code>를 바꿀지의 여부입니다.</p>
  43. <h2 id="-clear-"><code class="notranslate" translate="no">clear</code></h2>
  44. <p>이 pass를 적용하기 전에 화면을 초기화할지의 여부입니다.</p>
  45. <h2 id="-rendertoscreen-"><code class="notranslate" translate="no">renderToScreen</code></h2>
  46. <p>지정한 렌더 타겟이 아닌 캔버스에 렌더링할지의 여부입니다. 보통 <code class="notranslate" translate="no">EffectComposer</code>에 추가하는 마지막 pass에 이 옵션을 true로 설정합니다.</p>
  47. <p>간단한 예제를 만들어봅시다. <a href="responsive.html">반응형 디자인에 관한 글</a>에서 썼던 예제를 가져오겠습니다.</p>
  48. <p>추가로 먼저 <code class="notranslate" translate="no">EffectComposer</code> 인스턴스를 생성합니다.</p>
  49. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const composer = new EffectComposer(renderer);
  50. </pre>
  51. <p>다음으로 <code class="notranslate" translate="no">RenderPass</code>를 첫 pass로 추가합니다. 이 pass는 넘겨 받은 장면을 첫 렌더 타겟에 렌더링할 겁니다.</p>
  52. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">composer.addPass(new RenderPass(scene, camera));
  53. </pre>
  54. <p>다음으로 <code class="notranslate" translate="no">BloomPass</code>를 추가합니다. <code class="notranslate" translate="no">BloomPass</code>는 장면을 원래의 장면보다 작게 렌더링해 흐림(blur) 효과를 줍니다. 그리고 효과가 적용된 장면을 원래 장면에 덮어 씌우는 식으로 <em>블룸</em> 효과를 구현합니다.</p>
  55. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const bloomPass = new BloomPass(
  56. 1, // 강도
  57. 25, // 커널(kernel) 크기
  58. 4, // 시그마 ?
  59. 256, // 렌더 타겟의 해상도를 낮춤
  60. );
  61. composer.addPass(bloomPass);
  62. </pre>
  63. <p>마지막으로 원본 장면에 노이즈와 스캔라인(scanline)을 추가하는 <code class="notranslate" translate="no">FilmPass</code>를 추가합니다.</p>
  64. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const filmPass = new FilmPass(
  65. 0.5, // 강도
  66. false, // 흑백
  67. );
  68. composer.addPass(filmPass);
  69. </pre>
  70. <p>또 이 클래스들을 사용하기 위해 여러 스크립트를 불러와야 합니다.</p>
  71. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
  72. import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
  73. import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
  74. import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
  75. import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
  76. </pre>
  77. <p>대부분의 후처리에는 <code class="notranslate" translate="no">EffectComposer.js</code>와 <code class="notranslate" translate="no">RenderPass.js</code>가 필수입니다.</p>
  78. <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>
  79. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function render(now) {
  80. - time *= 0.001;
  81. +let then = 0;
  82. +function render(now) {
  83. + now *= 0.001; // 초 단위로 변환
  84. + const deltaTime = now - then;
  85. + then = now;
  86. if (resizeRendererToDisplaySize(renderer)) {
  87. const canvas = renderer.domElement;
  88. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  89. camera.updateProjectionMatrix();
  90. + composer.setSize(canvas.width, canvas.height);
  91. }
  92. cubes.forEach((cube, ndx) =&gt; {
  93. const speed = 1 + ndx * .1;
  94. - const rot = time * speed;
  95. + const rot = now * speed;
  96. cube.rotation.x = rot;
  97. cube.rotation.y = rot;
  98. });
  99. - renderer.render(scene, camera);
  100. + composer.render(deltaTime);
  101. requestAnimationFrame(render);
  102. }
  103. </pre>
  104. <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>
  105. <p></p><div translate="no" class="threejs_example_container notranslate">
  106. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing.html"></iframe></div>
  107. <a class="threejs_center" href="/manual/examples/postprocessing.html" target="_blank">새 탭에서 보기</a>
  108. </div>
  109. <p></p>
  110. <p>런타임에 효과의 속성을 변경할 때는 보통 uniform의 value 값을 바꿉니다. GUI를 추가해 이 속성을 조정할 수 있게 만들어보죠. 어떤 속성을 어떻게 조작할 수 있는지는 해당 효과의 소스 코드를 열어봐야 알 수 있습니다.</p>
  111. <p><a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/BloomPass.js"><code class="notranslate" translate="no">BloomPass.js</code></a>에서
  112. 아래 코드를 찾았습니다.</p>
  113. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">this.copyUniforms[ "opacity" ].value = strength;
  114. </pre>
  115. <p>아래처럼 하면 강도를 런타임에 바꿀 수 있겠네요.</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">bloomPass.copyUniforms.opacity.value = someValue;
  117. </pre>
  118. <p>마찬가지로 <a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/FilmPass.js"><code class="notranslate" translate="no">FilmPass.js</code></a>에서
  119. 아래 코드를 찾았습니다.</p>
  120. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">this.uniforms.intensity.value = intensity;
  121. this.uniforms.grayscale.value = grayscale;
  122. </pre>
  123. <p>이제 어떻게 값을 지정해야 하는지 알았으니 이 값을 조작하는 GUI를 만들어봅시다.</p>
  124. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
  125. </pre>
  126. <p>일단 모듈을 로드합니다.</p>
  127. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  128. {
  129. const folder = gui.addFolder('BloomPass');
  130. folder.add(bloomPass.copyUniforms.opacity, 'value', 0, 2).name('strength');
  131. folder.open();
  132. }
  133. {
  134. const folder = gui.addFolder('FilmPass');
  135. folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
  136. folder.add(filmPass.uniforms.intensity, 'value', 0, 1).name('intensity');
  137. folder.open();
  138. }
  139. </pre>
  140. <p>이제 각 설정을 조작할 수 있습니다.</p>
  141. <p></p><div translate="no" class="threejs_example_container notranslate">
  142. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-gui.html"></iframe></div>
  143. <a class="threejs_center" href="/manual/examples/postprocessing-gui.html" target="_blank">새 탭에서 보기</a>
  144. </div>
  145. <p></p>
  146. <p>여기까지 잘 따라왔다면 이제 효과를 직접 만들어볼 수 있습니다.</p>
  147. <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>
  148. <p>직접 예제를 만들어보는 게 도움이 될 테니 간단한 GLSL 후처리 쉐이더를 만들어봅시다. 이미지에 특정 색을 혼합하는 쉐이더를 만들 겁니다.</p>
  149. <p>Three.js에는 후처리를 도와주는 <code class="notranslate" translate="no">ShaderPass</code> 헬퍼 클래스가 있습니다. 인자로 vertex 쉐이더, fragment 쉐이더, 기본값으로 이루어진 객체를 받죠. 이 클래스는 이전 pass의 결과물에서 어떤 텍스처를 읽을지, 그리고 <code class="notranslate" translate="no">EffectComposer</code>의 렌더 타겟과 캔버스 중 어디에 렌더링할지를 결정할 겁니다.</p>
  150. <p>아래는 이전 pass의 결과물에 특정 색을 혼합하는 간단한 후처리 쉐이더입니다.</p>
  151. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const colorShader = {
  152. uniforms: {
  153. tDiffuse: { value: null },
  154. color: { value: new THREE.Color(0x88CCFF) },
  155. },
  156. vertexShader: `
  157. varying vec2 vUv;
  158. void main() {
  159. vUv = uv;
  160. gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
  161. }
  162. `,
  163. fragmentShader: `
  164. varying vec2 vUv;
  165. uniform sampler2D tDiffuse;
  166. uniform vec3 color;
  167. void main() {
  168. vec4 previousPassColor = texture2D(tDiffuse, vUv);
  169. gl_FragColor = vec4(
  170. previousPassColor.rgb * color,
  171. previousPassColor.a);
  172. }
  173. `,
  174. };
  175. </pre>
  176. <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>
  177. <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>
  178. <p>마지막으로 fragment 쉐이더를 생성합니다. 아래 코드로 이전 pass에서 넘겨준 결과물의 픽셀 색상값을 가져올 수 있습니다.</p>
  179. <pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">vec4 previousPassColor = texture2D(tDiffuse, vUv);
  180. </pre>
  181. <p>여기에 지정한 색상을 곱해 <code class="notranslate" translate="no">gl_FragColor</code>에 결과를 저장합니다.</p>
  182. <pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">gl_FragColor = vec4(
  183. previousPassColor.rgb * color,
  184. previousPassColor.a);
  185. </pre>
  186. <p>추가로 간단한 GUI를 만들어 rgb의 각 색상값을 조정할 수 있도록 합니다.</p>
  187. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  188. gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
  189. gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
  190. gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');
  191. </pre>
  192. <p>색을 혼합하는 간단한 후처리 쉐이더를 완성했습니다.</p>
  193. <p></p><div translate="no" class="threejs_example_container notranslate">
  194. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-custom.html"></iframe></div>
  195. <a class="threejs_center" href="/manual/examples/postprocessing-custom.html" target="_blank">새 탭에서 보기</a>
  196. </div>
  197. <p></p>
  198. <p>언급했듯 이 글에서 GLSL의 작성법과 사용자 지정 쉐이더를 만드는 법을 모두 다루기는 무리입니다. WebGL이 어떻게 동작하는지 알고 싶다면 <a href="https://webglfundamentals.org">이 시리즈</a>를 참고하세요. <a href="https://github.com/mrdoob/three.js/tree/master/examples/jsm/shaders">Three.js의 후처리 쉐이더 소스 코드</a>를 분석하는 것도 좋은 방법입니다. 상대적으로 복잡한 쉐이더도 있지만 작은 것부터 차근차근 살펴본다면 언젠가 전체를 이해할 수 있을 거예요.</p>
  199. <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/jsm/postprocessing">후처리 효과의 소스 코드</a>를 직접 분석해야 합니다. 부디 이 글과 이 시리즈의 <a href="rendertargets.html">렌더 타겟에 관한 글</a>이 좋은 출발점을 마련해주었으면 좋겠네요.</p>
  200. </div>
  201. </div>
  202. </div>
  203. <script src="../resources/prettify.js"></script>
  204. <script src="../resources/lesson.js"></script>
  205. </body></html>