1
0

picking.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>피킹(Picking)</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 – 피킹(Picking)">
  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>피킹(Picking)</h1>
  26. </div>
  27. <div class="lesson">
  28. <div class="lesson-main">
  29. <p><em>피킹(picking)</em>이란 사용자가 클릭 또는 터치한 물체를 가려내는 작업을 말합니다. 피킹을 구현하는 방법은 수없이 많지만, 각자 단점이 있습니다. 이 글에서는 이 방법 중 흔히 사용하는 2가지 방법만 살펴보겠습니다.</p>
  30. <p>아마 <em>피킹</em>을 구현하는 가장 흔한 방법은 광선 투사(ray casting)일 겁니다. 광선 투사란 포인터(커서)에서 장면의 절두체로 광선을 쏴 광선이 닿는 물체를 감지하는 기법을 말하죠. 이론적으로 가장 간단한 방법입니다.</p>
  31. <p>먼저 포인터의 좌표를 구한 뒤, 이 좌표를 카메라의 시선과 방향에 따라 3D 좌표로 변환합니다. 그리고 near 면에서 far 면까지의 광선을 구해 이 광선이 장면 안 각 물체의 삼각형과 교차하는지 확인합니다. 만약 장면 안에 1000개의 삼각형을 가진 물체가 1000개 있다면 백만 개의 삼각형을 일일이 확인해야 하는 셈이죠.</p>
  32. <p>이를 최적화하려면 몇 가지 방법을 시도해볼 수 있습니다. 하나는 먼저 물체를 감싼 경계(bounding) 좌표가 광선과 교차하는지 확인하고, 교차하지 않는다면 해당 물체의 삼각형을 확인하지 않는 것이죠.</p>
  33. <p>Three.js에는 이런 작업을 대신해주는 <code class="notranslate" translate="no">RayCaster</code> 클래스가 있습니다.</p>
  34. <p>한번 물체 100개가 있는 장면을 만들어 여기서 피킹을 구현해봅시다. 예제는 <a href="responsive.html">반응형 디자인</a>에서 썼던 예제를 가져와 사용하겠습니다.</p>
  35. <p>우선 카메라를 별도 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>의 자식으로 추가해 카메라가 셀카봉처럼 장면 주위를 돌 수 있도록 합니다.</p>
  36. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">*const fov = 60;
  37. const aspect = 2; // 캔버스 기본값
  38. const near = 0.1;
  39. *const far = 200;
  40. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  41. *camera.position.z = 30;
  42. const scene = new THREE.Scene();
  43. +scene.background = new THREE.Color('white');
  44. +// 카메라를 봉(pole)에 추가합니다.
  45. +// 이러면 봉을 회전시켜 카메라가 장면 주위를 돌도록 할 수 있습니다
  46. +const cameraPole = new THREE.Object3D();
  47. +scene.add(cameraPole);
  48. +cameraPole.add(camera);
  49. </pre>
  50. <p>그리고 <code class="notranslate" translate="no">render</code> 함수 안에서 카메라 봉을 돌립니다.</p>
  51. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">cameraPole.rotation.y = time * .1;
  52. </pre>
  53. <p>또한 카메라에 조명을 추가해 조명이 카메라와 같이 움직이도록 합니다.</p>
  54. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-scene.add(light);
  55. +camera.add(light);
  56. </pre>
  57. <p>정육면체 100개의 위치, 방향, 크기를 무작위로 설정해 생성합니다.</p>
  58. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  59. const boxHeight = 1;
  60. const boxDepth = 1;
  61. const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  62. function rand(min, max) {
  63. if (max === undefined) {
  64. max = min;
  65. min = 0;
  66. }
  67. return min + (max - min) * Math.random();
  68. }
  69. function randomColor() {
  70. return `hsl(${ rand(360) | 0 }, ${ rand(50, 100) | 0 }%, 50%)`;
  71. }
  72. const numObjects = 100;
  73. for (let i = 0; i &lt; numObjects; ++i) {
  74. const material = new THREE.MeshPhongMaterial({
  75. color: randomColor(),
  76. });
  77. const cube = new THREE.Mesh(geometry, material);
  78. scene.add(cube);
  79. cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
  80. cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
  81. cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
  82. }
  83. </pre>
  84. <p>이제 피킹을 구현해봅시다.</p>
  85. <p>피킹을 관리할 간단한 클래스를 만들겠습니다.</p>
  86. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  87. constructor() {
  88. this.raycaster = new THREE.Raycaster();
  89. this.pickedObject = null;
  90. this.pickedObjectSavedColor = 0;
  91. }
  92. pick(normalizedPosition, scene, camera, time) {
  93. // 이미 다른 물체를 피킹했다면 색을 복원합니다
  94. if (this.pickedObject) {
  95. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  96. this.pickedObject = undefined;
  97. }
  98. // 절두체 안에 광선을 쏩니다
  99. this.raycaster.setFromCamera(normalizedPosition, camera);
  100. // 광선과 교차하는 물체들을 배열로 만듭니다
  101. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  102. if (intersectedObjects.length) {
  103. // 첫 번째 물체가 제일 가까우므로 해당 물체를 고릅니다
  104. this.pickedObject = intersectedObjects[0].object;
  105. // 기존 색을 저장해둡니다
  106. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  107. // emissive 색을 빨강/노랑으로 빛나게 만듭니다
  108. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  109. }
  110. }
  111. }
  112. </pre>
  113. <p>위 클래스는 먼저 <code class="notranslate" translate="no">RayCaster</code> 인스턴스를 만들고 <code class="notranslate" translate="no">pick</code> 메서드를 호출하면 장면에 광선을 쏠 수 있게 해줍니다. 그리고 광선에 맞는 요소가 있으면 해당 요소 중 가장 첫 번째 요소의 색을 변경합니다.</p>
  114. <p>사용자가 마우스를 눌렀을 때(down)만 이 함수가 작동하도록 할 수도 있지만, 예제에서는 마우스 포인터 아래의 있는 요소를 피킹하도록 하겠습니다. 이를 구현하려면 먼저 포인터를 추적해야 합니다.</p>
  115. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickPosition = { x: 0, y: 0 };
  116. clearPickPosition();
  117. ...
  118. function getCanvasRelativePosition(event) {
  119. const rect = canvas.getBoundingClientRect();
  120. return {
  121. x: (event.clientX - rect.left) * canvas.width / rect.width,
  122. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  123. };
  124. }
  125. function setPickPosition(event) {
  126. const pos = getCanvasRelativePosition(event);
  127. pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
  128. pickPosition.y = (pos.y / canvas.height) * -2 + 1; // Y 축을 뒤집었음
  129. }
  130. function clearPickPosition() {
  131. /**
  132. * 마우스의 경우는 항상 위치가 있어 그다지 큰
  133. * 상관이 없지만, 터치 같은 경우 사용자가 손가락을
  134. * 떼면 피킹을 멈춰야 합니다. 지금은 일단 어떤 것도
  135. * 선택할 수 없는 값으로 지정해두었습니다
  136. **/
  137. pickPosition.x = -100000;
  138. pickPosition.y = -100000;
  139. }
  140. window.addEventListener('mousemove', setPickPosition);
  141. window.addEventListener('mouseout', clearPickPosition);
  142. window.addEventListener('mouseleave', clearPickPosition);
  143. </pre>
  144. <p>위 예제에서는 마우스의 좌표를 정규화(normalize)했습니다. 캔버스의 크기와 상관없이 왼쪽 끝이 -1, 오른쪽 끝이 +1인 벡터값이 필요하기 때문이죠. 마찬가지로 아래쪽 끝은 -1, 위쪽 끝은 +1입니다.</p>
  145. <p>모바일도 환경도 지원하기 위해 리스너를 더 추가하겠습니다.</p>
  146. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">window.addEventListener('touchstart', (event) =&gt; {
  147. event.preventDefault(); // 스크롤 이벤트 방지
  148. setPickPosition(event.touches[0]);
  149. }, { passive: false });
  150. window.addEventListener('touchmove', (event) =&gt; {
  151. setPickPosition(event.touches[0]);
  152. });
  153. window.addEventListener('touchend', clearPickPosition);
  154. </pre>
  155. <p>마지막으로 <code class="notranslate" translate="no">render</code> 함수에서 <code class="notranslate" translate="no">PickHelper</code>의 <code class="notranslate" translate="no">pick</code> 메서드를 호출합니다.</p>
  156. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const pickHelper = new PickHelper();
  157. function render(time) {
  158. time *= 0.001; // 초 단위로 변환
  159. ...
  160. + pickHelper.pick(pickPosition, scene, camera, time);
  161. renderer.render(scene, camera);
  162. ...
  163. </pre>
  164. <p>결과를 볼까요?</p>
  165. <p></p><div translate="no" class="threejs_example_container notranslate">
  166. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/picking-raycaster.html"></iframe></div>
  167. <a class="threejs_center" href="/manual/examples/picking-raycaster.html" target="_blank">새 탭에서 보기</a>
  168. </div>
  169. <p></p>
  170. <p>딱히 문제는 없어 보입니다. 실제로 사용하는 경우도 대부분 문제 없이 잘 되겠지만, 이 방법에는 몇 가지 문제점이 있습니다.</p>
  171. <ol>
  172. <li><p>CPU의 자원을 사용한다</p>
  173. <p> 자바스크립트 엔진은 각 요소를 돌며 광선이 요소의 경계 좌표 안에 교차하는지 확인합니다. 만약 교차할 경우, 해당 요소의 삼각형을 전부 돌며 광선과 교차하는 삼각형이 있는지 확인합니다.</p>
  174. <p> 이 방식의 장점은 자바스크립트가 교차하는 지점을 정확히 계산해 해당 데이터를 넘겨줄 수 있다는 점입니다. 예를 들어 교차가 발생한 지점에 특정 표시를 할 수 있겠죠.</p>
  175. <p> 대신 CPU가 할 일이 더 늘어난다는 점이 단점입니다. 요소가 가진 삼각형이 많을수록 더 느려지겠죠.</p>
  176. </li>
  177. <li><p>특이한 방식의 쉐이더나 변이를 감지하지 못한다</p>
  178. <p> 만약 장면에서 geometry를 변형하는 쉐이더를 사용한다면, 자바스크립트는 이 변형을 감지하지 못하기에 잘못된 값을 내놓을 겁니다. 제가 테스트해본 결과 스킨이 적용된 요소에는 이 방법이 먹히지 않습니다.</p>
  179. </li>
  180. <li><p>요소의 투명한 구멍을 처리하지 못한다.</p>
  181. </li>
  182. </ol>
  183. <p>예제를 하나 만들어보죠. 아래와 같은 텍스처를 정육면체에 적용해봅시다.</p>
  184. <div class="threejs_center"><img class="checkerboard" src="../examples/resources/images/frame.png"></div>
  185. <p>그다지 추가할 건 많지 않습니다.</p>
  186. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loader = new THREE.TextureLoader();
  187. +const texture = loader.load('resources/images/frame.png');
  188. const numObjects = 100;
  189. for (let i = 0; i &lt; numObjects; ++i) {
  190. const material = new THREE.MeshPhongMaterial({
  191. color: randomColor(),
  192. +map: texture,
  193. +transparent: true,
  194. +side: THREE.DoubleSide,
  195. +alphaTest: 0.1,
  196. });
  197. const cube = new THREE.Mesh(geometry, material);
  198. scene.add(cube);
  199. ...
  200. </pre>
  201. <p>예제를 실행시키면 바로 문제가 보일 겁니다.</p>
  202. <p></p><div translate="no" class="threejs_example_container notranslate">
  203. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/picking-raycaster-transparency.html"></iframe></div>
  204. <a class="threejs_center" href="/manual/examples/picking-raycaster-transparency.html" target="_blank">새 탭에서 보기</a>
  205. </div>
  206. <p></p>
  207. <p>정육면체의 빈 공간을 통해 무언가를 선택할 수가 없죠.</p>
  208. <div class="threejs_center"><img src="../resources/images/picking-transparent-issue.jpg" style="width: 635px;"></div>
  209. <p>이는 자바스크립트가 텍스처나 재질을 보고 해당 요소가 투명한지 판단하기가 어렵기 때문입니다.</p>
  210. <p>이 문제를 해결하려면 GPU 기반 피킹을 구현해야 합니다. 이론적으로는 간단하지만 위에서 사용한 광선 투사법보다는 좀 더 복잡하죠.</p>
  211. <p>GPU 피킹을 구현하려면 각 요소를 별도의 화면에서 고유한 색상으로 렌더링해야 합니다. 그리고 포인터 아래에 있는 픽셀의 색을 가져와 해당 요소가 선택됐는지 확인하는 거죠.</p>
  212. <p>이러면 위에서 언급한 문제점 2, 3번이 해결됩니다. 1번, 성능의 경우는 상황에 따라 천차만별이죠. 눈에 보이는 화면을 위해 한 번, 피킹을 위해 한 번, 이렇게 매 요소를 총 두 번씩 렌더링해야 합니다. 더 복잡한 해결책을 쓰면 렌더링을 한 번만 할 수도 있지만, 이 글에서는 일단 더 간단한 방법을 사용하겠습니다.</p>
  213. <p>성능 최적화를 위해 시도할 수 있는 방법이 하나 있습니다. 어차피 픽셀을 하나만 읽을 것이니, 카메라를 픽셀 하나만 렌더링하도록 설정하는 것이죠. <a href="/docs/#api/ko/cameras/PerspectiveCamera.setViewOffset"><code class="notranslate" translate="no">PerspectiveCamera.setViewOffset</code></a> 메서드를 사용하면 카메라의 특정 부분만 렌더링하도록 할 수 있습니다. 이러면 성능 향상에 조금이나마 도움이 되겠죠.</p>
  214. <p>현재 Three.js에서 이 기법을 구현하려면 장면 2개를 사용해야 합니다. 하나는 기존 mesh를 그대로 쓰고, 나머지 하나는 피킹용 재질을 적용한 mesh를 쓸 겁니다.</p>
  215. <p>먼저 두 번째 장면을 추가하고 배경을 검정으로 지정합니다.</p>
  216. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  217. scene.background = new THREE.Color('white');
  218. const pickingScene = new THREE.Scene();
  219. pickingScene.background = new THREE.Color(0);
  220. </pre>
  221. <p>각 정육면체를 장면에 추가할 때 <code class="notranslate" translate="no">pickingScene</code>의 같은 위치에 "피킹용 정육면체"를 추가합니다. 그리고 각 피킹용 정육면체에는 id로 쓸 고유 색상값을 지정한 뒤, 이 id 색상값으로 재질을 만들어 추가합니다. id 색상값을 정육면체의 키값으로 매핑해 놓으면 나중에 상응하는 정육면체를 바로 불러올 수 있겠죠.</p>
  222. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const idToObject = {};
  223. +const numObjects = 100;
  224. for (let i = 0; i &lt; numObjects; ++i) {
  225. + const id = i + 1;
  226. const material = new THREE.MeshPhongMaterial({
  227. color: randomColor(),
  228. map: texture,
  229. transparent: true,
  230. side: THREE.DoubleSide,
  231. alphaTest: 0.1,
  232. });
  233. const cube = new THREE.Mesh(geometry, material);
  234. scene.add(cube);
  235. + idToObject[id] = cube;
  236. cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
  237. cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
  238. cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
  239. + const pickingMaterial = new THREE.MeshPhongMaterial({
  240. + emissive: new THREE.Color().setHex(id, THREE.NoColorSpace),
  241. + color: new THREE.Color(0, 0, 0),
  242. + specular: new THREE.Color(0, 0, 0),
  243. + map: texture,
  244. + transparent: true,
  245. + side: THREE.DoubleSide,
  246. + alphaTest: 0.5,
  247. + blending: THREE.NoBlending,
  248. + });
  249. + const pickingCube = new THREE.Mesh(geometry, pickingMaterial);
  250. + pickingScene.add(pickingCube);
  251. + pickingCube.position.copy(cube.position);
  252. + pickingCube.rotation.copy(cube.rotation);
  253. + pickingCube.scale.copy(cube.scale);
  254. }
  255. </pre>
  256. <p>위 코드에서는 <a href="/docs/#api/ko/materials/MeshPhongMaterial"><code class="notranslate" translate="no">MeshPhongMaterial</code></a>로 편법을 사용했습니다. <code class="notranslate" translate="no">emissive</code> 속성을 id 색상값으로, <code class="notranslate" translate="no">color</code>와 <code class="notranslate" translate="no">specular</code> 속성을 0으로 설정하면 텍스처의 알파값이 <code class="notranslate" translate="no">alphaTest</code>보다 큰 부분만 id 색상값으로 보이겠죠. 또 <code class="notranslate" translate="no">blending</code> 속성을 <code class="notranslate" translate="no">THREE.NoBlending</code>으로 설정해 id 색상값이 알파값의 영향을 받지 않도록 했습니다.</p>
  257. <p>제가 사용한 편법이 최적의 해결책은 아닙니다. 여러가지 옵션을 껐다고 해도 여전히 조명 관련 연산을 실행할 테니까요. 코드를 더 최적화하려면 <code class="notranslate" translate="no">alphaTest</code> 값보다 높은 경우에만 id 색상을 렌더링하는 쉐이더를 직접 만들어야 합니다.</p>
  258. <p>광선 투사법을 쓸 때와 달리 픽셀을 하나만 사용하므로 위치값이 픽셀 하나만 가리키게 변경합니다.</p>
  259. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
  260. const pos = getCanvasRelativePosition(event);
  261. - pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
  262. - pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // Y 축을 뒤집었음
  263. + pickPosition.x = pos.x;
  264. + pickPosition.y = pos.y;
  265. }
  266. </pre>
  267. <p><code class="notranslate" translate="no">PickHelper</code> 클래스도 <code class="notranslate" translate="no">GPUPickHelper</code>로 변경합니다. <a href="rendertargets.html">렌더 타겟(render target)에 관한 글</a>에서 다룬 <a href="/docs/#api/ko/renderers/WebGLRenderTarget"><code class="notranslate" translate="no">WebGLRenderTarget</code></a>을 써 구현하되, 이번 렌더 타겟의 크기는 1x1, 1픽셀입니다.</p>
  268. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-class PickHelper {
  269. +class GPUPickHelper {
  270. constructor() {
  271. - this.raycaster = new THREE.Raycaster();
  272. + // 1x1 픽셀 크기의 렌더 타겟을 생성합니다
  273. + this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
  274. + this.pixelBuffer = new Uint8Array(4);
  275. this.pickedObject = null;
  276. this.pickedObjectSavedColor = 0;
  277. }
  278. pick(cssPosition, scene, camera, time) {
  279. + const {pickingTexture, pixelBuffer} = this;
  280. // 기존에 선택된 요소가 있는 경우 색을 복원합니다
  281. if (this.pickedObject) {
  282. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  283. this.pickedObject = undefined;
  284. }
  285. + // view offset을 마우스 포인터 아래 1픽셀로 설정합니다
  286. + const pixelRatio = renderer.getPixelRatio();
  287. + camera.setViewOffset(
  288. + renderer.getContext().drawingBufferWidth, // 전체 너비
  289. + renderer.getContext().drawingBufferHeight, // 전체 높이
  290. + cssPosition.x * pixelRatio | 0, // 사각 x 좌표
  291. + cssPosition.y * pixelRatio | 0, // 사각 y 좌표
  292. + 1, // 사각 좌표 width
  293. + 1, // 사각 좌표 height
  294. + );
  295. + // 장면을 렌더링합니다
  296. + renderer.setRenderTarget(pickingTexture)
  297. + renderer.render(scene, camera);
  298. + renderer.setRenderTarget(null);
  299. +
  300. + // view offset을 정상으로 돌려 원래의 화면을 렌더링하도록 합니다
  301. + camera.clearViewOffset();
  302. + // 픽셀을 감지합니다
  303. + renderer.readRenderTargetPixels(
  304. + pickingTexture,
  305. + 0, // x
  306. + 0, // y
  307. + 1, // width
  308. + 1, // height
  309. + pixelBuffer);
  310. +
  311. + const id =
  312. + (pixelBuffer[0] &lt;&lt; 16) |
  313. + (pixelBuffer[1] &lt;&lt; 8) |
  314. + (pixelBuffer[2] );
  315. // 절두체 안에 광선을 쏩니다
  316. - this.raycaster.setFromCamera(normalizedPosition, camera);
  317. // 광선과 교차하는 물체들을 배열로 만듭니다
  318. - const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  319. - if (intersectedObjects.length) {
  320. // 첫 번째 물체가 제일 가까우므로 해당 물체를 고릅니다
  321. - this.pickedObject = intersectedObjects[0].object;
  322. + const intersectedObject = idToObject[id];
  323. + if (intersectedObject) {
  324. // 첫 번째 물체가 제일 가까우므로 해당 물체를 고릅니다
  325. + this.pickedObject = intersectedObject;
  326. // 기존 색을 저장해둡니다
  327. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  328. // emissive 색을 빨강/노랑으로 빛나게 만듭니다
  329. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  330. }
  331. }
  332. }
  333. </pre>
  334. <p>인스턴스를 만드는 쪽도 수정합니다.</p>
  335. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const pickHelper = new PickHelper();
  336. +const pickHelper = new GPUPickHelper();
  337. </pre>
  338. <p><code class="notranslate" translate="no">pick</code> 메서드를 호출할 때 <code class="notranslate" translate="no">scene</code> 대신 <code class="notranslate" translate="no">pickScene</code>을 넘겨줍니다.</p>
  339. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- pickHelper.pick(pickPosition, scene, camera, time);
  340. + pickHelper.pick(pickPosition, pickScene, camera, time);
  341. </pre>
  342. <p>이제 투명한 부분을 관통해 요소를 선택할 수 있습니다.</p>
  343. <p></p><div translate="no" class="threejs_example_container notranslate">
  344. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/picking-gpu.html"></iframe></div>
  345. <a class="threejs_center" href="/manual/examples/picking-gpu.html" target="_blank">새 탭에서 보기</a>
  346. </div>
  347. <p></p>
  348. <p>이 글이 피킹을 구현하는 데 도움이 되었으면 좋겠네요. 나중에 요소를 마우스로 조작하는 법에 대해서도 한 번 써보겠습니다.</p>
  349. </div>
  350. </div>
  351. </div>
  352. <script src="../resources/prettify.js"></script>
  353. <script src="../resources/lesson.js"></script>
  354. </body></html>