cameras.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>카메라(Cameras)</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 – 카메라(Cameras)">
  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. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. <link rel="stylesheet" href="/manual/ko/lang.css">
  24. </head>
  25. <body>
  26. <div class="container">
  27. <div class="lesson-title">
  28. <h1>카메라(Cameras)</h1>
  29. </div>
  30. <div class="lesson">
  31. <div class="lesson-main">
  32. <p>※ 이 글은 Three.js의 튜토리얼 시리즈로서,
  33. 먼저 <a href="fundamentals.html">Three.js의 기본 구조에 관한 글</a>을
  34. 읽고 오길 권장합니다.</p>
  35. <p>이번 장에서는 카메라(cameras)에 대해 알아보겠습니다. <a href="fundamentals.html">첫 번째 장</a>에서
  36. 일부 다루긴 했지만, 중요 요소인 만큼 더 자세히 살펴볼 필요가 있습니다.</p>
  37. <p>Three.js에서 가장 자주 사용하는 카메라는 여태까지 썼던 <a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>(원근 카메라)입니다.
  38. 이 카메라는 멀리 있는 물체를 가까이 있는 것보다 상대적으로 작게 보이도록 해주죠.</p>
  39. <p><a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>는 <em>절두체(frustum)</em>를 만듭니다. <a href="https://ko.wikipedia.org/wiki/%EC%A0%88%EB%91%90%EC%B2%B4">절두체(frustum)는 입체(보통 원뿔이나 각뿔)를
  40. 절단하는 하나나 두 평행면 사이의 부분</a>을
  41. 의미하죠. 여기서 입체란 정육면체, 원뿔, 구, 원통, 절두체 등의 3D 요소입니다.</p>
  42. <div class="spread">
  43. <div><div data-diagram="shapeCube"></div><div>정육면체(cube)</div></div>
  44. <div><div data-diagram="shapeCone"></div><div>원뿔(cone)</div></div>
  45. <div><div data-diagram="shapeSphere"></div><div>구(sphere)</div></div>
  46. <div><div data-diagram="shapeCylinder"></div><div>원통(cylinder)</div></div>
  47. <div><div data-diagram="shapeFrustum"></div><div>절두체(frustum)</div></div>
  48. </div>
  49. <p>이걸 굳이 언급하는 이유는 글을 쓰는 저도 몇 년 동안 이를 몰랐기 때문입니다. 책이든 인터넷 글이든,
  50. <em>절두체</em>라는 단어를 봤다면 눈이 뒤집어졌을 겁니다. 입체의 이름을 알면 이해하기도, 기억하기도 훨씬
  51. 쉽죠 😅.</p>
  52. <p><a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>는 4가지 속성을 바탕으로 절두체를 만듭니다. <code class="notranslate" translate="no">near</code>는 절두체가 어디서 시작할지
  53. 결정하는 속성이고, <code class="notranslate" translate="no">far</code>는 절두체의 끝입니다. <code class="notranslate" translate="no">fov</code>는 시아갹(field of view)으로, <code class="notranslate" translate="no">near</code>와 카메라의
  54. 거리에 따라 절두체의 높이를 계산해 적용합니다. <code class="notranslate" translate="no">aspect</code>는 절두체의 너비에 관여하는 비율으로, 절두체의
  55. 너비는 절두체의 높이에 이 비율을 곱한 값입니다.</p>
  56. <p><img src="../resources/frustum-3d.svg" width="500" class="threejs_center"></p>
  57. <p><a href="lights.html">이전 장</a>에서 썼던 바닥면, 구체, 정육면체로 이루어진 예제를 다시 사용해
  58. 카메라의 속성을 조정할 수 있도록 만들겠습니다.</p>
  59. <p><code class="notranslate" translate="no">near</code> 속성은 항상 <code class="notranslate" translate="no">far</code> 속성보다 커야 하니, 이를 제어할 <code class="notranslate" translate="no">MinMaxGUIHelper</code> 헬퍼 클래스를
  60. 만들겠습니다. 이 클래스는 lil-gui가 제어할 <code class="notranslate" translate="no">min</code>과 <code class="notranslate" translate="no">max</code> 속성이 있고, lil-gui가 이를 조정할
  61. 때 지정한 두 가지 속성을 동시에 변경합니다.</p>
  62. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class MinMaxGUIHelper {
  63. constructor(obj, minProp, maxProp, minDif) {
  64. this.obj = obj;
  65. this.minProp = minProp;
  66. this.maxProp = maxProp;
  67. this.minDif = minDif;
  68. }
  69. get min() {
  70. return this.obj[this.minProp];
  71. }
  72. set min(v) {
  73. this.obj[this.minProp] = v;
  74. this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
  75. }
  76. get max() {
  77. return this.obj[this.maxProp];
  78. }
  79. set max(v) {
  80. this.obj[this.maxProp] = v;
  81. this.min = this.min; // min setter로 작동
  82. }
  83. }
  84. </pre>
  85. <p>이제 GUI를 만들어보죠.</p>
  86. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateCamera() {
  87. camera.updateProjectionMatrix();
  88. }
  89. const gui = new GUI();
  90. gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
  91. const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
  92. gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  93. gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  94. </pre>
  95. <p>카메라의 속성을 변경할 때마다 카메라의 <a href="/docs/#api/ko/cameras/PerspectiveCamera#updateProjectionMatrix"><code class="notranslate" translate="no">updateProjectionMatrix</code></a>
  96. 메서드를 호출해야 하므로, <code class="notranslate" translate="no">updateCamera</code>라는 함수를 만들어 값이 변경될 때마다 함수를 호출하도록
  97. 합니다.</p>
  98. <p></p><div translate="no" class="threejs_example_container notranslate">
  99. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-perspective.html"></iframe></div>
  100. <a class="threejs_center" href="/manual/examples/cameras-perspective.html" target="_blank">새 탭에서 보기</a>
  101. </div>
  102. <p></p>
  103. <p>값을 조정하며 카메라가 어떤 식으로 작동하는지 확인해보세요. <code class="notranslate" translate="no">aspect</code>는 창의 비율을 그대로 사용하도록
  104. 설정되어 있으므로, 이를 바꾸고 싶다면 예제를 새 창에서 열어 코드를 직접 수정해야 합니다.</p>
  105. <p>아직도 카메라가 어떤 식으로 작동하는지 보기 어려운가요? 까짓것 그럼 카메라를 하나 더 만들어보죠.
  106. 하나는 위의 예제와 같은 방식의 카메라이고, 다른 하나는 이 카메라의 시야와 절두체를 렌더링해
  107. 카메라가 어떻게 움직이는지 관찰할 수 있도록 만들겠습니다.</p>
  108. <p>Three.js의 가위 함수(scissor function)을 이용하면 쉽습니다. 가위 함수를 사용해 양쪽에
  109. 장면 두 개, 카메라 두 개를 렌더링하겠습니다.</p>
  110. <p>먼저 HTML과 CSS로 양쪽에 div 요소를 배치합니다. 이러면 각각의 카메라에 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>를
  111. 두어 이벤트 처리하기도 훨씬 간단합니다.</p>
  112. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  113. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  114. + &lt;div class="split"&gt;
  115. + &lt;div id="view1" tabindex="1"&gt;&lt;/div&gt;
  116. + &lt;div id="view2" tabindex="2"&gt;&lt;/div&gt;
  117. + &lt;/div&gt;
  118. &lt;/body&gt;
  119. </pre>
  120. <p>CSS로 두 div 요소가 canvas 위 양쪽에 자리하게 합니다.</p>
  121. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">.split {
  122. position: absolute;
  123. left: 0;
  124. top: 0;
  125. width: 100%;
  126. height: 100%;
  127. display: flex;
  128. }
  129. .split &gt; div {
  130. width: 100%;
  131. height: 100%;
  132. }
  133. </pre>
  134. <p>카메라의 절두체를 시각화할 <a href="/docs/#api/ko/helpers/CameraHelper"><code class="notranslate" translate="no">CameraHelper</code></a>를 추가합니다.</p>
  135. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cameraHelper = new THREE.CameraHelper(camera);
  136. ...
  137. scene.add(cameraHelper);
  138. </pre>
  139. <p>다음으로 두 div 요소를 참조합니다.</p>
  140. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const view1Elem = document.querySelector('#view1');
  141. const view2Elem = document.querySelector('#view2');
  142. </pre>
  143. <p>그리고 기존 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 왼쪽 div 요소의 이벤트에만 반응하도록 설정합니다.</p>
  144. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const controls = new OrbitControls(camera, canvas);
  145. +const controls = new OrbitControls(camera, view1Elem);
  146. </pre>
  147. <p>다음으로 <a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>와 두 번째 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>를 추가합니다. 이 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>를
  148. 두 번째 카메라에 종속시키고, 오른쪽 div 요소의 이벤트에만 반응하도록 합니다.</p>
  149. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const camera2 = new THREE.PerspectiveCamera(
  150. 60, // 시야각(fov)
  151. 2, // 비율(aspect)
  152. 0.1, // near
  153. 500, // far
  154. );
  155. camera2.position.set(40, 10, 30);
  156. camera2.lookAt(0, 5, 0);
  157. const controls2 = new OrbitControls(camera2, view2Elem);
  158. controls2.target.set(0, 5, 0);
  159. controls2.update();
  160. </pre>
  161. <p>끝으로 가위 함수를 사용해 화면을 분할하겠습니다. 카메라 각각의 시점에 따라
  162. 장면을 canvas의 양쪽에 나눠 렌더링하게끔 할 것입니다.</p>
  163. <p>아래의 함수는 canvas 위에 덮어 씌운 요소의 사각 좌표(rectangle)를 구합니다.
  164. 그리고 해당 사각 좌표로 <code class="notranslate" translate="no">renderer</code>의 화면(viewport)과 가위(scissor)의 값을
  165. 정의한 뒤, 사각 좌표의 가로세로 비율을 반환합니다.</p>
  166. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setScissorForElement(elem) {
  167. const canvasRect = canvas.getBoundingClientRect();
  168. const elemRect = elem.getBoundingClientRect();
  169. // canvas에 대응하는 사각형을 구하기
  170. const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
  171. const left = Math.max(0, elemRect.left - canvasRect.left);
  172. const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
  173. const top = Math.max(0, elemRect.top - canvasRect.top);
  174. const width = Math.min(canvasRect.width, right - left);
  175. const height = Math.min(canvasRect.height, bottom - top);
  176. // canvas의 일부분만 렌더링하도록 scissor 적용
  177. const positiveYUpBottom = canvasRect.height - bottom;
  178. renderer.setScissor(left, positiveYUpBottom, width, height);
  179. renderer.setViewport(left, positiveYUpBottom, width, height);
  180. // 비율 반환
  181. return width / height;
  182. }
  183. </pre>
  184. <p>이제 이 함수를 사용해 <code class="notranslate" translate="no">render</code> 함수에서 장면을 두 번 렌더링할 수 있습니다.</p>
  185. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> function render() {
  186. - if (resizeRendererToDisplaySize(renderer)) {
  187. - const canvas = renderer.domElement;
  188. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  189. - camera.updateProjectionMatrix();
  190. - }
  191. + resizeRendererToDisplaySize(renderer);
  192. +
  193. + // 가위 활성화
  194. + renderer.setScissorTest(true);
  195. +
  196. + // 기존 화면 렌더링
  197. + {
  198. + const aspect = setScissorForElement(view1Elem);
  199. +
  200. + // 비율에 따라 카메라 조정
  201. + camera.aspect = aspect;
  202. + camera.updateProjectionMatrix();
  203. + cameraHelper.update();
  204. +
  205. + // 기존 화면에서 가이드라인(CameraHelper)이 노출되지 않도록 설정
  206. + cameraHelper.visible = false;
  207. +
  208. + scene.background.set(0x000000);
  209. +
  210. + // 렌더링
  211. + renderer.render(scene, camera);
  212. + }
  213. +
  214. + // 두 번째 카메라 렌더링
  215. + {
  216. + const aspect = setScissorForElement(view2Elem);
  217. +
  218. + // 비율에 따라 카메라 조정
  219. + camera2.aspect = aspect;
  220. + camera2.updateProjectionMatrix();
  221. +
  222. + // 가이드라인 활성화
  223. + cameraHelper.visible = true;
  224. +
  225. + scene.background.set(0x000040);
  226. +
  227. + renderer.render(scene, camera2);
  228. + }
  229. - renderer.render(scene, camera);
  230. requestAnimationFrame(render);
  231. }
  232. requestAnimationFrame(render);
  233. }
  234. </pre>
  235. <p>위 예제에서는 분할된 두 화면을 구별하기 쉽두록 두 번째 화면의 배경을 진한
  236. 파란색으로 칠했습니다.</p>
  237. <p>또한 <code class="notranslate" translate="no">render</code> 함수 안에서 모든 것을 처리하기에, <code class="notranslate" translate="no">updateCamera</code> 함수도
  238. 제거하였습니다.</p>
  239. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function updateCamera() {
  240. - camera.updateProjectionMatrix();
  241. -}
  242. const gui = new GUI();
  243. -gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
  244. +gui.add(camera, 'fov', 1, 180);
  245. const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
  246. -gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  247. -gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  248. +gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near');
  249. +gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far');
  250. </pre>
  251. <p>이제 두 번째 화면에서 첫 번째 카메라의 절두체를 확인할 수 있습니다.</p>
  252. <p></p><div translate="no" class="threejs_example_container notranslate">
  253. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-perspective-2-scenes.html"></iframe></div>
  254. <a class="threejs_center" href="/manual/examples/cameras-perspective-2-scenes.html" target="_blank">새 탭에서 보기</a>
  255. </div>
  256. <p></p>
  257. <p>왼쪽은 기존의 화면과 같고 오른쪽에 왼쪽 카메라의 절두체가 보입니다.
  258. 패널에서 <code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code>, <code class="notranslate" translate="no">fov</code> 값을 조정하거나 마우스로 화면을 움직여보면,
  259. 오른쪽 화면의 절두체 안에 있는 물체만 왼쪽 화면에 노출됨을 확인할 수
  260. 있을 겁니다.</p>
  261. <p>누군가 이렇게 물을지도 모르겠네요. 그냥 <code class="notranslate" translate="no">near</code>를 0.0000000001로 설정하고 <code class="notranslate" translate="no">far</code>를
  262. 10000000000000로 설정해버리면요? 이러면 모든 게 항상 다 보이지 않나요? 이유를
  263. 설명하자면, GPU는 어떤 물체가 앞에 있거나 다른 물체의 뒤에 있을 때만 정밀도가
  264. 높기 때문입니다. 정밀도는 일정량이 <code class="notranslate" translate="no">near</code>와 <code class="notranslate" translate="no">far</code> 사이에 퍼져 있는데, 기본적으로
  265. 카메라에 가까울 수록 정밀도가 높고 멀수록 정밀도가 낮아집니다.</p>
  266. <p>현상을 직접 확인해보죠. 위의 예제를 수정해 20개의 구체를 한 줄로 세우겠습니다.</p>
  267. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  268. const sphereRadius = 3;
  269. const sphereWidthDivisions = 32;
  270. const sphereHeightDivisions = 16;
  271. const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  272. const numSpheres = 20;
  273. for (let i = 0; i &lt; numSpheres; ++i) {
  274. const sphereMat = new THREE.MeshPhongMaterial();
  275. sphereMat.color.setHSL(i * .73, 1, 0.5);
  276. const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  277. mesh.position.set(-sphereRadius - 1, sphereRadius + 2, i * sphereRadius * -2.2);
  278. scene.add(mesh);
  279. }
  280. }
  281. </pre>
  282. <p><code class="notranslate" translate="no">near</code> 속성을 0.00001로 설정합니다.</p>
  283. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 45;
  284. const aspect = 2; // canvas 기본값
  285. -const near = 0.1;
  286. +const near = 0.00001;
  287. const far = 100;
  288. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  289. </pre>
  290. <p>그리고 기존의 GUI 코드를 수정해 0.00001의 작은 단위도 설정할 수 있도록 합니다.</p>
  291. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  292. +gui.add(minMaxGUIHelper, 'min', 0.00001, 50, 0.00001).name('near').onChange(updateCamera);
  293. </pre>
  294. <p>어떤 결과가 나올 것 같나요?</p>
  295. <p></p><div translate="no" class="threejs_example_container notranslate">
  296. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-z-fighting.html"></iframe></div>
  297. <a class="threejs_center" href="/manual/examples/cameras-z-fighting.html" target="_blank">새 탭에서 보기</a>
  298. </div>
  299. <p></p>
  300. <p>이는 <em>z-파이팅(z-fighting, Stitching)</em>의 한 예입니다. 컴퓨터의 GPU가 어떤 픽셀이
  301. 앞이고 어떤 픽셀을 뒤로 보내야할지 결정할 정밀도가 모자를 때 발생하는 현상이죠.</p>
  302. <p>위 예제가 어떻게 해도 정상적으로 보인다면, 아래 이미지를 보기 바랍니다.</p>
  303. <div class="threejs_center"><img src="../resources/images/z-fighting.png" style="width: 570px;"></div>
  304. <p>한 가지 해결책은 Three.js에게 픽셀의 앞 뒤를 결정할 때 다른 방법을 쓰도록 설정하는
  305. 것입니다. <a href="/docs/#api/ko/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a>를 생성할 때 <code class="notranslate" translate="no">logarithmicDepthBuffer</code> 속성을 활성화해주면
  306. 되죠.</p>
  307. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  308. +const renderer = new THREE.WebGLRenderer({
  309. + antialias: true,
  310. + canvas,
  311. + logarithmicDepthBuffer: true,
  312. +});
  313. </pre>
  314. <p>대게의 경우 정상적으로 보일 겁니다.</p>
  315. <p></p><div translate="no" class="threejs_example_container notranslate">
  316. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-logarithmic-depth-buffer.html"></iframe></div>
  317. <a class="threejs_center" href="/manual/examples/cameras-logarithmic-depth-buffer.html" target="_blank">새 탭에서 보기</a>
  318. </div>
  319. <p></p>
  320. <p>문제가 그대로라면 <em>이 해결책을 쓸 수 없는 건가?</em>라는 의문에 빠질 수 있습니다.
  321. 이 해결책이 먹히지 않은 이유는 일부 GPU만 이 기능을 지원하기 때문이죠. 2018년
  322. 9월 기준으로 거의 모든 데스크탑 GPU가 이 기능을 지원하나, 모바일 기기는 대부분
  323. 이 기능을 지원하지 않습니다.</p>
  324. <p>이 기능을 쓰지 말아야 하는 또 다른 이유는, 이 기능이 일반적인 해결책보다 훨씬
  325. 성능이 나쁘기 때문입니다.</p>
  326. <p>게다가 이 기능을 활성화해도, <code class="notranslate" translate="no">near</code>를 더 작게, <code class="notranslate" translate="no">far</code>를 더 멀게 설정하다보면
  327. 결국 같은 현상을 만나게 될 겁니다.</p>
  328. <p>이는 항상 <code class="notranslate" translate="no">near</code>와 <code class="notranslate" translate="no">far</code>를 설정하는데 많은 공을 들여야 한다는 의미입니다.
  329. <code class="notranslate" translate="no">near</code>는 대상이 보이는 한 가장 멀게, <code class="notranslate" translate="no">far</code>도 대상이 보이는 한 카메라와 가장
  330. 가깝게 설정하는 것이 좋죠. 만약 거대한 공간을 렌더링하는 경우, 예를 들어 사람의
  331. 속눈썹과 50km 떨어진 산을 동시에 보이게 하려면 다른 해결책-나중에 다룰지도 모르는-을
  332. 찾아야 합니다. 당장은 <code class="notranslate" translate="no">near</code>와 <code class="notranslate" translate="no">far</code>를 적절하게 설정하는 게 중요하다는 것만
  333. 알아둡시다.</p>
  334. <p>두 번째로 자주 사용하는 카메라는 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>(정사영 카메라)입니다.
  335. 절두체 대신 <code class="notranslate" translate="no">left</code>, <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>로 육면체를
  336. 정의해 사용하죠. 육면체로 화면을 투사하기에 원근 효과가 없습니다.</p>
  337. <p>2분할 화면 예제를 수정해 첫 번째 화면을 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>로 바꾸겠습니다.</p>
  338. <p>먼저 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>를 만들어보죠.</p>
  339. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = -1;
  340. const right = 1;
  341. const top = 1;
  342. const bottom = -1;
  343. const near = 5;
  344. const far = 50;
  345. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  346. camera.zoom = 0.2;
  347. </pre>
  348. <p>먼저 <code class="notranslate" translate="no">left</code>와 <code class="notranslate" translate="no">bottom</code>을 -1로, <code class="notranslate" translate="no">right</code>와 <code class="notranslate" translate="no">top</code>을 1로 설정했습니다. 이러면 육면체는
  349. 기본적으로 너비 2칸, 높이 2칸이 되겠죠. 그리고 육면체의 비율을 조정해 <code class="notranslate" translate="no">left</code>와 <code class="notranslate" translate="no">top</code>의
  350. 값을 조정할 수 있도록, <code class="notranslate" translate="no">zoom</code> 속성을 이용해 카메라에 보이는 범위를 조정할 수 있도록
  351. 했습니다.</p>
  352. <p>다음으로 <code class="notranslate" translate="no">zoom</code> 속성을 조정할 GUI를 추가합니다.</p>
  353. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  354. +gui.add(camera, 'zoom', 0.01, 1, 0.01).listen();
  355. </pre>
  356. <p><code class="notranslate" translate="no">listen</code> 메서드를 호출하면 lil-gui가 변화를 감지합니다. 이렇게 한 이유는 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>이
  357. 마우스 휠 스크롤을 감지해 <code class="notranslate" translate="no">zoom</code> 속성을 변경하기 때문이죠.</p>
  358. <p>끝으로 왼쪽 화면을 렌더링할 때 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>를 업데이트하도록 설정합니다.</p>
  359. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  360. const aspect = setScissorForElement(view1Elem);
  361. // 요소의 비율에 맞춰 카메라 업데이트
  362. - camera.aspect = aspect;
  363. + camera.left = -aspect;
  364. + camera.right = aspect;
  365. camera.updateProjectionMatrix();
  366. cameraHelper.update();
  367. // 기존 화면에서 가이드라인(CameraHelper)이 노출되지 않도록 설정
  368. cameraHelper.visible = false;
  369. scene.background.set(0x000000);
  370. renderer.render(scene, camera);
  371. }
  372. </pre>
  373. <p>이제 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>가 어떻게 작동하는지 확인할 차례입니다.</p>
  374. <p></p><div translate="no" class="threejs_example_container notranslate">
  375. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-orthographic-2-scenes.html"></iframe></div>
  376. <a class="threejs_center" href="/manual/examples/cameras-orthographic-2-scenes.html" target="_blank">새 탭에서 보기</a>
  377. </div>
  378. <p></p>
  379. <p>Three.js에서 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>는 주로 2D 요소를 표현하기 위해 사용합니다.
  380. 카메라에 얼마나 많은 요소를 보여줄지만 결정하면 되죠. 만약 canvas의 1픽셀을
  381. 카메라의 한 칸과 같은 크기로 지정하고 싶다면...</p>
  382. <p>중점을 장면의 중심에 두고 1 픽셀을 Three.js의 한 칸으로 만들 수 있습니다.</p>
  383. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">camera.left = -canvas.width / 2;
  384. camera.right = canvas.width / 2;
  385. camera.top = canvas.height / 2;
  386. camera.bottom = -canvas.height / 2;
  387. camera.near = -1;
  388. camera.far = 1;
  389. camera.zoom = 1;
  390. </pre>
  391. <p>2D canvas처럼 중점을 상단 왼쪽에 두려면 다음과 같이 설정할 수 있죠.</p>
  392. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">camera.left = 0;
  393. camera.right = canvas.width;
  394. camera.top = 0;
  395. camera.bottom = canvas.height;
  396. camera.near = -1;
  397. camera.far = 1;
  398. camera.zoom = 1;
  399. </pre>
  400. <p>중점이 상단 왼쪽에 있을 경우의 좌표는 2D canvas처럼 0, 0입니다.</p>
  401. <p>한 번 만들어보죠! 먼저 카메라를 설정합니다.</p>
  402. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = 0;
  403. const right = 300; // canvas 기본 크기
  404. const top = 0;
  405. const bottom = 150; // canvas 기본 크기
  406. const near = -1;
  407. const far = 1;
  408. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  409. camera.zoom = 1;
  410. </pre>
  411. <p>다음으로 평면(plane) 6개를 만들어 각각 다른 텍스처를 적용하겠습니다.
  412. 각 평면마다 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">THREE.Object3D</code></a> 인스턴스를 만들어 평면의 부모로 설정합니다.
  413. 이러면 중점을 0, 0, 상단 좌측으로 지정해 좌표를 지정하기가 쉽습니다.</p>
  414. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
  415. const textures = [
  416. loader.load('resources/images/flower-1.jpg'),
  417. loader.load('resources/images/flower-2.jpg'),
  418. loader.load('resources/images/flower-3.jpg'),
  419. loader.load('resources/images/flower-4.jpg'),
  420. loader.load('resources/images/flower-5.jpg'),
  421. loader.load('resources/images/flower-6.jpg'),
  422. ];
  423. const planeSize = 256;
  424. const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  425. const planes = textures.map((texture) =&gt; {
  426. const planePivot = new THREE.Object3D();
  427. scene.add(planePivot);
  428. texture.magFilter = THREE.NearestFilter;
  429. const planeMat = new THREE.MeshBasicMaterial({
  430. map: texture,
  431. side: THREE.DoubleSide,
  432. });
  433. const mesh = new THREE.Mesh(planeGeo, planeMat);
  434. planePivot.add(mesh);
  435. // 평면을 움직여 상단 좌측이 중점이 되도록 설정
  436. mesh.position.set(planeSize / 2, planeSize / 2, 0);
  437. return planePivot;
  438. });
  439. </pre>
  440. <p>그리고 <code class="notranslate" translate="no">render</code> 함수 안에 canvas의 사이즈가 변경되었을 때 카메라를 업데이트하는
  441. 코드를 추가합니다.</p>
  442. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  443. if (resizeRendererToDisplaySize(renderer)) {
  444. camera.right = canvas.width;
  445. camera.bottom = canvas.height;
  446. camera.updateProjectionMatrix();
  447. }
  448. ...
  449. </pre>
  450. <p><code class="notranslate" translate="no">planes</code>는 <a href="/docs/#api/ko/objects/Mesh"><code class="notranslate" translate="no">THREE.Mesh</code></a>, 평면의 배열입니다. 시간값을 기반으로 이 평면이 따로
  451. 움직이도록 하겠습니다.</p>
  452. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  453. time *= 0.001; // 초 단위로 변경
  454. ...
  455. const distAcross = Math.max(20, canvas.width - planeSize);
  456. const distDown = Math.max(20, canvas.height - planeSize);
  457. // total distance to move across and back
  458. const xRange = distAcross * 2;
  459. const yRange = distDown * 2;
  460. const speed = 180;
  461. planes.forEach((plane, ndx) =&gt; {
  462. // compute a unique time for each plane
  463. const t = time * speed + ndx * 300;
  464. // get a value between 0 and range
  465. const xt = t % xRange;
  466. const yt = t % yRange;
  467. // set our position going forward if 0 to half of range
  468. // and backward if half of range to range
  469. const x = xt &lt; distAcross ? xt : xRange - xt;
  470. const y = yt &lt; distDown ? yt : yRange - yt;
  471. plane.position.set(x, y, 0);
  472. });
  473. renderer.render(scene, camera);
  474. </pre>
  475. <p>이미지가 완벽하게 가장자리에서 튕기는 것을 확인할 수 있을 겁니다. 2D canvas에서
  476. 픽셀값을 이용해 구현할 때와 같은 방식이죠.</p>
  477. <p></p><div translate="no" class="threejs_example_container notranslate">
  478. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-orthographic-canvas-top-left-origin.html"></iframe></div>
  479. <a class="threejs_center" href="/manual/examples/cameras-orthographic-canvas-top-left-origin.html" target="_blank">새 탭에서 보기</a>
  480. </div>
  481. <p></p>
  482. <p><a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>는 게임 엔진 에디터 등에서처럼 3D 모델링 결과물의 상, 하, 좌, 우,
  483. 앞, 뒤를 렌더링할 때도 사용합니다.</p>
  484. <div class="threejs_center"><img src="../resources/images/quad-viewport.png" style="width: 574px;"></div>
  485. <p>위 스크린샷의 1개의 화면만 원근(perspective) 카메라이고 나머지 3개는 정사영(orthographic)
  486. 카메라입니다.</p>
  487. <p>여기까지 카메라의 기초에 대해 살펴보았습니다. 카메라를 움직이는 방법에 대해서는 다른
  488. 글에서 좀 더 상세히 설명할 거예요. 다음은 장에서는 <a href="shadows.html">그림자(shadows)</a>에
  489. 대해 먼저 살펴보겠습니다.</p>
  490. <p><canvas id="c"></canvas></p>
  491. <script type="module" src="../resources/threejs-cameras.js"></script>
  492. </div>
  493. </div>
  494. </div>
  495. <script src="../resources/prettify.js"></script>
  496. <script src="../resources/lesson.js"></script>
  497. </body></html>