webxr-look-to-select.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>VR - Look 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 - Look 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="../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. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>VR - Look 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. Without one they won't work. See <a href="webxr.html">previous article</a>
  33. as to why</strong></p>
  34. <p>In the <a href="webxr.html">previous article</a> we went over
  35. a very simple VR example using three.js and we discussed
  36. the various kinds of VR systems.</p>
  37. <p>The simplest and possibly most common is the Google Cardboard style of VR which
  38. is basically a phone put into a $5 - $50 face mask. This kind of VR has no
  39. controller so people have to come up with creative solutions for allowing user
  40. input.</p>
  41. <p>The most common solution is "look to select" where if the
  42. user points their head at something for a moment it gets
  43. selected.</p>
  44. <p>Let's implement "look to select"! We'll start with
  45. <a href="webxr.html">an example from the previous article</a>
  46. and to do it we'll add the <code class="notranslate" translate="no">PickHelper</code> we made in
  47. <a href="picking.html">the article on picking</a>. Here it is.</p>
  48. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  49. constructor() {
  50. this.raycaster = new THREE.Raycaster();
  51. this.pickedObject = null;
  52. this.pickedObjectSavedColor = 0;
  53. }
  54. pick(normalizedPosition, scene, camera, time) {
  55. // restore the color if there is a picked object
  56. if (this.pickedObject) {
  57. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  58. this.pickedObject = undefined;
  59. }
  60. // cast a ray through the frustum
  61. this.raycaster.setFromCamera(normalizedPosition, camera);
  62. // get the list of objects the ray intersected
  63. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  64. if (intersectedObjects.length) {
  65. // pick the first object. It's the closest one
  66. this.pickedObject = intersectedObjects[0].object;
  67. // save its color
  68. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  69. // set its emissive color to flashing red/yellow
  70. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  71. }
  72. }
  73. }
  74. </pre>
  75. <p>For an explanation of that code <a href="picking.html">see the article on picking</a>.</p>
  76. <p>To use it we just need to create an instance and call it in our render loop</p>
  77. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const pickHelper = new PickHelper();
  78. ...
  79. function render(time) {
  80. time *= 0.001;
  81. ...
  82. + // 0, 0 is the center of the view in normalized coordinates.
  83. + pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  84. </pre>
  85. <p>In the original picking example we converted the mouse coordinates
  86. from CSS pixels into normalized coordinates that go from -1 to +1
  87. across the canvas.</p>
  88. <p>In this case though we will always pick where the camera is
  89. facing which is the center of the screen so we pass in <code class="notranslate" translate="no">0</code> for
  90. both <code class="notranslate" translate="no">x</code> and <code class="notranslate" translate="no">y</code> which is the center in normalized coordinates.</p>
  91. <p>And with that objects will flash when we look at them</p>
  92. <p></p><div translate="no" class="threejs_example_container notranslate">
  93. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select.html"></iframe></div>
  94. <a class="threejs_center" href="/manual/examples/webxr-look-to-select.html" target="_blank">click here to open in a separate window</a>
  95. </div>
  96. <p></p>
  97. <p>Typically we don't want selection to be immediate. Instead we require the user
  98. to keep the camera on the thing they want to select for a few moments to give them
  99. a chance not to select something by accident.</p>
  100. <p>To do that we need some kind of meter or gauge or some way
  101. to convey that the user must keep looking and for how long.</p>
  102. <p>One easy way we could do that is to make a 2 color texture
  103. and use a texture offset to slide the texture across a model.</p>
  104. <p>Let's do this by itself to see it work before we add it to
  105. the VR example.</p>
  106. <p>First we make an <a href="/docs/#api/en/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a></p>
  107. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = -2; // Use values for left
  108. const right = 2; // right, top and bottom
  109. const top = 1; // that match the default
  110. const bottom = -1; // canvas size.
  111. const near = -1;
  112. const far = 1;
  113. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  114. </pre>
  115. <p>And of course update it if the canvas changes size</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  117. time *= 0.001;
  118. if (resizeRendererToDisplaySize(renderer)) {
  119. const canvas = renderer.domElement;
  120. const aspect = canvas.clientWidth / canvas.clientHeight;
  121. + camera.left = -aspect;
  122. + camera.right = aspect;
  123. camera.updateProjectionMatrix();
  124. }
  125. ...
  126. </pre>
  127. <p>We now have a camera that shows 2 units above and below the center and aspect units
  128. left and right.</p>
  129. <p>Next let's make a 2 color texture. We'll use a <a href="/docs/#api/en/textures/DataTexture"><code class="notranslate" translate="no">DataTexture</code></a>
  130. which we've used a few <a href="indexed-textures.html">other</a>
  131. <a href="post-processing-3dlut.html">places</a>.</p>
  132. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeDataTexture(data, width, height) {
  133. const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat);
  134. texture.minFilter = THREE.NearestFilter;
  135. texture.magFilter = THREE.NearestFilter;
  136. texture.needsUpdate = true;
  137. return texture;
  138. }
  139. const cursorColors = new Uint8Array([
  140. 64, 64, 64, 64, // dark gray
  141. 255, 255, 255, 255, // white
  142. ]);
  143. const cursorTexture = makeDataTexture(cursorColors, 2, 1);
  144. </pre>
  145. <p>We'll then use that texture on a <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a></p>
  146. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const ringRadius = 0.4;
  147. const tubeRadius = 0.1;
  148. const tubeSegments = 4;
  149. const ringSegments = 64;
  150. const cursorGeometry = new THREE.TorusGeometry(
  151. ringRadius, tubeRadius, tubeSegments, ringSegments);
  152. const cursorMaterial = new THREE.MeshBasicMaterial({
  153. color: 'white',
  154. map: cursorTexture,
  155. transparent: true,
  156. blending: THREE.CustomBlending,
  157. blendSrc: THREE.OneMinusDstColorFactor,
  158. blendDst: THREE.OneMinusSrcColorFactor,
  159. });
  160. const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
  161. scene.add(cursor);
  162. </pre>
  163. <p>and then in <code class="notranslate" translate="no">render</code> lets adjust the texture's offset</p>
  164. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  165. time *= 0.001;
  166. if (resizeRendererToDisplaySize(renderer)) {
  167. const canvas = renderer.domElement;
  168. const aspect = canvas.clientWidth / canvas.clientHeight;
  169. camera.left = -aspect;
  170. camera.right = aspect;
  171. camera.updateProjectionMatrix();
  172. }
  173. + const fromStart = 0;
  174. + const fromEnd = 2;
  175. + const toStart = -0.5;
  176. + const toEnd = 0.5;
  177. + cursorTexture.offset.x = THREE.MathUtils.mapLinear(
  178. + time % 2,
  179. + fromStart, fromEnd,
  180. + toStart, toEnd);
  181. renderer.render(scene, camera);
  182. }
  183. </pre>
  184. <p><code class="notranslate" translate="no">THREE.MathUtils.mapLinear</code> takes a value that goes between <code class="notranslate" translate="no">fromStart</code> and <code class="notranslate" translate="no">fromEnd</code>
  185. and maps it to a value between <code class="notranslate" translate="no">toStart</code> and <code class="notranslate" translate="no">toEnd</code>. In the case above we're
  186. taking <code class="notranslate" translate="no">time % 2</code> which means a value that goes from 0 to 2 and maps
  187. that to a value that goes from -0.5 to 0.5</p>
  188. <p><a href="textures.html">Textures</a> are mapped to geometry using normalized texture coordinates
  189. that go from 0 to 1. That means our 2x1 pixel image, set to the default
  190. wrapping mode of <code class="notranslate" translate="no">THREE.ClampToEdge</code>, if we adjust the
  191. texture coordinates by -0.5 then the entire mesh will be the first color
  192. and if we adjust the texture coordinates by +0.5 the entire mesh will
  193. be the second color. In between with the filtering set to <code class="notranslate" translate="no">THREE.NearestFilter</code>
  194. we'll be able to move the transition between the 2 colors through the geometry.</p>
  195. <p>Let's add a background texture while we're at it just like we
  196. covered in <a href="backgrounds.html">the article on backgrounds</a>.
  197. We'll just use a 2x2 set of colors but set the texture's repeat
  198. settings to give us an 8x8 grid. This will give our cursor something
  199. to be rendered over so we can check it against different colors.</p>
  200. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const backgroundColors = new Uint8Array([
  201. + 0, 0, 0, 255, // black
  202. + 90, 38, 38, 255, // dark red
  203. + 100, 175, 103, 255, // medium green
  204. + 255, 239, 151, 255, // light yellow
  205. +]);
  206. +const backgroundTexture = makeDataTexture(backgroundColors, 2, 2);
  207. +backgroundTexture.wrapS = THREE.RepeatWrapping;
  208. +backgroundTexture.wrapT = THREE.RepeatWrapping;
  209. +backgroundTexture.repeat.set(4, 4);
  210. const scene = new THREE.Scene();
  211. +scene.background = backgroundTexture;
  212. </pre>
  213. <p>Now if we run that you'll see we get a circle like gauge
  214. and that we can set where the gauge is.</p>
  215. <p></p><div translate="no" class="threejs_example_container notranslate">
  216. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select-selector.html"></iframe></div>
  217. <a class="threejs_center" href="/manual/examples/webxr-look-to-select-selector.html" target="_blank">click here to open in a separate window</a>
  218. </div>
  219. <p></p>
  220. <p>A few things to notice <strong>and try</strong>.</p>
  221. <ul>
  222. <li><p>We set the <code class="notranslate" translate="no">cursorMaterial</code>'s <code class="notranslate" translate="no">blending</code>, <code class="notranslate" translate="no">blendSrc</code> and <code class="notranslate" translate="no">blendDst</code>
  223. properties as follows</p>
  224. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> blending: THREE.CustomBlending,
  225. blendSrc: THREE.OneMinusDstColorFactor,
  226. blendDst: THREE.OneMinusSrcColorFactor,
  227. </pre><p>This gives as an <em>inverse</em> type of effect. Comment out
  228. those 3 lines and you'll see the difference. I'm just guessing
  229. the inverse effect is best here as that way we can hopefully
  230. see the cursor regardless of the colors it is over.</p>
  231. </li>
  232. <li><p>We use a <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a> and not a <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a></p>
  233. <p>For whatever reason the <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> uses a flat
  234. UV mapping scheme. Because of this if we use a <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a>
  235. the texture slides horizontally across the ring instead of
  236. around it like it does above.</p>
  237. <p>Try it out, change the <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a> to a <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a>
  238. (it's just commented out in the example above) and you'll see what I
  239. mean.</p>
  240. <p>The <em>proper</em> thing to do (for some definition of <em>proper</em>) would be
  241. to either use the <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> but fix the texture coordinates
  242. so they go around the ring. Or else, generate our own ring geometry.
  243. But, the torus works just fine. Placed directly in front of the camera
  244. with a <a href="/docs/#api/en/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a> it will look exactly like a ring and the
  245. texture coordinates go around the ring so it works for our needs.</p>
  246. </li>
  247. </ul>
  248. <p>Let's integrate it with our VR code above. </p>
  249. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  250. - constructor() {
  251. + constructor(camera) {
  252. this.raycaster = new THREE.Raycaster();
  253. this.pickedObject = null;
  254. - this.pickedObjectSavedColor = 0;
  255. + const cursorColors = new Uint8Array([
  256. + 64, 64, 64, 64, // dark gray
  257. + 255, 255, 255, 255, // white
  258. + ]);
  259. + this.cursorTexture = makeDataTexture(cursorColors, 2, 1);
  260. +
  261. + const ringRadius = 0.4;
  262. + const tubeRadius = 0.1;
  263. + const tubeSegments = 4;
  264. + const ringSegments = 64;
  265. + const cursorGeometry = new THREE.TorusGeometry(
  266. + ringRadius, tubeRadius, tubeSegments, ringSegments);
  267. +
  268. + const cursorMaterial = new THREE.MeshBasicMaterial({
  269. + color: 'white',
  270. + map: this.cursorTexture,
  271. + transparent: true,
  272. + blending: THREE.CustomBlending,
  273. + blendSrc: THREE.OneMinusDstColorFactor,
  274. + blendDst: THREE.OneMinusSrcColorFactor,
  275. + });
  276. + const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
  277. + // add the cursor as a child of the camera
  278. + camera.add(cursor);
  279. + // and move it in front of the camera
  280. + cursor.position.z = -1;
  281. + const scale = 0.05;
  282. + cursor.scale.set(scale, scale, scale);
  283. + this.cursor = cursor;
  284. +
  285. + this.selectTimer = 0;
  286. + this.selectDuration = 2;
  287. + this.lastTime = 0;
  288. }
  289. pick(normalizedPosition, scene, camera, time) {
  290. + const elapsedTime = time - this.lastTime;
  291. + this.lastTime = time;
  292. - // restore the color if there is a picked object
  293. - if (this.pickedObject) {
  294. - this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  295. - this.pickedObject = undefined;
  296. - }
  297. + const lastPickedObject = this.pickedObject;
  298. + this.pickedObject = undefined;
  299. // cast a ray through the frustum
  300. this.raycaster.setFromCamera(normalizedPosition, camera);
  301. // get the list of objects the ray intersected
  302. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  303. if (intersectedObjects.length) {
  304. // pick the first object. It's the closest one
  305. this.pickedObject = intersectedObjects[0].object;
  306. - // save its color
  307. - this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  308. - // set its emissive color to flashing red/yellow
  309. - this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  310. }
  311. + // show the cursor only if it's hitting something
  312. + this.cursor.visible = this.pickedObject ? true : false;
  313. +
  314. + let selected = false;
  315. +
  316. + // if we're looking at the same object as before
  317. + // increment time select timer
  318. + if (this.pickedObject &amp;&amp; lastPickedObject === this.pickedObject) {
  319. + this.selectTimer += elapsedTime;
  320. + if (this.selectTimer &gt;= this.selectDuration) {
  321. + this.selectTimer = 0;
  322. + selected = true;
  323. + }
  324. + } else {
  325. + this.selectTimer = 0;
  326. + }
  327. +
  328. + // set cursor material to show the timer state
  329. + const fromStart = 0;
  330. + const fromEnd = this.selectDuration;
  331. + const toStart = -0.5;
  332. + const toEnd = 0.5;
  333. + this.cursorTexture.offset.x = THREE.MathUtils.mapLinear(
  334. + this.selectTimer,
  335. + fromStart, fromEnd,
  336. + toStart, toEnd);
  337. +
  338. + return selected ? this.pickedObject : undefined;
  339. }
  340. }
  341. </pre>
  342. <p>You can see the code above we added all the code to create
  343. the cursor geometry, texture, and material and we added it
  344. as a child of the camera so it will always be in front of
  345. the camera. Note we need to add the camera to the scene
  346. otherwise the cursor won't be rendered.</p>
  347. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+scene.add(camera);
  348. </pre>
  349. <p>We then check if the thing we're picking this time is the same as it was last
  350. time. If so we add the elapsed time to a timer and if the timer reaches its
  351. limit we return the selected item.</p>
  352. <p>Now let's use that to pick the cubes. As a simple example
  353. we'll add 3 spheres as well. When a cube is picked with hide
  354. the cube and un-hide the corresponding sphere.</p>
  355. <p>So first we'll make a sphere geometry</p>
  356. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  357. const boxHeight = 1;
  358. const boxDepth = 1;
  359. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  360. +const boxGeometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  361. +
  362. +const sphereRadius = 0.5;
  363. +const sphereGeometry = new THREE.SphereGeometry(sphereRadius);
  364. </pre>
  365. <p>Then let's create 3 pairs of box and sphere meshes. We'll
  366. use a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"><code class="notranslate" translate="no">Map</code></a>
  367. so that we can associate each <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> with its partner.</p>
  368. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const cubes = [
  369. - makeInstance(geometry, 0x44aa88, 0),
  370. - makeInstance(geometry, 0x8844aa, -2),
  371. - makeInstance(geometry, 0xaa8844, 2),
  372. -];
  373. +const meshToMeshMap = new Map();
  374. +[
  375. + { x: 0, boxColor: 0x44aa88, sphereColor: 0xFF4444, },
  376. + { x: 2, boxColor: 0x8844aa, sphereColor: 0x44FF44, },
  377. + { x: -2, boxColor: 0xaa8844, sphereColor: 0x4444FF, },
  378. +].forEach((info) =&gt; {
  379. + const {x, boxColor, sphereColor} = info;
  380. + const sphere = makeInstance(sphereGeometry, sphereColor, x);
  381. + const box = makeInstance(boxGeometry, boxColor, x);
  382. + // hide the sphere
  383. + sphere.visible = false;
  384. + // map the sphere to the box
  385. + meshToMeshMap.set(box, sphere);
  386. + // map the box to the sphere
  387. + meshToMeshMap.set(sphere, box);
  388. +});
  389. </pre>
  390. <p>In <code class="notranslate" translate="no">render</code> where we rotate the cubes we need to iterate over <code class="notranslate" translate="no">meshToMeshMap</code>
  391. instead of <code class="notranslate" translate="no">cubes</code>.</p>
  392. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-cubes.forEach((cube, ndx) =&gt; {
  393. +let ndx = 0;
  394. +for (const mesh of meshToMeshMap.keys()) {
  395. const speed = 1 + ndx * .1;
  396. const rot = time * speed;
  397. - cube.rotation.x = rot;
  398. - cube.rotation.y = rot;
  399. -});
  400. + mesh.rotation.x = rot;
  401. + mesh.rotation.y = rot;
  402. + ++ndx;
  403. +}
  404. </pre>
  405. <p>And now we can use our new <code class="notranslate" translate="no">PickHelper</code> implementation
  406. to select one of the objects. When selected we hide
  407. that object and un-hide its partner.</p>
  408. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 0, 0 is the center of the view in normalized coordinates.
  409. -pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  410. +const selectedObject = pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  411. +if (selectedObject) {
  412. + selectedObject.visible = false;
  413. + const partnerObject = meshToMeshMap.get(selectedObject);
  414. + partnerObject.visible = true;
  415. +}
  416. </pre>
  417. <p>And with that we should have a pretty decent <em>look to select</em> implementation.</p>
  418. <p></p><div translate="no" class="threejs_example_container notranslate">
  419. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select-w-cursor.html"></iframe></div>
  420. <a class="threejs_center" href="/manual/examples/webxr-look-to-select-w-cursor.html" target="_blank">click here to open in a separate window</a>
  421. </div>
  422. <p></p>
  423. <p>I hope this example gave some ideas of how to implement a "look to select"
  424. type of Google Cardboard level UX. Sliding textures using texture coordinates
  425. offsets is also a commonly useful technique.</p>
  426. <p>Next up <a href="webxr-point-to-select.html">let's allow the user that has a VR controller to point at and move things</a>.</p>
  427. </div>
  428. </div>
  429. </div>
  430. <script src="../resources/prettify.js"></script>
  431. <script src="../resources/lesson.js"></script>
  432. </body></html>