indexed-textures.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <!DOCTYPE html><html lang="fr"><head>
  2. <meta charset="utf-8">
  3. <title>Textures Indexées pour la Sélection et la Couleur</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 – Textures Indexées pour la Sélection et la Couleur">
  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. <script type="importmap">
  14. {
  15. "imports": {
  16. "three": "../../build/three.module.js"
  17. }
  18. }
  19. </script>
  20. </head>
  21. <body>
  22. <div class="container">
  23. <div class="lesson-title">
  24. <h1>Textures Indexées pour la Sélection et la Couleur</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>Cet article est une continuation de <a href="align-html-elements-to-3d.html">un article sur l'alignement des éléments HTML en 3D</a>.
  29. Si vous ne l'avez pas encore lu, vous devriez commencer par là avant de continuer ici.</p>
  30. <p>Parfois, l'utilisation de three.js nécessite de trouver des solutions créatives.
  31. Je ne suis pas sûr que ce soit une excellente solution, mais j'ai pensé la partager et
  32. vous pouvez voir si elle suggère des solutions pour vos besoins.</p>
  33. <p>Dans <a href="align-html-elements-to-3d.html">l'article précédent</a>, nous
  34. avons affiché les noms de pays autour d'un globe 3D. Comment pourrions-nous permettre à
  35. l'utilisateur de sélectionner un pays et d'afficher sa sélection ?</p>
  36. <p>La première idée qui vient à l'esprit est de générer la géométrie pour chaque pays.
  37. Nous pourrions <a href="picking.html">utiliser une solution de picking</a> comme nous l'avons vu précédemment.
  38. Nous construirions une géométrie 3D pour chaque pays. Si l'utilisateur clique sur le maillage de
  39. ce pays, nous saurions quel pays a été cliqué.</p>
  40. <p>Donc, juste pour vérifier cette solution, j'ai essayé de générer des maillages 3D de tous les pays
  41. en utilisant les mêmes données que celles que j'ai utilisées pour générer les contours
  42. <a href="align-html-elements-to-3d.html">dans l'article précédent</a>.
  43. Le résultat était un fichier GLTF (.glb) binaire de 15,5 Mo. Faire télécharger 15,5 Mo
  44. à l'utilisateur me semble excessif.</p>
  45. <p>Il existe de nombreuses façons de compresser les données. La première serait probablement
  46. d'appliquer un algorithme pour réduire la résolution des contours. Je n'ai pas passé
  47. de temps à explorer cette solution. Pour les frontières des États-Unis, c'est probablement un
  48. gain énorme. Pour les frontières du Canada, probablement beaucoup moins.</p>
  49. <p>Une autre solution serait d'utiliser simplement la compression de données réelle. Par exemple, la compression Gzip
  50. du fichier l'a réduit à 11 Mo. C'est 30% de moins, mais probablement pas suffisant.</p>
  51. <p>Nous pourrions stocker toutes les données sous forme de valeurs de plage sur 16 bits au lieu de valeurs flottantes sur 32 bits.
  52. Ou nous pourrions utiliser quelque chose comme <a href="https://google.github.io/draco/">la compression Draco</a>
  53. et peut-être que cela suffirait. Je n'ai pas vérifié et je vous encourage à vérifier
  54. par vous-même et à me dire comment ça se passe, car j'aimerais le savoir. 😅</p>
  55. <p>Dans mon cas, j'ai pensé à <a href="picking.html">la solution de picking GPU</a>
  56. que nous avons abordée à la fin de <a href="picking.html">l'article sur le picking</a>. Dans
  57. cette solution, nous avons dessiné chaque maillage avec une couleur unique qui représentait
  58. l'ID de ce maillage. Nous avons ensuite dessiné tous les maillages et regardé la couleur
  59. sur laquelle on a cliqué.</p>
  60. <p>En nous inspirant de cela, nous pourrions pré-générer une carte des pays où
  61. la couleur de chaque pays est son numéro d'index dans notre tableau de pays. Nous pourrions
  62. alors utiliser une technique de picking GPU similaire. Nous dessinerions le globe hors écran en utilisant
  63. cette texture d'index. Regarder la couleur du pixel sur lequel l'utilisateur clique
  64. nous donnerait l'ID du pays.</p>
  65. <p>Donc, j'<a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">ai écrit du code</a>
  66. pour générer une telle texture. La voici.</p>
  67. <div class="threejs_center"><img src="../examples/resources/data/world/country-index-texture.png" style="width: 700px;"></div>
  68. <p>Note : Les données utilisées pour générer cette texture proviennent de <a href="http://thematicmapping.org/downloads/world_borders.php">ce site web</a>
  69. et sont donc sous licence <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>.</p>
  70. <p>Elle ne fait que 217 Ko, bien mieux que les 14 Mo pour les maillages de pays. En fait, nous pourrions probablement
  71. même réduire la résolution, mais 217 Ko semble suffisant pour l'instant.</p>
  72. <p>Alors essayons de l'utiliser pour sélectionner des pays.</p>
  73. <p>En prenant du code de <a href="picking.html">l'exemple de picking GPU</a>, nous avons besoin
  74. d'une scène pour le picking.</p>
  75. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickingScene = new THREE.Scene();
  76. pickingScene.background = new THREE.Color(0);
  77. </pre>
  78. <p>et nous devons ajouter le globe avec notre texture d'index à la
  79. scène de picking.</p>
  80. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  81. const loader = new THREE.TextureLoader();
  82. const geometry = new THREE.SphereGeometry(1, 64, 32);
  83. + const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
  84. + indexTexture.minFilter = THREE.NearestFilter;
  85. + indexTexture.magFilter = THREE.NearestFilter;
  86. +
  87. + const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
  88. + pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
  89. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  90. const material = new THREE.MeshBasicMaterial({map: texture});
  91. scene.add(new THREE.Mesh(geometry, material));
  92. }
  93. </pre>
  94. <p>Ensuite, copions la classe <code class="notranslate" translate="no">GPUPickingHelper</code> que nous avons
  95. utilisée précédemment avec quelques modifications mineures.</p>
  96. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class GPUPickHelper {
  97. constructor() {
  98. // créer une cible de rendu de 1x1 pixel
  99. this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
  100. this.pixelBuffer = new Uint8Array(4);
  101. - this.pickedObject = null;
  102. - this.pickedObjectSavedColor = 0;
  103. }
  104. pick(cssPosition, scene, camera) {
  105. const {pickingTexture, pixelBuffer} = this;
  106. // définir le décalage de la vue pour représenter juste un seul pixel sous la souris
  107. const pixelRatio = renderer.getPixelRatio();
  108. camera.setViewOffset(
  109. renderer.getContext().drawingBufferWidth, // largeur totale
  110. renderer.getContext().drawingBufferHeight, // haut total
  111. cssPosition.x * pixelRatio | 0, // coordonnée x du rectangle
  112. cssPosition.y * pixelRatio | 0, // coordonnée y du rectangle
  113. 1, // largeur du rectangle
  114. 1, // hauteur du rectangle
  115. );
  116. // effectuer le rendu de la scène
  117. renderer.setRenderTarget(pickingTexture);
  118. renderer.render(scene, camera);
  119. renderer.setRenderTarget(null);
  120. // effacer le décalage de la vue pour que le rendu revienne à la normale
  121. camera.clearViewOffset();
  122. // lire le pixel
  123. renderer.readRenderTargetPixels(
  124. pickingTexture,
  125. 0, // x
  126. 0, // y
  127. 1, // width
  128. 1, // height
  129. pixelBuffer);
  130. + const id =
  131. + (pixelBuffer[0] &lt;&lt; 16) |
  132. + (pixelBuffer[1] &lt;&lt; 8) |
  133. + (pixelBuffer[2] &lt;&lt; 0);
  134. +
  135. + return id;
  136. - const id =
  137. - (pixelBuffer[0] &lt;&lt; 16) |
  138. - (pixelBuffer[1] &lt;&lt; 8) |
  139. - (pixelBuffer[2] );
  140. - const intersectedObject = idToObject[id];
  141. - if (intersectedObject) {
  142. - // pick the first object. It's the closest one
  143. - this.pickedObject = intersectedObject;
  144. - // save its color
  145. - this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  146. - // set its emissive color to flashing red/yellow
  147. - this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  148. - }
  149. }
  150. }
  151. </pre>
  152. <p>Maintenant, nous pouvons l'utiliser pour sélectionner des pays.</p>
  153. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new GPUPickHelper();
  154. function getCanvasRelativePosition(event) {
  155. const rect = canvas.getBoundingClientRect();
  156. return {
  157. x: (event.clientX - rect.left) * canvas.width / rect.width,
  158. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  159. };
  160. }
  161. function pickCountry(event) {
  162. // sortir si les données ne sont pas encore chargées
  163. if (!countryInfos) {
  164. return;
  165. }
  166. const position = getCanvasRelativePosition(event);
  167. const id = pickHelper.pick(position, pickingScene, camera);
  168. if (id &gt; 0) {
  169. // nous avons cliqué sur un pays. Basculer sa propriété 'selected'
  170. const countryInfo = countryInfos[id - 1];
  171. const selected = !countryInfo.selected;
  172. // si nous sélectionnons ce pays et que les touches modificatrices ne sont pas
  173. // enfoncées, désélectionner tout le reste.
  174. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  175. unselectAllCountries();
  176. }
  177. numCountriesSelected += selected ? 1 : -1;
  178. countryInfo.selected = selected;
  179. } else if (numCountriesSelected) {
  180. // l'océan ou le ciel a été cliqué
  181. unselectAllCountries();
  182. }
  183. requestRenderIfNotRequested();
  184. }
  185. function unselectAllCountries() {
  186. numCountriesSelected = 0;
  187. countryInfos.forEach((countryInfo) =&gt; {
  188. countryInfo.selected = false;
  189. });
  190. }
  191. canvas.addEventListener('pointerup', pickCountry);
  192. </pre>
  193. <p>Le code ci-dessus définit/annule la propriété <code class="notranslate" translate="no">selected</code> sur
  194. le tableau de pays. Si <code class="notranslate" translate="no">shift</code> ou <code class="notranslate" translate="no">ctrl</code> ou <code class="notranslate" translate="no">cmd</code>
  195. est enfoncé, vous pouvez sélectionner plus d'un pays.</p>
  196. <p>Il ne reste plus qu'à afficher les pays sélectionnés. Pour l'instant,
  197. mettons simplement à jour les labels.</p>
  198. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateLabels() {
  199. // sortir si les données ne sont pas encore chargées
  200. if (!countryInfos) {
  201. return;
  202. }
  203. const large = settings.minArea * settings.minArea;
  204. // obtenir une matrice qui représente une orientation relative de la caméra
  205. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  206. // obtenir la position de la caméra
  207. camera.getWorldPosition(cameraPosition);
  208. for (const countryInfo of countryInfos) {
  209. - const {position, elem, area} = countryInfo;
  210. - // large enough?
  211. - if (area &lt; large) {
  212. + const {position, elem, area, selected} = countryInfo;
  213. + const largeEnough = area &gt;= large;
  214. + const show = selected || (numCountriesSelected === 0 &amp;&amp; largeEnough);
  215. + if (!show) {
  216. elem.style.display = 'none';
  217. continue;
  218. }
  219. ...
  220. </pre>
  221. <p>et avec cela, nous devrions pouvoir sélectionner des pays</p>
  222. <p></p><div translate="no" class="threejs_example_container notranslate">
  223. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/indexed-textures-picking.html"></iframe></div>
  224. <a class="threejs_center" href="/manual/examples/indexed-textures-picking.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  225. </div>
  226. <p></p>
  227. <p>Le code affiche toujours les pays en fonction de leur superficie, mais si vous
  228. en cliquez sur un, seul celui-ci aura un label.</p>
  229. <p>Cela semble donc une solution raisonnable pour sélectionner des pays,
  230. mais qu'en est-il de la mise en évidence des pays sélectionnés ?</p>
  231. <p>Pour cela, nous pouvons nous inspirer des <em>graphiques palettisés</em>.</p>
  232. <p><a href="https://en.wikipedia.org/wiki/Palette_%28computing%29">Les graphiques palettisés</a>
  233. ou <a href="https://en.wikipedia.org/wiki/Indexed_color">couleurs indexées</a> sont
  234. ce qu'utilisaient les anciens systèmes comme l'Atari 800, l'Amiga, la NES,
  235. la Super Nintendo et même les anciens PC IBM. Au lieu de stocker des images bitmap
  236. en couleurs RGBA (8 bits par couleur, 32 octets par pixel ou plus), ils stockaient
  237. des images bitmap en valeurs de 8 bits ou moins. La valeur de chaque pixel était un index
  238. dans une palette. Par exemple, une valeur
  239. de 3 dans l'image signifie "afficher la couleur 3". La couleur que représente la couleur n°3 est
  240. définie ailleurs dans ce qu'on appelle une "palette".</p>
  241. <p>En JavaScript, vous pouvez l'imaginer comme ceci</p>
  242. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const face7x7PixelImageData = [
  243. 0, 1, 1, 1, 1, 1, 0,
  244. 1, 0, 0, 0, 0, 0, 1,
  245. 1, 0, 2, 0, 2, 0, 1,
  246. 1, 0, 0, 0, 0, 0, 1,
  247. 1, 0, 3, 3, 3, 0, 1,
  248. 1, 0, 0, 0, 0, 0, 1,
  249. 0, 1, 1, 1, 1, 1, 1,
  250. ];
  251. const palette = [
  252. [255, 255, 255], // white
  253. [ 0, 0, 0], // black
  254. [ 0, 255, 255], // cyan
  255. [255, 0, 0], // red
  256. ];
  257. </pre>
  258. <p>Où chaque pixel dans les données de l'image est un index dans la palette. Si vous interprétiez
  259. les données de l'image à travers la palette ci-dessus, vous obtiendriez cette image</p>
  260. <div class="threejs_center"><img src="../resources/images/7x7-indexed-face.png"></div>
  261. <p>Dans notre cas, nous avons déjà une texture ci-dessus qui a un ID différent
  262. par pays. Ainsi, nous pourrions utiliser cette même texture à travers une texture de palette
  263. pour donner à chaque pays sa propre couleur. En modifiant la texture de palette,
  264. nous pouvons colorer chaque pays individuellement. Par exemple, en mettant
  265. toute la texture de palette en noir, puis en attribuant une couleur différente à l'entrée
  266. d'un pays dans la palette, nous pouvons mettre en évidence uniquement ce pays.</p>
  267. <p>Pour réaliser des graphiques à index palettisés, il faut du code shader personnalisé.
  268. Modifions les shaders par défaut dans three.js.
  269. De cette façon, nous pourrons utiliser l'éclairage et d'autres fonctionnalités si nous le souhaitons.</p>
  270. <p>Comme nous l'avons vu dans <a href="optimize-lots-of-objects-animated.html">l'article sur l'animation de nombreux objets</a>,
  271. nous pouvons modifier les shaders par défaut en ajoutant une fonction à la propriété
  272. <code class="notranslate" translate="no">onBeforeCompile</code> d'un matériau.</p>
  273. <p>Le shader de fragment par défaut ressemble à ceci avant la compilation.</p>
  274. <pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#include &lt;common&gt;
  275. #include &lt;color_pars_fragment&gt;
  276. #include &lt;uv_pars_fragment&gt;
  277. #include &lt;map_pars_fragment&gt;
  278. #include &lt;alphamap_pars_fragment&gt;
  279. #include &lt;aomap_pars_fragment&gt;
  280. #include &lt;lightmap_pars_fragment&gt;
  281. #include &lt;envmap_pars_fragment&gt;
  282. #include &lt;fog_pars_fragment&gt;
  283. #include &lt;specularmap_pars_fragment&gt;
  284. #include &lt;logdepthbuf_pars_fragment&gt;
  285. #include &lt;clipping_planes_pars_fragment&gt;
  286. void main() {
  287. #include &lt;clipping_planes_fragment&gt;
  288. vec4 diffuseColor = vec4( diffuse, opacity );
  289. #include &lt;logdepthbuf_fragment&gt;
  290. #include &lt;map_fragment&gt;
  291. #include &lt;color_fragment&gt;
  292. #include &lt;alphamap_fragment&gt;
  293. #include &lt;alphatest_fragment&gt;
  294. #include &lt;specularmap_fragment&gt;
  295. ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
  296. #ifdef USE_LIGHTMAP
  297. reflectedLight.indirectDiffuse += texture2D( lightMap, vLightMapUv ).xyz * lightMapIntensity;
  298. #else
  299. reflectedLight.indirectDiffuse += vec3( 1.0 );
  300. #endif
  301. #include &lt;aomap_fragment&gt;
  302. reflectedLight.indirectDiffuse *= diffuseColor.rgb;
  303. vec3 outgoingLight = reflectedLight.indirectDiffuse;
  304. #include &lt;envmap_fragment&gt;
  305. gl_FragColor = vec4( outgoingLight, diffuseColor.a );
  306. #include &lt;premultiplied_alpha_fragment&gt;
  307. #include &lt;tonemapping_fragment&gt;
  308. #include &lt;colorspace_fragment&gt;
  309. #include &lt;fog_fragment&gt;
  310. }
  311. </pre>
  312. <p><a href="https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders/ShaderChunk">En fouillant dans tous ces extraits</a>,
  313. nous constatons que three.js utilise une variable appelée <code class="notranslate" translate="no">diffuseColor</code> pour gérer la
  314. couleur de base du matériau. Il la définit dans l'<a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js">extrait</a> <code class="notranslate" translate="no">&lt;color_fragment&gt;</code>,
  315. nous devrions donc pouvoir la modifier après ce point.</p>
  316. <p><code class="notranslate" translate="no">diffuseColor</code> à ce stade du shader devrait déjà être la couleur de
  317. notre texture de contour, nous pouvons donc chercher la couleur dans une texture de palette
  318. et les mélanger pour le résultat final.</p>
  319. <p>Comme nous l'<a href="optimize-lots-of-objects-animated.html">avons fait précédemment</a>, nous allons créer un tableau
  320. de chaînes de recherche et de remplacement et les appliquer au shader dans
  321. <a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate" translate="no">Material.onBeforeCompile</code></a>.</p>
  322. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  323. const loader = new THREE.TextureLoader();
  324. const geometry = new THREE.SphereGeometry(1, 64, 32);
  325. const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
  326. indexTexture.minFilter = THREE.NearestFilter;
  327. indexTexture.magFilter = THREE.NearestFilter;
  328. const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
  329. pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
  330. + const fragmentShaderReplacements = [
  331. + {
  332. + from: '#include &lt;common&gt;',
  333. + to: `
  334. + #include &lt;common&gt;
  335. + uniform sampler2D indexTexture;
  336. + uniform sampler2D paletteTexture;
  337. + uniform float paletteTextureWidth;
  338. + `,
  339. + },
  340. + {
  341. + from: '#include &lt;color_fragment&gt;',
  342. + to: `
  343. + #include &lt;color_fragment&gt;
  344. + {
  345. + vec4 indexColor = texture2D(indexTexture, vUv);
  346. + float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
  347. + vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
  348. + vec4 paletteColor = texture2D(paletteTexture, paletteUV);
  349. + // diffuseColor.rgb += paletteColor.rgb; // white outlines
  350. + diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb; // black outlines
  351. + }
  352. + `,
  353. + },
  354. + ];
  355. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  356. const material = new THREE.MeshBasicMaterial({map: texture});
  357. + material.onBeforeCompile = function(shader) {
  358. + fragmentShaderReplacements.forEach((rep) =&gt; {
  359. + shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
  360. + });
  361. + };
  362. scene.add(new THREE.Mesh(geometry, material));
  363. }
  364. </pre>
  365. <p>Comme vous pouvez le voir ci-dessus, nous ajoutons 3 uniformes, <code class="notranslate" translate="no">indexTexture</code>, <code class="notranslate" translate="no">paletteTexture</code>,
  366. et <code class="notranslate" translate="no">paletteTextureWidth</code>. Nous obtenons une couleur à partir de <code class="notranslate" translate="no">indexTexture</code>
  367. et la convertissons en index. <code class="notranslate" translate="no">vUv</code> sont les coordonnées de texture fournies par
  368. three.js. Nous utilisons ensuite cet index pour obtenir une couleur à partir de la texture de palette.
  369. Nous mélangeons ensuite le résultat avec la <code class="notranslate" translate="no">diffuseColor</code> actuelle. La <code class="notranslate" translate="no">diffuseColor</code>
  370. à ce stade est notre texture de contour noir et blanc, donc si nous ajoutons les 2 couleurs,
  371. nous obtiendrons des contours blancs. Si nous soustrayons la couleur diffuse actuelle, nous obtiendrons
  372. des contours noirs.</p>
  373. <p>Avant de pouvoir effectuer le rendu, nous devons configurer la texture de palette
  374. et ces 3 uniformes.</p>
  375. <p>Pour la texture de palette, elle doit juste être suffisamment large pour
  376. contenir une couleur par pays + une pour l'océan (id = 0).
  377. Il y a 240 et quelques pays. Nous pourrions attendre que la
  378. liste des pays se charge pour obtenir un nombre exact ou le chercher.
  379. Il n'y a pas beaucoup de mal à choisir un nombre plus grand,
  380. donc choisissons 512.</p>
  381. <p>Voici le code pour créer la texture de palette</p>
  382. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const maxNumCountries = 512;
  383. const paletteTextureWidth = maxNumCountries;
  384. const paletteTextureHeight = 1;
  385. const palette = new Uint8Array(paletteTextureWidth * 4);
  386. const paletteTexture = new THREE.DataTexture(
  387. palette, paletteTextureWidth, paletteTextureHeight);
  388. paletteTexture.minFilter = THREE.NearestFilter;
  389. paletteTexture.magFilter = THREE.NearestFilter;
  390. </pre>
  391. <p>Une <a href="/docs/#api/en/textures/DataTexture"><code class="notranslate" translate="no">DataTexture</code></a> nous permet de donner des données brutes à une texture. Dans ce cas,
  392. nous lui donnons 512 couleurs RGBA, 4 octets chacune où chaque octet représente
  393. respectivement le rouge, le vert et le bleu en utilisant des valeurs allant de 0 à 255.</p>
  394. <p>Remplissons-la avec des couleurs aléatoires juste pour voir si ça fonctionne</p>
  395. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let i = 1; i &lt; palette.length; ++i) {
  396. palette[i] = Math.random() * 256;
  397. }
  398. // définir la couleur de l'océan (index #0)
  399. palette.set([100, 200, 255, 255], 0);
  400. paletteTexture.needsUpdate = true;
  401. </pre>
  402. <p>Chaque fois que nous voulons que three.js mette à jour la texture de palette avec
  403. le contenu du tableau <code class="notranslate" translate="no">palette</code>, nous devons définir <code class="notranslate" translate="no">paletteTexture.needsUpdate</code>
  404. sur <code class="notranslate" translate="no">true</code>.</p>
  405. <p>Et ensuite, nous devons toujours définir les uniformes sur le matériau.</p>
  406. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.SphereGeometry(1, 64, 32);
  407. const material = new THREE.MeshBasicMaterial({map: texture});
  408. material.onBeforeCompile = function(shader) {
  409. fragmentShaderReplacements.forEach((rep) =&gt; {
  410. shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
  411. });
  412. + shader.uniforms.paletteTexture = {value: paletteTexture};
  413. + shader.uniforms.indexTexture = {value: indexTexture};
  414. + shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
  415. };
  416. scene.add(new THREE.Mesh(geometry, material));
  417. </pre>
  418. <p>et avec cela, nous obtenons des pays colorés aléatoirement.</p>
  419. <p></p><div translate="no" class="threejs_example_container notranslate">
  420. <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>
  421. <a class="threejs_center" href="/manual/examples/indexed-textures-random-colors.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  422. </div>
  423. <p></p>
  424. <p>Maintenant que nous pouvons voir que les textures d'index et de palette fonctionnent,
  425. manipulons la palette pour la mise en évidence.</p>
  426. <p>Faisons d'abord une fonction qui nous permettra de passer une couleur de style three.js
  427. et de nous donner les valeurs que nous pouvons mettre dans la texture de palette.</p>
  428. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempColor = new THREE.Color();
  429. function get255BasedColor(color) {
  430. tempColor.set(color);
  431. const base = tempColor.toArray().map(v =&gt; v * 255);
  432. base.push(255); // alpha
  433. return base;
  434. }
  435. </pre>
  436. <p>L'appeler comme ceci <code class="notranslate" translate="no">color = get255BasedColor('red')</code> retournera
  437. un tableau comme <code class="notranslate" translate="no">[255, 0, 0, 255]</code>.</p>
  438. <p>Ensuite, utilisons-la pour créer quelques couleurs et remplir la
  439. palette.</p>
  440. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const selectedColor = get255BasedColor('red');
  441. const unselectedColor = get255BasedColor('#444');
  442. const oceanColor = get255BasedColor('rgb(100,200,255)');
  443. resetPalette();
  444. function setPaletteColor(index, color) {
  445. palette.set(color, index * 4);
  446. }
  447. function resetPalette() {
  448. // définir toutes les couleurs sur la couleur non sélectionnée
  449. for (let i = 1; i &lt; maxNumCountries; ++i) {
  450. setPaletteColor(i, unselectedColor);
  451. }
  452. // définir la couleur de l'océan (index #0)
  453. setPaletteColor(0, oceanColor);
  454. paletteTexture.needsUpdate = true;
  455. }
  456. </pre>
  457. <p>Maintenant, utilisons ces fonctions pour mettre à jour la palette lorsqu'un pays
  458. est sélectionné</p>
  459. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  460. const rect = canvas.getBoundingClientRect();
  461. return {
  462. x: (event.clientX - rect.left) * canvas.width / rect.width,
  463. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  464. };
  465. }
  466. function pickCountry(event) {
  467. // sortir si les données ne sont pas encore chargées
  468. if (!countryInfos) {
  469. return;
  470. }
  471. const position = getCanvasRelativePosition(event);
  472. const id = pickHelper.pick(position, pickingScene, camera);
  473. if (id &gt; 0) {
  474. const countryInfo = countryInfos[id - 1];
  475. const selected = !countryInfo.selected;
  476. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  477. unselectAllCountries();
  478. }
  479. numCountriesSelected += selected ? 1 : -1;
  480. countryInfo.selected = selected;
  481. + setPaletteColor(id, selected ? selectedColor : unselectedColor);
  482. + paletteTexture.needsUpdate = true;
  483. } else if (numCountriesSelected) {
  484. unselectAllCountries();
  485. }
  486. requestRenderIfNotRequested();
  487. }
  488. function unselectAllCountries() {
  489. numCountriesSelected = 0;
  490. countryInfos.forEach((countryInfo) =&gt; {
  491. countryInfo.selected = false;
  492. });
  493. + resetPalette();
  494. }
  495. </pre>
  496. <p>et avec cela, nous devrions pouvoir mettre en évidence 1 ou plusieurs pays.</p>
  497. <p></p><div translate="no" class="threejs_example_container notranslate">
  498. <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>
  499. <a class="threejs_center" href="/manual/examples/indexed-textures-picking-and-highlighting.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  500. </div>
  501. <p></p>
  502. <p>Cela semble fonctionner !</p>
  503. <p>Un petit détail est que nous ne pouvons pas faire tourner le globe sans changer
  504. l'état de sélection. Si nous sélectionnons un pays et voulons ensuite
  505. faire pivoter le globe, la sélection changera.</p>
  506. <p>Essayons de régler cela. Rapidement, nous pouvons vérifier 2 choses.
  507. Le temps écoulé entre le clic et le lâcher. Une autre est de savoir si
  508. l'utilisateur a réellement déplacé la souris. Si le
  509. temps est court ou s'il n'a pas bougé la souris, c'était
  510. probablement un clic. Sinon, il essayait probablement de
  511. faire glisser le globe.</p>
  512. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const maxClickTimeMs = 200;
  513. +const maxMoveDeltaSq = 5 * 5;
  514. +const startPosition = {};
  515. +let startTimeMs;
  516. +
  517. +function recordStartTimeAndPosition(event) {
  518. + startTimeMs = performance.now();
  519. + const pos = getCanvasRelativePosition(event);
  520. + startPosition.x = pos.x;
  521. + startPosition.y = pos.y;
  522. +}
  523. function getCanvasRelativePosition(event) {
  524. const rect = canvas.getBoundingClientRect();
  525. return {
  526. x: (event.clientX - rect.left) * canvas.width / rect.width,
  527. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  528. };
  529. }
  530. function pickCountry(event) {
  531. // sortir si les données ne sont pas encore chargées
  532. if (!countryInfos) {
  533. return;
  534. }
  535. + // s'il s'est écoulé un certain temps depuis que l'utilisateur a commencé
  536. + // alors supposer qu'il s'agissait d'une action de glissement, pas de sélection
  537. + const clickTimeMs = performance.now() - startTimeMs;
  538. + if (clickTimeMs &gt; maxClickTimeMs) {
  539. + return;
  540. + }
  541. +
  542. + // s'ils ont bougé, supposer qu'il s'agissait d'une action de glissement
  543. + const position = getCanvasRelativePosition(event);
  544. + const moveDeltaSq = (startPosition.x - position.x) ** 2 +
  545. + (startPosition.y - position.y) ** 2;
  546. + if (moveDeltaSq &gt; maxMoveDeltaSq) {
  547. + return;
  548. + }
  549. - const position = {x: event.clientX, y: event.clientY};
  550. const id = pickHelper.pick(position, pickingScene, camera);
  551. if (id &gt; 0) {
  552. const countryInfo = countryInfos[id - 1];
  553. const selected = !countryInfo.selected;
  554. if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
  555. unselectAllCountries();
  556. }
  557. numCountriesSelected += selected ? 1 : -1;
  558. countryInfo.selected = selected;
  559. setPaletteColor(id, selected ? selectedColor : unselectedColor);
  560. paletteTexture.needsUpdate = true;
  561. } else if (numCountriesSelected) {
  562. unselectAllCountries();
  563. }
  564. requestRenderIfNotRequested();
  565. }
  566. function unselectAllCountries() {
  567. numCountriesSelected = 0;
  568. countryInfos.forEach((countryInfo) =&gt; {
  569. countryInfo.selected = false;
  570. });
  571. resetPalette();
  572. }
  573. +canvas.addEventListener('pointerdown', recordStartTimeAndPosition);
  574. canvas.addEventListener('pointerup', pickCountry);
  575. </pre>
  576. <p>et avec ces modifications, il <em>semble</em> que cela fonctionne pour moi.</p>
  577. <p></p><div translate="no" class="threejs_example_container notranslate">
  578. <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>
  579. <a class="threejs_center" href="/manual/examples/indexed-textures-picking-debounced.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  580. </div>
  581. <p></p>
  582. <p>Je ne suis pas expert en UX, donc j'aimerais savoir s'il existe une meilleure
  583. solution.</p>
  584. <p>J'espère que cela vous a donné une idée de l'utilité des graphiques indexés et de la façon dont vous pouvez modifier les shaders créés par three.js pour ajouter des fonctionnalités simples. L'utilisation de GLSL, le langage dans lequel les shaders sont écrits, est trop vaste pour cet article. Il y a quelques liens vers des informations dans <a href="post-processing.html">l'article sur le post-traitement</a>.</p>
  585. </div>
  586. </div>
  587. </div>
  588. <script src="../resources/prettify.js"></script>
  589. <script src="../resources/lesson.js"></script>
  590. </body></html>
粤ICP备19079148号