2
0

picking.html 22 KB

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