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.
		
		
		
		
		
			
		
			
				
					
					
						
							730 lines
						
					
					
						
							32 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							730 lines
						
					
					
						
							32 KiB
						
					
					
				
								<!DOCTYPE html><html lang="en"><head>
							 | 
						|
								    <meta charset="utf-8">
							 | 
						|
								    <title>Aligning HTML Elements to 3D</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 – Aligning HTML Elements to 3D">
							 | 
						|
								    <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>Aligning HTML Elements to 3D</h1>
							 | 
						|
								      </div>
							 | 
						|
								      <div class="lesson">
							 | 
						|
								        <div class="lesson-main">
							 | 
						|
								          <p>This article is part of a series of articles about three.js. The first article
							 | 
						|
								is <a href="fundamentals.html">three.js fundamentals</a>. If you haven't read that
							 | 
						|
								yet and you're new to three.js you might want to consider starting there. </p>
							 | 
						|
								<p>Sometimes you'd like to display some text in your 3D scene. You have many options
							 | 
						|
								each with pluses and minuses.</p>
							 | 
						|
								<ul>
							 | 
						|
								<li><p>Use 3D text</p>
							 | 
						|
								<p>If you look at the <a href="primitives.html">primitives article</a> you'll see <a href="/docs/#api/en/geometries/TextGeometry"><code class="notranslate" translate="no">TextGeometry</code></a> which
							 | 
						|
								makes 3D text. This might be useful for flying logos but probably not so useful for stats, info,
							 | 
						|
								or labelling lots of things.</p>
							 | 
						|
								</li>
							 | 
						|
								<li><p>Use a texture with 2D text drawn into it.</p>
							 | 
						|
								<p>The article on <a href="canvas-textures.html">using a Canvas as a texture</a> shows using
							 | 
						|
								a canvas as a texture. You can draw text into a canvas and <a href="billboards.html">display it as a billboard</a>.
							 | 
						|
								The plus here might be that the text is integrated into the 3D scene. For something like a computer terminal
							 | 
						|
								shown in a 3D scene this might be perfect.</p>
							 | 
						|
								</li>
							 | 
						|
								<li><p>Use HTML Elements and position them to match the 3D</p>
							 | 
						|
								<p>The benefits to this approach is you can use all of HTML. Your HTML can have multiple elements. It can
							 | 
						|
								by styled with CSS. It can also be selected by the user as it is actual text. </p>
							 | 
						|
								</li>
							 | 
						|
								</ul>
							 | 
						|
								<p>This article will cover this last approach.</p>
							 | 
						|
								<p>Let's start simple. We'll make a 3D scene with a few primitives and then add a label to each primitive. We'll start
							 | 
						|
								with an example from <a href="responsive.html">the article on responsive pages</a> </p>
							 | 
						|
								<p>We'll add some <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> like we did in <a href="lights.html">the article on lighting</a>.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
							 | 
						|
								+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
							 | 
						|
								</pre>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const controls = new OrbitControls(camera, canvas);
							 | 
						|
								controls.target.set(0, 0, 0);
							 | 
						|
								controls.update();
							 | 
						|
								</pre>
							 | 
						|
								<p>We need to provide an HTML element to contain our label elements</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
							 | 
						|
								-  <canvas id="c"></canvas>
							 | 
						|
								+  <div id="container">
							 | 
						|
								+    <canvas id="c"></canvas>
							 | 
						|
								+    <div id="labels"></div>
							 | 
						|
								+  </div>
							 | 
						|
								</body>
							 | 
						|
								</pre>
							 | 
						|
								<p>By putting both the canvas and the <code class="notranslate" translate="no"><div id="labels"></code> inside a
							 | 
						|
								parent container we can make them overlap with this CSS</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
							 | 
						|
								-    width: 100%;
							 | 
						|
								-    height: 100%;
							 | 
						|
								+    width: 100%;  /* let our container decide our size */
							 | 
						|
								+    height: 100%;
							 | 
						|
								    display: block;
							 | 
						|
								}
							 | 
						|
								+#container {
							 | 
						|
								+  position: relative;  /* makes this the origin of its children */
							 | 
						|
								+  width: 100%;
							 | 
						|
								+  height: 100%;
							 | 
						|
								+  overflow: hidden;
							 | 
						|
								+}
							 | 
						|
								+#labels {
							 | 
						|
								+  position: absolute;  /* let us position ourself inside the container */
							 | 
						|
								+  left: 0;             /* make our position the top left of the container */
							 | 
						|
								+  top: 0;
							 | 
						|
								+  color: white;
							 | 
						|
								+}
							 | 
						|
								</pre>
							 | 
						|
								<p>let's also add some CSS for the labels themselves</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels>div {
							 | 
						|
								  position: absolute;  /* let us position them inside the container */
							 | 
						|
								  left: 0;             /* make their default position the top left of the container */
							 | 
						|
								  top: 0;
							 | 
						|
								  cursor: pointer;     /* change the cursor to a hand when over us */
							 | 
						|
								  font-size: large;
							 | 
						|
								  user-select: none;   /* don't let the text get selected */
							 | 
						|
								  text-shadow:         /* create a black outline */
							 | 
						|
								    -1px -1px 0 #000,
							 | 
						|
								     0   -1px 0 #000,
							 | 
						|
								     1px -1px 0 #000,
							 | 
						|
								     1px  0   0 #000,
							 | 
						|
								     1px  1px 0 #000,
							 | 
						|
								     0    1px 0 #000,
							 | 
						|
								    -1px  1px 0 #000,
							 | 
						|
								    -1px  0   0 #000;
							 | 
						|
								}
							 | 
						|
								#labels>div:hover {
							 | 
						|
								  color: red;
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Now into our code we don't have to add too much. We had a function
							 | 
						|
								<code class="notranslate" translate="no">makeInstance</code> that we used to generate cubes. Let's make it
							 | 
						|
								so it also adds a label element.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const labelContainerElem = document.querySelector('#labels');
							 | 
						|
								
							 | 
						|
								-function makeInstance(geometry, color, x) {
							 | 
						|
								+function makeInstance(geometry, color, x, name) {
							 | 
						|
								  const material = new THREE.MeshPhongMaterial({color});
							 | 
						|
								
							 | 
						|
								  const cube = new THREE.Mesh(geometry, material);
							 | 
						|
								  scene.add(cube);
							 | 
						|
								
							 | 
						|
								  cube.position.x = x;
							 | 
						|
								
							 | 
						|
								+  const elem = document.createElement('div');
							 | 
						|
								+  elem.textContent = name;
							 | 
						|
								+  labelContainerElem.appendChild(elem);
							 | 
						|
								
							 | 
						|
								-  return cube;
							 | 
						|
								+  return {cube, elem};
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>As you can see we're adding a <code class="notranslate" translate="no"><div></code> to the container, one for each cube. We're
							 | 
						|
								also returning an object with both the <code class="notranslate" translate="no">cube</code> and the <code class="notranslate" translate="no">elem</code> for the label.</p>
							 | 
						|
								<p>Calling it we need to provide a name for each</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [
							 | 
						|
								-  makeInstance(geometry, 0x44aa88,  0),
							 | 
						|
								-  makeInstance(geometry, 0x8844aa, -2),
							 | 
						|
								-  makeInstance(geometry, 0xaa8844,  2),
							 | 
						|
								+  makeInstance(geometry, 0x44aa88,  0, 'Aqua'),
							 | 
						|
								+  makeInstance(geometry, 0x8844aa, -2, 'Purple'),
							 | 
						|
								+  makeInstance(geometry, 0xaa8844,  2, 'Gold'),
							 | 
						|
								];
							 | 
						|
								</pre>
							 | 
						|
								<p>What remains is positioning the label elements at render time</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
							 | 
						|
								
							 | 
						|
								...
							 | 
						|
								
							 | 
						|
								-cubes.forEach((cube, ndx) => {
							 | 
						|
								+cubes.forEach((cubeInfo, ndx) => {
							 | 
						|
								+  const {cube, elem} = cubeInfo;
							 | 
						|
								  const speed = 1 + ndx * .1;
							 | 
						|
								  const rot = time * speed;
							 | 
						|
								  cube.rotation.x = rot;
							 | 
						|
								  cube.rotation.y = rot;
							 | 
						|
								
							 | 
						|
								+  // get the position of the center of the cube
							 | 
						|
								+  cube.updateWorldMatrix(true, false);
							 | 
						|
								+  cube.getWorldPosition(tempV);
							 | 
						|
								+
							 | 
						|
								+  // get the normalized screen coordinate of that position
							 | 
						|
								+  // x and y will be in the -1 to +1 range with x = -1 being
							 | 
						|
								+  // on the left and y = -1 being on the bottom
							 | 
						|
								+  tempV.project(camera);
							 | 
						|
								+
							 | 
						|
								+  // convert the normalized position to CSS coordinates
							 | 
						|
								+  const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
							 | 
						|
								+  const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
							 | 
						|
								+
							 | 
						|
								+  // move the elem to that position
							 | 
						|
								+  elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
							 | 
						|
								});
							 | 
						|
								</pre>
							 | 
						|
								<p>And with that we have labels aligned to their corresponding objects.</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/align-html-to-3d.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/align-html-to-3d.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>There are a couple of issues we probably want to deal with.</p>
							 | 
						|
								<p>One is that if we rotate the objects so they overlap all the labels
							 | 
						|
								overlap as well.</p>
							 | 
						|
								<div class="threejs_center"><img src="../resources/images/overlapping-labels.png" style="width: 307px;"></div>
							 | 
						|
								
							 | 
						|
								<p>Another is that if we zoom way out so that the objects go outside
							 | 
						|
								the frustum the labels will still appear.</p>
							 | 
						|
								<p>A possible solution to the problem of overlapping objects is to use
							 | 
						|
								the <a href="picking.html">picking code from the article on picking</a>.
							 | 
						|
								We'll pass in the position of the object on the screen and then
							 | 
						|
								ask the <code class="notranslate" translate="no">RayCaster</code> to tell us which objects were intersected.
							 | 
						|
								If our object is not the first one then we are not in the front.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
							 | 
						|
								+const raycaster = new THREE.Raycaster();
							 | 
						|
								
							 | 
						|
								...
							 | 
						|
								
							 | 
						|
								cubes.forEach((cubeInfo, ndx) => {
							 | 
						|
								  const {cube, elem} = cubeInfo;
							 | 
						|
								  const speed = 1 + ndx * .1;
							 | 
						|
								  const rot = time * speed;
							 | 
						|
								  cube.rotation.x = rot;
							 | 
						|
								  cube.rotation.y = rot;
							 | 
						|
								
							 | 
						|
								  // get the position of the center of the cube
							 | 
						|
								  cube.updateWorldMatrix(true, false);
							 | 
						|
								  cube.getWorldPosition(tempV);
							 | 
						|
								
							 | 
						|
								  // get the normalized screen coordinate of that position
							 | 
						|
								  // x and y will be in the -1 to +1 range with x = -1 being
							 | 
						|
								  // on the left and y = -1 being on the bottom
							 | 
						|
								  tempV.project(camera);
							 | 
						|
								
							 | 
						|
								+  // ask the raycaster for all the objects that intersect
							 | 
						|
								+  // from the eye toward this object's position
							 | 
						|
								+  raycaster.setFromCamera(tempV, camera);
							 | 
						|
								+  const intersectedObjects = raycaster.intersectObjects(scene.children);
							 | 
						|
								+  // We're visible if the first intersection is this object.
							 | 
						|
								+  const show = intersectedObjects.length && cube === intersectedObjects[0].object;
							 | 
						|
								+
							 | 
						|
								+  if (!show) {
							 | 
						|
								+    // hide the label
							 | 
						|
								+    elem.style.display = 'none';
							 | 
						|
								+  } else {
							 | 
						|
								+    // un-hide the label
							 | 
						|
								+    elem.style.display = '';
							 | 
						|
								
							 | 
						|
								    // convert the normalized position to CSS coordinates
							 | 
						|
								    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
							 | 
						|
								    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
							 | 
						|
								
							 | 
						|
								    // move the elem to that position
							 | 
						|
								    elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
							 | 
						|
								+  }
							 | 
						|
								});
							 | 
						|
								</pre>
							 | 
						|
								<p>This handles overlapping.</p>
							 | 
						|
								<p>To handle going outside the frustum we can add this check if the origin of
							 | 
						|
								the object is outside the frustum by checking <code class="notranslate" translate="no">tempV.z</code></p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-  if (!show) {
							 | 
						|
								+  if (!show || Math.abs(tempV.z) > 1) {
							 | 
						|
								    // hide the label
							 | 
						|
								    elem.style.display = 'none';
							 | 
						|
								</pre>
							 | 
						|
								<p>This <em>kind of</em> works because the normalized coordinates we computed include a <code class="notranslate" translate="no">z</code>
							 | 
						|
								value that goes from -1 when at the <code class="notranslate" translate="no">near</code> part of our camera frustum to +1 when
							 | 
						|
								at the <code class="notranslate" translate="no">far</code> part of our camera frustum.</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/align-html-to-3d-w-hiding.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-hiding.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>For the frustum check, the solution above fails as we're only checking the origin of the object. For a large
							 | 
						|
								object. That origin might go outside the frustum but half of the object might still be in the frustum.</p>
							 | 
						|
								<p>A more correct solution would be to check if the object itself is in the frustum
							 | 
						|
								or not. Unfortunate that check is slow. For 3 cubes it will not be a problem
							 | 
						|
								but for many objects it might be.</p>
							 | 
						|
								<p>Three.js provides some functions to check if an object's bounding sphere is
							 | 
						|
								in a frustum</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// at init time
							 | 
						|
								const frustum = new THREE.Frustum();
							 | 
						|
								const viewProjection = new THREE.Matrix4();
							 | 
						|
								
							 | 
						|
								...
							 | 
						|
								
							 | 
						|
								// before checking
							 | 
						|
								camera.updateMatrix();
							 | 
						|
								camera.updateMatrixWorld();
							 | 
						|
								camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
							 | 
						|
								
							 | 
						|
								...
							 | 
						|
								
							 | 
						|
								// then for each mesh
							 | 
						|
								someMesh.updateMatrix();
							 | 
						|
								someMesh.updateMatrixWorld();
							 | 
						|
								
							 | 
						|
								viewProjection.multiplyMatrices(
							 | 
						|
								    camera.projectionMatrix, camera.matrixWorldInverse);
							 | 
						|
								frustum.setFromProjectionMatrix(viewProjection);
							 | 
						|
								const inFrustum = frustum.contains(someMesh));
							 | 
						|
								</pre>
							 | 
						|
								<p>Our current overlapping solution has similar issues. Picking is slow. We could
							 | 
						|
								use gpu based picking like we covered in the <a href="picking.html">picking
							 | 
						|
								article</a> but that is also not free. Which solution you
							 | 
						|
								chose depends on your needs.</p>
							 | 
						|
								<p>Another issue is the order the labels appear. If we change the code to have
							 | 
						|
								longer labels</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [
							 | 
						|
								-  makeInstance(geometry, 0x44aa88,  0, 'Aqua'),
							 | 
						|
								-  makeInstance(geometry, 0x8844aa, -2, 'Purple'),
							 | 
						|
								-  makeInstance(geometry, 0xaa8844,  2, 'Gold'),
							 | 
						|
								+  makeInstance(geometry, 0x44aa88,  0, 'Aqua Colored Box'),
							 | 
						|
								+  makeInstance(geometry, 0x8844aa, -2, 'Purple Colored Box'),
							 | 
						|
								+  makeInstance(geometry, 0xaa8844,  2, 'Gold Colored Box'),
							 | 
						|
								];
							 | 
						|
								</pre>
							 | 
						|
								<p>and set the CSS so these don't wrap</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels>div {
							 | 
						|
								+  white-space: nowrap;
							 | 
						|
								</pre>
							 | 
						|
								<p>Then we can run into this issue</p>
							 | 
						|
								<div class="threejs_center"><img src="../resources/images/label-sorting-issue.png" style="width: 401px;"></div>
							 | 
						|
								
							 | 
						|
								<p>You can see above the purple box is in the back but its label is in front of the aqua box.</p>
							 | 
						|
								<p>We can fix this by setting the <code class="notranslate" translate="no">zIndex</code> of each element. The projected position has a <code class="notranslate" translate="no">z</code> value
							 | 
						|
								that goes from -1 in front to positive 1 in back. <code class="notranslate" translate="no">zIndex</code> is required to be an integer and goes the
							 | 
						|
								opposite direction meaning for <code class="notranslate" translate="no">zIndex</code> greater values are in front so the following code should work.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// convert the normalized position to CSS coordinates
							 | 
						|
								const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
							 | 
						|
								const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
							 | 
						|
								
							 | 
						|
								// move the elem to that position
							 | 
						|
								elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
							 | 
						|
								
							 | 
						|
								+// set the zIndex for sorting
							 | 
						|
								+elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
							 | 
						|
								</pre>
							 | 
						|
								<p>Because of the way the projected z value works we need to pick a large number to spread out the values
							 | 
						|
								otherwise many will have the same value. To make sure the labels don't overlap with other parts of
							 | 
						|
								the page we can tell the browser to create a new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context">stacking context</a>
							 | 
						|
								by setting the <code class="notranslate" translate="no">z-index</code> of the container of the labels</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels {
							 | 
						|
								  position: absolute;  /* let us position ourself inside the container */
							 | 
						|
								+  z-index: 0;          /* make a new stacking context so children don't sort with rest of page */
							 | 
						|
								  left: 0;             /* make our position the top left of the container */
							 | 
						|
								  top: 0;
							 | 
						|
								  color: white;
							 | 
						|
								  z-index: 0;
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>and now the labels should always be in the correct order.</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/align-html-to-3d-w-sorting.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-sorting.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>While we're at it let's do one more example to show one more issue.
							 | 
						|
								Let's draw a globe like Google Maps and label the countries.</p>
							 | 
						|
								<p>I found <a href="http://thematicmapping.org/downloads/world_borders.php">this data</a>
							 | 
						|
								which contains the borders of countries. It's licensed as
							 | 
						|
								<a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>.</p>
							 | 
						|
								<p>I <a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">wrote some code</a>
							 | 
						|
								to load the data, and generate country outlines and some JSON data with the names
							 | 
						|
								of the countries and their locations.</p>
							 | 
						|
								<div class="threejs_center"><img src="../examples/resources/data/world/country-outlines-4k.png" style="background: black; width: 700px"></div>
							 | 
						|
								
							 | 
						|
								<p>The JSON data is an array of entries something like this</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-json" translate="no">[
							 | 
						|
								  {
							 | 
						|
								    "name": "Algeria",
							 | 
						|
								    "min": [
							 | 
						|
								      -8.667223,
							 | 
						|
								      18.976387
							 | 
						|
								    ],
							 | 
						|
								    "max": [
							 | 
						|
								      11.986475,
							 | 
						|
								      37.091385
							 | 
						|
								    ],
							 | 
						|
								    "area": 238174,
							 | 
						|
								    "lat": 28.163,
							 | 
						|
								    "lon": 2.632,
							 | 
						|
								    "population": {
							 | 
						|
								      "2005": 32854159
							 | 
						|
								    }
							 | 
						|
								  },
							 | 
						|
								  ...
							 | 
						|
								</pre>
							 | 
						|
								<p>where min, max, lat, lon, are all in latitude and longitude degrees.</p>
							 | 
						|
								<p>Let's load it up. The code is based on the examples from <a href="optimize-lots-of-objects.html">optimizing lots of
							 | 
						|
								objects</a> though we are not drawing lots
							 | 
						|
								of objects we'll be using the same solutions for <a href="rendering-on-demand.html">rendering on
							 | 
						|
								demand</a>.</p>
							 | 
						|
								<p>The first thing is to make a sphere and use the outline texture.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
							 | 
						|
								  const loader = new THREE.TextureLoader();
							 | 
						|
								  const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
							 | 
						|
								  const geometry = new THREE.SphereGeometry(1, 64, 32);
							 | 
						|
								  const material = new THREE.MeshBasicMaterial({map: texture});
							 | 
						|
								  scene.add(new THREE.Mesh(geometry, material));
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Then let's load the JSON file by first making a loader</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadJSON(url) {
							 | 
						|
								  const req = await fetch(url);
							 | 
						|
								  return req.json();
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>and then calling it</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">let countryInfos;
							 | 
						|
								async function loadCountryData() {
							 | 
						|
								  countryInfos = await loadJSON('resources/data/world/country-info.json');
							 | 
						|
								     ...
							 | 
						|
								  }
							 | 
						|
								  requestRenderIfNotRequested();
							 | 
						|
								}
							 | 
						|
								loadCountryData();
							 | 
						|
								</pre>
							 | 
						|
								<p>Now let's use that data to generate and place the labels.</p>
							 | 
						|
								<p>In the article on <a href="optimize-lots-of-objects.html">optimizing lots of objects</a>
							 | 
						|
								we had setup a small scene graph of helper objects to make it easy to 
							 | 
						|
								compute latitude and longitude positions on our globe. See that article 
							 | 
						|
								for an explanation of how they work.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lonFudge = Math.PI * 1.5;
							 | 
						|
								const latFudge = Math.PI;
							 | 
						|
								// these helpers will make it easy to position the boxes
							 | 
						|
								// We can rotate the lon helper on its Y axis to the longitude
							 | 
						|
								const lonHelper = new THREE.Object3D();
							 | 
						|
								// We rotate the latHelper on its X axis to the latitude
							 | 
						|
								const latHelper = new THREE.Object3D();
							 | 
						|
								lonHelper.add(latHelper);
							 | 
						|
								// The position helper moves the object to the edge of the sphere
							 | 
						|
								const positionHelper = new THREE.Object3D();
							 | 
						|
								positionHelper.position.z = 1;
							 | 
						|
								latHelper.add(positionHelper);
							 | 
						|
								</pre>
							 | 
						|
								<p>We'll use that to compute a position for each label</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
							 | 
						|
								for (const countryInfo of countryInfos) {
							 | 
						|
								  const {lat, lon, name} = countryInfo;
							 | 
						|
								
							 | 
						|
								  // adjust the helpers to point to the latitude and longitude
							 | 
						|
								  lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
							 | 
						|
								  latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
							 | 
						|
								
							 | 
						|
								  // get the position of the lat/lon
							 | 
						|
								  positionHelper.updateWorldMatrix(true, false);
							 | 
						|
								  const position = new THREE.Vector3();
							 | 
						|
								  positionHelper.getWorldPosition(position);
							 | 
						|
								  countryInfo.position = position;
							 | 
						|
								
							 | 
						|
								  // add an element for each country
							 | 
						|
								  const elem = document.createElement('div');
							 | 
						|
								  elem.textContent = name;
							 | 
						|
								  labelParentElem.appendChild(elem);
							 | 
						|
								  countryInfo.elem = elem;
							 | 
						|
								</pre>
							 | 
						|
								<p>The code above looks very similar to the code we wrote for making cube labels
							 | 
						|
								making an element per label. When we're done we have an array, <code class="notranslate" translate="no">countryInfos</code>,
							 | 
						|
								with one entry for each country to which we've added an <code class="notranslate" translate="no">elem</code> property for
							 | 
						|
								the label element for that country and a <code class="notranslate" translate="no">position</code> with its position on the
							 | 
						|
								globe.</p>
							 | 
						|
								<p>Just like we did for the cubes we need to update the position of the
							 | 
						|
								labels and render time.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
							 | 
						|
								
							 | 
						|
								function updateLabels() {
							 | 
						|
								  // exit if we have not yet loaded the JSON file
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  for (const countryInfo of countryInfos) {
							 | 
						|
								    const {position, elem} = countryInfo;
							 | 
						|
								
							 | 
						|
								    // get the normalized screen coordinate of that position
							 | 
						|
								    // x and y will be in the -1 to +1 range with x = -1 being
							 | 
						|
								    // on the left and y = -1 being on the bottom
							 | 
						|
								    tempV.copy(position);
							 | 
						|
								    tempV.project(camera);
							 | 
						|
								
							 | 
						|
								    // convert the normalized position to CSS coordinates
							 | 
						|
								    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
							 | 
						|
								    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
							 | 
						|
								
							 | 
						|
								    // move the elem to that position
							 | 
						|
								    elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
							 | 
						|
								
							 | 
						|
								    // set the zIndex for sorting
							 | 
						|
								    elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>You can see the code above is substantially similar to the cube example before.
							 | 
						|
								The only major difference is we pre-computed the label positions at init time.
							 | 
						|
								We can do this because the globe never moves. Only our camera moves.</p>
							 | 
						|
								<p>Lastly we need to call <code class="notranslate" translate="no">updateLabels</code> in our render loop</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
							 | 
						|
								  renderRequested = false;
							 | 
						|
								
							 | 
						|
								  if (resizeRendererToDisplaySize(renderer)) {
							 | 
						|
								    const canvas = renderer.domElement;
							 | 
						|
								    camera.aspect = canvas.clientWidth / canvas.clientHeight;
							 | 
						|
								    camera.updateProjectionMatrix();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  controls.update();
							 | 
						|
								
							 | 
						|
								+  updateLabels();
							 | 
						|
								
							 | 
						|
								  renderer.render(scene, camera);
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>And this is what we get</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/align-html-elements-to-3d-globe-too-many-labels.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>That is way too many labels!</p>
							 | 
						|
								<p>We have 2 problems.</p>
							 | 
						|
								<ol>
							 | 
						|
								<li><p>Labels facing away from us are showing up.</p>
							 | 
						|
								</li>
							 | 
						|
								<li><p>There are too many labels.</p>
							 | 
						|
								</li>
							 | 
						|
								</ol>
							 | 
						|
								<p>For issue #1 we can't really use the <code class="notranslate" translate="no">RayCaster</code> like we did above as there is
							 | 
						|
								nothing to intersect except the sphere. Instead what we can do is check if that
							 | 
						|
								particular country is facing away from us or not. This works because the label
							 | 
						|
								positions are around a sphere. In fact we're using a unit sphere, a sphere with
							 | 
						|
								a radius of 1.0. That means the positions are already unit directions making
							 | 
						|
								the math relatively easy.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
							 | 
						|
								+const cameraToPoint = new THREE.Vector3();
							 | 
						|
								+const cameraPosition = new THREE.Vector3();
							 | 
						|
								+const normalMatrix = new THREE.Matrix3();
							 | 
						|
								
							 | 
						|
								function updateLabels() {
							 | 
						|
								  // exit if we have not yet loaded the JSON file
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								+  const minVisibleDot = 0.2;
							 | 
						|
								+  // get a matrix that represents a relative orientation of the camera
							 | 
						|
								+  normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
							 | 
						|
								+  // get the camera's position
							 | 
						|
								+  camera.getWorldPosition(cameraPosition);
							 | 
						|
								  for (const countryInfo of countryInfos) {
							 | 
						|
								    const {position, elem} = countryInfo;
							 | 
						|
								
							 | 
						|
								+    // Orient the position based on the camera's orientation.
							 | 
						|
								+    // Since the sphere is at the origin and the sphere is a unit sphere
							 | 
						|
								+    // this gives us a camera relative direction vector for the position.
							 | 
						|
								+    tempV.copy(position);
							 | 
						|
								+    tempV.applyMatrix3(normalMatrix);
							 | 
						|
								+
							 | 
						|
								+    // compute the direction to this position from the camera
							 | 
						|
								+    cameraToPoint.copy(position);
							 | 
						|
								+    cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
							 | 
						|
								+
							 | 
						|
								+    // get the dot product of camera relative direction to this position
							 | 
						|
								+    // on the globe with the direction from the camera to that point.
							 | 
						|
								+    // 1 = facing directly towards the camera
							 | 
						|
								+    // 0 = exactly on tangent of the sphere from the camera
							 | 
						|
								+    // < 0 = facing away
							 | 
						|
								+    const dot = tempV.dot(cameraToPoint);
							 | 
						|
								+
							 | 
						|
								+    // if the orientation is not facing us hide it.
							 | 
						|
								+    if (dot < minVisibleDot) {
							 | 
						|
								+      elem.style.display = 'none';
							 | 
						|
								+      continue;
							 | 
						|
								+    }
							 | 
						|
								+
							 | 
						|
								+    // restore the element to its default display style
							 | 
						|
								+    elem.style.display = '';
							 | 
						|
								
							 | 
						|
								    // get the normalized screen coordinate of that position
							 | 
						|
								    // x and y will be in the -1 to +1 range with x = -1 being
							 | 
						|
								    // on the left and y = -1 being on the bottom
							 | 
						|
								    tempV.copy(position);
							 | 
						|
								    tempV.project(camera);
							 | 
						|
								
							 | 
						|
								    // convert the normalized position to CSS coordinates
							 | 
						|
								    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
							 | 
						|
								    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
							 | 
						|
								
							 | 
						|
								    // move the elem to that position
							 | 
						|
								    countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
							 | 
						|
								
							 | 
						|
								    // set the zIndex for sorting
							 | 
						|
								    elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Above we use the positions as a direction and get that direction relative to the
							 | 
						|
								camera. Then we get the camera relative direction from the camera to that
							 | 
						|
								position on the globe and take the <em>dot product</em>. The dot product returns the cosine
							 | 
						|
								of the angle between the to vectors. This gives us a value from -1
							 | 
						|
								to +1 where -1 means the label is facing the camera, 0 means the label is directly
							 | 
						|
								on the edge of the sphere relative to the camera, and anything greater than zero is
							 | 
						|
								behind. We then use that value to show or hide the element.</p>
							 | 
						|
								<div class="spread">
							 | 
						|
								  <div>
							 | 
						|
								    <div data-diagram="dotProduct" style="height: 400px"></div>
							 | 
						|
								  </div>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p>In the diagram above we can see the dot product of the direction the label is
							 | 
						|
								facing to direction from the camera to that position. If you rotate the
							 | 
						|
								direction you'll see the dot product is -1.0 when the direction is directly
							 | 
						|
								facing the camera, it's 0.0 when exactly on the tangent of the sphere relative
							 | 
						|
								to the camera or to put it another way it's 0 when the 2 vectors are
							 | 
						|
								perpendicular to each other, 90 degrees It's greater than zero with the label is
							 | 
						|
								behind the sphere.</p>
							 | 
						|
								<p>For issue #2, too many labels we need some way to decide which labels
							 | 
						|
								to show. One way would be to only show labels for large countries.
							 | 
						|
								The data we're loading contains min and max values for the area a
							 | 
						|
								country covers. From that we can compute an area and then use that
							 | 
						|
								area to decide whether or not to display the country.</p>
							 | 
						|
								<p>At init time let's compute the area</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
							 | 
						|
								for (const countryInfo of countryInfos) {
							 | 
						|
								  const {lat, lon, min, max, name} = countryInfo;
							 | 
						|
								
							 | 
						|
								  // adjust the helpers to point to the latitude and longitude
							 | 
						|
								  lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
							 | 
						|
								  latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
							 | 
						|
								
							 | 
						|
								  // get the position of the lat/lon
							 | 
						|
								  positionHelper.updateWorldMatrix(true, false);
							 | 
						|
								  const position = new THREE.Vector3();
							 | 
						|
								  positionHelper.getWorldPosition(position);
							 | 
						|
								  countryInfo.position = position;
							 | 
						|
								
							 | 
						|
								+  // compute the area for each country
							 | 
						|
								+  const width = max[0] - min[0];
							 | 
						|
								+  const height = max[1] - min[1];
							 | 
						|
								+  const area = width * height;
							 | 
						|
								+  countryInfo.area = area;
							 | 
						|
								
							 | 
						|
								  // add an element for each country
							 | 
						|
								  const elem = document.createElement('div');
							 | 
						|
								  elem.textContent = name;
							 | 
						|
								  labelParentElem.appendChild(elem);
							 | 
						|
								  countryInfo.elem = elem;
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Then at render time let's use the area to decide to display the label
							 | 
						|
								or not</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const large = 20 * 20;
							 | 
						|
								const maxVisibleDot = 0.2;
							 | 
						|
								// get a matrix that represents a relative orientation of the camera
							 | 
						|
								normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
							 | 
						|
								// get the camera's position
							 | 
						|
								camera.getWorldPosition(cameraPosition);
							 | 
						|
								for (const countryInfo of countryInfos) {
							 | 
						|
								-  const {position, elem} = countryInfo;
							 | 
						|
								+  const {position, elem, area} = countryInfo;
							 | 
						|
								+  // large enough?
							 | 
						|
								+  if (area < large) {
							 | 
						|
								+    elem.style.display = 'none';
							 | 
						|
								+    continue;
							 | 
						|
								+  }
							 | 
						|
								
							 | 
						|
								  ...
							 | 
						|
								</pre>
							 | 
						|
								<p>Finally, since I'm not sure what good values are for these settings lets
							 | 
						|
								add a GUI so we can play with them</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
							 | 
						|
								import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
							 | 
						|
								+import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
							 | 
						|
								</pre>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const settings = {
							 | 
						|
								+  minArea: 20,
							 | 
						|
								+  maxVisibleDot: -0.2,
							 | 
						|
								+};
							 | 
						|
								+const gui = new GUI({width: 300});
							 | 
						|
								+gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested);
							 | 
						|
								+gui.add(settings, 'maxVisibleDot', -1, 1, 0.01).onChange(requestRenderIfNotRequested);
							 | 
						|
								
							 | 
						|
								function updateLabels() {
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								-  const large = 20 * 20;
							 | 
						|
								-  const maxVisibleDot = -0.2;
							 | 
						|
								+  const large = settings.minArea * settings.minArea;
							 | 
						|
								  // get a matrix that represents a relative orientation of the camera
							 | 
						|
								  normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
							 | 
						|
								  // get the camera's position
							 | 
						|
								  camera.getWorldPosition(cameraPosition);
							 | 
						|
								  for (const countryInfo of countryInfos) {
							 | 
						|
								
							 | 
						|
								    ...
							 | 
						|
								
							 | 
						|
								    // if the orientation is not facing us hide it.
							 | 
						|
								-    if (dot > maxVisibleDot) {
							 | 
						|
								+    if (dot > settings.maxVisibleDot) {
							 | 
						|
								      elem.style.display = 'none';
							 | 
						|
								      continue;
							 | 
						|
								    }
							 | 
						|
								</pre>
							 | 
						|
								<p>and here's the result</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/align-html-elements-to-3d-globe.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>You can see as you rotate the earth labels that go behind disappear.
							 | 
						|
								Adjust the <code class="notranslate" translate="no">minVisibleDot</code> to see the cutoff change.
							 | 
						|
								You can also adjust the <code class="notranslate" translate="no">minArea</code> value to see larger or smaller countries
							 | 
						|
								appear.</p>
							 | 
						|
								<p>The more I worked on this the more I realized just how much
							 | 
						|
								work is put into Google Maps. They have also have to decide which labels to
							 | 
						|
								show. I'm pretty sure they use all kinds of criteria. For example your current
							 | 
						|
								location, your default language setting, your account settings if you have an
							 | 
						|
								account, they probably use population or popularity, they might give priority
							 | 
						|
								to the countries in the center of the view, etc ... Lots to think about.</p>
							 | 
						|
								<p>In any case I hope these examples gave you some idea of how to align HTML
							 | 
						|
								elements with your 3D. A few things I might change.</p>
							 | 
						|
								<p>Next up let's make it so you can <a href="indexed-textures.html">pick and highlight a country</a>.</p>
							 | 
						|
								<p><link rel="stylesheet" href="../resources/threejs-align-html-elements-to-3d.css"></p>
							 | 
						|
								<script type="module" src="../resources/threejs-align-html-elements-to-3d.js"></script>
							 | 
						|
								
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								    </div>
							 | 
						|
								  
							 | 
						|
								  <script src="/manual/resources/prettify.js"></script>
							 | 
						|
								  <script src="/manual/resources/lesson.js"></script>
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								</body></html>
							 |