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.
		
		
		
		
		
			
		
			
				
					
					
						
							640 lines
						
					
					
						
							29 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							640 lines
						
					
					
						
							29 KiB
						
					
					
				
								<!DOCTYPE html><html lang="en"><head>
							 | 
						|
								    <meta charset="utf-8">
							 | 
						|
								    <title>Indexed Textures for Picking and Color</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 – Indexed Textures for Picking and Color">
							 | 
						|
								    <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>Indexed Textures for Picking and Color</h1>
							 | 
						|
								      </div>
							 | 
						|
								      <div class="lesson">
							 | 
						|
								        <div class="lesson-main">
							 | 
						|
								          <p>This article is a continuation of <a href="align-html-elements-to-3d.html">an article about aligning html elements to 3d</a>.
							 | 
						|
								If you haven't read that yet you should start there before continuing here.</p>
							 | 
						|
								<p>Sometimes using three.js requires coming up with creative solutions.
							 | 
						|
								I'm not sure this is a great solution but I thought I'd share it and
							 | 
						|
								you can see if it suggests any solutions for your needs.</p>
							 | 
						|
								<p>In the <a href="align-html-elements-to-3d.html">previous article</a> we
							 | 
						|
								displayed country names around a 3d globe. How would we go about letting
							 | 
						|
								the user select a country and show their selection?</p>
							 | 
						|
								<p>The first idea that comes to mind is to generate geometry for each country.
							 | 
						|
								We could <a href="picking.html">use a picking solution</a> like we covered before.
							 | 
						|
								We'd build 3D geometry for each country. If the user clicks on the mesh for
							 | 
						|
								that country we'd know what country was clicked.</p>
							 | 
						|
								<p>So, just to check that solution I tried generating 3D meshes of all the countries
							 | 
						|
								using the same data I used to generate the outlines
							 | 
						|
								<a href="align-html-elements-to-3d.html">in the previous article</a>.
							 | 
						|
								The result was a 15.5meg binary GLTF (.glb) file. Making the user download 15.5meg
							 | 
						|
								sounds like too much to me.</p>
							 | 
						|
								<p>There are lots of ways to compress the data. The first would probably be
							 | 
						|
								to apply some algorithm to lower the resolution of the outlines. I didn't spend
							 | 
						|
								any time pursuing that solution. For borders of the USA that's probably a huge
							 | 
						|
								win. For a borders of Canada probably much less. </p>
							 | 
						|
								<p>Another solution would be to use just actual data compression. For example gzipping
							 | 
						|
								the file brought it down to 11meg. That's 30% less but arguably not enough.</p>
							 | 
						|
								<p>We could store all the data as 16bit ranged values instead of 32bit float values.
							 | 
						|
								Or we could use something like <a href="https://google.github.io/draco/">draco compression</a>
							 | 
						|
								and maybe that would be enough. I didn't check and I would encourage you to check
							 | 
						|
								yourself and tell me how it goes as I'd love to know. 😅</p>
							 | 
						|
								<p>In my case I thought about <a href="picking.html">the GPU picking solution</a>
							 | 
						|
								we covered at the end of <a href="picking.html">the article on picking</a>. In
							 | 
						|
								that solution we drew every mesh with a unique color that represented that
							 | 
						|
								mesh's id. We then drew all the meshes and looked at the color that was clicked
							 | 
						|
								on.</p>
							 | 
						|
								<p>Taking inspiration from that we could pre-generate a map of countries where
							 | 
						|
								each country's color is its index number in our array of countries. We could
							 | 
						|
								then use a similar GPU picking technique. We'd draw the globe off screen using
							 | 
						|
								this index texture. Looking at the color of the pixel the user clicks would
							 | 
						|
								tell us the country id.</p>
							 | 
						|
								<p>So, I <a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">wrote some code</a>
							 | 
						|
								to generate such a texture. Here it is. </p>
							 | 
						|
								<div class="threejs_center"><img src="../examples/resources/data/world/country-index-texture.png" style="width: 700px;"></div>
							 | 
						|
								
							 | 
						|
								<p>Note: The data used to generate this texture comes from <a href="http://thematicmapping.org/downloads/world_borders.php">this website</a>
							 | 
						|
								and is therefore licensed as <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>.</p>
							 | 
						|
								<p>It's only 217k, much better than the 14meg for the country meshes. In fact we could probably
							 | 
						|
								even lower the resolution but 217k seems good enough for now.</p>
							 | 
						|
								<p>So let's try using it for picking countries.</p>
							 | 
						|
								<p>Grabbing code from the <a href="picking.html">gpu picking example</a> we need
							 | 
						|
								a scene for picking.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickingScene = new THREE.Scene();
							 | 
						|
								pickingScene.background = new THREE.Color(0);
							 | 
						|
								</pre>
							 | 
						|
								<p>and we need to add the globe with the our index texture to the
							 | 
						|
								picking scene.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
							 | 
						|
								  const loader = new THREE.TextureLoader();
							 | 
						|
								  const geometry = new THREE.SphereGeometry(1, 64, 32);
							 | 
						|
								
							 | 
						|
								+  const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
							 | 
						|
								+  indexTexture.minFilter = THREE.NearestFilter;
							 | 
						|
								+  indexTexture.magFilter = THREE.NearestFilter;
							 | 
						|
								+
							 | 
						|
								+  const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
							 | 
						|
								+  pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
							 | 
						|
								
							 | 
						|
								  const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
							 | 
						|
								  const material = new THREE.MeshBasicMaterial({map: texture});
							 | 
						|
								  scene.add(new THREE.Mesh(geometry, material));
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Then let's copy over the <code class="notranslate" translate="no">GPUPickingHelper</code> class we used
							 | 
						|
								before with a few minor changes.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class GPUPickHelper {
							 | 
						|
								  constructor() {
							 | 
						|
								    // create a 1x1 pixel render target
							 | 
						|
								    this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
							 | 
						|
								    this.pixelBuffer = new Uint8Array(4);
							 | 
						|
								-    this.pickedObject = null;
							 | 
						|
								-    this.pickedObjectSavedColor = 0;
							 | 
						|
								  }
							 | 
						|
								  pick(cssPosition, scene, camera) {
							 | 
						|
								    const {pickingTexture, pixelBuffer} = this;
							 | 
						|
								
							 | 
						|
								    // set the view offset to represent just a single pixel under the mouse
							 | 
						|
								    const pixelRatio = renderer.getPixelRatio();
							 | 
						|
								    camera.setViewOffset(
							 | 
						|
								        renderer.getContext().drawingBufferWidth,   // full width
							 | 
						|
								        renderer.getContext().drawingBufferHeight,  // full top
							 | 
						|
								        cssPosition.x * pixelRatio | 0,             // rect x
							 | 
						|
								        cssPosition.y * pixelRatio | 0,             // rect y
							 | 
						|
								        1,                                          // rect width
							 | 
						|
								        1,                                          // rect height
							 | 
						|
								    );
							 | 
						|
								    // render the scene
							 | 
						|
								    renderer.setRenderTarget(pickingTexture);
							 | 
						|
								    renderer.render(scene, camera);
							 | 
						|
								    renderer.setRenderTarget(null);
							 | 
						|
								    // clear the view offset so rendering returns to normal
							 | 
						|
								    camera.clearViewOffset();
							 | 
						|
								    //read the pixel
							 | 
						|
								    renderer.readRenderTargetPixels(
							 | 
						|
								        pickingTexture,
							 | 
						|
								        0,   // x
							 | 
						|
								        0,   // y
							 | 
						|
								        1,   // width
							 | 
						|
								        1,   // height
							 | 
						|
								        pixelBuffer);
							 | 
						|
								
							 | 
						|
								+    const id =
							 | 
						|
								+        (pixelBuffer[0] << 16) |
							 | 
						|
								+        (pixelBuffer[1] <<  8) |
							 | 
						|
								+        (pixelBuffer[2] <<  0);
							 | 
						|
								+
							 | 
						|
								+    return id;
							 | 
						|
								-    const id =
							 | 
						|
								-        (pixelBuffer[0] << 16) |
							 | 
						|
								-        (pixelBuffer[1] <<  8) |
							 | 
						|
								-        (pixelBuffer[2]      );
							 | 
						|
								-    const intersectedObject = idToObject[id];
							 | 
						|
								-    if (intersectedObject) {
							 | 
						|
								-      // pick the first object. It's the closest one
							 | 
						|
								-      this.pickedObject = intersectedObject;
							 | 
						|
								-      // 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>Now we can use that to pick countries.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new GPUPickHelper();
							 | 
						|
								
							 | 
						|
								function getCanvasRelativePosition(event) {
							 | 
						|
								  const rect = canvas.getBoundingClientRect();
							 | 
						|
								  return {
							 | 
						|
								    x: (event.clientX - rect.left) * canvas.width  / rect.width,
							 | 
						|
								    y: (event.clientY - rect.top ) * canvas.height / rect.height,
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function pickCountry(event) {
							 | 
						|
								  // exit if we have not loaded the data yet
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const position = getCanvasRelativePosition(event);
							 | 
						|
								  const id = pickHelper.pick(position, pickingScene, camera);
							 | 
						|
								  if (id > 0) {
							 | 
						|
								    // we clicked a country. Toggle its 'selected' property
							 | 
						|
								    const countryInfo = countryInfos[id - 1];
							 | 
						|
								    const selected = !countryInfo.selected;
							 | 
						|
								    // if we're selecting this country and modifiers are not
							 | 
						|
								    // pressed unselect everything else.
							 | 
						|
								    if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
							 | 
						|
								      unselectAllCountries();
							 | 
						|
								    }
							 | 
						|
								    numCountriesSelected += selected ? 1 : -1;
							 | 
						|
								    countryInfo.selected = selected;
							 | 
						|
								  } else if (numCountriesSelected) {
							 | 
						|
								    // the ocean or sky was clicked
							 | 
						|
								    unselectAllCountries();
							 | 
						|
								  }
							 | 
						|
								  requestRenderIfNotRequested();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function unselectAllCountries() {
							 | 
						|
								  numCountriesSelected = 0;
							 | 
						|
								  countryInfos.forEach((countryInfo) => {
							 | 
						|
								    countryInfo.selected = false;
							 | 
						|
								  });
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								canvas.addEventListener('pointerup', pickCountry);
							 | 
						|
								</pre>
							 | 
						|
								<p>The code above sets/unsets the <code class="notranslate" translate="no">selected</code> property on
							 | 
						|
								the array of countries. If <code class="notranslate" translate="no">shift</code> or <code class="notranslate" translate="no">ctrl</code> or <code class="notranslate" translate="no">cmd</code>
							 | 
						|
								is pressed then you can select more than one country.</p>
							 | 
						|
								<p>All that's left is showing the selected countries. For now
							 | 
						|
								let's just update the labels.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateLabels() {
							 | 
						|
								  // exit if we have not loaded the data yet
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  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) {
							 | 
						|
								-    const {position, elem, area} = countryInfo;
							 | 
						|
								-    // large enough?
							 | 
						|
								-    if (area < large) {
							 | 
						|
								+    const {position, elem, area, selected} = countryInfo;
							 | 
						|
								+    const largeEnough = area >= large;
							 | 
						|
								+    const show = selected || (numCountriesSelected === 0 && largeEnough);
							 | 
						|
								+    if (!show) {
							 | 
						|
								      elem.style.display = 'none';
							 | 
						|
								      continue;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    ...
							 | 
						|
								</pre>
							 | 
						|
								<p>and with that we should be able to pick countries</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/indexed-textures-picking.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/indexed-textures-picking.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>The code stills shows countries based on their area but if you
							 | 
						|
								click one just that one will have a label.</p>
							 | 
						|
								<p>So that seems like a reasonable solution for picking countries
							 | 
						|
								but what about highlighting the selected countries?</p>
							 | 
						|
								<p>For that we can take inspiration from <em>paletted graphics</em>.</p>
							 | 
						|
								<p><a href="https://en.wikipedia.org/wiki/Palette_%28computing%29">Paletted graphics</a>
							 | 
						|
								or <a href="https://en.wikipedia.org/wiki/Indexed_color">Indexed Color</a> is
							 | 
						|
								what older systems like the Atari 800, Amiga, NES,
							 | 
						|
								Super Nintendo, and even older IBM PCs used. Instead of storing bitmaps
							 | 
						|
								as RGBA colors 8bits per color, 32 bytes per pixel or more, they stored
							 | 
						|
								bitmaps as 8bit values or less. The value for each pixel was an index
							 | 
						|
								into a palette. So for example a value
							 | 
						|
								of 3 in the image means "display color 3". What color color#3 is is
							 | 
						|
								defined somewhere else called a "palette".</p>
							 | 
						|
								<p>In JavaScript you can think of it like this</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const face7x7PixelImageData = [
							 | 
						|
								  0, 1, 1, 1, 1, 1, 0,
							 | 
						|
								  1, 0, 0, 0, 0, 0, 1,
							 | 
						|
								  1, 0, 2, 0, 2, 0, 1,
							 | 
						|
								  1, 0, 0, 0, 0, 0, 1,
							 | 
						|
								  1, 0, 3, 3, 3, 0, 1,
							 | 
						|
								  1, 0, 0, 0, 0, 0, 1,
							 | 
						|
								  0, 1, 1, 1, 1, 1, 1,
							 | 
						|
								];
							 | 
						|
								
							 | 
						|
								const palette = [
							 | 
						|
								  [255, 255, 255],  // white
							 | 
						|
								  [  0,   0,   0],  // black
							 | 
						|
								  [  0, 255, 255],  // cyan
							 | 
						|
								  [255,   0,   0],  // red
							 | 
						|
								];
							 | 
						|
								</pre>
							 | 
						|
								<p>Where each pixel in the image data is an index into palette. If you interpreted
							 | 
						|
								the image data through the palette above you'd get this image</p>
							 | 
						|
								<div class="threejs_center"><img src="../resources/images/7x7-indexed-face.png"></div>
							 | 
						|
								
							 | 
						|
								<p>In our case we already have a texture above that has a different id
							 | 
						|
								per country. So, we could use that same texture through a palette
							 | 
						|
								texture to give each country its own color. By changing the palette
							 | 
						|
								texture we can color each individual country. For example by setting
							 | 
						|
								the entire palette texture to black and then for one country's entry
							 | 
						|
								in the palette a different color, we can highlight just that country.</p>
							 | 
						|
								<p>To do paletted index graphics requires some custom shader code.
							 | 
						|
								Let's modify the default shaders in three.js.
							 | 
						|
								That way we can use lighting and other features if we want.</p>
							 | 
						|
								<p>Like we covered in <a href="optimize-lots-of-objects-animated.html">the article on animating lots of objects</a>
							 | 
						|
								we can modify the default shaders by adding a function to a material's
							 | 
						|
								<code class="notranslate" translate="no">onBeforeCompile</code> property.</p>
							 | 
						|
								<p>The default fragment shader looks something like this before compiling.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#include <common>
							 | 
						|
								#include <color_pars_fragment>
							 | 
						|
								#include <uv_pars_fragment>
							 | 
						|
								#include <uv2_pars_fragment>
							 | 
						|
								#include <map_pars_fragment>
							 | 
						|
								#include <alphamap_pars_fragment>
							 | 
						|
								#include <aomap_pars_fragment>
							 | 
						|
								#include <lightmap_pars_fragment>
							 | 
						|
								#include <envmap_pars_fragment>
							 | 
						|
								#include <fog_pars_fragment>
							 | 
						|
								#include <specularmap_pars_fragment>
							 | 
						|
								#include <logdepthbuf_pars_fragment>
							 | 
						|
								#include <clipping_planes_pars_fragment>
							 | 
						|
								void main() {
							 | 
						|
								    #include <clipping_planes_fragment>
							 | 
						|
								    vec4 diffuseColor = vec4( diffuse, opacity );
							 | 
						|
								    #include <logdepthbuf_fragment>
							 | 
						|
								    #include <map_fragment>
							 | 
						|
								    #include <color_fragment>
							 | 
						|
								    #include <alphamap_fragment>
							 | 
						|
								    #include <alphatest_fragment>
							 | 
						|
								    #include <specularmap_fragment>
							 | 
						|
								    ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
							 | 
						|
								    #ifdef USE_LIGHTMAP
							 | 
						|
								        reflectedLight.indirectDiffuse += texture2D( lightMap, vUv2 ).xyz * lightMapIntensity;
							 | 
						|
								    #else
							 | 
						|
								        reflectedLight.indirectDiffuse += vec3( 1.0 );
							 | 
						|
								    #endif
							 | 
						|
								    #include <aomap_fragment>
							 | 
						|
								    reflectedLight.indirectDiffuse *= diffuseColor.rgb;
							 | 
						|
								    vec3 outgoingLight = reflectedLight.indirectDiffuse;
							 | 
						|
								    #include <envmap_fragment>
							 | 
						|
								    gl_FragColor = vec4( outgoingLight, diffuseColor.a );
							 | 
						|
								    #include <premultiplied_alpha_fragment>
							 | 
						|
								    #include <tonemapping_fragment>
							 | 
						|
								    #include <encodings_fragment>
							 | 
						|
								    #include <fog_fragment>
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p><a href="https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders/ShaderChunk">Digging through all those snippets</a>
							 | 
						|
								we find that three.js uses a variable called <code class="notranslate" translate="no">diffuseColor</code> to manage the
							 | 
						|
								base material color. It sets this in the <code class="notranslate" translate="no"><color_fragment></code> <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js">snippet</a>
							 | 
						|
								so we should be able to modify it after that point.</p>
							 | 
						|
								<p><code class="notranslate" translate="no">diffuseColor</code> at that point in the shader should already be the color from
							 | 
						|
								our outline texture so we can look up the color from a palette texture
							 | 
						|
								and mix them for the final result.</p>
							 | 
						|
								<p>Like we <a href="optimize-lots-of-objects-animated.html">did before</a> we'll make an array
							 | 
						|
								of search and replacement strings and apply them to the shader in
							 | 
						|
								<a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate" translate="no">Material.onBeforeCompile</code></a>.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
							 | 
						|
								  const loader = new THREE.TextureLoader();
							 | 
						|
								  const geometry = new THREE.SphereGeometry(1, 64, 32);
							 | 
						|
								
							 | 
						|
								  const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
							 | 
						|
								  indexTexture.minFilter = THREE.NearestFilter;
							 | 
						|
								  indexTexture.magFilter = THREE.NearestFilter;
							 | 
						|
								
							 | 
						|
								  const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
							 | 
						|
								  pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
							 | 
						|
								
							 | 
						|
								+  const fragmentShaderReplacements = [
							 | 
						|
								+    {
							 | 
						|
								+      from: '#include <common>',
							 | 
						|
								+      to: `
							 | 
						|
								+        #include <common>
							 | 
						|
								+        uniform sampler2D indexTexture;
							 | 
						|
								+        uniform sampler2D paletteTexture;
							 | 
						|
								+        uniform float paletteTextureWidth;
							 | 
						|
								+      `,
							 | 
						|
								+    },
							 | 
						|
								+    {
							 | 
						|
								+      from: '#include <color_fragment>',
							 | 
						|
								+      to: `
							 | 
						|
								+        #include <color_fragment>
							 | 
						|
								+        {
							 | 
						|
								+          vec4 indexColor = texture2D(indexTexture, vUv);
							 | 
						|
								+          float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
							 | 
						|
								+          vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
							 | 
						|
								+          vec4 paletteColor = texture2D(paletteTexture, paletteUV);
							 | 
						|
								+          // diffuseColor.rgb += paletteColor.rgb;   // white outlines
							 | 
						|
								+          diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
							 | 
						|
								+        }
							 | 
						|
								+      `,
							 | 
						|
								+    },
							 | 
						|
								+  ];
							 | 
						|
								
							 | 
						|
								  const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
							 | 
						|
								  const material = new THREE.MeshBasicMaterial({map: texture});
							 | 
						|
								+  material.onBeforeCompile = function(shader) {
							 | 
						|
								+    fragmentShaderReplacements.forEach((rep) => {
							 | 
						|
								+      shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
							 | 
						|
								+    });
							 | 
						|
								+  };
							 | 
						|
								  scene.add(new THREE.Mesh(geometry, material));
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Above can see above we add 3 uniforms, <code class="notranslate" translate="no">indexTexture</code>, <code class="notranslate" translate="no">paletteTexture</code>,
							 | 
						|
								and <code class="notranslate" translate="no">paletteTextureWidth</code>. We get a color from the <code class="notranslate" translate="no">indexTexture</code>
							 | 
						|
								and convert it to an index. <code class="notranslate" translate="no">vUv</code> is the texture coordinates provided by
							 | 
						|
								three.js. We then use that index to get a color out of the palette texture.
							 | 
						|
								We then mix the result with the current <code class="notranslate" translate="no">diffuseColor</code>. The <code class="notranslate" translate="no">diffuseColor</code>
							 | 
						|
								at this point is our black and white outline texture so if we add the 2 colors
							 | 
						|
								we'll get white outlines. If we subtract the current diffuse color we'll get
							 | 
						|
								black outlines.</p>
							 | 
						|
								<p>Before we can render we need to setup the palette texture
							 | 
						|
								and these 3 uniforms.</p>
							 | 
						|
								<p>For the palette texture it just needs to be wide enough to
							 | 
						|
								hold one color per country + one for the ocean (id = 0).
							 | 
						|
								There are 240 something countries. We could wait until the
							 | 
						|
								list of countries loads to get an exact number or look it up.
							 | 
						|
								There's not much harm in just picking some larger number so
							 | 
						|
								let's choose 512.</p>
							 | 
						|
								<p>Here's the code to create the palette texture</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const maxNumCountries = 512;
							 | 
						|
								const paletteTextureWidth = maxNumCountries;
							 | 
						|
								const paletteTextureHeight = 1;
							 | 
						|
								const palette = new Uint8Array(paletteTextureWidth * 4);
							 | 
						|
								const paletteTexture = new THREE.DataTexture(
							 | 
						|
								    palette, paletteTextureWidth, paletteTextureHeight);
							 | 
						|
								paletteTexture.minFilter = THREE.NearestFilter;
							 | 
						|
								paletteTexture.magFilter = THREE.NearestFilter;
							 | 
						|
								</pre>
							 | 
						|
								<p>A <a href="/docs/#api/en/textures/DataTexture"><code class="notranslate" translate="no">DataTexture</code></a> let's us give a texture raw data. In this case
							 | 
						|
								we're giving it 512 RGBA colors, 4 bytes each where each byte is
							 | 
						|
								red, green, and blue respectively using values that go from 0 to 255.</p>
							 | 
						|
								<p>Let's fill it with random colors just to see it work</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let i = 1; i < palette.length; ++i) {
							 | 
						|
								  palette[i] = Math.random() * 256;
							 | 
						|
								}
							 | 
						|
								// set the ocean color (index #0)
							 | 
						|
								palette.set([100, 200, 255, 255], 0);
							 | 
						|
								paletteTexture.needsUpdate = true;
							 | 
						|
								</pre>
							 | 
						|
								<p>Anytime we want three.js to update the palette texture with
							 | 
						|
								the contents of the <code class="notranslate" translate="no">palette</code> array we need to set <code class="notranslate" translate="no">paletteTexture.needsUpdate</code>
							 | 
						|
								to <code class="notranslate" translate="no">true</code>.</p>
							 | 
						|
								<p>And then we still need to set the uniforms on the material.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.SphereGeometry(1, 64, 32);
							 | 
						|
								const material = new THREE.MeshBasicMaterial({map: texture});
							 | 
						|
								material.onBeforeCompile = function(shader) {
							 | 
						|
								  fragmentShaderReplacements.forEach((rep) => {
							 | 
						|
								    shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
							 | 
						|
								  });
							 | 
						|
								+  shader.uniforms.paletteTexture = {value: paletteTexture};
							 | 
						|
								+  shader.uniforms.indexTexture = {value: indexTexture};
							 | 
						|
								+  shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
							 | 
						|
								};
							 | 
						|
								scene.add(new THREE.Mesh(geometry, material));
							 | 
						|
								</pre>
							 | 
						|
								<p>and with that we get randomly colored countries.</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/indexed-textures-random-colors.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/indexed-textures-random-colors.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>Now that we can see the index and palette textures are working
							 | 
						|
								let's manipulate the palette for highlighting.</p>
							 | 
						|
								<p>First let's make function that will let us pass in a three.js
							 | 
						|
								style color and give us values we can put in the palette texture.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempColor = new THREE.Color();
							 | 
						|
								function get255BasedColor(color) {
							 | 
						|
								  tempColor.set(color);
							 | 
						|
								  const base = tempColor.toArray().map(v => v * 255);
							 | 
						|
								  base.push(255); // alpha
							 | 
						|
								  return base;
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Calling it like this <code class="notranslate" translate="no">color = get255BasedColor('red')</code> will
							 | 
						|
								return an array like <code class="notranslate" translate="no">[255, 0, 0, 255]</code>.</p>
							 | 
						|
								<p>Next let's use it to make a few colors and fill out the
							 | 
						|
								palette.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const selectedColor = get255BasedColor('red');
							 | 
						|
								const unselectedColor = get255BasedColor('#444');
							 | 
						|
								const oceanColor = get255BasedColor('rgb(100,200,255)');
							 | 
						|
								resetPalette();
							 | 
						|
								
							 | 
						|
								function setPaletteColor(index, color) {
							 | 
						|
								  palette.set(color, index * 4);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function resetPalette() {
							 | 
						|
								  // make all colors the unselected color
							 | 
						|
								  for (let i = 1; i < maxNumCountries; ++i) {
							 | 
						|
								    setPaletteColor(i, unselectedColor);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // set the ocean color (index #0)
							 | 
						|
								  setPaletteColor(0, oceanColor);
							 | 
						|
								  paletteTexture.needsUpdate = true;
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>Now let's use those functions to update the palette when a country
							 | 
						|
								is selected</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
							 | 
						|
								  const rect = canvas.getBoundingClientRect();
							 | 
						|
								  return {
							 | 
						|
								    x: (event.clientX - rect.left) * canvas.width  / rect.width,
							 | 
						|
								    y: (event.clientY - rect.top ) * canvas.height / rect.height,
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function pickCountry(event) {
							 | 
						|
								  // exit if we have not loaded the data yet
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const position = getCanvasRelativePosition(event);
							 | 
						|
								  const id = pickHelper.pick(position, pickingScene, camera);
							 | 
						|
								  if (id > 0) {
							 | 
						|
								    const countryInfo = countryInfos[id - 1];
							 | 
						|
								    const selected = !countryInfo.selected;
							 | 
						|
								    if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
							 | 
						|
								      unselectAllCountries();
							 | 
						|
								    }
							 | 
						|
								    numCountriesSelected += selected ? 1 : -1;
							 | 
						|
								    countryInfo.selected = selected;
							 | 
						|
								+    setPaletteColor(id, selected ? selectedColor : unselectedColor);
							 | 
						|
								+    paletteTexture.needsUpdate = true;
							 | 
						|
								  } else if (numCountriesSelected) {
							 | 
						|
								    unselectAllCountries();
							 | 
						|
								  }
							 | 
						|
								  requestRenderIfNotRequested();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function unselectAllCountries() {
							 | 
						|
								  numCountriesSelected = 0;
							 | 
						|
								  countryInfos.forEach((countryInfo) => {
							 | 
						|
								    countryInfo.selected = false;
							 | 
						|
								  });
							 | 
						|
								+  resetPalette();
							 | 
						|
								}
							 | 
						|
								</pre>
							 | 
						|
								<p>and we that we should be able to highlight 1 or more countries.</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/indexed-textures-picking-and-highlighting.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/indexed-textures-picking-and-highlighting.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>That seems to work!</p>
							 | 
						|
								<p>One minor thing is we can't spin the globe without changing
							 | 
						|
								the selection state. If we select a country and then want to
							 | 
						|
								rotate the globe the selection will change.</p>
							 | 
						|
								<p>Let's try to fix that. Off the top of my head we can check 2 things.
							 | 
						|
								How much time passed between clicking and letting go.
							 | 
						|
								Another is did the user actually move the mouse. If the
							 | 
						|
								time is short or if they didn't move the mouse then it
							 | 
						|
								was probably a click. Otherwise they were probably trying
							 | 
						|
								to drag the globe.</p>
							 | 
						|
								<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const maxClickTimeMs = 200;
							 | 
						|
								+const maxMoveDeltaSq = 5 * 5;
							 | 
						|
								+const startPosition = {};
							 | 
						|
								+let startTimeMs;
							 | 
						|
								+
							 | 
						|
								+function recordStartTimeAndPosition(event) {
							 | 
						|
								+  startTimeMs = performance.now();
							 | 
						|
								+  const pos = getCanvasRelativePosition(event);
							 | 
						|
								+  startPosition.x = pos.x;
							 | 
						|
								+  startPosition.y = pos.y;
							 | 
						|
								+}
							 | 
						|
								
							 | 
						|
								function getCanvasRelativePosition(event) {
							 | 
						|
								  const rect = canvas.getBoundingClientRect();
							 | 
						|
								  return {
							 | 
						|
								    x: (event.clientX - rect.left) * canvas.width  / rect.width,
							 | 
						|
								    y: (event.clientY - rect.top ) * canvas.height / rect.height,
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function pickCountry(event) {
							 | 
						|
								  // exit if we have not loaded the data yet
							 | 
						|
								  if (!countryInfos) {
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								+  // if it's been a moment since the user started
							 | 
						|
								+  // then assume it was a drag action, not a select action
							 | 
						|
								+  const clickTimeMs = performance.now() - startTimeMs;
							 | 
						|
								+  if (clickTimeMs > maxClickTimeMs) {
							 | 
						|
								+    return;
							 | 
						|
								+  }
							 | 
						|
								+
							 | 
						|
								+  // if they moved assume it was a drag action
							 | 
						|
								+  const position = getCanvasRelativePosition(event);
							 | 
						|
								+  const moveDeltaSq = (startPosition.x - position.x) ** 2 +
							 | 
						|
								+                      (startPosition.y - position.y) ** 2;
							 | 
						|
								+  if (moveDeltaSq > maxMoveDeltaSq) {
							 | 
						|
								+    return;
							 | 
						|
								+  }
							 | 
						|
								
							 | 
						|
								-  const position = {x: event.clientX, y: event.clientY};
							 | 
						|
								  const id = pickHelper.pick(position, pickingScene, camera);
							 | 
						|
								  if (id > 0) {
							 | 
						|
								    const countryInfo = countryInfos[id - 1];
							 | 
						|
								    const selected = !countryInfo.selected;
							 | 
						|
								    if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
							 | 
						|
								      unselectAllCountries();
							 | 
						|
								    }
							 | 
						|
								    numCountriesSelected += selected ? 1 : -1;
							 | 
						|
								    countryInfo.selected = selected;
							 | 
						|
								    setPaletteColor(id, selected ? selectedColor : unselectedColor);
							 | 
						|
								    paletteTexture.needsUpdate = true;
							 | 
						|
								  } else if (numCountriesSelected) {
							 | 
						|
								    unselectAllCountries();
							 | 
						|
								  }
							 | 
						|
								  requestRenderIfNotRequested();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function unselectAllCountries() {
							 | 
						|
								  numCountriesSelected = 0;
							 | 
						|
								  countryInfos.forEach((countryInfo) => {
							 | 
						|
								    countryInfo.selected = false;
							 | 
						|
								  });
							 | 
						|
								  resetPalette();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								+canvas.addEventListener('pointerdown', recordStartTimeAndPosition);
							 | 
						|
								canvas.addEventListener('pointerup', pickCountry);
							 | 
						|
								</pre>
							 | 
						|
								<p>and with those changes it <em>seems</em> like it works to me.</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/indexed-textures-picking-debounced.html"></iframe></div>
							 | 
						|
								  <a class="threejs_center" href="/manual/examples/indexed-textures-picking-debounced.html" target="_blank">click here to open in a separate window</a>
							 | 
						|
								</div>
							 | 
						|
								
							 | 
						|
								<p></p>
							 | 
						|
								<p>I'm not a UX expert so I'd love to hear if there is a better
							 | 
						|
								solution.</p>
							 | 
						|
								<p>I hope that gave you some idea of how indexed graphics can be useful
							 | 
						|
								and how you can modify the shaders three.js makes to add simple features.
							 | 
						|
								How to use GLSL, the language the shaders are written in, is too much for
							 | 
						|
								this article. There are a few links to some info in
							 | 
						|
								<a href="post-processing.html">the article on post processing</a>.</p>
							 | 
						|
								
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								  <script src="/manual/resources/prettify.js"></script>
							 | 
						|
								  <script src="/manual/resources/lesson.js"></script>
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								</body></html>
							 | 
						|
								
							 |