shadows.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>그림자<(Shadows)</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 – 그림자(Shadows)">
  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>그림자(Shadows)</h1>
  26. </div>
  27. <div class="lesson">
  28. <div class="lesson-main">
  29. <p>※ 이 글은 Three.js의 튜토리얼 시리즈로서,
  30. 먼저 <a href="fundamentals.html">Three.js의 기본 구조에 관한 글</a>을
  31. 읽고 오길 권장합니다.</p>
  32. <p>※ 이전 글인 <a href="cameras.html">카메라에 관한 글</a>과
  33. <a href="lights.html">조명에 관한 글</a>에서 이 장을 읽는 꼭 필요한 내용을
  34. 다루었으니 꼭 먼저 읽고 오시기 바랍니다.</p>
  35. <p>3D 그래픽에서 그림자란 그리 간단한 주제가 아닙니다. 그림자를 구현하는 방법은
  36. 아주 많지만 모두 단점이 있기에 어떤 것이 가장 효율적이라고 말하기 어렵습니다.
  37. 이는 Three.js에서 제공하는 방법도 마찬가지이죠.</p>
  38. <p>Three.js는 기본적으로 <em>그림자 맵(shadow maps)</em>을 사용합니다. 그림자 맵이란
  39. <em>그림자를 만드는 빛의 영향을 받는, 그림자를 드리우는 모든 물체를 빛의 시점에서
  40. 렌더링</em>하는 기법을 말합니다. 중요하니 <strong>한 번 더 읽어보세요!</strong></p>
  41. <p>다시 말해, 공간 안에 20개의 물체와 5개의 조명이 있고, 20개의 물체 모두
  42. 그림자를 드리우며 5개의 조명 모두 그림자를 지게 한다면, 한 장면을 만들기
  43. 위해 총 6번 화면을 렌더링할 것이라는 이야기입니다. 먼저 조명 1번에 대해
  44. 20개의 물체를 전부 렌더링하고, 다음에는 2번 조명, 그 다음에는 3번...
  45. 이렇게 처음 5번 렌더링한 결과물을 합쳐 최종 결과물을 만드는 것이죠.</p>
  46. <p>만약 여기에 포인트(point) 조명을 하나 추가하면 조명 하나 때문에 6번을 다시
  47. 렌더링해야 합니다.</p>
  48. <p>이 때문에 그림자를 지게 하는 조명을 여러개 만들기보다 다른 방법을 찾는
  49. 경우가 보통입니다. 주로 사용하는 방법은 조명이 여러개 있어도 하나의 조명만
  50. 그림자를 지게끔 설정하는 것이죠.</p>
  51. <p>물론 라이트맵(lightmaps)이나 앰비언트 오클루전(ambient occlusion)을 이용해
  52. 빛의 영향을 미리 계산할 수도 있습니다. 이러면 정적 조명이나 정적 빛 반사를
  53. 사용하는 것이기에 수정하기가 어렵지만, 적어도 성능은 빠릅니다. 이 두 가지
  54. 모두 나중에 별도로 다룰 것입니다.</p>
  55. <p>가짜 그림자를 사용하는 방법도 있습니다. 평면을 만들고, 흑백 텍스처를 입혀
  56. 땅 위에 그림자가 있을 만한 위치에 가져다 놓는 것이죠.</p>
  57. <p>예를 들어 아래 텍스처를 사용해 가짜 그림자를 만들어보겠습니다.</p>
  58. <div class="threejs_center"><img src="../examples/resources/images/roundshadow.png"></div>
  59. <p><a href="cameras.html">이전 글</a>에서 작성했던 코드를 일부 활용하겠습니다.</p>
  60. <p>먼저 배경을 흰색으로 칠합니다.</p>
  61. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  62. +scene.background = new THREE.Color('white');
  63. </pre>
  64. <p>같은 체크판 무늬 땅을 사용하되, 땅이 조명의 영향을 받을 필요는 없으니
  65. <a href="/docs/#api/ko/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>을 사용하겠습니다.</p>
  66. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loader = new THREE.TextureLoader();
  67. {
  68. const planeSize = 40;
  69. - const loader = new THREE.TextureLoader();
  70. const texture = loader.load('resources/images/checker.png');
  71. texture.wrapS = THREE.RepeatWrapping;
  72. texture.wrapT = THREE.RepeatWrapping;
  73. texture.magFilter = THREE.NearestFilter;
  74. const repeats = planeSize / 2;
  75. texture.repeat.set(repeats, repeats);
  76. const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  77. const planeMat = new THREE.MeshBasicMaterial({
  78. map: texture,
  79. side: THREE.DoubleSide,
  80. });
  81. + planeMat.color.setRGB(1.5, 1.5, 1.5);
  82. const mesh = new THREE.Mesh(planeGeo, planeMat);
  83. mesh.rotation.x = Math.PI * -.5;
  84. scene.add(mesh);
  85. }
  86. </pre>
  87. <p>평면의 색상을 <code class="notranslate" translate="no">1.5, 1.5, 1.5</code>로 설정했습니다. 체크판 텍스처의 색상을 1.5, 1.5, 1.5 만큼
  88. 곱해준 것이죠. 체크판 원본 텍스처의 색상이 0x808080(회색), 0xC0C0C0(옅은 회색)이므로,
  89. 여기에 1.5를 곱해주면 흰색, 옅은 회색 체크판이 됩니다.</p>
  90. <p>이제 그림자 텍스처를 로드해보죠.</p>
  91. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const shadowTexture = loader.load('resources/images/roundshadow.png');
  92. </pre>
  93. <p>구체와 관련된 객체를 분류하기 위해 배열을 만들겠습니다.</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sphereShadowBases = [];
  95. </pre>
  96. <p>다음으로 구체 geometry를 만듭니다.</p>
  97. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sphereRadius = 1;
  98. const sphereWidthDivisions = 32;
  99. const sphereHeightDivisions = 16;
  100. const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  101. </pre>
  102. <p>가짜 그림자를 위한 평면 <code class="notranslate" translate="no">geometry</code>도 만듭니다.</p>
  103. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const planeSize = 1;
  104. const shadowGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  105. </pre>
  106. <p>이제 구체를 아주 많이 만들겠습니다. 각각 구체마다 <code class="notranslate" translate="no">컨테이너</code> 역할을 할
  107. <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">THREE.Object3D</code></a>를 만들고, 그림자 평면 mesh, 구체 mesh를 이
  108. 컨테이너의 자식으로 만듭니다. 이러면 구체와 그림자를 동시에 움직일 수
  109. 있죠. z-파이팅 현상을 막기 위해 그림자는 땅보다 약간 위에 둡니다.
  110. 또 <code class="notranslate" translate="no">depthWrite</code> 속성을 false로 설정해 그림자끼리 충돌하는 현상을
  111. 막습니다. 이 충돌 현상은 <a href="transparency.html">다른 글</a>에서
  112. 더 자세히 이야기할 거예요. 그림자는 빛을 반사하지 않으니 <a href="/docs/#api/ko/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>을
  113. 사용합니다.</p>
  114. <p>구체의 색상을 각각 다르게 지정하고, 컨테이너, 구체 mesh, 그림자 mesh와
  115. 구체의 처음 y축 좌표를 배열에 기록합니다.</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const numSpheres = 15;
  117. for (let i = 0; i &lt; numSpheres; ++i) {
  118. // 구체와 그림자가 같이 움직이도록 컨테이너(base)를 만듭니다
  119. const base = new THREE.Object3D();
  120. scene.add(base);
  121. /**
  122. * 그림자를 컨테이너에 추가합니다
  123. * 주의: 여기서는 각 구체의 투명도를 따로 설정할 수 있도록
  124. * 재질을 각각 따로 만듬
  125. */
  126. const shadowMat = new THREE.MeshBasicMaterial({
  127. map: shadowTexture,
  128. transparent: true, // 땅이 보이도록
  129. depthWrite: false, // 그림자를 따로 정렬하지 않도록
  130. });
  131. const shadowMesh = new THREE.Mesh(shadowGeo, shadowMat);
  132. shadowMesh.position.y = 0.001; // 그림자를 땅에서 살짝 위에 배치
  133. shadowMesh.rotation.x = Math.PI * -.5;
  134. const shadowSize = sphereRadius * 4;
  135. shadowMesh.scale.set(shadowSize, shadowSize, shadowSize);
  136. base.add(shadowMesh);
  137. // 구체를 컨테이너에 추가
  138. const u = i / numSpheres; // 반복문이 진행됨에 따라 0에서 1사이 값을 지정
  139. const sphereMat = new THREE.MeshPhongMaterial();
  140. sphereMat.color.setHSL(u, 1, .75);
  141. const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
  142. sphereMesh.position.set(0, sphereRadius + 2, 0);
  143. base.add(sphereMesh);
  144. // y축 좌표를 포함해 나머지 요소를 기록
  145. sphereShadowBases.push({ base, sphereMesh, shadowMesh, y: sphereMesh.position.y });
  146. }
  147. </pre>
  148. <p>조명은 2개를 만들겠습니다. 하나는 <a href="/docs/#api/ko/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>, 강도를 2로 설정해 화면을 아주
  149. 밝게 설정할 겁니다.</p>
  150. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  151. const skyColor = 0xB1E1FF; // 하늘색
  152. const groundColor = 0xB97A20; // 오렌지 브라운
  153. const intensity = 2;
  154. const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
  155. scene.add(light);
  156. }
  157. </pre>
  158. <p>다른 하나는 구체의 윤곽을 좀 더 분명하게 해 줄 <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>입니다.</p>
  159. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  160. const color = 0xFFFFFF;
  161. const intensity = 1;
  162. const light = new THREE.DirectionalLight(color, intensity);
  163. light.position.set(0, 10, 5);
  164. light.target.position.set(-5, 0, 0);
  165. scene.add(light);
  166. scene.add(light.target);
  167. }
  168. </pre>
  169. <p>이대로도 렌더링해도 좋지만, 구체들에 애니메이션을 한 번 줘봅시다.
  170. 컨테이너를 움직여 구체, 그림자가 xz축 평면을 따라 움직이게 하고,
  171. <a href="/docs/#api/ko/math/Math.abs(Math.sin(time))"><code class="notranslate" translate="no">Math.abs(Math.sin(time))</code></a>를 사용해 구체에 공처럼 통통 튀는
  172. 애니메이션을 넣어줍니다. 또 그림자 재질의 투명도를 조절해 구체가
  173. 높을수록 그림자가 옅어지도록 합니다.</p>
  174. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  175. time *= 0.001; // 초 단위로 변환
  176. ...
  177. sphereShadowBases.forEach((sphereShadowBase, ndx) =&gt; {
  178. const { base, sphereMesh, shadowMesh, y } = sphereShadowBase;
  179. // u는 구체의 반복문을 실행하면서 인덱스에 따라 0 이상, 1 이하로 지정됩니다
  180. const u = ndx / sphereShadowBases.length;
  181. /**
  182. * 컨테이너의 위치를 계산합니다. 구체와 그림자가
  183. * 컨테이너에 종속적이므로 위치가 같이 변합니다
  184. */
  185. const speed = time * .2;
  186. const angle = speed + u * Math.PI * 2 * (ndx % 1 ? 1 : -1);
  187. const radius = Math.sin(speed - ndx) * 10;
  188. base.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
  189. // yOff 값은 0 이상 1 이하입니다
  190. const yOff = Math.abs(Math.sin(time * 2 + ndx));
  191. // 구체를 위아래로 튕김
  192. sphereMesh.position.y = y + THREE.MathUtils.lerp(-2, 2, yOff);
  193. // 구체가 위로 올라갈수록 그림자가 옅어짐
  194. shadowMesh.material.opacity = THREE.MathUtils.lerp(1, .25, yOff);
  195. });
  196. ...
  197. </pre>
  198. <p>15가지 색상의 탱탱볼을 완성했습니다.</p>
  199. <p></p><div translate="no" class="threejs_example_container notranslate">
  200. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-fake.html"></iframe></div>
  201. <a class="threejs_center" href="/manual/examples/shadows-fake.html" target="_blank">새 탭에서 보기</a>
  202. </div>
  203. <p></p>
  204. <p>물론 다른 모양의 그림자를 사용해야 하는 경우도 있습니다. 그림자의 경계를 분명하게
  205. 하고 싶을 수도 있죠. 하지만 모든 물체의 그림자를 둥글게 표현하는 것이 좋은 경우도
  206. 분명 있습니다. 모든 그림자를 둥글게 표현한 예 중 하나는 <a href="https://www.google.com/search?tbm=isch&amp;q=animal+crossing+pocket+camp+screenshots">동물의 숲 포켓 캠프</a>입니다.
  207. 자연스럽고 성능면에서도 이득이죠. <a href="https://www.google.com/search?q=monument+valley+screenshots&amp;tbm=isch">Monument Valley</a>도
  208. 메인 캐릭터에 이런 그림자를 사용한 것으로 보입니다.</p>
  209. <p>이제 그림자 맵을 살펴보겠습니다. 그림자를 드리울 수 있는 조명은 3가지, <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>,
  210. <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>, <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>입니다.</p>
  211. <p><a href="lights.html">조명에 관한 글</a>에서 썼던 예제로 먼저 <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>부터
  212. 살펴보죠.</p>
  213. <p>먼저 renderer의 그림자 맵 옵션을 켜야 합니다.</p>
  214. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  215. +renderer.shadowMap.enabled = true;
  216. </pre>
  217. <p>조명도 그림자를 드리우도록 옵션을 활성화합니다.</p>
  218. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const light = new THREE.DirectionalLight(color, intensity);
  219. +light.castShadow = true;
  220. </pre>
  221. <p>또한 장면(scene) 안 각 mesh에 그림자를 드리울지, 그림자의 영향을 받을지 설정해줘야 합니다.</p>
  222. <p>바닥 아래는 굳이 신경 쓸 필요가 없으니 평면(바닥)은 그림자의 영향만 받게 하겠습니다.</p>
  223. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mesh = new THREE.Mesh(planeGeo, planeMat);
  224. mesh.receiveShadow = true;
  225. </pre>
  226. <p>정육면체와 구체는 그림자도 드리우고, 영향도 받도록 설정합니다.</p>
  227. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  228. mesh.castShadow = true;
  229. mesh.receiveShadow = true;
  230. ...
  231. const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  232. mesh.castShadow = true;
  233. mesh.receiveShadow = true;
  234. </pre>
  235. <p>이제 실행해보죠.</p>
  236. <p></p><div translate="no" class="threejs_example_container notranslate">
  237. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-directional-light.html"></iframe></div>
  238. <a class="threejs_center" href="/manual/examples/shadows-directional-light.html" target="_blank">새 탭에서 보기</a>
  239. </div>
  240. <p></p>
  241. <p>이런, 그림자 일부가 잘려나간 것이 보이나요?</p>
  242. <p>이는 빛의 시점에서 장면을 렌더링해 그림자 맵을 만들기 때문입니다. 위 예제를 예로 들면
  243. <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>의 위치에 카메라가 있고, 해당 조명의 목표를 바라보는 것이죠. 조명의
  244. 그림자에는 별도의 카메라가 있고, 이전에 <a href="cameras.html">카메라에 관한 글</a>에서
  245. 설명한 것처럼 일정 공간 안의 그림자만 렌더링합니다. 위 예제에서는 그 공간이 너무 좁은
  246. 것이죠.</p>
  247. <p>그림자용 카메라를 시각화하기 위해 조명의 그림자 속성에서 카메라를 가져와 <a href="/docs/#api/ko/helpers/CameraHelper"><code class="notranslate" translate="no">CameraHelper</code></a>를
  248. 생성한 뒤, 장면에 추가하겠습니다.</p>
  249. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cameraHelper = new THREE.CameraHelper(light.shadow.camera);
  250. scene.add(cameraHelper);
  251. </pre>
  252. <p>이제 그림자가 렌더링되는 공간을 확인할 수 있을 겁니다.</p>
  253. <p></p><div translate="no" class="threejs_example_container notranslate">
  254. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-directional-light-with-camera-helper.html"></iframe></div>
  255. <a class="threejs_center" href="/manual/examples/shadows-directional-light-with-camera-helper.html" target="_blank">새 탭에서 보기</a>
  256. </div>
  257. <p></p>
  258. <p>target의 x 값을 조정해보면 그림자용 카메라 범위 안에 있는 곳에만 그림자가 보이는
  259. 것을 확인할 수 있을 겁니다.</p>
  260. <p>이 공간의 크기는 이 카메라의 속성을 수정해 바꿀 수 있습니다.</p>
  261. <p>그림자용 카메라의 속성을 수정하는 GUI를 추가해보죠. <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>는 빛이 평행으로
  262. 나아가므로, <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>는 그림자용 카메라로 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>(정사영 카메라)를
  263. 사용합니다. <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>가 뭔지 잘 기억나지 않는다면, <a href="cameras.html">카메라에 관한 이전 글</a>을
  264. 참고하세요.</p>
  265. <p><a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>의 시야는 육면체나 <em>절두체(frustum)</em>로 정의한다고 했었죠. <code class="notranslate" translate="no">left</code>,
  266. <code class="notranslate" translate="no">right</code>, <code class="notranslate" translate="no">top</code>, <code class="notranslate" translate="no">bottom</code>, <code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code>, <code class="notranslate" translate="no">zoom</code> 속성을 지정해서요.</p>
  267. <p>lil-gui가 쓸 간단한 헬퍼 클래스를 하나 더 만들겠습니다. 이 <code class="notranslate" translate="no">DimensionGUIHelper</code>는
  268. 객체와 속성 이름 2개를 인자로 받아, GUI가 하나의 값을 조정할 때 하나의 값은 양수로,
  269. 다른 값은 음수로 지정합니다. 이렇게 하면 <code class="notranslate" translate="no">left</code>와 <code class="notranslate" translate="no">right</code>값을 <code class="notranslate" translate="no">width</code>로, <code class="notranslate" translate="no">up</code>과
  270. <code class="notranslate" translate="no">down</code>값을 <code class="notranslate" translate="no">height</code>로 바꾸어 조작할 수 있죠.</p>
  271. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class DimensionGUIHelper {
  272. constructor(obj, minProp, maxProp) {
  273. this.obj = obj;
  274. this.minProp = minProp;
  275. this.maxProp = maxProp;
  276. }
  277. get value() {
  278. return this.obj[this.maxProp] * 2;
  279. }
  280. set value(v) {
  281. this.obj[this.maxProp] = v / 2;
  282. this.obj[this.minProp] = v / -2;
  283. }
  284. }
  285. </pre>
  286. <p>또한 <a href="cameras.html">이전 글</a>에서 썼던 <code class="notranslate" translate="no">MinMaxGUIHelper</code>를 가져와 <code class="notranslate" translate="no">near</code>와
  287. <code class="notranslate" translate="no">far</code> 속성을 조작하는 데 사용하겠습니다.</p>
  288. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  289. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  290. gui.add(light, 'intensity', 0, 2, 0.01);
  291. +{
  292. + const folder = gui.addFolder('Shadow Camera');
  293. + folder.open();
  294. + folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 100)
  295. + .name('width')
  296. + .onChange(updateCamera);
  297. + folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 100)
  298. + .name('height')
  299. + .onChange(updateCamera);
  300. + const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1);
  301. + folder.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  302. + folder.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  303. + folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera);
  304. +}
  305. </pre>
  306. <p>그리고 값이 바뀔 때마다 <code class="notranslate" translate="no">updateCamera</code> 함수를 호출하도록 합니다. 이 함수 안에서는
  307. 조명, 조명 헬퍼, 조명의 그림자용 카메라, 그림자용 카메라의 헬퍼를 업데이트할 거예요.</p>
  308. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateCamera() {
  309. // 헬퍼가 가이드라인을 그릴 때 필요한 조명 목표(target)의 matrixWorld를 업데이트 합니다
  310. light.target.updateMatrixWorld();
  311. helper.update();
  312. // 그림자용 카메라의 투영 행렬(projection matrix)를 업데이트합니다
  313. light.shadow.camera.updateProjectionMatrix();
  314. // 그림자용 카메라를 보기 위해 설치한 카메라의 헬퍼를 업데이트합니다
  315. cameraHelper.update();
  316. }
  317. updateCamera();
  318. </pre>
  319. <p>이제 그림자용 카메라에 GUI가 생겼으니, 값들을 조정하며 놀아봅시다.</p>
  320. <p></p><div translate="no" class="threejs_example_container notranslate">
  321. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-directional-light-with-camera-gui.html"></iframe></div>
  322. <a class="threejs_center" href="/manual/examples/shadows-directional-light-with-camera-gui.html" target="_blank">새 탭에서 보기</a>
  323. </div>
  324. <p></p>
  325. <p><code class="notranslate" translate="no">width</code>와 <code class="notranslate" translate="no">height</code> 속성을 30 정도로 조정하면 그림자가 있어야 할만한 공간은
  326. 대부분 그림자용 카메라 안에 속할 겁니다.</p>
  327. <p>하지만 여기서 의문이 하나 생깁니다. 어째서 <code class="notranslate" translate="no">width</code>와 <code class="notranslate" translate="no">height</code>를 완전 큰 값으로
  328. 설정해 모든 요소를 다 포함하도록 하지 않는 걸까요? <code class="notranslate" translate="no">width</code>와 <code class="notranslate" translate="no">height</code>를 100 정도로
  329. 설정해보세요. 아래와 같은 현상이 나타날 겁니다.</p>
  330. <div class="threejs_center"><img src="../resources/images/low-res-shadow-map.png" style="width: 369px"></div>
  331. <p>왜 그림자의 해상도가 낮아졌을까요?</p>
  332. <p>이는 그림자 관련 설정을 할 때 항상 주의해야하는 부분입니다. 사실 그림자 맵은 그림자가
  333. 포함된 하나의 텍스처입니다. 이 텍스처는 크기가 정해져 있죠. 위 예제에서 카메라의 공간을
  334. 늘리면, 이 텍스처 또한 늘어납니다. 다시 말해 공간을 크게 설정할수록 그림자가 더 각져
  335. 보일 거라는 얘기죠.</p>
  336. <p>그림자 맵의 해상도는 <code class="notranslate" translate="no">light.shadow.mapSize</code> 속성의 <code class="notranslate" translate="no">width</code>와 <code class="notranslate" translate="no">height</code> 속성으로 설정합니다(기본값은
  337. 512x512). 그림자 맵은 크게 설정할수록 메모리를 많이 차지하고, 연산이 더 복잡해지므로
  338. 가능한 작게 설정하는 것이 좋습니다. 이는 그림자용 카메라의 공간도 마찬가지죠. 작을 수록
  339. 그림자의 퀄리티가 좋아질 테니 가능한 공간을 작게 설정하는 것이 좋습니다. 또한 기기마다
  340. 렌더링할 수 있는 텍스처의 용량이 정해져 있으니 주의해야 합니다. Three.js에서 이 용량은
  341. <a href="/docs/#api/ko/renderers/WebGLRenderer#capabilities"><code class="notranslate" translate="no">renderer.capabilities.maxTextureSize</code></a>로 확인할 수 있습니다.</p>
  342. <p><a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>는 그림자용 카메라로 <a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>(원근 카메라)를 사용합니다. <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>의
  343. 그림자용 카메라는 거의 모든 속성을 직접 변경할 수 있었지만, <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>의 그림자용 카메라는
  344. 조명 속성의 영향을 받습니다. 카메라의 <code class="notranslate" translate="no">fov</code> 속성은 <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>의 <code class="notranslate" translate="no">angle</code> 속성과 직접 연결되어
  345. 있죠. <code class="notranslate" translate="no">aspect</code>는 그림자 맵의 크기에 따라 자동으로 정해집니다.</p>
  346. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const light = new THREE.DirectionalLight(color, intensity);
  347. +const light = new THREE.SpotLight(color, intensity);
  348. </pre>
  349. <p>추가로 <a href="lights.html">이전 글</a>에서 썼던 <code class="notranslate" translate="no">penumbra(반음영)</code>, <code class="notranslate" translate="no">angle</code> 설정을 가져오겠습니다.</p>
  350. <p></p><div translate="no" class="threejs_example_container notranslate">
  351. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-spot-light-with-camera-gui.html"></iframe></div>
  352. <a class="threejs_center" href="/manual/examples/shadows-spot-light-with-camera-gui.html" target="_blank">새 탭에서 보기</a>
  353. </div>
  354. <p></p>
  355. <p>마지막으로 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>를 살펴보죠. <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>는 모든 방향으로 빛을 발산하기에
  356. 관련 설정은 <code class="notranslate" translate="no">near</code>와 <code class="notranslate" translate="no">far</code> 정도입니다. 그리고 사실 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>의 그림자는 정육면체의
  357. 각 면에 <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>를 하나씩, 총 6개의 그림자를 놓은 것과 같습니다. 한 방향에 한
  358. 번씩, 총 6번을 렌더링해야 하니 렌더링 속도가 훨씬 느리겠죠.</p>
  359. <p>이번에는 장면 주위에 상자를 두어 벽과 천장에도 그림자가 생길 수 있도록 해보겠습니다.
  360. 먼저 재질의 <code class="notranslate" translate="no">side</code> 속성을 <code class="notranslate" translate="no">THREE.BackSide</code>로 설정해 외부 상자의 밖이 아닌 안을 렌더링
  361. 하도록 합니다. 바닥과 마찬가지로 그림자를 드리우지 않도록 설정하고, z-파이팅 현상을
  362. 피하기 위해 외부 상자를 바닥보다 살짝 아래에 둡니다.</p>
  363. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  364. const cubeSize = 30;
  365. const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  366. const cubeMat = new THREE.MeshPhongMaterial({
  367. color: '#CCC',
  368. side: THREE.BackSide,
  369. });
  370. const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  371. mesh.receiveShadow = true;
  372. mesh.position.set(0, cubeSize / 2 - 0.1, 0);
  373. scene.add(mesh);
  374. }
  375. </pre>
  376. <p>물론 조명도 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>로 바꿔야죠.</p>
  377. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const light = new THREE.SpotLight(color, intensity);
  378. +const light = new THREE.PointLight(color, intensity);
  379. ....
  380. // 조명이 위치를 확인하기 쉽도록 헬퍼 추가
  381. +const helper = new THREE.PointLightHelper(light);
  382. +scene.add(helper);
  383. </pre>
  384. <p></p><div translate="no" class="threejs_example_container notranslate">
  385. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/shadows-point-light.html"></iframe></div>
  386. <a class="threejs_center" href="/manual/examples/shadows-point-light.html" target="_blank">새 탭에서 보기</a>
  387. </div>
  388. <p></p>
  389. <p>GUI의 <code class="notranslate" translate="no">position</code> 속성을 조정해 조명을 움직이면 벽에 그림자가 지는 걸
  390. 확인할 수 있을 겁니다. 다른 그림자와 마찬가지로 <code class="notranslate" translate="no">near</code> 값보다 가까운
  391. 곳은 그림자가 지지 않고, <code class="notranslate" translate="no">far</code> 값보다 먼 곳은 항상 그림자가 지죠.</p>
  392. </div>
  393. </div>
  394. </div>
  395. <script src="../resources/prettify.js"></script>
  396. <script src="../resources/lesson.js"></script>
  397. </body></html>