You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					459 lines
				
				21 KiB
			
		
		
			
		
	
	
					459 lines
				
				21 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								<!DOCTYPE html><html lang="en"><head>
							 | 
						||
| 
								 | 
							
								    <meta charset="utf-8">
							 | 
						||
| 
								 | 
							
								    <title>VR - Look to Select</title>
							 | 
						||
| 
								 | 
							
								    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
							 | 
						||
| 
								 | 
							
								    <meta name="twitter:card" content="summary_large_image">
							 | 
						||
| 
								 | 
							
								    <meta name="twitter:site" content="@threejs">
							 | 
						||
| 
								 | 
							
								    <meta name="twitter:title" content="Three.js – VR - Look to Select">
							 | 
						||
| 
								 | 
							
								    <meta property="og:image" content="https://threejs.org/files/share.png">
							 | 
						||
| 
								 | 
							
								    <link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
							 | 
						||
| 
								 | 
							
								    <link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    <link rel="stylesheet" href="/manual/resources/lesson.css">
							 | 
						||
| 
								 | 
							
								    <link rel="stylesheet" href="/manual/resources/lang.css">
							 | 
						||
| 
								 | 
							
								<!-- Import maps polyfill -->
							 | 
						||
| 
								 | 
							
								<!-- Remove this when import maps will be widely supported -->
							 | 
						||
| 
								 | 
							
								<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<script type="importmap">
							 | 
						||
| 
								 | 
							
								{
							 | 
						||
| 
								 | 
							
								  "imports": {
							 | 
						||
| 
								 | 
							
								    "three": "../../build/three.module.js"
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								</script>
							 | 
						||
| 
								 | 
							
								  </head>
							 | 
						||
| 
								 | 
							
								  <body>
							 | 
						||
| 
								 | 
							
								    <div class="container">
							 | 
						||
| 
								 | 
							
								      <div class="lesson-title">
							 | 
						||
| 
								 | 
							
								        <h1>VR - Look to Select</h1>
							 | 
						||
| 
								 | 
							
								      </div>
							 | 
						||
| 
								 | 
							
								      <div class="lesson">
							 | 
						||
| 
								 | 
							
								        <div class="lesson-main">
							 | 
						||
| 
								 | 
							
								          <p><strong>NOTE: The examples on this page require a VR capable
							 | 
						||
| 
								 | 
							
								device. Without one they won't work. See <a href="webxr.html">previous article</a>
							 | 
						||
| 
								 | 
							
								as to why</strong></p>
							 | 
						||
| 
								 | 
							
								<p>In the <a href="webxr.html">previous article</a> we went over
							 | 
						||
| 
								 | 
							
								a very simple VR example using three.js and we discussed
							 | 
						||
| 
								 | 
							
								the various kinds of VR systems.</p>
							 | 
						||
| 
								 | 
							
								<p>The simplest and possibly most common is the Google Cardboard style of VR which
							 | 
						||
| 
								 | 
							
								is basically a phone put into a $5 - $50 face mask. This kind of VR has no
							 | 
						||
| 
								 | 
							
								controller so people have to come up with creative solutions for allowing user
							 | 
						||
| 
								 | 
							
								input.</p>
							 | 
						||
| 
								 | 
							
								<p>The most common solution is "look to select" where if the
							 | 
						||
| 
								 | 
							
								user points their head at something for a moment it gets
							 | 
						||
| 
								 | 
							
								selected.</p>
							 | 
						||
| 
								 | 
							
								<p>Let's implement "look to select"! We'll start with
							 | 
						||
| 
								 | 
							
								<a href="webxr.html">an example from the previous article</a>
							 | 
						||
| 
								 | 
							
								and to do it we'll add the <code class="notranslate" translate="no">PickHelper</code> we made in
							 | 
						||
| 
								 | 
							
								<a href="picking.html">the article on picking</a>. Here it is.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
							 | 
						||
| 
								 | 
							
								  constructor() {
							 | 
						||
| 
								 | 
							
								    this.raycaster = new THREE.Raycaster();
							 | 
						||
| 
								 | 
							
								    this.pickedObject = null;
							 | 
						||
| 
								 | 
							
								    this.pickedObjectSavedColor = 0;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  pick(normalizedPosition, scene, camera, time) {
							 | 
						||
| 
								 | 
							
								    // restore the color if there is a picked object
							 | 
						||
| 
								 | 
							
								    if (this.pickedObject) {
							 | 
						||
| 
								 | 
							
								      this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
							 | 
						||
| 
								 | 
							
								      this.pickedObject = undefined;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // cast a ray through the frustum
							 | 
						||
| 
								 | 
							
								    this.raycaster.setFromCamera(normalizedPosition, camera);
							 | 
						||
| 
								 | 
							
								    // get the list of objects the ray intersected
							 | 
						||
| 
								 | 
							
								    const intersectedObjects = this.raycaster.intersectObjects(scene.children);
							 | 
						||
| 
								 | 
							
								    if (intersectedObjects.length) {
							 | 
						||
| 
								 | 
							
								      // pick the first object. It's the closest one
							 | 
						||
| 
								 | 
							
								      this.pickedObject = intersectedObjects[0].object;
							 | 
						||
| 
								 | 
							
								      // save its color
							 | 
						||
| 
								 | 
							
								      this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
							 | 
						||
| 
								 | 
							
								      // set its emissive color to flashing red/yellow
							 | 
						||
| 
								 | 
							
								      this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>For an explanation of that code <a href="picking.html">see the article on picking</a>.</p>
							 | 
						||
| 
								 | 
							
								<p>To use it we just need to create an instance and call it in our render loop</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const pickHelper = new PickHelper();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								...
							 | 
						||
| 
								 | 
							
								function render(time) {
							 | 
						||
| 
								 | 
							
								  time *= 0.001;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  ...
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								+  // 0, 0 is the center of the view in normalized coordinates.
							 | 
						||
| 
								 | 
							
								+  pickHelper.pick({x: 0, y: 0}, scene, camera, time);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>In the original picking example we converted the mouse coordinates
							 | 
						||
| 
								 | 
							
								from CSS pixels into normalized coordinates that go from -1 to +1
							 | 
						||
| 
								 | 
							
								across the canvas.</p>
							 | 
						||
| 
								 | 
							
								<p>In this case though we will always pick where the camera is
							 | 
						||
| 
								 | 
							
								facing which is the center of the screen so we pass in <code class="notranslate" translate="no">0</code> for
							 | 
						||
| 
								 | 
							
								both <code class="notranslate" translate="no">x</code> and <code class="notranslate" translate="no">y</code> which is the center in normalized coordinates.</p>
							 | 
						||
| 
								 | 
							
								<p>And with that objects will flash when we look at them</p>
							 | 
						||
| 
								 | 
							
								<p></p><div translate="no" class="threejs_example_container notranslate">
							 | 
						||
| 
								 | 
							
								  <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>
							 | 
						||
| 
								 | 
							
								  <a class="threejs_center" href="/manual/examples/webxr-look-to-select.html" target="_blank">click here to open in a separate window</a>
							 | 
						||
| 
								 | 
							
								</div>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<p></p>
							 | 
						||
| 
								 | 
							
								<p>Typically we don't want selection to be immediate. Instead we require the user
							 | 
						||
| 
								 | 
							
								to keep the camera on the thing they want to select for a few moments to give them
							 | 
						||
| 
								 | 
							
								a chance not to select something by accident.</p>
							 | 
						||
| 
								 | 
							
								<p>To do that we need some kind of meter or gauge or some way
							 | 
						||
| 
								 | 
							
								to convey that the user must keep looking and for how long.</p>
							 | 
						||
| 
								 | 
							
								<p>One easy way we could do that is to make a 2 color texture
							 | 
						||
| 
								 | 
							
								and use a texture offset to slide the texture across a model.</p>
							 | 
						||
| 
								 | 
							
								<p>Let's do this by itself to see it work before we add it to
							 | 
						||
| 
								 | 
							
								the VR example.</p>
							 | 
						||
| 
								 | 
							
								<p>First we make an <a href="/docs/#api/en/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a></p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = -2;    // Use values for left
							 | 
						||
| 
								 | 
							
								const right = 2;    // right, top and bottom
							 | 
						||
| 
								 | 
							
								const top = 1;      // that match the default
							 | 
						||
| 
								 | 
							
								const bottom = -1;  // canvas size.
							 | 
						||
| 
								 | 
							
								const near = -1;
							 | 
						||
| 
								 | 
							
								const far = 1;
							 | 
						||
| 
								 | 
							
								const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>And of course update it if the canvas changes size</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
							 | 
						||
| 
								 | 
							
								  time *= 0.001;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (resizeRendererToDisplaySize(renderer)) {
							 | 
						||
| 
								 | 
							
								    const canvas = renderer.domElement;
							 | 
						||
| 
								 | 
							
								    const aspect = canvas.clientWidth / canvas.clientHeight;
							 | 
						||
| 
								 | 
							
								+    camera.left = -aspect;
							 | 
						||
| 
								 | 
							
								+    camera.right = aspect;
							 | 
						||
| 
								 | 
							
								    camera.updateProjectionMatrix();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  ...
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>We now have a camera that shows 2 units above and below the center and aspect units
							 | 
						||
| 
								 | 
							
								left and right.</p>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								which we've used a few <a href="indexed-textures.html">other</a> 
							 | 
						||
| 
								 | 
							
								<a href="post-processing-3dlut.html">places</a>.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeDataTexture(data, width, height) {
							 | 
						||
| 
								 | 
							
								  const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat);
							 | 
						||
| 
								 | 
							
								  texture.minFilter = THREE.NearestFilter;
							 | 
						||
| 
								 | 
							
								  texture.magFilter = THREE.NearestFilter;
							 | 
						||
| 
								 | 
							
								  texture.needsUpdate = true;
							 | 
						||
| 
								 | 
							
								  return texture;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const cursorColors = new Uint8Array([
							 | 
						||
| 
								 | 
							
								  64, 64, 64, 64,       // dark gray
							 | 
						||
| 
								 | 
							
								  255, 255, 255, 255,   // white
							 | 
						||
| 
								 | 
							
								]);
							 | 
						||
| 
								 | 
							
								const cursorTexture = makeDataTexture(cursorColors, 2, 1);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const ringRadius = 0.4;
							 | 
						||
| 
								 | 
							
								const tubeRadius = 0.1;
							 | 
						||
| 
								 | 
							
								const tubeSegments = 4;
							 | 
						||
| 
								 | 
							
								const ringSegments = 64;
							 | 
						||
| 
								 | 
							
								const cursorGeometry = new THREE.TorusGeometry(
							 | 
						||
| 
								 | 
							
								    ringRadius, tubeRadius, tubeSegments, ringSegments);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const cursorMaterial = new THREE.MeshBasicMaterial({
							 | 
						||
| 
								 | 
							
								  color: 'white',
							 | 
						||
| 
								 | 
							
								  map: cursorTexture,
							 | 
						||
| 
								 | 
							
								  transparent: true,
							 | 
						||
| 
								 | 
							
								  blending: THREE.CustomBlending,
							 | 
						||
| 
								 | 
							
								  blendSrc: THREE.OneMinusDstColorFactor,
							 | 
						||
| 
								 | 
							
								  blendDst: THREE.OneMinusSrcColorFactor,
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
							 | 
						||
| 
								 | 
							
								scene.add(cursor);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>and then in <code class="notranslate" translate="no">render</code> lets adjust the texture's offset</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
							 | 
						||
| 
								 | 
							
								  time *= 0.001;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (resizeRendererToDisplaySize(renderer)) {
							 | 
						||
| 
								 | 
							
								    const canvas = renderer.domElement;
							 | 
						||
| 
								 | 
							
								    const aspect = canvas.clientWidth / canvas.clientHeight;
							 | 
						||
| 
								 | 
							
								    camera.left = -aspect;
							 | 
						||
| 
								 | 
							
								    camera.right = aspect;
							 | 
						||
| 
								 | 
							
								    camera.updateProjectionMatrix();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								+  const fromStart = 0;
							 | 
						||
| 
								 | 
							
								+  const fromEnd = 2;
							 | 
						||
| 
								 | 
							
								+  const toStart = -0.5;
							 | 
						||
| 
								 | 
							
								+  const toEnd = 0.5;
							 | 
						||
| 
								 | 
							
								+  cursorTexture.offset.x = THREE.MathUtils.mapLinear(
							 | 
						||
| 
								 | 
							
								+      time % 2,
							 | 
						||
| 
								 | 
							
								+      fromStart, fromEnd,
							 | 
						||
| 
								 | 
							
								+      toStart, toEnd);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  renderer.render(scene, camera);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								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
							 | 
						||
| 
								 | 
							
								taking <code class="notranslate" translate="no">time % 2</code> which means a value that goes from 0 to 2 and maps
							 | 
						||
| 
								 | 
							
								that to a value that goes from -0.5 to 0.5</p>
							 | 
						||
| 
								 | 
							
								<p><a href="textures.html">Textures</a> are mapped to geometry using normalized texture coordinates
							 | 
						||
| 
								 | 
							
								that go from 0 to 1. That means our 2x1 pixel image, set to the default
							 | 
						||
| 
								 | 
							
								wrapping mode of <code class="notranslate" translate="no">THREE.ClampToEdge</code>, if we adjust the
							 | 
						||
| 
								 | 
							
								texture coordinates by -0.5 then the entire mesh will be the first color
							 | 
						||
| 
								 | 
							
								and if we adjust the texture coordinates by +0.5 the entire mesh will
							 | 
						||
| 
								 | 
							
								be the second color. In between with the filtering set to <code class="notranslate" translate="no">THREE.NearestFilter</code>
							 | 
						||
| 
								 | 
							
								we'll be able to move the transition between the 2 colors through the geometry.</p>
							 | 
						||
| 
								 | 
							
								<p>Let's add a background texture while we're at it just like we
							 | 
						||
| 
								 | 
							
								covered in <a href="backgrounds.html">the article on backgrounds</a>.
							 | 
						||
| 
								 | 
							
								We'll just use a 2x2 set of colors but set the texture's repeat
							 | 
						||
| 
								 | 
							
								settings to give us an 8x8 grid. This will give our cursor something
							 | 
						||
| 
								 | 
							
								to be rendered over so we can check it against different colors.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const backgroundColors = new Uint8Array([
							 | 
						||
| 
								 | 
							
								+    0,   0,   0, 255,  // black
							 | 
						||
| 
								 | 
							
								+   90,  38,  38, 255,  // dark red
							 | 
						||
| 
								 | 
							
								+  100, 175, 103, 255,  // medium green
							 | 
						||
| 
								 | 
							
								+  255, 239, 151, 255,  // light yellow
							 | 
						||
| 
								 | 
							
								+]);
							 | 
						||
| 
								 | 
							
								+const backgroundTexture = makeDataTexture(backgroundColors, 2, 2);
							 | 
						||
| 
								 | 
							
								+backgroundTexture.wrapS = THREE.RepeatWrapping;
							 | 
						||
| 
								 | 
							
								+backgroundTexture.wrapT = THREE.RepeatWrapping;
							 | 
						||
| 
								 | 
							
								+backgroundTexture.repeat.set(4, 4);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const scene = new THREE.Scene();
							 | 
						||
| 
								 | 
							
								+scene.background = backgroundTexture;
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>Now if we run that you'll see we get a circle like gauge
							 | 
						||
| 
								 | 
							
								and that we can set where the gauge is.</p>
							 | 
						||
| 
								 | 
							
								<p></p><div translate="no" class="threejs_example_container notranslate">
							 | 
						||
| 
								 | 
							
								  <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>
							 | 
						||
| 
								 | 
							
								  <a class="threejs_center" href="/manual/examples/webxr-look-to-select-selector.html" target="_blank">click here to open in a separate window</a>
							 | 
						||
| 
								 | 
							
								</div>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<p></p>
							 | 
						||
| 
								 | 
							
								<p>A few things to notice <strong>and try</strong>.</p>
							 | 
						||
| 
								 | 
							
								<ul>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								properties as follows</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate notranslate" translate="no">  blending: THREE.CustomBlending,
							 | 
						||
| 
								 | 
							
								  blendSrc: THREE.OneMinusDstColorFactor,
							 | 
						||
| 
								 | 
							
								  blendDst: THREE.OneMinusSrcColorFactor,
							 | 
						||
| 
								 | 
							
								</pre><p>This gives as an <em>inverse</em> type of effect. Comment out
							 | 
						||
| 
								 | 
							
								those 3 lines and you'll see the difference. I'm just guessing
							 | 
						||
| 
								 | 
							
								the inverse effect is best here as that way we can hopefully
							 | 
						||
| 
								 | 
							
								see the cursor regardless of the colors it is over.</p>
							 | 
						||
| 
								 | 
							
								</li>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								<p>For whatever reason the <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> uses a flat
							 | 
						||
| 
								 | 
							
								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>
							 | 
						||
| 
								 | 
							
								the texture slides horizontally across the ring instead of
							 | 
						||
| 
								 | 
							
								around it like it does above.</p>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								(it's just commented out in the example above) and you'll see what I
							 | 
						||
| 
								 | 
							
								mean.</p>
							 | 
						||
| 
								 | 
							
								<p>The <em>proper</em> thing to do (for some definition of <em>proper</em>) would be
							 | 
						||
| 
								 | 
							
								to either use the <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> but fix the texture coordinates
							 | 
						||
| 
								 | 
							
								so they go around the ring. Or else, generate our own ring geometry.
							 | 
						||
| 
								 | 
							
								But, the torus works just fine. Placed directly in front of the camera
							 | 
						||
| 
								 | 
							
								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
							 | 
						||
| 
								 | 
							
								texture coordinates go around the ring so it works for our needs.</p>
							 | 
						||
| 
								 | 
							
								</li>
							 | 
						||
| 
								 | 
							
								</ul>
							 | 
						||
| 
								 | 
							
								<p>Let's integrate it with our VR code above. </p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
							 | 
						||
| 
								 | 
							
								-  constructor() {
							 | 
						||
| 
								 | 
							
								+  constructor(camera) {
							 | 
						||
| 
								 | 
							
								    this.raycaster = new THREE.Raycaster();
							 | 
						||
| 
								 | 
							
								    this.pickedObject = null;
							 | 
						||
| 
								 | 
							
								-    this.pickedObjectSavedColor = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								+    const cursorColors = new Uint8Array([
							 | 
						||
| 
								 | 
							
								+      64, 64, 64, 64,       // dark gray
							 | 
						||
| 
								 | 
							
								+      255, 255, 255, 255,   // white
							 | 
						||
| 
								 | 
							
								+    ]);
							 | 
						||
| 
								 | 
							
								+    this.cursorTexture = makeDataTexture(cursorColors, 2, 1);
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    const ringRadius = 0.4;
							 | 
						||
| 
								 | 
							
								+    const tubeRadius = 0.1;
							 | 
						||
| 
								 | 
							
								+    const tubeSegments = 4;
							 | 
						||
| 
								 | 
							
								+    const ringSegments = 64;
							 | 
						||
| 
								 | 
							
								+    const cursorGeometry = new THREE.TorusGeometry(
							 | 
						||
| 
								 | 
							
								+        ringRadius, tubeRadius, tubeSegments, ringSegments);
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    const cursorMaterial = new THREE.MeshBasicMaterial({
							 | 
						||
| 
								 | 
							
								+      color: 'white',
							 | 
						||
| 
								 | 
							
								+      map: this.cursorTexture,
							 | 
						||
| 
								 | 
							
								+      transparent: true,
							 | 
						||
| 
								 | 
							
								+      blending: THREE.CustomBlending,
							 | 
						||
| 
								 | 
							
								+      blendSrc: THREE.OneMinusDstColorFactor,
							 | 
						||
| 
								 | 
							
								+      blendDst: THREE.OneMinusSrcColorFactor,
							 | 
						||
| 
								 | 
							
								+    });
							 | 
						||
| 
								 | 
							
								+    const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
							 | 
						||
| 
								 | 
							
								+    // add the cursor as a child of the camera
							 | 
						||
| 
								 | 
							
								+    camera.add(cursor);
							 | 
						||
| 
								 | 
							
								+    // and move it in front of the camera
							 | 
						||
| 
								 | 
							
								+    cursor.position.z = -1;
							 | 
						||
| 
								 | 
							
								+    const scale = 0.05;
							 | 
						||
| 
								 | 
							
								+    cursor.scale.set(scale, scale, scale);
							 | 
						||
| 
								 | 
							
								+    this.cursor = cursor;
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    this.selectTimer = 0;
							 | 
						||
| 
								 | 
							
								+    this.selectDuration = 2;
							 | 
						||
| 
								 | 
							
								+    this.lastTime = 0;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  pick(normalizedPosition, scene, camera, time) {
							 | 
						||
| 
								 | 
							
								+    const elapsedTime = time - this.lastTime;
							 | 
						||
| 
								 | 
							
								+    this.lastTime = time;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								-    // restore the color if there is a picked object
							 | 
						||
| 
								 | 
							
								-    if (this.pickedObject) {
							 | 
						||
| 
								 | 
							
								-      this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
							 | 
						||
| 
								 | 
							
								-      this.pickedObject = undefined;
							 | 
						||
| 
								 | 
							
								-    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								+    const lastPickedObject = this.pickedObject;
							 | 
						||
| 
								 | 
							
								+    this.pickedObject = undefined;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // cast a ray through the frustum
							 | 
						||
| 
								 | 
							
								    this.raycaster.setFromCamera(normalizedPosition, camera);
							 | 
						||
| 
								 | 
							
								    // get the list of objects the ray intersected
							 | 
						||
| 
								 | 
							
								    const intersectedObjects = this.raycaster.intersectObjects(scene.children);
							 | 
						||
| 
								 | 
							
								    if (intersectedObjects.length) {
							 | 
						||
| 
								 | 
							
								      // pick the first object. It's the closest one
							 | 
						||
| 
								 | 
							
								      this.pickedObject = intersectedObjects[0].object;
							 | 
						||
| 
								 | 
							
								-      // save its color
							 | 
						||
| 
								 | 
							
								-      this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
							 | 
						||
| 
								 | 
							
								-      // set its emissive color to flashing red/yellow
							 | 
						||
| 
								 | 
							
								-      this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								+    // show the cursor only if it's hitting something
							 | 
						||
| 
								 | 
							
								+    this.cursor.visible = this.pickedObject ? true : false;
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    let selected = false;
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    // if we're looking at the same object as before
							 | 
						||
| 
								 | 
							
								+    // increment time select timer
							 | 
						||
| 
								 | 
							
								+    if (this.pickedObject && lastPickedObject === this.pickedObject) {
							 | 
						||
| 
								 | 
							
								+      this.selectTimer += elapsedTime;
							 | 
						||
| 
								 | 
							
								+      if (this.selectTimer >= this.selectDuration) {
							 | 
						||
| 
								 | 
							
								+        this.selectTimer = 0;
							 | 
						||
| 
								 | 
							
								+        selected = true;
							 | 
						||
| 
								 | 
							
								+      }
							 | 
						||
| 
								 | 
							
								+    } else {
							 | 
						||
| 
								 | 
							
								+      this.selectTimer = 0;
							 | 
						||
| 
								 | 
							
								+    }
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    // set cursor material to show the timer state
							 | 
						||
| 
								 | 
							
								+    const fromStart = 0;
							 | 
						||
| 
								 | 
							
								+    const fromEnd = this.selectDuration;
							 | 
						||
| 
								 | 
							
								+    const toStart = -0.5;
							 | 
						||
| 
								 | 
							
								+    const toEnd = 0.5;
							 | 
						||
| 
								 | 
							
								+    this.cursorTexture.offset.x = THREE.MathUtils.mapLinear(
							 | 
						||
| 
								 | 
							
								+        this.selectTimer,
							 | 
						||
| 
								 | 
							
								+        fromStart, fromEnd,
							 | 
						||
| 
								 | 
							
								+        toStart, toEnd);
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+    return selected ? this.pickedObject : undefined;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>You can see the code above we added all the code to create
							 | 
						||
| 
								 | 
							
								the cursor geometry, texture, and material and we added it
							 | 
						||
| 
								 | 
							
								as a child of the camera so it will always be in front of
							 | 
						||
| 
								 | 
							
								the camera. Note we need to add the camera to the scene
							 | 
						||
| 
								 | 
							
								otherwise the cursor won't be rendered.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+scene.add(camera);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>We then check if the thing we're picking this time is the same as it was last
							 | 
						||
| 
								 | 
							
								time. If so we add the elapsed time to a timer and if the timer reaches its
							 | 
						||
| 
								 | 
							
								limit we return the selected item.</p>
							 | 
						||
| 
								 | 
							
								<p>Now let's use that to pick the cubes. As a simple example
							 | 
						||
| 
								 | 
							
								we'll add 3 spheres as well. When a cube is picked with hide
							 | 
						||
| 
								 | 
							
								the cube and un-hide the corresponding sphere.</p>
							 | 
						||
| 
								 | 
							
								<p>So first we'll make a sphere geometry</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
							 | 
						||
| 
								 | 
							
								const boxHeight = 1;
							 | 
						||
| 
								 | 
							
								const boxDepth = 1;
							 | 
						||
| 
								 | 
							
								-const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
							 | 
						||
| 
								 | 
							
								+const boxGeometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
							 | 
						||
| 
								 | 
							
								+
							 | 
						||
| 
								 | 
							
								+const sphereRadius = 0.5;
							 | 
						||
| 
								 | 
							
								+const sphereGeometry = new THREE.SphereGeometry(sphereRadius);
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>Then let's create 3 pairs of box and sphere meshes. We'll
							 | 
						||
| 
								 | 
							
								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>
							 | 
						||
| 
								 | 
							
								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>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const cubes = [
							 | 
						||
| 
								 | 
							
								-  makeInstance(geometry, 0x44aa88,  0),
							 | 
						||
| 
								 | 
							
								-  makeInstance(geometry, 0x8844aa, -2),
							 | 
						||
| 
								 | 
							
								-  makeInstance(geometry, 0xaa8844,  2),
							 | 
						||
| 
								 | 
							
								-];
							 | 
						||
| 
								 | 
							
								+const meshToMeshMap = new Map();
							 | 
						||
| 
								 | 
							
								+[
							 | 
						||
| 
								 | 
							
								+  { x:  0, boxColor: 0x44aa88, sphereColor: 0xFF4444, },
							 | 
						||
| 
								 | 
							
								+  { x:  2, boxColor: 0x8844aa, sphereColor: 0x44FF44, },
							 | 
						||
| 
								 | 
							
								+  { x: -2, boxColor: 0xaa8844, sphereColor: 0x4444FF, },
							 | 
						||
| 
								 | 
							
								+].forEach((info) => {
							 | 
						||
| 
								 | 
							
								+  const {x, boxColor, sphereColor} = info;
							 | 
						||
| 
								 | 
							
								+  const sphere = makeInstance(sphereGeometry, sphereColor, x);
							 | 
						||
| 
								 | 
							
								+  const box = makeInstance(boxGeometry, boxColor, x);
							 | 
						||
| 
								 | 
							
								+  // hide the sphere
							 | 
						||
| 
								 | 
							
								+  sphere.visible = false;
							 | 
						||
| 
								 | 
							
								+  // map the sphere to the box
							 | 
						||
| 
								 | 
							
								+  meshToMeshMap.set(box, sphere);
							 | 
						||
| 
								 | 
							
								+  // map the box to the sphere
							 | 
						||
| 
								 | 
							
								+  meshToMeshMap.set(sphere, box);
							 | 
						||
| 
								 | 
							
								+});
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								instead of <code class="notranslate" translate="no">cubes</code>.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-cubes.forEach((cube, ndx) => {
							 | 
						||
| 
								 | 
							
								+let ndx = 0;
							 | 
						||
| 
								 | 
							
								+for (const mesh of meshToMeshMap.keys()) {
							 | 
						||
| 
								 | 
							
								  const speed = 1 + ndx * .1;
							 | 
						||
| 
								 | 
							
								  const rot = time * speed;
							 | 
						||
| 
								 | 
							
								-  cube.rotation.x = rot;
							 | 
						||
| 
								 | 
							
								-  cube.rotation.y = rot;
							 | 
						||
| 
								 | 
							
								-});
							 | 
						||
| 
								 | 
							
								+  mesh.rotation.x = rot;
							 | 
						||
| 
								 | 
							
								+  mesh.rotation.y = rot;
							 | 
						||
| 
								 | 
							
								+  ++ndx;
							 | 
						||
| 
								 | 
							
								+}
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>And now we can use our new <code class="notranslate" translate="no">PickHelper</code> implementation
							 | 
						||
| 
								 | 
							
								to select one of the objects. When selected we hide
							 | 
						||
| 
								 | 
							
								that object and un-hide its partner.</p>
							 | 
						||
| 
								 | 
							
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 0, 0 is the center of the view in normalized coordinates.
							 | 
						||
| 
								 | 
							
								-pickHelper.pick({x: 0, y: 0}, scene, camera, time);
							 | 
						||
| 
								 | 
							
								+const selectedObject = pickHelper.pick({x: 0, y: 0}, scene, camera, time);
							 | 
						||
| 
								 | 
							
								+if (selectedObject) {
							 | 
						||
| 
								 | 
							
								+  selectedObject.visible = false;
							 | 
						||
| 
								 | 
							
								+  const partnerObject = meshToMeshMap.get(selectedObject);
							 | 
						||
| 
								 | 
							
								+  partnerObject.visible = true;
							 | 
						||
| 
								 | 
							
								+}
							 | 
						||
| 
								 | 
							
								</pre>
							 | 
						||
| 
								 | 
							
								<p>And with that we should have a pretty decent <em>look to select</em> implementation.</p>
							 | 
						||
| 
								 | 
							
								<p></p><div translate="no" class="threejs_example_container notranslate">
							 | 
						||
| 
								 | 
							
								  <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>
							 | 
						||
| 
								 | 
							
								  <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>
							 | 
						||
| 
								 | 
							
								</div>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<p></p>
							 | 
						||
| 
								 | 
							
								<p>I hope this example gave some ideas of how to implement a "look to select"
							 | 
						||
| 
								 | 
							
								type of Google Cardboard level UX. Sliding textures using texture coordinates
							 | 
						||
| 
								 | 
							
								offsets is also a commonly useful technique.</p>
							 | 
						||
| 
								 | 
							
								<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>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        </div>
							 | 
						||
| 
								 | 
							
								      </div>
							 | 
						||
| 
								 | 
							
								    </div>
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  <script src="/manual/resources/prettify.js"></script>
							 | 
						||
| 
								 | 
							
								  <script src="/manual/resources/lesson.js"></script>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								</body></html>
							 |