webxr-point-to-select.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>VR - 3DOF Point to Select</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 – VR - 3DOF Point to Select">
  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. <!-- 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. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>VR - 3DOF Point to Select</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p><strong>NOTE: The examples on this page require a VR capable
  32. device with a pointing device. Without one they won't work. See <a href="webxr.html">this article</a>
  33. as to why</strong></p>
  34. <p>In the <a href="webxr-look-to-select.html">previous article</a> we went over
  35. a very simple VR example where we let the user choose things by
  36. pointing via looking. In this article we will take it one step further
  37. and let the user choose with a pointing device </p>
  38. <p>Three.js makes is relatively easy by providing 2 controller objects in VR
  39. and tries to handle both cases of a single 3DOF controller and two 6DOF
  40. controllers. Each of the controllers are <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> objects which give
  41. the orientation and position of that controller. They also provide
  42. <code class="notranslate" translate="no">selectstart</code>, <code class="notranslate" translate="no">select</code> and <code class="notranslate" translate="no">selectend</code> events when the user starts pressing,
  43. is pressing, and stops pressing (ends) the "main" button on the controller.</p>
  44. <p>Starting with the last example from <a href="webxr-look-to-select.html">the previous article</a>
  45. let's change the <code class="notranslate" translate="no">PickHelper</code> into a <code class="notranslate" translate="no">ControllerPickHelper</code>.</p>
  46. <p>Our new implementation will emit a <code class="notranslate" translate="no">select</code> event that gives us the object that was picked
  47. so to use it we'll just need to do this.</p>
  48. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new ControllerPickHelper(scene);
  49. pickHelper.addEventListener('select', (event) =&gt; {
  50. event.selectedObject.visible = false;
  51. const partnerObject = meshToMeshMap.get(event.selectedObject);
  52. partnerObject.visible = true;
  53. });
  54. </pre>
  55. <p>Remember from our previous code <code class="notranslate" translate="no">meshToMeshMap</code> maps our boxes and spheres to
  56. each other so if we have one we can look up its partner through <code class="notranslate" translate="no">meshToMeshMap</code>
  57. so here we're just hiding the selected object and un-hiding its partner.</p>
  58. <p>As for the actual implementation of <code class="notranslate" translate="no">ControllerPickHelper</code>, first we need
  59. to add the VR controller objects to the scene and to those add some 3D lines
  60. we can use to display where the user is pointing. We save off both the controllers
  61. and the their lines.</p>
  62. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper {
  63. constructor(scene) {
  64. const pointerGeometry = new THREE.BufferGeometry().setFromPoints([
  65. new THREE.Vector3(0, 0, 0),
  66. new THREE.Vector3(0, 0, -1),
  67. ]);
  68. this.controllers = [];
  69. for (let i = 0; i &lt; 2; ++i) {
  70. const controller = renderer.xr.getController(i);
  71. scene.add(controller);
  72. const line = new THREE.Line(pointerGeometry);
  73. line.scale.z = 5;
  74. controller.add(line);
  75. this.controllers.push({controller, line});
  76. }
  77. }
  78. }
  79. </pre>
  80. <p>Without doing anything else this alone would give us 1 or 2 lines in the scene
  81. showing where the user's pointing devices are and which way they are pointing.</p>
  82. <p>One problem we have though, we don't want have our <code class="notranslate" translate="no">RayCaster</code> pick the line itself
  83. so an easy solution is separate the objects we wanted to be able to pick from the
  84. objects we don't by parenting them under another <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>.</p>
  85. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  86. +// object to put pickable objects on so we can easily
  87. +// separate them from non-pickable objects
  88. +const pickRoot = new THREE.Object3D();
  89. +scene.add(pickRoot);
  90. ...
  91. function makeInstance(geometry, color, x) {
  92. const material = new THREE.MeshPhongMaterial({color});
  93. const cube = new THREE.Mesh(geometry, material);
  94. - scene.add(cube);
  95. + pickRoot.add(cube);
  96. ...
  97. </pre>
  98. <p>Next let's add some code to pick from the controllers. This is the first time
  99. we've picked with something not the camera. In our <a href="picking.html">article on picking</a>
  100. the user uses the mouse or finger to pick which means picking comes from the camera
  101. into the screen. In <a href="webxr-look-to-select.html">the previous article</a> we
  102. were picking based on which way the user is looking so again that comes from the
  103. camera. This time though we're picking from the position of the controllers so
  104. we're not using the camera.</p>
  105. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper {
  106. constructor(scene) {
  107. + this.raycaster = new THREE.Raycaster();
  108. + this.objectToColorMap = new Map();
  109. + this.controllerToObjectMap = new Map();
  110. + this.tempMatrix = new THREE.Matrix4();
  111. const pointerGeometry = new THREE.BufferGeometry().setFromPoints([
  112. new THREE.Vector3(0, 0, 0),
  113. new THREE.Vector3(0, 0, -1),
  114. ]);
  115. this.controllers = [];
  116. for (let i = 0; i &lt; 2; ++i) {
  117. const controller = renderer.xr.getController(i);
  118. scene.add(controller);
  119. const line = new THREE.Line(pointerGeometry);
  120. line.scale.z = 5;
  121. controller.add(line);
  122. this.controllers.push({controller, line});
  123. }
  124. }
  125. + update(pickablesParent, time) {
  126. + this.reset();
  127. + for (const {controller, line} of this.controllers) {
  128. + // cast a ray through the from the controller
  129. + this.tempMatrix.identity().extractRotation(controller.matrixWorld);
  130. + this.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  131. + this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(this.tempMatrix);
  132. + // get the list of objects the ray intersected
  133. + const intersections = this.raycaster.intersectObjects(pickablesParent.children);
  134. + if (intersections.length) {
  135. + const intersection = intersections[0];
  136. + // make the line touch the object
  137. + line.scale.z = intersection.distance;
  138. + // pick the first object. It's the closest one
  139. + const pickedObject = intersection.object;
  140. + // save which object this controller picked
  141. + this.controllerToObjectMap.set(controller, pickedObject);
  142. + // highlight the object if we haven't already
  143. + if (this.objectToColorMap.get(pickedObject) === undefined) {
  144. + // save its color
  145. + this.objectToColorMap.set(pickedObject, pickedObject.material.emissive.getHex());
  146. + // set its emissive color to flashing red/yellow
  147. + pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFF2000 : 0xFF0000);
  148. + }
  149. + } else {
  150. + line.scale.z = 5;
  151. + }
  152. + }
  153. + }
  154. }
  155. </pre>
  156. <p>Like before we use a <a href="/docs/#api/en/core/Raycaster"><code class="notranslate" translate="no">Raycaster</code></a> but this time we take the ray from the controller.
  157. Our previous <code class="notranslate" translate="no">PickHelper</code> there was only one thing picking but here we have up to 2
  158. controllers, one for each hand. We save off which object each controller is
  159. looking at in <code class="notranslate" translate="no">controllerToObjectMap</code>. We also save off the original emissive color in
  160. <code class="notranslate" translate="no">objectToColorMap</code> and we make the line long enough to touch whatever it's pointing at.</p>
  161. <p>We need to add some code to reset these settings every frame.</p>
  162. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper {
  163. ...
  164. + _reset() {
  165. + // restore the colors
  166. + this.objectToColorMap.forEach((color, object) =&gt; {
  167. + object.material.emissive.setHex(color);
  168. + });
  169. + this.objectToColorMap.clear();
  170. + this.controllerToObjectMap.clear();
  171. + }
  172. update(pickablesParent, time) {
  173. + this._reset();
  174. ...
  175. }
  176. </pre>
  177. <p>Next we want to emit a <code class="notranslate" translate="no">select</code> event when the user clicks the controller.
  178. To do that we can extend three.js's <a href="/docs/#api/en/core/EventDispatcher"><code class="notranslate" translate="no">EventDispatcher</code></a> and then we'll check
  179. when we get a <code class="notranslate" translate="no">select</code> event from the controller, then if that controller
  180. is pointing at something we emit what that controller is pointing at
  181. as our own <code class="notranslate" translate="no">select</code> event.</p>
  182. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-class ControllerPickHelper {
  183. +class ControllerPickHelper extends THREE.EventDispatcher {
  184. constructor(scene) {
  185. + super();
  186. this.raycaster = new THREE.Raycaster();
  187. this.objectToColorMap = new Map(); // object to save color and picked object
  188. this.controllerToObjectMap = new Map();
  189. this.tempMatrix = new THREE.Matrix4();
  190. const pointerGeometry = new THREE.BufferGeometry().setFromPoints([
  191. new THREE.Vector3(0, 0, 0),
  192. new THREE.Vector3(0, 0, -1),
  193. ]);
  194. this.controllers = [];
  195. for (let i = 0; i &lt; 2; ++i) {
  196. const controller = renderer.xr.getController(i);
  197. + controller.addEventListener('select', (event) =&gt; {
  198. + const controller = event.target;
  199. + const selectedObject = this.controllerToObjectMap.get(controller);
  200. + if (selectedObject) {
  201. + this.dispatchEvent({type: 'select', controller, selectedObject});
  202. + }
  203. + });
  204. scene.add(controller);
  205. const line = new THREE.Line(pointerGeometry);
  206. line.scale.z = 5;
  207. controller.add(line);
  208. this.controllers.push({controller, line});
  209. }
  210. }
  211. }
  212. </pre>
  213. <p>All that is left is to call <code class="notranslate" translate="no">update</code> in our render loop</p>
  214. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  215. ...
  216. + pickHelper.update(pickablesParent, time);
  217. renderer.render(scene, camera);
  218. }
  219. </pre>
  220. <p>and assuming you have a VR device with a controller you should
  221. be able to use the controllers to pick things.</p>
  222. <p></p><div translate="no" class="threejs_example_container notranslate">
  223. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-point-to-select.html"></iframe></div>
  224. <a class="threejs_center" href="/manual/examples/webxr-point-to-select.html" target="_blank">click here to open in a separate window</a>
  225. </div>
  226. <p></p>
  227. <p>And what if we wanted to be able to move the objects?</p>
  228. <p>That's relatively easy. Let's move our controller 'select' listener
  229. code out into a function so we can use it for more than one thing.</p>
  230. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper extends THREE.EventDispatcher {
  231. constructor(scene) {
  232. super();
  233. ...
  234. this.controllers = [];
  235. + const selectListener = (event) =&gt; {
  236. + const controller = event.target;
  237. + const selectedObject = this.controllerToObjectMap.get(event.target);
  238. + if (selectedObject) {
  239. + this.dispatchEvent({type: 'select', controller, selectedObject});
  240. + }
  241. + };
  242. for (let i = 0; i &lt; 2; ++i) {
  243. const controller = renderer.xr.getController(i);
  244. - controller.addEventListener('select', (event) =&gt; {
  245. - const controller = event.target;
  246. - const selectedObject = this.controllerToObjectMap.get(event.target);
  247. - if (selectedObject) {
  248. - this.dispatchEvent({type: 'select', controller, selectedObject});
  249. - }
  250. - });
  251. + controller.addEventListener('select', selectListener);
  252. ...
  253. </pre>
  254. <p>Then let's use it for both <code class="notranslate" translate="no">selectstart</code> and <code class="notranslate" translate="no">select</code></p>
  255. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper extends THREE.EventDispatcher {
  256. constructor(scene) {
  257. super();
  258. ...
  259. this.controllers = [];
  260. const selectListener = (event) =&gt; {
  261. const controller = event.target;
  262. const selectedObject = this.controllerToObjectMap.get(event.target);
  263. if (selectedObject) {
  264. - this.dispatchEvent({type: 'select', controller, selectedObject});
  265. + this.dispatchEvent({type: event.type, controller, selectedObject});
  266. }
  267. };
  268. for (let i = 0; i &lt; 2; ++i) {
  269. const controller = renderer.xr.getController(i);
  270. controller.addEventListener('select', selectListener);
  271. controller.addEventListener('selectstart', selectListener);
  272. ...
  273. </pre>
  274. <p>and let's also pass on the <code class="notranslate" translate="no">selectend</code> event which three.js sends out
  275. when you user lets of the button on the controller.</p>
  276. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ControllerPickHelper extends THREE.EventDispatcher {
  277. constructor(scene) {
  278. super();
  279. ...
  280. this.controllers = [];
  281. const selectListener = (event) =&gt; {
  282. const controller = event.target;
  283. const selectedObject = this.controllerToObjectMap.get(event.target);
  284. if (selectedObject) {
  285. this.dispatchEvent({type: event.type, controller, selectedObject});
  286. }
  287. };
  288. + const endListener = (event) =&gt; {
  289. + const controller = event.target;
  290. + this.dispatchEvent({type: event.type, controller});
  291. + };
  292. for (let i = 0; i &lt; 2; ++i) {
  293. const controller = renderer.xr.getController(i);
  294. controller.addEventListener('select', selectListener);
  295. controller.addEventListener('selectstart', selectListener);
  296. + controller.addEventListener('selectend', endListener);
  297. ...
  298. </pre>
  299. <p>Now let's change the code so when we get a <code class="notranslate" translate="no">selectstart</code> event we'll
  300. remove the selected object from the scene and make it a child of the controller.
  301. This means it will move with the controller. When we get a <code class="notranslate" translate="no">selectend</code>
  302. event we'll put it back in the scene.</p>
  303. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new ControllerPickHelper(scene);
  304. -pickHelper.addEventListener('select', (event) =&gt; {
  305. - event.selectedObject.visible = false;
  306. - const partnerObject = meshToMeshMap.get(event.selectedObject);
  307. - partnerObject.visible = true;
  308. -});
  309. +const controllerToSelection = new Map();
  310. +pickHelper.addEventListener('selectstart', (event) =&gt; {
  311. + const {controller, selectedObject} = event;
  312. + const existingSelection = controllerToSelection.get(controller);
  313. + if (!existingSelection) {
  314. + controllerToSelection.set(controller, {
  315. + object: selectedObject,
  316. + parent: selectedObject.parent,
  317. + });
  318. + controller.attach(selectedObject);
  319. + }
  320. +});
  321. +
  322. +pickHelper.addEventListener('selectend', (event) =&gt; {
  323. + const {controller} = event;
  324. + const selection = controllerToSelection.get(controller);
  325. + if (selection) {
  326. + controllerToSelection.delete(controller);
  327. + selection.parent.attach(selection.object);
  328. + }
  329. +});
  330. </pre>
  331. <p>When an object is selected we save off that object and its
  332. original parent. When the user is done we can put the object back.</p>
  333. <p>We use the <a href="/docs/#api/en/core/Object3D.attach"><code class="notranslate" translate="no">Object3D.attach</code></a> to re-parent
  334. the selected objects. These functions let us change the parent
  335. of an object without changing its orientation and position in the
  336. scene. </p>
  337. <p>And with that we should be able to move the objects around with a 6DOF
  338. controller or at least change their orientation with a 3DOF controller</p>
  339. <p></p><div translate="no" class="threejs_example_container notranslate">
  340. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-point-to-select-w-move.html"></iframe></div>
  341. <a class="threejs_center" href="/manual/examples/webxr-point-to-select-w-move.html" target="_blank">click here to open in a separate window</a>
  342. </div>
  343. <p></p>
  344. <p>To be honest I'm not 100% sure this <code class="notranslate" translate="no">ControllerPickHelper</code> is
  345. the best way to organize the code but it's useful to demonstrating
  346. the various parts of getting something simple working in VR
  347. in three.js</p>
  348. </div>
  349. </div>
  350. </div>
  351. <script src="/manual/resources/prettify.js"></script>
  352. <script src="/manual/resources/lesson.js"></script>
  353. </body></html>