indexed-textures.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Indexed Textures for Picking and Color</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – Indexed Textures for Picking and Color">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>Indexed Textures for Picking and Color</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>This article is a continuation of <a href="align-html-elements-to-3d.html">an article about aligning html elements to 3d</a>.
  32. If you haven't read that yet you should start there before continuing here.</p>
  33. <p>Sometimes using three.js requires coming up with creative solutions.
  34. I'm not sure this is a great solution but I thought I'd share it and
  35. you can see if it suggests any solutions for your needs.</p>
  36. <p>In the <a href="align-html-elements-to-3d.html">previous article</a> we
  37. displayed country names around a 3d globe. How would we go about letting
  38. the user select a country and show their selection?</p>
  39. <p>The first idea that comes to mind is to generate geometry for each country.
  40. We could <a href="picking.html">use a picking solution</a> like we covered before.
  41. We'd build 3D geometry for each country. If the user clicks on the mesh for
  42. that country we'd know what country was clicked.</p>
  43. <p>So, just to check that solution I tried generating 3D meshes of all the countries
  44. using the same data I used to generate the outlines
  45. <a href="align-html-elements-to-3d.html">in the previous article</a>.
  46. The result was a 15.5meg binary GLTF (.glb) file. Making the user download 15.5meg
  47. sounds like too much to me.</p>
  48. <p>There are lots of ways to compress the data. The first would probably be
  49. to apply some algorithm to lower the resolution of the outlines. I didn't spend
  50. any time pursuing that solution. For borders of the USA that's probably a huge
  51. win. For a borders of Canada probably much less. </p>
  52. <p>Another solution would be to use just actual data compression. For example gzipping
  53. the file brought it down to 11meg. That's 30% less but arguably not enough.</p>
  54. <p>We could store all the data as 16bit ranged values instead of 32bit float values.
  55. Or we could use something like <a href="https://google.github.io/draco/">draco compression</a>
  56. and maybe that would be enough. I didn't check and I would encourage you to check
  57. yourself and tell me how it goes as I'd love to know. 😅</p>
  58. <p>In my case I thought about <a href="picking.html">the GPU picking solution</a>
  59. we covered at the end of <a href="picking.html">the article on picking</a>. In
  60. that solution we drew every mesh with a unique color that represented that
  61. mesh's id. We then drew all the meshes and looked at the color that was clicked
  62. on.</p>
  63. <p>Taking inspiration from that we could pre-generate a map of countries where
  64. each country's color is its index number in our array of countries. We could
  65. then use a similar GPU picking technique. We'd draw the globe off screen using
  66. this index texture. Looking at the color of the pixel the user clicks would
  67. tell us the country id.</p>
  68. <p>So, I <a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">wrote some code</a>
  69. to generate such a texture. Here it is. </p>
  70. <div class="threejs_center"><img src="../examples/resources/data/world/country-index-texture.png" style="width: 700px;"></div>
  71. <p>Note: The data used to generate this texture comes from <a href="http://thematicmapping.org/downloads/world_borders.php">this website</a>
  72. and is therefore licensed as <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>.</p>
  73. <p>It's only 217k, much better than the 14meg for the country meshes. In fact we could probably
  74. even lower the resolution but 217k seems good enough for now.</p>
  75. <p>So let's try using it for picking countries.</p>
  76. <p>Grabbing code from the <a href="picking.html">gpu picking example</a> we need
  77. a scene for picking.</p>
  78. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickingScene = new THREE.Scene();
  79. pickingScene.background = new THREE.Color(0);
  80. </pre>
  81. <p>and we need to add the globe with the our index texture to the
  82. picking scene.</p>
  83. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  84. const loader = new THREE.TextureLoader();
  85. const geometry = new THREE.SphereGeometry(1, 64, 32);
  86. + const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
  87. + indexTexture.minFilter = THREE.NearestFilter;
  88. + indexTexture.magFilter = THREE.NearestFilter;
  89. +
  90. + const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
  91. + pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
  92. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  93. const material = new THREE.MeshBasicMaterial({map: texture});
  94. scene.add(new THREE.Mesh(geometry, material));
  95. }
  96. </pre>
  97. <p>Then let's copy over the <code class="notranslate" translate="no">GPUPickingHelper</code> class we used
  98. before with a few minor changes.</p>
  99. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class GPUPickHelper {
  100. constructor() {
  101. // create a 1x1 pixel render target
  102. this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
  103. this.pixelBuffer = new Uint8Array(4);
  104. - this.pickedObject = null;
  105. - this.pickedObjectSavedColor = 0;
  106. }
  107. pick(cssPosition, scene, camera) {
  108. const {pickingTexture, pixelBuffer} = this;
  109. // set the view offset to represent just a single pixel under the mouse
  110. const pixelRatio = renderer.getPixelRatio();
  111. camera.setViewOffset(
  112. renderer.getContext().drawingBufferWidth, // full width
  113. renderer.getContext().drawingBufferHeight, // full top
  114. cssPosition.x * pixelRatio | 0, // rect x
  115. cssPosition.y * pixelRatio | 0, // rect y
  116. 1, // rect width
  117. 1, // rect height
  118. );
  119. // render the scene
  120. renderer.setRenderTarget(pickingTexture);
  121. renderer.render(scene, camera);
  122. renderer.setRenderTarget(null);
  123. // clear the view offset so rendering returns to normal
  124. camera.clearViewOffset();
  125. //read the pixel
  126. renderer.readRenderTargetPixels(
  127. pickingTexture,
  128. 0, // x
  129. 0, // y
  130. 1, // width
  131. 1, // height
  132. pixelBuffer);
  133. + const id =
  134. + (pixelBuffer[0] &lt;&lt; 16) |
  135. + (pixelBuffer[1] &lt;&lt; 8) |
  136. + (pixelBuffer[2] &lt;&lt; 0);
  137. +
  138. + return id;
  139. - const id =
  140. - (pixelBuffer[0] &lt;&lt; 16) |
  141. - (pixelBuffer[1] &lt;&lt; 8) |
  142. - (pixelBuffer[2] );
  143. - const intersectedObject = idToObject[id];
  144. - if (intersectedObject) {
  145. - // pick the first object. It's the closest one
  146. - this.pickedObject = intersectedObject;
  147. - // save its color
  148. - this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  149. - // set its emissive color to flashing red/yellow
  150. - this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  151. - }
  152. }
  153. }
  154. </pre>
  155. <p>Now we can use that to pick countries.</p>
  156. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new GPUPickHelper();
  157. function getCanvasRelativePosition(event) {
  158. const rect = canvas.getBoundingClientRect();
  159. return {
  160. x: (event.clientX - rect.left) * canvas.width / rect.width,
  161. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  162. };
  163. }
  164. function pickCountry(event) {
  165. // exit if we have not loaded the data yet
  166. if (!countryInfos) {
  167. return;
  168. }
  169. const position = getCanvasRelativePosition(event);
  170. const id = pickHelper.pick(position, pickingScene, camera);
  171. if (id &gt; 0) {
  172. // we clicked a country. Toggle its 'selected' property
  173. const countryInfo = countryInfos[id - 1];
  174. const selected = !countryInfo.selected;
  175. // if we're selecting this country and modifiers are not
  176. // pressed unselect everything else.
  177. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  178. unselectAllCountries();
  179. }
  180. numCountriesSelected += selected ? 1 : -1;
  181. countryInfo.selected = selected;
  182. } else if (numCountriesSelected) {
  183. // the ocean or sky was clicked
  184. unselectAllCountries();
  185. }
  186. requestRenderIfNotRequested();
  187. }
  188. function unselectAllCountries() {
  189. numCountriesSelected = 0;
  190. countryInfos.forEach((countryInfo) =&gt; {
  191. countryInfo.selected = false;
  192. });
  193. }
  194. canvas.addEventListener('pointerup', pickCountry);
  195. </pre>
  196. <p>The code above sets/unsets the <code class="notranslate" translate="no">selected</code> property on
  197. 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>
  198. is pressed then you can select more than one country.</p>
  199. <p>All that's left is showing the selected countries. For now
  200. let's just update the labels.</p>
  201. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateLabels() {
  202. // exit if we have not loaded the data yet
  203. if (!countryInfos) {
  204. return;
  205. }
  206. const large = settings.minArea * settings.minArea;
  207. // get a matrix that represents a relative orientation of the camera
  208. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  209. // get the camera's position
  210. camera.getWorldPosition(cameraPosition);
  211. for (const countryInfo of countryInfos) {
  212. - const {position, elem, area} = countryInfo;
  213. - // large enough?
  214. - if (area &lt; large) {
  215. + const {position, elem, area, selected} = countryInfo;
  216. + const largeEnough = area &gt;= large;
  217. + const show = selected || (numCountriesSelected === 0 &amp;&amp; largeEnough);
  218. + if (!show) {
  219. elem.style.display = 'none';
  220. continue;
  221. }
  222. ...
  223. </pre>
  224. <p>and with that we should be able to pick countries</p>
  225. <p></p><div translate="no" class="threejs_example_container notranslate">
  226. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/indexed-textures-picking.html"></iframe></div>
  227. <a class="threejs_center" href="/manual/examples/indexed-textures-picking.html" target="_blank">click here to open in a separate window</a>
  228. </div>
  229. <p></p>
  230. <p>The code stills shows countries based on their area but if you
  231. click one just that one will have a label.</p>
  232. <p>So that seems like a reasonable solution for picking countries
  233. but what about highlighting the selected countries?</p>
  234. <p>For that we can take inspiration from <em>paletted graphics</em>.</p>
  235. <p><a href="https://en.wikipedia.org/wiki/Palette_%28computing%29">Paletted graphics</a>
  236. or <a href="https://en.wikipedia.org/wiki/Indexed_color">Indexed Color</a> is
  237. what older systems like the Atari 800, Amiga, NES,
  238. Super Nintendo, and even older IBM PCs used. Instead of storing bitmaps
  239. as RGBA colors 8bits per color, 32 bytes per pixel or more, they stored
  240. bitmaps as 8bit values or less. The value for each pixel was an index
  241. into a palette. So for example a value
  242. of 3 in the image means "display color 3". What color color#3 is is
  243. defined somewhere else called a "palette".</p>
  244. <p>In JavaScript you can think of it like this</p>
  245. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const face7x7PixelImageData = [
  246. 0, 1, 1, 1, 1, 1, 0,
  247. 1, 0, 0, 0, 0, 0, 1,
  248. 1, 0, 2, 0, 2, 0, 1,
  249. 1, 0, 0, 0, 0, 0, 1,
  250. 1, 0, 3, 3, 3, 0, 1,
  251. 1, 0, 0, 0, 0, 0, 1,
  252. 0, 1, 1, 1, 1, 1, 1,
  253. ];
  254. const palette = [
  255. [255, 255, 255], // white
  256. [ 0, 0, 0], // black
  257. [ 0, 255, 255], // cyan
  258. [255, 0, 0], // red
  259. ];
  260. </pre>
  261. <p>Where each pixel in the image data is an index into palette. If you interpreted
  262. the image data through the palette above you'd get this image</p>
  263. <div class="threejs_center"><img src="../resources/images/7x7-indexed-face.png"></div>
  264. <p>In our case we already have a texture above that has a different id
  265. per country. So, we could use that same texture through a palette
  266. texture to give each country its own color. By changing the palette
  267. texture we can color each individual country. For example by setting
  268. the entire palette texture to black and then for one country's entry
  269. in the palette a different color, we can highlight just that country.</p>
  270. <p>To do paletted index graphics requires some custom shader code.
  271. Let's modify the default shaders in three.js.
  272. That way we can use lighting and other features if we want.</p>
  273. <p>Like we covered in <a href="optimize-lots-of-objects-animated.html">the article on animating lots of objects</a>
  274. we can modify the default shaders by adding a function to a material's
  275. <code class="notranslate" translate="no">onBeforeCompile</code> property.</p>
  276. <p>The default fragment shader looks something like this before compiling.</p>
  277. <pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#include &lt;common&gt;
  278. #include &lt;color_pars_fragment&gt;
  279. #include &lt;uv_pars_fragment&gt;
  280. #include &lt;map_pars_fragment&gt;
  281. #include &lt;alphamap_pars_fragment&gt;
  282. #include &lt;aomap_pars_fragment&gt;
  283. #include &lt;lightmap_pars_fragment&gt;
  284. #include &lt;envmap_pars_fragment&gt;
  285. #include &lt;fog_pars_fragment&gt;
  286. #include &lt;specularmap_pars_fragment&gt;
  287. #include &lt;logdepthbuf_pars_fragment&gt;
  288. #include &lt;clipping_planes_pars_fragment&gt;
  289. void main() {
  290. #include &lt;clipping_planes_fragment&gt;
  291. vec4 diffuseColor = vec4( diffuse, opacity );
  292. #include &lt;logdepthbuf_fragment&gt;
  293. #include &lt;map_fragment&gt;
  294. #include &lt;color_fragment&gt;
  295. #include &lt;alphamap_fragment&gt;
  296. #include &lt;alphatest_fragment&gt;
  297. #include &lt;specularmap_fragment&gt;
  298. ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
  299. #ifdef USE_LIGHTMAP
  300. reflectedLight.indirectDiffuse += texture2D( lightMap, vLightMapUv ).xyz * lightMapIntensity;
  301. #else
  302. reflectedLight.indirectDiffuse += vec3( 1.0 );
  303. #endif
  304. #include &lt;aomap_fragment&gt;
  305. reflectedLight.indirectDiffuse *= diffuseColor.rgb;
  306. vec3 outgoingLight = reflectedLight.indirectDiffuse;
  307. #include &lt;envmap_fragment&gt;
  308. gl_FragColor = vec4( outgoingLight, diffuseColor.a );
  309. #include &lt;premultiplied_alpha_fragment&gt;
  310. #include &lt;tonemapping_fragment&gt;
  311. #include &lt;encodings_fragment&gt;
  312. #include &lt;fog_fragment&gt;
  313. }
  314. </pre>
  315. <p><a href="https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders/ShaderChunk">Digging through all those snippets</a>
  316. we find that three.js uses a variable called <code class="notranslate" translate="no">diffuseColor</code> to manage the
  317. base material color. It sets this in the <code class="notranslate" translate="no">&lt;color_fragment&gt;</code> <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js">snippet</a>
  318. so we should be able to modify it after that point.</p>
  319. <p><code class="notranslate" translate="no">diffuseColor</code> at that point in the shader should already be the color from
  320. our outline texture so we can look up the color from a palette texture
  321. and mix them for the final result.</p>
  322. <p>Like we <a href="optimize-lots-of-objects-animated.html">did before</a> we'll make an array
  323. of search and replacement strings and apply them to the shader in
  324. <a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate" translate="no">Material.onBeforeCompile</code></a>.</p>
  325. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  326. const loader = new THREE.TextureLoader();
  327. const geometry = new THREE.SphereGeometry(1, 64, 32);
  328. const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
  329. indexTexture.minFilter = THREE.NearestFilter;
  330. indexTexture.magFilter = THREE.NearestFilter;
  331. const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
  332. pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
  333. + const fragmentShaderReplacements = [
  334. + {
  335. + from: '#include &lt;common&gt;',
  336. + to: `
  337. + #include &lt;common&gt;
  338. + uniform sampler2D indexTexture;
  339. + uniform sampler2D paletteTexture;
  340. + uniform float paletteTextureWidth;
  341. + `,
  342. + },
  343. + {
  344. + from: '#include &lt;color_fragment&gt;',
  345. + to: `
  346. + #include &lt;color_fragment&gt;
  347. + {
  348. + vec4 indexColor = texture2D(indexTexture, vUv);
  349. + float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
  350. + vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
  351. + vec4 paletteColor = texture2D(paletteTexture, paletteUV);
  352. + // diffuseColor.rgb += paletteColor.rgb; // white outlines
  353. + diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb; // black outlines
  354. + }
  355. + `,
  356. + },
  357. + ];
  358. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  359. const material = new THREE.MeshBasicMaterial({map: texture});
  360. + material.onBeforeCompile = function(shader) {
  361. + fragmentShaderReplacements.forEach((rep) =&gt; {
  362. + shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
  363. + });
  364. + };
  365. scene.add(new THREE.Mesh(geometry, material));
  366. }
  367. </pre>
  368. <p>Above can see above we add 3 uniforms, <code class="notranslate" translate="no">indexTexture</code>, <code class="notranslate" translate="no">paletteTexture</code>,
  369. and <code class="notranslate" translate="no">paletteTextureWidth</code>. We get a color from the <code class="notranslate" translate="no">indexTexture</code>
  370. and convert it to an index. <code class="notranslate" translate="no">vUv</code> is the texture coordinates provided by
  371. three.js. We then use that index to get a color out of the palette texture.
  372. We then mix the result with the current <code class="notranslate" translate="no">diffuseColor</code>. The <code class="notranslate" translate="no">diffuseColor</code>
  373. at this point is our black and white outline texture so if we add the 2 colors
  374. we'll get white outlines. If we subtract the current diffuse color we'll get
  375. black outlines.</p>
  376. <p>Before we can render we need to setup the palette texture
  377. and these 3 uniforms.</p>
  378. <p>For the palette texture it just needs to be wide enough to
  379. hold one color per country + one for the ocean (id = 0).
  380. There are 240 something countries. We could wait until the
  381. list of countries loads to get an exact number or look it up.
  382. There's not much harm in just picking some larger number so
  383. let's choose 512.</p>
  384. <p>Here's the code to create the palette texture</p>
  385. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const maxNumCountries = 512;
  386. const paletteTextureWidth = maxNumCountries;
  387. const paletteTextureHeight = 1;
  388. const palette = new Uint8Array(paletteTextureWidth * 4);
  389. const paletteTexture = new THREE.DataTexture(
  390. palette, paletteTextureWidth, paletteTextureHeight);
  391. paletteTexture.minFilter = THREE.NearestFilter;
  392. paletteTexture.magFilter = THREE.NearestFilter;
  393. </pre>
  394. <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
  395. we're giving it 512 RGBA colors, 4 bytes each where each byte is
  396. red, green, and blue respectively using values that go from 0 to 255.</p>
  397. <p>Let's fill it with random colors just to see it work</p>
  398. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let i = 1; i &lt; palette.length; ++i) {
  399. palette[i] = Math.random() * 256;
  400. }
  401. // set the ocean color (index #0)
  402. palette.set([100, 200, 255, 255], 0);
  403. paletteTexture.needsUpdate = true;
  404. </pre>
  405. <p>Anytime we want three.js to update the palette texture with
  406. the contents of the <code class="notranslate" translate="no">palette</code> array we need to set <code class="notranslate" translate="no">paletteTexture.needsUpdate</code>
  407. to <code class="notranslate" translate="no">true</code>.</p>
  408. <p>And then we still need to set the uniforms on the material.</p>
  409. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.SphereGeometry(1, 64, 32);
  410. const material = new THREE.MeshBasicMaterial({map: texture});
  411. material.onBeforeCompile = function(shader) {
  412. fragmentShaderReplacements.forEach((rep) =&gt; {
  413. shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
  414. });
  415. + shader.uniforms.paletteTexture = {value: paletteTexture};
  416. + shader.uniforms.indexTexture = {value: indexTexture};
  417. + shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
  418. };
  419. scene.add(new THREE.Mesh(geometry, material));
  420. </pre>
  421. <p>and with that we get randomly colored countries.</p>
  422. <p></p><div translate="no" class="threejs_example_container notranslate">
  423. <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>
  424. <a class="threejs_center" href="/manual/examples/indexed-textures-random-colors.html" target="_blank">click here to open in a separate window</a>
  425. </div>
  426. <p></p>
  427. <p>Now that we can see the index and palette textures are working
  428. let's manipulate the palette for highlighting.</p>
  429. <p>First let's make function that will let us pass in a three.js
  430. style color and give us values we can put in the palette texture.</p>
  431. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempColor = new THREE.Color();
  432. function get255BasedColor(color) {
  433. tempColor.set(color);
  434. const base = tempColor.toArray().map(v =&gt; v * 255);
  435. base.push(255); // alpha
  436. return base;
  437. }
  438. </pre>
  439. <p>Calling it like this <code class="notranslate" translate="no">color = get255BasedColor('red')</code> will
  440. return an array like <code class="notranslate" translate="no">[255, 0, 0, 255]</code>.</p>
  441. <p>Next let's use it to make a few colors and fill out the
  442. palette.</p>
  443. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const selectedColor = get255BasedColor('red');
  444. const unselectedColor = get255BasedColor('#444');
  445. const oceanColor = get255BasedColor('rgb(100,200,255)');
  446. resetPalette();
  447. function setPaletteColor(index, color) {
  448. palette.set(color, index * 4);
  449. }
  450. function resetPalette() {
  451. // make all colors the unselected color
  452. for (let i = 1; i &lt; maxNumCountries; ++i) {
  453. setPaletteColor(i, unselectedColor);
  454. }
  455. // set the ocean color (index #0)
  456. setPaletteColor(0, oceanColor);
  457. paletteTexture.needsUpdate = true;
  458. }
  459. </pre>
  460. <p>Now let's use those functions to update the palette when a country
  461. is selected</p>
  462. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  463. const rect = canvas.getBoundingClientRect();
  464. return {
  465. x: (event.clientX - rect.left) * canvas.width / rect.width,
  466. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  467. };
  468. }
  469. function pickCountry(event) {
  470. // exit if we have not loaded the data yet
  471. if (!countryInfos) {
  472. return;
  473. }
  474. const position = getCanvasRelativePosition(event);
  475. const id = pickHelper.pick(position, pickingScene, camera);
  476. if (id &gt; 0) {
  477. const countryInfo = countryInfos[id - 1];
  478. const selected = !countryInfo.selected;
  479. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  480. unselectAllCountries();
  481. }
  482. numCountriesSelected += selected ? 1 : -1;
  483. countryInfo.selected = selected;
  484. + setPaletteColor(id, selected ? selectedColor : unselectedColor);
  485. + paletteTexture.needsUpdate = true;
  486. } else if (numCountriesSelected) {
  487. unselectAllCountries();
  488. }
  489. requestRenderIfNotRequested();
  490. }
  491. function unselectAllCountries() {
  492. numCountriesSelected = 0;
  493. countryInfos.forEach((countryInfo) =&gt; {
  494. countryInfo.selected = false;
  495. });
  496. + resetPalette();
  497. }
  498. </pre>
  499. <p>and we that we should be able to highlight 1 or more countries.</p>
  500. <p></p><div translate="no" class="threejs_example_container notranslate">
  501. <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>
  502. <a class="threejs_center" href="/manual/examples/indexed-textures-picking-and-highlighting.html" target="_blank">click here to open in a separate window</a>
  503. </div>
  504. <p></p>
  505. <p>That seems to work!</p>
  506. <p>One minor thing is we can't spin the globe without changing
  507. the selection state. If we select a country and then want to
  508. rotate the globe the selection will change.</p>
  509. <p>Let's try to fix that. Off the top of my head we can check 2 things.
  510. How much time passed between clicking and letting go.
  511. Another is did the user actually move the mouse. If the
  512. time is short or if they didn't move the mouse then it
  513. was probably a click. Otherwise they were probably trying
  514. to drag the globe.</p>
  515. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const maxClickTimeMs = 200;
  516. +const maxMoveDeltaSq = 5 * 5;
  517. +const startPosition = {};
  518. +let startTimeMs;
  519. +
  520. +function recordStartTimeAndPosition(event) {
  521. + startTimeMs = performance.now();
  522. + const pos = getCanvasRelativePosition(event);
  523. + startPosition.x = pos.x;
  524. + startPosition.y = pos.y;
  525. +}
  526. function getCanvasRelativePosition(event) {
  527. const rect = canvas.getBoundingClientRect();
  528. return {
  529. x: (event.clientX - rect.left) * canvas.width / rect.width,
  530. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  531. };
  532. }
  533. function pickCountry(event) {
  534. // exit if we have not loaded the data yet
  535. if (!countryInfos) {
  536. return;
  537. }
  538. + // if it's been a moment since the user started
  539. + // then assume it was a drag action, not a select action
  540. + const clickTimeMs = performance.now() - startTimeMs;
  541. + if (clickTimeMs &gt; maxClickTimeMs) {
  542. + return;
  543. + }
  544. +
  545. + // if they moved assume it was a drag action
  546. + const position = getCanvasRelativePosition(event);
  547. + const moveDeltaSq = (startPosition.x - position.x) ** 2 +
  548. + (startPosition.y - position.y) ** 2;
  549. + if (moveDeltaSq &gt; maxMoveDeltaSq) {
  550. + return;
  551. + }
  552. - const position = {x: event.clientX, y: event.clientY};
  553. const id = pickHelper.pick(position, pickingScene, camera);
  554. if (id &gt; 0) {
  555. const countryInfo = countryInfos[id - 1];
  556. const selected = !countryInfo.selected;
  557. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  558. unselectAllCountries();
  559. }
  560. numCountriesSelected += selected ? 1 : -1;
  561. countryInfo.selected = selected;
  562. setPaletteColor(id, selected ? selectedColor : unselectedColor);
  563. paletteTexture.needsUpdate = true;
  564. } else if (numCountriesSelected) {
  565. unselectAllCountries();
  566. }
  567. requestRenderIfNotRequested();
  568. }
  569. function unselectAllCountries() {
  570. numCountriesSelected = 0;
  571. countryInfos.forEach((countryInfo) =&gt; {
  572. countryInfo.selected = false;
  573. });
  574. resetPalette();
  575. }
  576. +canvas.addEventListener('pointerdown', recordStartTimeAndPosition);
  577. canvas.addEventListener('pointerup', pickCountry);
  578. </pre>
  579. <p>and with those changes it <em>seems</em> like it works to me.</p>
  580. <p></p><div translate="no" class="threejs_example_container notranslate">
  581. <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>
  582. <a class="threejs_center" href="/manual/examples/indexed-textures-picking-debounced.html" target="_blank">click here to open in a separate window</a>
  583. </div>
  584. <p></p>
  585. <p>I'm not a UX expert so I'd love to hear if there is a better
  586. solution.</p>
  587. <p>I hope that gave you some idea of how indexed graphics can be useful
  588. and how you can modify the shaders three.js makes to add simple features.
  589. How to use GLSL, the language the shaders are written in, is too much for
  590. this article. There are a few links to some info in
  591. <a href="post-processing.html">the article on post processing</a>.</p>
  592. </div>
  593. </div>
  594. </div>
  595. <script src="../resources/prettify.js"></script>
  596. <script src="../resources/lesson.js"></script>
  597. </body></html>