align-html-elements-to-3d.html 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. <!DOCTYPE html><html lang="fr"><head>
  2. <meta charset="utf-8">
  3. <title>Aligner les éléments HTML en 3D</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 – Aligner les éléments HTML en 3D">
  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>Aligner les éléments HTML en 3D</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>Cet article fait partie d'une série d'articles sur three.js. Le premier article
  29. est <a href="fundamentals.html">les bases de three.js</a>. Si vous ne l'avez pas
  30. encore lu et que vous débutez avec three.js, vous pourriez vouloir commencer par là. </p>
  31. <p>Parfois, vous aimeriez afficher du texte dans votre scène 3D. Vous avez plusieurs options,
  32. chacune avec ses avantages et ses inconvénients.</p>
  33. <ul>
  34. <li><p>Utiliser du texte 3D</p>
  35. <p>Si vous regardez l'<a href="primitives.html">article sur les primitives</a>, vous verrez la <a href="/docs/#examples/en/geometries/TextGeometry"><code class="notranslate" translate="no">TextGeometry</code></a> qui
  36. permet de créer du texte 3D. Cela peut être utile pour des logos volants, mais probablement moins pour des statistiques, des informations,
  37. ou l'étiquetage de nombreux éléments.</p>
  38. </li>
  39. <li><p>Utiliser une texture avec du texte 2D dessiné dessus.</p>
  40. <p>L'article sur <a href="canvas-textures.html">l'utilisation d'un Canvas comme texture</a> montre comment utiliser
  41. un canvas comme texture. Vous pouvez dessiner du texte dans un canvas et l'<a href="billboards.html">afficher comme un panneau (billboard)</a>.
  42. L'avantage ici pourrait être que le texte est intégré à la scène 3D. Pour quelque chose comme un terminal d'ordinateur
  43. montré dans une scène 3D, cela pourrait être parfait.</p>
  44. </li>
  45. <li><p>Utiliser des éléments HTML et les positionner pour correspondre à la 3D</p>
  46. <p>L'avantage de cette approche est que vous pouvez utiliser tout le HTML. Votre HTML peut contenir plusieurs éléments. Il peut
  47. être stylisé avec du CSS. Il peut également être sélectionné par l'utilisateur car c'est du vrai texte. </p>
  48. </li>
  49. </ul>
  50. <p>Cet article couvrira cette dernière approche.</p>
  51. <p>Commençons simplement. Nous allons créer une scène 3D avec quelques primitives et ajouter une étiquette à chaque primitive. Nous commencerons
  52. avec un exemple tiré de <a href="responsive.html">l'article sur les pages responsives</a> </p>
  53. <p>Nous allons ajouter des <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> comme nous l'avons fait dans <a href="lights.html">l'article sur l'éclairage</a>.</p>
  54. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  55. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  56. </pre>
  57. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const controls = new OrbitControls(camera, canvas);
  58. controls.target.set(0, 0, 0);
  59. controls.update();
  60. </pre>
  61. <p>Nous devons fournir un élément HTML pour contenir nos éléments d'étiquette</p>
  62. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  63. - &lt;canvas id="c"&gt;&lt;/canvas&gt;
  64. + &lt;div id="container"&gt;
  65. + &lt;canvas id="c"&gt;&lt;/canvas&gt;
  66. + &lt;div id="labels"&gt;&lt;/div&gt;
  67. + &lt;/div&gt;
  68. &lt;/body&gt;
  69. </pre>
  70. <p>En plaçant à la fois le canvas et le <code class="notranslate" translate="no">&lt;div id="labels"&gt;</code> à l'intérieur d'un
  71. conteneur parent, nous pouvons les faire se superposer avec ce CSS</p>
  72. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
  73. - width: 100%;
  74. - height: 100%;
  75. + width: 100%; /* laisser notre conteneur décider de notre taille */
  76. + height: 100%;
  77. display: block;
  78. }
  79. +#container {
  80. + position: relative; /* fait de ceci l'origine de ses enfants */
  81. + width: 100%;
  82. + height: 100%;
  83. + overflow: hidden;
  84. +}
  85. +#labels {
  86. + position: absolute; /* nous permet de nous positionner à l'intérieur du conteneur */
  87. + left: 0; /* place notre position en haut à gauche du conteneur */
  88. + top: 0;
  89. + color: white;
  90. +}
  91. </pre>
  92. <p>ajoutons également du CSS pour les étiquettes elles-mêmes</p>
  93. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels&gt;div {
  94. position: absolute; /* nous permet de les positionner à l'intérieur du conteneur */
  95. left: 0; /* place leur position par défaut en haut à gauche du conteneur */
  96. top: 0;
  97. cursor: pointer; /* change le curseur en main quand la souris est dessus */
  98. font-size: large;
  99. user-select: none; /* empêche la sélection du texte */
  100. text-shadow: /* crée un contour noir */
  101. -1px -1px 0 #000,
  102. 0 -1px 0 #000,
  103. 1px -1px 0 #000,
  104. 1px 0 0 #000,
  105. 1px 1px 0 #000,
  106. 0 1px 0 #000,
  107. -1px 1px 0 #000,
  108. -1px 0 0 #000;
  109. }
  110. #labels&gt;div:hover {
  111. color: red;
  112. }
  113. </pre>
  114. <p>Maintenant, dans notre code, nous n'avons pas grand-chose à ajouter. Nous avions une fonction
  115. <code class="notranslate" translate="no">makeInstance</code> que nous utilisions pour générer des cubes. Faisons en sorte
  116. qu'elle ajoute également un élément d'étiquette.</p>
  117. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const labelContainerElem = document.querySelector('#labels');
  118. -function makeInstance(geometry, color, x) {
  119. +function makeInstance(geometry, color, x, name) {
  120. const material = new THREE.MeshPhongMaterial({color});
  121. const cube = new THREE.Mesh(geometry, material);
  122. scene.add(cube);
  123. cube.position.x = x;
  124. + const elem = document.createElement('div');
  125. + elem.textContent = name;
  126. + labelContainerElem.appendChild(elem);
  127. - return cube;
  128. + return {cube, elem};
  129. }
  130. </pre>
  131. <p>Comme vous pouvez le voir, nous ajoutons un <code class="notranslate" translate="no">&lt;div&gt;</code> au conteneur, un pour chaque cube. Nous
  132. retournons également un objet avec à la fois le <code class="notranslate" translate="no">cube</code> et l'<code class="notranslate" translate="no">elem</code> pour l'étiquette.</p>
  133. <p>Pour l'appeler, nous devons fournir un nom pour chacun</p>
  134. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [
  135. - makeInstance(geometry, 0x44aa88, 0),
  136. - makeInstance(geometry, 0x8844aa, -2),
  137. - makeInstance(geometry, 0xaa8844, 2),
  138. + makeInstance(geometry, 0x44aa88, 0, 'Aqua'),
  139. + makeInstance(geometry, 0x8844aa, -2, 'Purple'),
  140. + makeInstance(geometry, 0xaa8844, 2, 'Gold'),
  141. ];
  142. </pre>
  143. <p>Ce qui reste est le positionnement des éléments d'étiquette au moment du rendu</p>
  144. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  145. ...
  146. -cubes.forEach((cube, ndx) =&gt; {
  147. +cubes.forEach((cubeInfo, ndx) =&gt; {
  148. + const {cube, elem} = cubeInfo;
  149. const speed = 1 + ndx * .1;
  150. const rot = time * speed;
  151. cube.rotation.x = rot;
  152. cube.rotation.y = rot;
  153. + // obtient la position du centre du cube
  154. + cube.updateWorldMatrix(true, false);
  155. + cube.getWorldPosition(tempV);
  156. +
  157. + // obtient la coordonnée d'écran normalisée de cette position
  158. + // x et y seront dans la plage de -1 à +1, avec x = -1 étant
  159. + // à gauche et y = -1 étant en bas
  160. + tempV.project(camera);
  161. +
  162. + // convertit la position normalisée en coordonnées CSS
  163. + const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  164. + const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  165. +
  166. + // déplace l'élément à cette position
  167. + elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  168. });
  169. </pre>
  170. <p>Et avec cela, nous avons des étiquettes alignées sur leurs objets correspondants.</p>
  171. <p></p><div translate="no" class="threejs_example_container notranslate">
  172. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d.html"></iframe></div>
  173. <a class="threejs_center" href="/manual/examples/align-html-to-3d.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  174. </div>
  175. <p></p>
  176. <p>Il y a quelques problèmes que nous voudrons probablement résoudre.</p>
  177. <p>L'un d'eux est que si nous faisons pivoter les objets de manière à ce qu'ils se chevauchent, toutes les étiquettes
  178. se chevauchent également.</p>
  179. <div class="threejs_center"><img src="../resources/images/overlapping-labels.png" style="width: 307px;"></div>
  180. <p>Un autre est que si nous dézoomons beaucoup, de sorte que les objets sortent
  181. du frustum, les étiquettes apparaîtront toujours.</p>
  182. <p>Une solution possible au problème des objets qui se chevauchent est d'utiliser
  183. le <a href="picking.html">code de sélection (picking) de l'article sur la sélection</a>.
  184. Nous passerons la position de l'objet à l'écran, puis nous demanderons
  185. au <code class="notranslate" translate="no">RayCaster</code> de nous dire quels objets ont été intersectés.
  186. Si notre objet n'est pas le premier, alors il n'est pas à l'avant.</p>
  187. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  188. +const raycaster = new THREE.Raycaster();
  189. ...
  190. cubes.forEach((cubeInfo, ndx) =&gt; {
  191. const {cube, elem} = cubeInfo;
  192. const speed = 1 + ndx * .1;
  193. const rot = time * speed;
  194. cube.rotation.x = rot;
  195. cube.rotation.y = rot;
  196. // obtient la position du centre du cube
  197. cube.updateWorldMatrix(true, false);
  198. cube.getWorldPosition(tempV);
  199. // obtient la coordonnée d'écran normalisée de cette position
  200. // x et y seront dans la plage de -1 à +1, avec x = -1 étant
  201. // à gauche et y = -1 étant en bas
  202. tempV.project(camera);
  203. + // demande au raycaster tous les objets qui intersectent
  204. + // depuis l'œil vers la position de cet objet
  205. + raycaster.setFromCamera(tempV, camera);
  206. + const intersectedObjects = raycaster.intersectObjects(scene.children);
  207. + // Nous sommes visibles si la première intersection est cet objet.
  208. + const show = intersectedObjects.length &amp;&amp; cube === intersectedObjects[0].object;
  209. +
  210. + if (!show) {
  211. + // cache l'étiquette
  212. + elem.style.display = 'none';
  213. + } else {
  214. + // affiche l'étiquette
  215. + elem.style.display = '';
  216. // convertit la position normalisée en coordonnées CSS
  217. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  218. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  219. // déplace l'élément à cette position
  220. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  221. + }
  222. });
  223. </pre>
  224. <p>Cela gère le chevauchement.</p>
  225. <p>Pour gérer la sortie du frustum, nous pouvons ajouter cette vérification si l'origine de
  226. l'objet est en dehors du frustum en vérifiant <code class="notranslate" translate="no">tempV.z</code></p>
  227. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- if (!show) {
  228. + if (!show || Math.abs(tempV.z) &gt; 1) {
  229. // cache l'étiquette
  230. elem.style.display = 'none';
  231. </pre>
  232. <p>Cela fonctionne <em>plus ou moins</em> car les coordonnées normalisées que nous avons calculées incluent une valeur <code class="notranslate" translate="no">z</code>
  233. qui va de -1 lorsqu'elle est à la partie <code class="notranslate" translate="no">near</code> de notre frustum de caméra à +1 lorsqu'elle est
  234. à la partie <code class="notranslate" translate="no">far</code> de notre frustum de caméra.</p>
  235. <p></p><div translate="no" class="threejs_example_container notranslate">
  236. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-hiding.html"></iframe></div>
  237. <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-hiding.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  238. </div>
  239. <p></p>
  240. <p>Pour la vérification du frustum, la solution ci-dessus échoue car nous ne vérifions que l'origine de l'objet. Pour un objet
  241. volumineux, cette origine pourrait sortir du frustum, mais la moitié de l'objet pourrait encore s'y trouver.</p>
  242. <p>Une solution plus correcte serait de vérifier si l'objet lui-même est dans le frustum
  243. ou non. Malheureusement, cette vérification est lente. Pour 3 cubes, ce ne sera pas un problème,
  244. mais pour de nombreux objets, cela pourrait l'être.</p>
  245. <p>Three.js fournit quelques fonctions pour vérifier si la sphère englobante d'un objet est
  246. dans un frustum</p>
  247. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// au moment de l'initialisation
  248. const frustum = new THREE.Frustum();
  249. const viewProjection = new THREE.Matrix4();
  250. ...
  251. // avant de vérifier
  252. camera.updateMatrix();
  253. camera.updateMatrixWorld();
  254. camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
  255. ...
  256. // puis pour chaque maillage
  257. someMesh.updateMatrix();
  258. someMesh.updateMatrixWorld();
  259. viewProjection.multiplyMatrices(
  260. camera.projectionMatrix, camera.matrixWorldInverse);
  261. frustum.setFromProjectionMatrix(viewProjection);
  262. const inFrustum = frustum.contains(someMesh));
  263. </pre>
  264. <p>Notre solution actuelle de chevauchement a des problèmes similaires. La sélection est lente. Nous pourrions
  265. utiliser la sélection basée sur le GPU comme nous l'avons vu dans l'<a href="picking.html">article sur
  266. la sélection</a>, mais ce n'est pas non plus gratuit. La solution que vous
  267. choisirez dépend de vos besoins.</p>
  268. <p>Un autre problème est l'ordre d'apparition des étiquettes. Si nous modifions le code pour avoir
  269. des étiquettes plus longues</p>
  270. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [
  271. - makeInstance(geometry, 0x44aa88, 0, 'Aqua'),
  272. - makeInstance(geometry, 0x8844aa, -2, 'Purple'),
  273. - makeInstance(geometry, 0xaa8844, 2, 'Gold'),
  274. + makeInstance(geometry, 0x44aa88, 0, 'Boîte Couleur Aqua'),
  275. + makeInstance(geometry, 0x8844aa, -2, 'Boîte Couleur Violet'),
  276. + makeInstance(geometry, 0xaa8844, 2, 'Boîte Couleur Or'),
  277. ];
  278. </pre>
  279. <p>et définir le CSS de manière à ce qu'elles ne s'enroulent pas (wrap)</p>
  280. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels&gt;div {
  281. + white-space: nowrap;
  282. </pre>
  283. <p>Alors nous pouvons rencontrer ce problème</p>
  284. <div class="threejs_center"><img src="../resources/images/label-sorting-issue.png" style="width: 401px;"></div>
  285. <p>Vous pouvez voir ci-dessus que la boîte violette est à l'arrière, mais son étiquette est devant la boîte aqua.</p>
  286. <p>Nous pouvons résoudre ce problème en définissant le <code class="notranslate" translate="no">zIndex</code> de chaque élément. La position projetée a une valeur <code class="notranslate" translate="no">z</code>
  287. qui va de -1 à l'avant à +1 à l'arrière. Le <code class="notranslate" translate="no">zIndex</code> doit être un entier et va dans la direction
  288. opposée, ce qui signifie que pour le <code class="notranslate" translate="no">zIndex</code>, les valeurs plus grandes sont à l'avant, donc le code suivant devrait fonctionner.</p>
  289. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// convertit la position normalisée en coordonnées CSS
  290. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  291. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  292. // déplace l'élément à cette position
  293. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  294. +// définit le zIndex pour le tri
  295. +elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
  296. </pre>
  297. <p>En raison de la façon dont fonctionne la valeur z projetée, nous devons choisir un grand nombre pour étaler les valeurs,
  298. sinon beaucoup auront la même valeur. Pour s'assurer que les étiquettes ne se chevauchent pas avec d'autres parties de
  299. la page, nous pouvons demander au navigateur de créer un nouveau <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context">contexte d'empilement</a>
  300. en définissant le <code class="notranslate" translate="no">z-index</code> du conteneur des étiquettes</p>
  301. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels {
  302. position: absolute; /* nous permet de nous positionner à l'intérieur du conteneur */
  303. + z-index: 0; /* crée un nouveau contexte d'empilement pour que les enfants ne soient pas triés avec le reste de la page */
  304. left: 0; /* place notre position en haut à gauche du conteneur */
  305. top: 0;
  306. color: white;
  307. z-index: 0;
  308. }
  309. </pre>
  310. <p>et maintenant les étiquettes devraient toujours être dans le bon ordre.</p>
  311. <p></p><div translate="no" class="threejs_example_container notranslate">
  312. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-sorting.html"></iframe></div>
  313. <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-sorting.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  314. </div>
  315. <p></p>
  316. <p>Tant que nous y sommes, faisons un autre exemple pour montrer un problème supplémentaire.
  317. Dessinons un globe comme Google Maps et étiquetons les pays.</p>
  318. <p>J'ai trouvé <a href="http://thematicmapping.org/downloads/world_borders.php">ces données</a>
  319. qui contiennent les frontières des pays. Elles sont sous licence
  320. <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>.</p>
  321. <p>J'<a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">ai écrit du code</a>
  322. pour charger les données et générer les contours des pays ainsi que des données JSON avec les noms
  323. des pays et leurs emplacements.</p>
  324. <div class="threejs_center"><img src="../examples/resources/data/world/country-outlines-4k.png" style="background: black; width: 700px"></div>
  325. <p>Les données JSON sont un tableau d'entrées ressemblant à ceci</p>
  326. <pre class="prettyprint showlinemods notranslate lang-json" translate="no">[
  327. {
  328. "name": "Algeria",
  329. "min": [
  330. -8.667223,
  331. 18.976387
  332. ],
  333. "max": [
  334. 11.986475,
  335. 37.091385
  336. ],
  337. "area": 238174,
  338. "lat": 28.163,
  339. "lon": 2.632,
  340. "population": {
  341. "2005": 32854159
  342. }
  343. },
  344. ...
  345. </pre>
  346. <p>où min, max, lat, lon sont tous en degrés de latitude et de longitude.</p>
  347. <p>Chargeons-les. Le code est basé sur les exemples de l'<a href="optimize-lots-of-objects.html">optimisation de nombreux
  348. objets</a>. Bien que nous ne dessinions pas beaucoup
  349. d'objets, nous utiliserons les mêmes solutions pour le <a href="rendering-on-demand.html">rendu à la demande</a>.</p>
  350. <p>La première chose est de créer une sphère et d'utiliser la texture des contours.</p>
  351. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  352. const loader = new THREE.TextureLoader();
  353. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  354. const geometry = new THREE.SphereGeometry(1, 64, 32);
  355. const material = new THREE.MeshBasicMaterial({map: texture});
  356. scene.add(new THREE.Mesh(geometry, material));
  357. }
  358. </pre>
  359. <p>Ensuite, chargeons le fichier JSON en créant d'abord un chargeur</p>
  360. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadJSON(url) {
  361. const req = await fetch(url);
  362. return req.json();
  363. }
  364. </pre>
  365. <p>puis en l'appelant</p>
  366. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let countryInfos;
  367. async function loadCountryData() {
  368. countryInfos = await loadJSON('resources/data/world/country-info.json');
  369. ...
  370. }
  371. requestRenderIfNotRequested();
  372. }
  373. loadCountryData();
  374. </pre>
  375. <p>Maintenant, utilisons ces données pour générer et placer les étiquettes.</p>
  376. <p>Dans l'article sur l'<a href="optimize-lots-of-objects.html">optimisation de nombreux objets</a>,
  377. nous avions mis en place un petit graphe de scène d'objets auxiliaires pour faciliter le
  378. calcul des positions de latitude et de longitude sur notre globe. Consultez cet article
  379. pour une explication de leur fonctionnement.</p>
  380. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lonFudge = Math.PI * 1.5;
  381. const latFudge = Math.PI;
  382. // ces helpers (aides) faciliteront le positionnement des boîtes
  383. // Nous pouvons faire pivoter le lon helper sur son axe Y pour la longitude
  384. const lonHelper = new THREE.Object3D();
  385. // Nous faisons pivoter le latHelper sur son axe X pour la latitude
  386. const latHelper = new THREE.Object3D();
  387. lonHelper.add(latHelper);
  388. // Le position helper déplace l'objet vers le bord de la sphère
  389. const positionHelper = new THREE.Object3D();
  390. positionHelper.position.z = 1;
  391. latHelper.add(positionHelper);
  392. </pre>
  393. <p>Nous utiliserons cela pour calculer une position pour chaque étiquette</p>
  394. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
  395. for (const countryInfo of countryInfos) {
  396. const {lat, lon, name} = countryInfo;
  397. // ajuste les aides pour pointer vers la latitude et la longitude
  398. lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
  399. latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
  400. // obtient la position de la lat/lon
  401. positionHelper.updateWorldMatrix(true, false);
  402. const position = new THREE.Vector3();
  403. positionHelper.getWorldPosition(position);
  404. countryInfo.position = position;
  405. // ajoute un élément pour chaque pays
  406. const elem = document.createElement('div');
  407. elem.textContent = name;
  408. labelParentElem.appendChild(elem);
  409. countryInfo.elem = elem;
  410. </pre>
  411. <p>Le code ci-dessus ressemble beaucoup au code que nous avons écrit pour créer les étiquettes de cube,
  412. créant un élément par étiquette. Lorsque nous avons terminé, nous avons un tableau, <code class="notranslate" translate="no">countryInfos</code>,
  413. avec une entrée pour chaque pays, à laquelle nous avons ajouté une propriété <code class="notranslate" translate="no">elem</code> pour
  414. l'élément d'étiquette de ce pays et une <code class="notranslate" translate="no">position</code> avec sa position sur le
  415. globe.</p>
  416. <p>Tout comme nous l'avons fait pour les cubes, nous devons mettre à jour la position des
  417. étiquettes et le temps de rendu.</p>
  418. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  419. function updateLabels() {
  420. // quitte si nous n'avons pas encore chargé le fichier JSON
  421. if (!countryInfos) {
  422. return;
  423. }
  424. for (const countryInfo of countryInfos) {
  425. const {position, elem} = countryInfo;
  426. // obtient la coordonnée d'écran normalisée de cette position
  427. // x et y seront dans la plage de -1 à +1, avec x = -1 étant
  428. // à gauche et y = -1 étant en bas
  429. tempV.copy(position);
  430. tempV.project(camera);
  431. // convertit la position normalisée en coordonnées CSS
  432. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  433. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  434. // déplace l'élément à cette position
  435. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  436. // définit le zIndex pour le tri
  437. elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
  438. }
  439. }
  440. </pre>
  441. <p>Vous pouvez voir que le code ci-dessus est sensiblement similaire à l'exemple des cubes précédent.
  442. La seule différence majeure est que nous avons précalculé les positions des étiquettes au moment de l'initialisation.
  443. Nous pouvons le faire car le globe ne bouge jamais. Seule notre caméra bouge.</p>
  444. <p>Enfin, nous devons appeler <code class="notranslate" translate="no">updateLabels</code> dans notre boucle de rendu</p>
  445. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  446. renderRequested = false;
  447. if (resizeRendererToDisplaySize(renderer)) {
  448. const canvas = renderer.domElement;
  449. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  450. camera.updateProjectionMatrix();
  451. }
  452. controls.update();
  453. + updateLabels();
  454. renderer.render(scene, camera);
  455. }
  456. </pre>
  457. <p>Et voici ce que nous obtenons</p>
  458. <p></p><div translate="no" class="threejs_example_container notranslate">
  459. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html"></iframe></div>
  460. <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  461. </div>
  462. <p></p>
  463. <p>Il y a beaucoup trop d'étiquettes !</p>
  464. <p>Nous avons 2 problèmes.</p>
  465. <ol>
  466. <li><p>Les étiquettes qui font face à l'opposé de nous apparaissent.</p>
  467. </li>
  468. <li><p>Il y a trop d'étiquettes.</p>
  469. </li>
  470. </ol>
  471. <p>Pour le problème n°1, nous ne pouvons pas vraiment utiliser le <code class="notranslate" translate="no">RayCaster</code> comme nous l'avons fait ci-dessus car il n'y a
  472. rien à intersecter, à part la sphère. Au lieu de cela, ce que nous pouvons faire est de vérifier si ce
  473. pays particulier est tourné vers l'opposé de nous ou non. Cela fonctionne car les positions des étiquettes
  474. sont autour d'une sphère. En fait, nous utilisons une sphère unitaire, une sphère avec
  475. un rayon de 1,0. Cela signifie que les positions sont déjà des vecteurs unitaires, ce qui
  476. rend les calculs relativement faciles.</p>
  477. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  478. +const cameraToPoint = new THREE.Vector3();
  479. +const cameraPosition = new THREE.Vector3();
  480. +const normalMatrix = new THREE.Matrix3();
  481. function updateLabels() {
  482. // quitte si nous n'avons pas encore chargé le fichier JSON
  483. if (!countryInfos) {
  484. return;
  485. }
  486. + const minVisibleDot = 0.2;
  487. + // obtient une matrice qui représente une orientation relative de la caméra
  488. + normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  489. + // obtient la position de la caméra
  490. + camera.getWorldPosition(cameraPosition);
  491. for (const countryInfo of countryInfos) {
  492. const {position, elem} = countryInfo;
  493. + // Oriente la position en fonction de l'orientation de la caméra.
  494. + // Comme la sphère est à l'origine et que la sphère est une sphère unitaire
  495. + // cela nous donne un vecteur direction relatif à la caméra pour la position.
  496. + tempV.copy(position);
  497. + tempV.applyMatrix3(normalMatrix);
  498. +
  499. + // calcule la direction vers cette position depuis la caméra
  500. + cameraToPoint.copy(position);
  501. + cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
  502. +
  503. + // obtient le produit scalaire de la direction relative à la caméra vers cette position
  504. + // sur le globe avec la direction de la caméra vers ce point.
  505. + // 1 = fait face directement à la caméra
  506. + // 0 = exactement tangente à la sphère vue de la caméra
  507. + // &lt; 0 = fait face à l'opposé
  508. + const dot = tempV.dot(cameraToPoint);
  509. +
  510. + // si l'orientation ne nous fait pas face, la cacher.
  511. + if (dot &lt; minVisibleDot) {
  512. + elem.style.display = 'none';
  513. + continue;
  514. + }
  515. +
  516. + // restaure le style d'affichage par défaut de l'élément
  517. + elem.style.display = '';
  518. // obtient la coordonnée d'écran normalisée de cette position
  519. // x et y seront dans la plage de -1 à +1, avec x = -1 étant
  520. // à gauche et y = -1 étant en bas
  521. tempV.copy(position);
  522. tempV.project(camera);
  523. // convertit la position normalisée en coordonnées CSS
  524. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  525. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  526. // déplace l'élément à cette position
  527. countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  528. // définit le zIndex pour le tri
  529. elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
  530. }
  531. }
  532. </pre>
  533. <p>Ci-dessus, nous utilisons les positions comme direction et obtenons cette direction par rapport à la
  534. caméra. Ensuite, nous obtenons la direction relative à la caméra depuis la caméra vers cette position sur le globe et calculons le <em>produit scalaire</em>. Le produit scalaire renvoie le cosinus
  535. de l'angle entre les deux vecteurs. Cela nous donne une valeur de -1
  536. à +1, où -1 signifie que l'étiquette fait face à la caméra, 0 signifie que l'étiquette est exactement
  537. sur le bord de la sphère par rapport à la caméra, et toute valeur supérieure à zéro est
  538. derrière. Nous utilisons ensuite cette valeur pour afficher ou masquer l'élément.</p>
  539. <div class="spread">
  540. <div>
  541. <div data-diagram="dotProduct" style="height: 400px"></div>
  542. </div>
  543. </div>
  544. <p>Dans le diagramme ci-dessus, nous pouvons voir le produit scalaire de la direction vers laquelle l'étiquette est
  545. orientée et de la direction de la caméra vers cette position. Si vous faites pivoter la
  546. direction, vous verrez que le produit scalaire est de -1,0 lorsque la direction est directement
  547. face à la caméra, il est de 0,0 lorsqu'il est exactement tangent à la sphère par rapport
  548. à la caméra, ou pour le dire autrement, il est de 0 lorsque les 2 vecteurs sont
  549. perpendiculaires l'un à l'autre, à 90 degrés. Il est supérieur à zéro lorsque l'étiquette est
  550. derrière la sphère.</p>
  551. <p>Pour le problème n°2, trop d'étiquettes, nous avons besoin d'un moyen de décider quelles étiquettes
  552. afficher. Une façon serait de n'afficher les étiquettes que pour les grands pays.
  553. Les données que nous chargeons contiennent les valeurs min et max pour la superficie qu'un
  554. pays couvre. À partir de là, nous pouvons calculer une superficie, puis utiliser cette
  555. superficie pour décider d'afficher ou non le pays.</p>
  556. <p>Au moment de l'initialisation, calculons la superficie</p>
  557. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
  558. for (const countryInfo of countryInfos) {
  559. const {lat, lon, min, max, name} = countryInfo;
  560. // ajuste les aides pour pointer vers la latitude et la longitude
  561. lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
  562. latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
  563. // obtient la position de la lat/lon
  564. positionHelper.updateWorldMatrix(true, false);
  565. const position = new THREE.Vector3();
  566. positionHelper.getWorldPosition(position);
  567. countryInfo.position = position;
  568. + // calcule la superficie pour chaque pays
  569. + const width = max[0] - min[0];
  570. + const height = max[1] - min[1];
  571. + const area = width * height;
  572. + countryInfo.area = area;
  573. // ajoute un élément pour chaque pays
  574. const elem = document.createElement('div');
  575. elem.textContent = name;
  576. labelParentElem.appendChild(elem);
  577. countryInfo.elem = elem;
  578. }
  579. </pre>
  580. <p>Puis au moment du rendu, utilisons la superficie pour décider d'afficher l'étiquette
  581. ou non</p>
  582. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const large = 20 * 20;
  583. const maxVisibleDot = 0.2;
  584. // obtient une matrice qui représente une orientation relative de la caméra
  585. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  586. // obtient la position de la caméra
  587. camera.getWorldPosition(cameraPosition);
  588. for (const countryInfo of countryInfos) {
  589. - const {position, elem} = countryInfo;
  590. + const {position, elem, area} = countryInfo;
  591. + // assez grand ?
  592. + if (area &lt; large) {
  593. + elem.style.display = 'none';
  594. + continue;
  595. + }
  596. ...
  597. </pre>
  598. <p>Enfin, comme je ne suis pas sûr des bonnes valeurs pour ces paramètres, ajoutons
  599. une GUI pour que nous puissions jouer avec elles</p>
  600. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  601. import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  602. +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
  603. </pre>
  604. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const settings = {
  605. + minArea: 20,
  606. + maxVisibleDot: -0.2,
  607. +};
  608. +const gui = new GUI({width: 300});
  609. +gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested);
  610. +gui.add(settings, 'maxVisibleDot', -1, 1, 0.01).onChange(requestRenderIfNotRequested);
  611. function updateLabels() {
  612. if (!countryInfos) {
  613. return;
  614. }
  615. - const large = 20 * 20;
  616. - const maxVisibleDot = -0.2;
  617. + const large = settings.minArea * settings.minArea;
  618. // obtient une matrice qui représente une orientation relative de la caméra
  619. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  620. // obtient la position de la caméra
  621. camera.getWorldPosition(cameraPosition);
  622. for (const countryInfo of countryInfos) {
  623. ...
  624. // si l'orientation ne nous fait pas face, la cacher.
  625. - if (dot &gt; maxVisibleDot) {
  626. + if (dot &gt; settings.maxVisibleDot) {
  627. elem.style.display = 'none';
  628. continue;
  629. }
  630. </pre>
  631. <p>et voici le résultat</p>
  632. <p></p><div translate="no" class="threejs_example_container notranslate">
  633. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe.html"></iframe></div>
  634. <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  635. </div>
  636. <p></p>
  637. <p>Vous pouvez voir qu'en faisant pivoter la Terre, les étiquettes qui passent derrière disparaissent.
  638. Ajustez le <code class="notranslate" translate="no">minVisibleDot</code> pour voir le changement de seuil.
  639. Vous pouvez également ajuster la valeur de <code class="notranslate" translate="no">minArea</code> pour voir apparaître des pays plus grands ou plus petits.</p>
  640. <p>Plus j'ai travaillé là-dessus, plus j'ai réalisé l'énorme travail
  641. investi dans Google Maps. Eux aussi doivent décider quelles étiquettes afficher. Je suis à peu près sûr qu'ils utilisent toutes sortes de critères. Par exemple, votre position actuelle, votre paramètre de langue par défaut, les paramètres de votre compte si vous en avez un, ils utilisent probablement la population ou la popularité, ils pourraient donner la priorité aux pays au centre de la vue, etc... Beaucoup de choses à considérer.</p>
  642. <p>En tout cas, j'espère que ces exemples vous ont donné une idée de la façon d'aligner les éléments HTML
  643. avec votre 3D. Quelques choses que je pourrais changer.</p>
  644. <p>Prochaine étape, faisons en sorte que vous puissiez <a href="indexed-textures.html">sélectionner et surligner un pays</a>.</p>
  645. <p><link rel="stylesheet" href="../resources/threejs-align-html-elements-to-3d.css"></p>
  646. <script type="module" src="../resources/threejs-align-html-elements-to-3d.js"></script>
  647. </div>
  648. </div>
  649. </div>
  650. <script src="../resources/prettify.js"></script>
  651. <script src="../resources/lesson.js"></script>
  652. </body></html>
粤ICP备19079148号