voxel-geometry.html 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116
  1. <!DOCTYPE html><html lang="fr"><head>
  2. <meta charset="utf-8">
  3. <title>Géométrie Voxel (type Minecraft)</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 – Géométrie Voxel (type Minecraft)">
  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>Géométrie Voxel (type Minecraft)</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>J'ai vu ce sujet revenir plus d'une fois à divers endroits.
  29. C'est fondamentalement, "Comment faire un affichage de voxels comme Minecraft".</p>
  30. <p>La plupart des gens essaient d'abord en créant une géométrie de cube, puis
  31. en faisant un maillage à chaque position de voxel. Juste pour le plaisir, j'ai essayé
  32. cela. J'ai créé un <code class="notranslate" translate="no">Uint8Array</code> de 16777216 éléments pour représenter
  33. un cube de voxels de 256x256x256.</p>
  34. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 256;
  35. const cell = new Uint8Array(cellSize * cellSize * cellSize);
  36. </pre>
  37. <p>J'ai ensuite fait une seule couche avec une sorte de collines de
  38. vagues sinusoïdales comme ceci</p>
  39. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  40. for (let z = 0; z &lt; cellSize; ++z) {
  41. for (let x = 0; x &lt; cellSize; ++x) {
  42. const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
  43. if (height &gt; y &amp;&amp; height &lt; y + 1) {
  44. const offset = y * cellSize * cellSize +
  45. z * cellSize +
  46. x;
  47. cell[offset] = 1;
  48. }
  49. }
  50. }
  51. }
  52. </pre>
  53. <p>J'ai ensuite parcouru toutes les cellules et si elles n'étaient pas
  54. à 0, j'ai créé un maillage avec un cube.</p>
  55. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.BoxGeometry(1, 1, 1);
  56. const material = new THREE.MeshPhongMaterial({color: 'green'});
  57. for (let y = 0; y &lt; cellSize; ++y) {
  58. for (let z = 0; z &lt; cellSize; ++z) {
  59. for (let x = 0; x &lt; cellSize; ++x) {
  60. const offset = y * cellSize * cellSize +
  61. z * cellSize +
  62. x;
  63. const block = cell[offset];
  64. const mesh = new THREE.Mesh(geometry, material);
  65. mesh.position.set(x, y, z);
  66. scene.add(mesh);
  67. }
  68. }
  69. }
  70. </pre>
  71. <p>Le reste du code est basé sur l'exemple de
  72. <a href="rendering-on-demand.html">l'article sur le rendu à la demande</a>.</p>
  73. <p></p><div translate="no" class="threejs_example_container notranslate">
  74. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-separate-cubes.html"></iframe></div>
  75. <a class="threejs_center" href="/manual/examples/voxel-geometry-separate-cubes.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  76. </div>
  77. <p></p>
  78. <p>Cela prend un certain temps pour démarrer et si vous essayez de bouger la caméra
  79. c'est probablement trop lent. Comme dans <a href="optimize-lots-of-objects.html">l'article sur l'optimisation de nombreux objets</a>
  80. le problème est qu'il y a juste beaucoup trop d'objets. 256x256
  81. fait 65536 boîtes !</p>
  82. <p>L'utilisation de <a href="rendering-on-demand.html">la technique de fusion de la géométrie</a>
  83. résoudra le problème pour cet exemple, mais que se passerait-il si, au lieu de faire une simple couche, nous remplissions tout ce qui se trouve sous le sol avec des voxels ?
  84. En d'autres termes, changez la boucle qui remplit les voxels comme ceci :</p>
  85. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  86. for (let z = 0; z &lt; cellSize; ++z) {
  87. for (let x = 0; x &lt; cellSize; ++x) {
  88. const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
  89. - if (height &gt; y &amp;&amp; height &lt; y + 1) {
  90. + if (height &lt; y + 1) {
  91. const offset = y * cellSize * cellSize +
  92. z * cellSize +
  93. x;
  94. cell[offset] = 1;
  95. }
  96. }
  97. }
  98. }
  99. </pre>
  100. <p>J'ai essayé une fois juste pour voir les résultats. Ça a mouliné pendant
  101. environ une minute, puis ça a planté avec un message <em>manque de mémoire</em> 😅</p>
  102. <p>Il y a plusieurs problèmes, mais le plus important est
  103. que nous créons toutes ces faces à l'intérieur des cubes que
  104. nous ne pouvons en fait jamais voir.</p>
  105. <p>En d'autres termes, disons que nous avons une boîte de voxels
  106. 3x2x2. En fusionnant les cubes, nous obtenons ceci :</p>
  107. <div class="spread">
  108. <div data-diagram="mergedCubes" style="height: 300px;"></div>
  109. </div>
  110. <p>mais nous voulons vraiment ceci</p>
  111. <div class="spread">
  112. <div data-diagram="culledCubes" style="height: 300px;"></div>
  113. </div>
  114. <p>Dans la boîte du haut, il y a des faces entre les voxels. Des faces
  115. qui sont un gâchis car elles ne peuvent pas être vues. Ce n'est pas seulement
  116. une face entre chaque voxel, il y a 2 faces, une pour
  117. chaque voxel faisant face à son voisin qui sont un gâchis. Toutes ces faces supplémentaires,
  118. surtout pour un grand volume de voxels, tueront les performances.</p>
  119. <p>Il devrait être clair que nous ne pouvons pas simplement fusionner la géométrie.
  120. Nous devons la construire nous-mêmes, en tenant compte du fait
  121. que si un voxel a un voisin adjacent, il n'a pas besoin de la
  122. face qui fait face à ce voisin.</p>
  123. <p>Le problème suivant est que 256x256x256 est tout simplement trop grand. 16 Mo représentent beaucoup de mémoire et
  124. si rien d'autre n'y est, une grande partie de l'espace est vide, ce qui représente beaucoup de mémoire gaspillée. C'est aussi un nombre énorme de voxels, 16 millions ! C'est trop à
  125. considérer d'un coup.</p>
  126. <p>Une solution consiste à diviser la zone en zones plus petites.
  127. Toute zone qui ne contient rien n'a pas besoin de stockage. Utilisons
  128. des zones de 32x32x32 (soit 32k) et ne créons une zone que si elle contient quelque chose.
  129. Nous appellerons l'une de ces zones plus grandes de 32x32x32 une "cellule".</p>
  130. <p>Découpons cela en morceaux. Tout d'abord, créons une classe pour gérer les données de voxel.</p>
  131. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  132. constructor(cellSize) {
  133. this.cellSize = cellSize;
  134. }
  135. }
  136. </pre>
  137. <p>Créons la fonction qui génère la géométrie pour une cellule.
  138. Supposons que vous passiez une position de cellule.
  139. En d'autres termes, si vous voulez la géométrie pour la cellule qui couvre les voxels (0-31x, 0-31y, 0-31z)
  140. alors vous passerez 0,0,0. Pour la cellule qui couvre les voxels (32-63x, 0-31y, 0-31z), vous passerez
  141. 1,0,0.</p>
  142. <p>Nous devons pouvoir vérifier les voxels voisins, alors supposons que notre classe
  143. dispose d'une fonction <code class="notranslate" translate="no">getVoxel</code> qui, étant donné une position de voxel, renvoie la valeur
  144. du voxel à cet endroit. En d'autres termes, si vous lui passez 35,0,0 et que la cellSize est de 32,
  145. elle regardera la cellule 1,0,0 et dans cette cellule, elle regardera le voxel 3,0,0.
  146. En utilisant cette fonction, nous pouvons regarder les voxels voisins d'un voxel, même s'ils
  147. se trouvent dans des cellules voisines.</p>
  148. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  149. constructor(cellSize) {
  150. this.cellSize = cellSize;
  151. }
  152. + generateGeometryDataForCell(cellX, cellY, cellZ) {
  153. + const {cellSize} = this;
  154. + const startX = cellX * cellSize;
  155. + const startY = cellY * cellSize;
  156. + const startZ = cellZ * cellSize;
  157. +
  158. + for (let y = 0; y &lt; cellSize; ++y) {
  159. + const voxelY = startY + y;
  160. + for (let z = 0; z &lt; cellSize; ++z) {
  161. + const voxelZ = startZ + z;
  162. + for (let x = 0; x &lt; cellSize; ++x) {
  163. + const voxelX = startX + x;
  164. + const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  165. + if (voxel) {
  166. + for (const {dir} of VoxelWorld.faces) {
  167. + const neighbor = this.getVoxel(
  168. + voxelX + dir[0],
  169. + voxelY + dir[1],
  170. + voxelZ + dir[2]);
  171. + if (!neighbor) {
  172. + // ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face ici.
  173. + // here.
  174. + }
  175. + }
  176. + }
  177. + }
  178. + }
  179. + }
  180. + }
  181. }
  182. +VoxelWorld.faces = [
  183. + { // gauche
  184. + dir: [ -1, 0, 0, ],
  185. + },
  186. + { // droite
  187. + dir: [ 1, 0, 0, ],
  188. + },
  189. + { // bas
  190. + dir: [ 0, -1, 0, ],
  191. + },
  192. + { // haut
  193. + dir: [ 0, 1, 0, ],
  194. + },
  195. + { // arrière
  196. + dir: [ 0, 0, -1, ],
  197. + },
  198. + { // avant
  199. + dir: [ 0, 0, 1, ],
  200. + },
  201. +];
  202. </pre>
  203. <p>Donc, en utilisant le code ci-dessus, nous savons quand nous avons besoin d'une face. Générons les faces.</p>
  204. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  205. constructor(cellSize) {
  206. this.cellSize = cellSize;
  207. }
  208. generateGeometryDataForCell(cellX, cellY, cellZ) {
  209. const {cellSize} = this;
  210. + const positions = [];
  211. + const normals = [];
  212. + const indices = [];
  213. const startX = cellX * cellSize;
  214. const startY = cellY * cellSize;
  215. const startZ = cellZ * cellSize;
  216. for (let y = 0; y &lt; cellSize; ++y) {
  217. const voxelY = startY + y;
  218. for (let z = 0; z &lt; cellSize; ++z) {
  219. const voxelZ = startZ + z;
  220. for (let x = 0; x &lt; cellSize; ++x) {
  221. const voxelX = startX + x;
  222. const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  223. if (voxel) {
  224. - for (const {dir} of VoxelWorld.faces) {
  225. + for (const {dir, corners} of VoxelWorld.faces) {
  226. const neighbor = this.getVoxel(
  227. voxelX + dir[0],
  228. voxelY + dir[1],
  229. voxelZ + dir[2]);
  230. if (!neighbor) {
  231. // ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face.
  232. + const ndx = positions.length / 3;
  233. + for (const pos of corners) {
  234. + positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
  235. + normals.push(...dir);
  236. + }
  237. + indices.push(
  238. + ndx, ndx + 1, ndx + 2,
  239. + ndx + 2, ndx + 1, ndx + 3,
  240. + );
  241. }
  242. }
  243. }
  244. }
  245. }
  246. }
  247. + return {
  248. + positions,
  249. + normals,
  250. + indices,
  251. };
  252. }
  253. }
  254. VoxelWorld.faces = [
  255. { // gauche
  256. dir: [ -1, 0, 0, ],
  257. + corners: [
  258. + [ 0, 1, 0 ],
  259. + [ 0, 0, 0 ],
  260. + [ 0, 1, 1 ],
  261. + [ 0, 0, 1 ],
  262. + ],
  263. },
  264. { // droite
  265. dir: [ 1, 0, 0, ],
  266. + corners: [
  267. + [ 1, 1, 1 ],
  268. + [ 1, 0, 1 ],
  269. + [ 1, 1, 0 ],
  270. + [ 1, 0, 0 ],
  271. + ],
  272. },
  273. { // bas
  274. dir: [ 0, -1, 0, ],
  275. + corners: [
  276. + [ 1, 0, 1 ],
  277. + [ 0, 0, 1 ],
  278. + [ 1, 0, 0 ],
  279. + [ 0, 0, 0 ],
  280. + ],
  281. },
  282. { // haut
  283. dir: [ 0, 1, 0, ],
  284. + corners: [
  285. + [ 0, 1, 1 ],
  286. + [ 1, 1, 1 ],
  287. + [ 0, 1, 0 ],
  288. + [ 1, 1, 0 ],
  289. + ],
  290. },
  291. { // arrière
  292. dir: [ 0, 0, -1, ],
  293. + corners: [
  294. + [ 1, 0, 0 ],
  295. + [ 0, 0, 0 ],
  296. + [ 1, 1, 0 ],
  297. + [ 0, 1, 0 ],
  298. + ],
  299. },
  300. { // avant
  301. dir: [ 0, 0, 1, ],
  302. + corners: [
  303. + [ 0, 0, 1 ],
  304. + [ 1, 0, 1 ],
  305. + [ 0, 1, 1 ],
  306. + [ 1, 1, 1 ],
  307. + ],
  308. },
  309. ];
  310. </pre>
  311. <p>Le code ci-dessus générerait des données de géométrie de base pour nous. Il suffit de fournir
  312. la fonction <code class="notranslate" translate="no">getVoxel</code>. Commençons par une seule cellule codée en dur.</p>
  313. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  314. constructor(cellSize) {
  315. this.cellSize = cellSize;
  316. + this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  317. }
  318. + getCellForVoxel(x, y, z) {
  319. + const {cellSize} = this;
  320. + const cellX = Math.floor(x / cellSize);
  321. + const cellY = Math.floor(y / cellSize);
  322. + const cellZ = Math.floor(z / cellSize);
  323. + if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  324. + return null
  325. + }
  326. + return this.cell;
  327. + }
  328. + getVoxel(x, y, z) {
  329. + const cell = this.getCellForVoxel(x, y, z);
  330. + if (!cell) {
  331. + return 0;
  332. + }
  333. + const {cellSize} = this;
  334. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  335. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  336. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  337. + const voxelOffset = voxelY * cellSize * cellSize +
  338. + voxelZ * cellSize +
  339. + voxelX;
  340. + return cell[voxelOffset];
  341. + }
  342. generateGeometryDataForCell(cellX, cellY, cellZ) {
  343. ...
  344. }
  345. </pre>
  346. <p>Cela semble fonctionner. Créons une fonction <code class="notranslate" translate="no">setVoxel</code>
  347. pour pouvoir définir des données.</p>
  348. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  349. constructor(cellSize) {
  350. this.cellSize = cellSize;
  351. this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  352. }
  353. getCellForVoxel(x, y, z) {
  354. const {cellSize} = this;
  355. const cellX = Math.floor(x / cellSize);
  356. const cellY = Math.floor(y / cellSize);
  357. const cellZ = Math.floor(z / cellSize);
  358. if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  359. return null
  360. }
  361. return this.cell;
  362. }
  363. + setVoxel(x, y, z, v) {
  364. + let cell = this.getCellForVoxel(x, y, z);
  365. + if (!cell) {
  366. + return; // TODO : ajouter une nouvelle cellule ?
  367. + }
  368. + const {cellSize} = this;
  369. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  370. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  371. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  372. + const voxelOffset = voxelY * cellSize * cellSize +
  373. + voxelZ * cellSize +
  374. + voxelX;
  375. + cell[voxelOffset] = v;
  376. + }
  377. getVoxel(x, y, z) {
  378. const cell = this.getCellForVoxel(x, y, z);
  379. if (!cell) {
  380. return 0;
  381. }
  382. const {cellSize} = this;
  383. const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  384. const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  385. const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  386. const voxelOffset = voxelY * cellSize * cellSize +
  387. voxelZ * cellSize +
  388. voxelX;
  389. return cell[voxelOffset];
  390. }
  391. generateGeometryDataForCell(cellX, cellY, cellZ) {
  392. ...
  393. }
  394. </pre>
  395. <p>Hmmm, je vois beaucoup de code répété. Arrangeons ça</p>
  396. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  397. constructor(cellSize) {
  398. this.cellSize = cellSize;
  399. + this.cellSliceSize = cellSize * cellSize;
  400. this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  401. }
  402. getCellForVoxel(x, y, z) {
  403. const {cellSize} = this;
  404. const cellX = Math.floor(x / cellSize);
  405. const cellY = Math.floor(y / cellSize);
  406. const cellZ = Math.floor(z / cellSize);
  407. if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  408. return null;
  409. }
  410. return this.cell;
  411. }
  412. + computeVoxelOffset(x, y, z) {
  413. + const {cellSize, cellSliceSize} = this;
  414. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  415. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  416. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  417. + return voxelY * cellSliceSize +
  418. + voxelZ * cellSize +
  419. + voxelX;
  420. + }
  421. setVoxel(x, y, z, v) {
  422. const cell = this.getCellForVoxel(x, y, z);
  423. if (!cell) {
  424. return; // TODO : ajouter une nouvelle cellule ?
  425. }
  426. - const {cellSize} = this;
  427. - const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  428. - const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  429. - const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  430. - const voxelOffset = voxelY * cellSize * cellSize +
  431. - voxelZ * cellSize +
  432. - voxelX;
  433. + const voxelOffset = this.computeVoxelOffset(x, y, z);
  434. cell[voxelOffset] = v;
  435. }
  436. getVoxel(x, y, z) {
  437. const cell = this.getCellForVoxel(x, y, z);
  438. if (!cell) {
  439. return 0;
  440. }
  441. - const {cellSize} = this;
  442. - const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  443. - const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  444. - const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  445. - const voxelOffset = voxelY * cellSize * cellSize +
  446. - voxelZ * cellSize +
  447. - voxelX;
  448. + const voxelOffset = this.computeVoxelOffset(x, y, z);
  449. return cell[voxelOffset];
  450. }
  451. generateGeometryDataForCell(cellX, cellY, cellZ) {
  452. ...
  453. }
  454. </pre>
  455. <p>Maintenant, créons du code pour remplir la première cellule avec des voxels.</p>
  456. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 32;
  457. const world = new VoxelWorld(cellSize);
  458. for (let y = 0; y &lt; cellSize; ++y) {
  459. for (let z = 0; z &lt; cellSize; ++z) {
  460. for (let x = 0; x &lt; cellSize; ++x) {
  461. const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
  462. if (y &lt; height) {
  463. world.setVoxel(x, y, z, 1);
  464. }
  465. }
  466. }
  467. }
  468. </pre>
  469. <p>et du code pour effectivement générer la géométrie comme nous l'avons vu dans
  470. <a href="custom-buffergeometry.html">l'article sur BufferGeometry personnalisé</a>.</p>
  471. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
  472. const geometry = new THREE.BufferGeometry();
  473. const material = new THREE.MeshLambertMaterial({color: 'green'});
  474. const positionNumComponents = 3;
  475. const normalNumComponents = 3;
  476. geometry.setAttribute(
  477. 'position',
  478. new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  479. geometry.setAttribute(
  480. 'normal',
  481. new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  482. geometry.setIndex(indices);
  483. const mesh = new THREE.Mesh(geometry, material);
  484. scene.add(mesh);
  485. </pre>
  486. <p>essayons</p>
  487. <p></p><div translate="no" class="threejs_example_container notranslate">
  488. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces.html"></iframe></div>
  489. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  490. </div>
  491. <p></p>
  492. <p>Cela semble fonctionner ! D'accord, ajoutons des textures.</p>
  493. <p>En cherchant sur le net, j'ai trouvé <a href="https://www.minecraftforum.net/forums/mapping-and-modding-java-edition/resource-packs/1245961-16x-1-7-4-wip-flourish">cet ensemble</a>
  494. de textures minecraft sous licence <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA</a> par <a href="https://www.minecraftforum.net/members/Joshtimus">Joshtimus</a>.
  495. J'en ai choisi quelques-unes au hasard et j'ai construit cette <a href="https://www.google.com/search?q=texture+atlas">texture atlas</a>.</p>
  496. <div class="threejs_center"><img class="checkerboard" src="../examples/resources/images/minecraft/flourish-cc-by-nc-sa.png" style="width: 512px; image-rendering: pixelated;"></div>
  497. <p>Pour simplifier les choses, elles sont arrangées un type de voxel par colonne,
  498. où la rangée supérieure est le côté d'un voxel. La 2ème rangée est
  499. le dessus du voxel, et la 3ème rangée est le dessous du voxel.</p>
  500. <p>Sachant cela, nous pouvons ajouter des informations à nos données <code class="notranslate" translate="no">VoxelWorld.faces</code>
  501. pour spécifier pour chaque face quelle rangée utiliser et les UVs à utiliser
  502. pour cette face.</p>
  503. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">VoxelWorld.faces = [
  504. { // gauche
  505. + uvRow: 0,
  506. dir: [ -1, 0, 0, ],
  507. corners: [
  508. - [ 0, 1, 0 ],
  509. - [ 0, 0, 0 ],
  510. - [ 0, 1, 1 ],
  511. - [ 0, 0, 1 ],
  512. + { pos: [ 0, 1, 0 ], uv: [ 0, 1 ], },
  513. + { pos: [ 0, 0, 0 ], uv: [ 0, 0 ], },
  514. + { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
  515. + { pos: [ 0, 0, 1 ], uv: [ 1, 0 ], },
  516. ],
  517. },
  518. { // droite
  519. + uvRow: 0,
  520. dir: [ 1, 0, 0, ],
  521. corners: [
  522. - [ 1, 1, 1 ],
  523. - [ 1, 0, 1 ],
  524. - [ 1, 1, 0 ],
  525. - [ 1, 0, 0 ],
  526. + { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
  527. + { pos: [ 1, 0, 1 ], uv: [ 0, 0 ], },
  528. + { pos: [ 1, 1, 0 ], uv: [ 1, 1 ], },
  529. + { pos: [ 1, 0, 0 ], uv: [ 1, 0 ], },
  530. + ],
  531. },
  532. { // bas
  533. + uvRow: 1,
  534. dir: [ 0, -1, 0, ],
  535. corners: [
  536. - [ 1, 0, 1 ],
  537. - [ 0, 0, 1 ],
  538. - [ 1, 0, 0 ],
  539. - [ 0, 0, 0 ],
  540. + { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
  541. + { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
  542. + { pos: [ 1, 0, 0 ], uv: [ 1, 1 ], },
  543. + { pos: [ 0, 0, 0 ], uv: [ 0, 1 ], },
  544. + ],
  545. },
  546. { // haut
  547. + uvRow: 2,
  548. dir: [ 0, 1, 0, ],
  549. corners: [
  550. - [ 0, 1, 1 ],
  551. - [ 1, 1, 1 ],
  552. - [ 0, 1, 0 ],
  553. - [ 1, 1, 0 ],
  554. + { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
  555. + { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
  556. + { pos: [ 0, 1, 0 ], uv: [ 1, 0 ], },
  557. + { pos: [ 1, 1, 0 ], uv: [ 0, 0 ], },
  558. + ],
  559. },
  560. { // arrière
  561. + uvRow: 0,
  562. dir: [ 0, 0, -1, ],
  563. corners: [
  564. - [ 1, 0, 0 ],
  565. - [ 0, 0, 0 ],
  566. - [ 1, 1, 0 ],
  567. - [ 0, 1, 0 ],
  568. + { pos: [ 1, 0, 0 ], uv: [ 0, 0 ], },
  569. + { pos: [ 0, 0, 0 ], uv: [ 1, 0 ], },
  570. + { pos: [ 1, 1, 0 ], uv: [ 0, 1 ], },
  571. + { pos: [ 0, 1, 0 ], uv: [ 1, 1 ], },
  572. + ],
  573. },
  574. { // avant
  575. + uvRow: 0,
  576. dir: [ 0, 0, 1, ],
  577. corners: [
  578. - [ 0, 0, 1 ],
  579. - [ 1, 0, 1 ],
  580. - [ 0, 1, 1 ],
  581. - [ 1, 1, 1 ],
  582. + { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
  583. + { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
  584. + { pos: [ 0, 1, 1 ], uv: [ 0, 1 ], },
  585. + { pos: [ 1, 1, 1 ], uv: [ 1, 1 ], },
  586. + ],
  587. },
  588. ];
  589. </pre>
  590. <p>Et nous pouvons mettre à jour le code pour utiliser ces données. Nous devons
  591. connaître la taille d'une tuile dans la texture atlas et les dimensions
  592. de la texture.</p>
  593. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  594. - constructor(cellSize) {
  595. - this.cellSize = cellSize;
  596. + constructor(options) {
  597. + this.cellSize = options.cellSize;
  598. + this.tileSize = options.tileSize;
  599. + this.tileTextureWidth = options.tileTextureWidth;
  600. + this.tileTextureHeight = options.tileTextureHeight;
  601. + const {cellSize} = this;
  602. + this.cellSliceSize = cellSize * cellSize;
  603. + this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  604. + }
  605. ...
  606. generateGeometryDataForCell(cellX, cellY, cellZ) {
  607. - const {cellSize} = this;
  608. + const {cellSize, tileSize, tileTextureWidth, tileTextureHeight} = this;
  609. const positions = [];
  610. const normals = [];
  611. + const uvs = [];
  612. const indices = [];
  613. const startX = cellX * cellSize;
  614. const startY = cellY * cellSize;
  615. const startZ = cellZ * cellSize;
  616. for (let y = 0; y &lt; cellSize; ++y) {
  617. const voxelY = startY + y;
  618. for (let z = 0; z &lt; cellSize; ++z) {
  619. const voxelZ = startZ + z;
  620. for (let x = 0; x &lt; cellSize; ++x) {
  621. const voxelX = startX + x;
  622. const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  623. if (voxel) {
  624. const uvVoxel = voxel - 1; // le voxel 0 est le ciel, donc pour les UVs nous commençons à 0
  625. // There is a voxel here but do we need faces for it?
  626. - for (const {dir, corners} of VoxelWorld.faces) {
  627. + for (const {dir, corners, uvRow} of VoxelWorld.faces) {
  628. const neighbor = this.getVoxel(
  629. voxelX + dir[0],
  630. voxelY + dir[1],
  631. voxelZ + dir[2]);
  632. if (!neighbor) {
  633. // ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face.
  634. const ndx = positions.length / 3;
  635. - for (const pos of corners) {
  636. + for (const {pos, uv} of corners) {
  637. positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
  638. normals.push(...dir);
  639. + uvs.push(
  640. + (uvVoxel + uv[0]) * tileSize / tileTextureWidth,
  641. + 1 - (uvRow + 1 - uv[1]) * tileSize / tileTextureHeight);
  642. }
  643. indices.push(
  644. ndx, ndx + 1, ndx + 2,
  645. ndx + 2, ndx + 1, ndx + 3,
  646. );
  647. }
  648. }
  649. }
  650. }
  651. }
  652. }
  653. return {
  654. positions,
  655. normals,
  656. uvs,
  657. indices,
  658. };
  659. }
  660. }
  661. </pre>
  662. <p>Nous devons ensuite <a href="textures.html">charger la texture</a></p>
  663. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
  664. const texture = loader.load('resources/images/minecraft/flourish-cc-by-nc-sa.png', render);
  665. texture.magFilter = THREE.NearestFilter;
  666. texture.minFilter = THREE.NearestFilter;
  667. texture.colorSpace = THREE.SRGBColorSpace;
  668. </pre>
  669. <p>et passer les paramètres à la classe <code class="notranslate" translate="no">VoxelWorld</code></p>
  670. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const tileSize = 16;
  671. +const tileTextureWidth = 256;
  672. +const tileTextureHeight = 64;
  673. -const world = new VoxelWorld(cellSize);
  674. +const world = new VoxelWorld({
  675. + cellSize,
  676. + tileSize,
  677. + tileTextureWidth,
  678. + tileTextureHeight,
  679. +});
  680. </pre>
  681. <p>Utilisons réellement les UVs lors de la création de la géométrie
  682. et la texture lorsque nous fabriquons le matériau</p>
  683. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
  684. +const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(0, 0, 0);
  685. const geometry = new THREE.BufferGeometry();
  686. -const material = new THREE.MeshLambertMaterial({color: 'green'});
  687. +const material = new THREE.MeshLambertMaterial({
  688. + map: texture,
  689. + side: THREE.DoubleSide,
  690. + alphaTest: 0.1,
  691. + transparent: true,
  692. +});
  693. const positionNumComponents = 3;
  694. const normalNumComponents = 3;
  695. +const uvNumComponents = 2;
  696. geometry.setAttribute(
  697. 'position',
  698. new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  699. geometry.setAttribute(
  700. 'normal',
  701. new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  702. +geometry.setAttribute(
  703. + 'uv',
  704. + new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  705. geometry.setIndex(indices);
  706. const mesh = new THREE.Mesh(geometry, material);
  707. scene.add(mesh);
  708. </pre>
  709. <p>Une dernière chose, nous devons réellement définir certains voxels
  710. pour utiliser différentes textures.</p>
  711. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  712. for (let z = 0; z &lt; cellSize; ++z) {
  713. for (let x = 0; x &lt; cellSize; ++x) {
  714. const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
  715. if (y &lt; height) {
  716. - world.setVoxel(x, y, z, 1);
  717. + world.setVoxel(x, y, z, randInt(1, 17));
  718. + }
  719. + }
  720. + }
  721. +}
  722. +
  723. +function randInt(min, max) {
  724. + return Math.floor(Math.random() * (max - min) + min);
  725. +}
  726. </pre>
  727. <p>et avec cela, nous obtenons des textures !</p>
  728. <p></p><div translate="no" class="threejs_example_container notranslate">
  729. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-with-textures.html"></iframe></div>
  730. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces-with-textures.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  731. </div>
  732. <p></p>
  733. <p>Supportons maintenant plus d'une cellule.</p>
  734. <p>Pour ce faire, stockons les cellules dans un objet en utilisant des cell ids.
  735. Un cell id sera simplement les coordonnées d'une cellule séparées par
  736. une virgule. En d'autres termes, si nous demandons le voxel 35,0,0,
  737. qui est dans la cellule 1,0,0, son id est donc <code class="notranslate" translate="no">"1,0,0"</code>.</p>
  738. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  739. constructor(options) {
  740. this.cellSize = options.cellSize;
  741. this.tileSize = options.tileSize;
  742. this.tileTextureWidth = options.tileTextureWidth;
  743. this.tileTextureHeight = options.tileTextureHeight;
  744. const {cellSize} = this;
  745. this.cellSliceSize = cellSize * cellSize;
  746. - this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  747. + this.cells = {};
  748. }
  749. + computeCellId(x, y, z) {
  750. + const {cellSize} = this;
  751. + const cellX = Math.floor(x / cellSize);
  752. + const cellY = Math.floor(y / cellSize);
  753. + const cellZ = Math.floor(z / cellSize);
  754. + return `${cellX},${cellY},${cellZ}`;
  755. + }
  756. + getCellForVoxel(x, y, z) {
  757. - const cellX = Math.floor(x / cellSize);
  758. - const cellY = Math.floor(y / cellSize);
  759. - const cellZ = Math.floor(z / cellSize);
  760. - if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  761. - return null;
  762. - }
  763. - return this.cell;
  764. + return this.cells[this.computeCellId(x, y, z)];
  765. }
  766. ...
  767. }
  768. </pre>
  769. <p>et maintenant nous pouvons faire en sorte que <code class="notranslate" translate="no">setVoxel</code> ajoute de nouvelles cellules si
  770. nous essayons de définir un voxel dans une cellule qui n'existe pas encore</p>
  771. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> setVoxel(x, y, z, v) {
  772. - const cell = this.getCellForVoxel(x, y, z);
  773. + let cell = this.getCellForVoxel(x, y, z);
  774. if (!cell) {
  775. - return 0;
  776. + cell = this.addCellForVoxel(x, y, z);
  777. }
  778. const voxelOffset = this.computeVoxelOffset(x, y, z);
  779. cell[voxelOffset] = v;
  780. }
  781. + addCellForVoxel(x, y, z) {
  782. + const cellId = this.computeCellId(x, y, z);
  783. + let cell = this.cells[cellId];
  784. + if (!cell) {
  785. + const {cellSize} = this;
  786. + cell = new Uint8Array(cellSize * cellSize * cellSize);
  787. + this.cells[cellId] = cell;
  788. + }
  789. + return cell;
  790. + }
  791. </pre>
  792. <p>Rendons cela modifiable.</p>
  793. <p>Tout d'abord, nous ajouterons une UI. En utilisant des boutons radio, nous pouvons créer un tableau de tuiles 8x2</p>
  794. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  795. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  796. + &lt;div id="ui"&gt;
  797. + &lt;div class="tiles"&gt;
  798. + &lt;input type="radio" name="voxel" id="voxel1" value="1"&gt;&lt;label for="voxel1" style="background-position: -0% -0%"&gt;&lt;/label&gt;
  799. + &lt;input type="radio" name="voxel" id="voxel2" value="2"&gt;&lt;label for="voxel2" style="background-position: -100% -0%"&gt;&lt;/label&gt;
  800. + &lt;input type="radio" name="voxel" id="voxel3" value="3"&gt;&lt;label for="voxel3" style="background-position: -200% -0%"&gt;&lt;/label&gt;
  801. + &lt;input type="radio" name="voxel" id="voxel4" value="4"&gt;&lt;label for="voxel4" style="background-position: -300% -0%"&gt;&lt;/label&gt;
  802. + &lt;input type="radio" name="voxel" id="voxel5" value="5"&gt;&lt;label for="voxel5" style="background-position: -400% -0%"&gt;&lt;/label&gt;
  803. + &lt;input type="radio" name="voxel" id="voxel6" value="6"&gt;&lt;label for="voxel6" style="background-position: -500% -0%"&gt;&lt;/label&gt;
  804. + &lt;input type="radio" name="voxel" id="voxel7" value="7"&gt;&lt;label for="voxel7" style="background-position: -600% -0%"&gt;&lt;/label&gt;
  805. + &lt;input type="radio" name="voxel" id="voxel8" value="8"&gt;&lt;label for="voxel8" style="background-position: -700% -0%"&gt;&lt;/label&gt;
  806. + &lt;/div&gt;
  807. + &lt;div class="tiles"&gt;
  808. + &lt;input type="radio" name="voxel" id="voxel9" value="9" &gt;&lt;label for="voxel9" style="background-position: -800% -0%"&gt;&lt;/label&gt;
  809. + &lt;input type="radio" name="voxel" id="voxel10" value="10"&gt;&lt;label for="voxel10" style="background-position: -900% -0%"&gt;&lt;/label&gt;
  810. + &lt;input type="radio" name="voxel" id="voxel11" value="11"&gt;&lt;label for="voxel11" style="background-position: -1000% -0%"&gt;&lt;/label&gt;
  811. + &lt;input type="radio" name="voxel" id="voxel12" value="12"&gt;&lt;label for="voxel12" style="background-position: -1100% -0%"&gt;&lt;/label&gt;
  812. + &lt;input type="radio" name="voxel" id="voxel13" value="13"&gt;&lt;label for="voxel13" style="background-position: -1200% -0%"&gt;&lt;/label&gt;
  813. + &lt;input type="radio" name="voxel" id="voxel14" value="14"&gt;&lt;label for="voxel14" style="background-position: -1300% -0%"&gt;&lt;/label&gt;
  814. + &lt;input type="radio" name="voxel" id="voxel15" value="15"&gt;&lt;label for="voxel15" style="background-position: -1400% -0%"&gt;&lt;/label&gt;
  815. + &lt;input type="radio" name="voxel" id="voxel16" value="16"&gt;&lt;label for="voxel16" style="background-position: -1500% -0%"&gt;&lt;/label&gt;
  816. + &lt;/div&gt;
  817. + &lt;/div&gt;
  818. &lt;/body&gt;
  819. </pre>
  820. <p>Et ajouter du CSS pour le styliser, afficher les tuiles et mettre en évidence
  821. la sélection actuelle</p>
  822. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">body {
  823. margin: 0;
  824. }
  825. #c {
  826. width: 100%;
  827. height: 100%;
  828. display: block;
  829. }
  830. +#ui {
  831. + position: absolute;
  832. + left: 10px;
  833. + top: 10px;
  834. + background: rgba(0, 0, 0, 0.8);
  835. + padding: 5px;
  836. +}
  837. +#ui input[type=radio] {
  838. + width: 0;
  839. + height: 0;
  840. + display: none;
  841. +}
  842. +#ui input[type=radio] + label {
  843. + background-image: url('resources/images/minecraft/flourish-cc-by-nc-sa.png');
  844. + background-size: 1600% 400%;
  845. + image-rendering: pixelated;
  846. + width: 64px;
  847. + height: 64px;
  848. + display: inline-block;
  849. +}
  850. +#ui input[type=radio]:checked + label {
  851. + outline: 3px solid red;
  852. +}
  853. +@media (max-width: 600px), (max-height: 600px) {
  854. + #ui input[type=radio] + label {
  855. + width: 32px;
  856. + height: 32px;
  857. + }
  858. +}
  859. </pre>
  860. <p>L'expérience utilisateur (UX) sera la suivante. Si aucune tuile n'est sélectionnée et que vous cliquez sur un voxel, ce voxel sera effacé, ou si vous cliquez sur un voxel et que vous maintenez la touche Maj enfoncée, il sera effacé. Sinon, si une tuile est sélectionnée, elle sera ajoutée. Vous pouvez désélectionner le type de tuile sélectionné en cliquant à nouveau dessus.</p>
  861. <p>Ce code permettra à l'utilisateur de désélectionner le
  862. bouton radio surligné.</p>
  863. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let currentVoxel = 0;
  864. let currentId;
  865. document.querySelectorAll('#ui .tiles input[type=radio][name=voxel]').forEach((elem) =&gt; {
  866. elem.addEventListener('click', allowUncheck);
  867. });
  868. function allowUncheck() {
  869. if (this.id === currentId) {
  870. this.checked = false;
  871. currentId = undefined;
  872. currentVoxel = 0;
  873. } else {
  874. currentId = this.id;
  875. currentVoxel = parseInt(this.value);
  876. }
  877. }
  878. </pre>
  879. <p>Et le code ci-dessous nous permettra de définir un voxel en fonction de l'endroit
  880. où l'utilisateur clique. Il utilise un code similaire à celui que nous avons
  881. fait dans <a href="picking.html">l'article sur la sélection</a>
  882. mais il n'utilise pas le <code class="notranslate" translate="no">RayCaster</code> intégré. Au lieu de cela,
  883. il utilise <code class="notranslate" translate="no">VoxelWorld.intersectRay</code> qui renvoie
  884. la position d'intersection et la normale de la face
  885. touchée.</p>
  886. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  887. const rect = canvas.getBoundingClientRect();
  888. return {
  889. x: (event.clientX - rect.left) * canvas.width / rect.width,
  890. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  891. };
  892. }
  893. function placeVoxel(event) {
  894. const pos = getCanvasRelativePosition(event);
  895. const x = (pos.x / canvas.width ) * 2 - 1;
  896. const y = (pos.y / canvas.height) * -2 + 1; // notez que nous inversons Y
  897. const start = new THREE.Vector3();
  898. const end = new THREE.Vector3();
  899. start.setFromMatrixPosition(camera.matrixWorld);
  900. end.set(x, y, 1).unproject(camera);
  901. const intersection = world.intersectRay(start, end);
  902. if (intersection) {
  903. const voxelId = event.shiftKey ? 0 : currentVoxel;
  904. // le point d'intersection est sur la face. Cela signifie
  905. // que l'imprécision mathématique pourrait nous placer de chaque côté de la face.
  906. // alors allons à la moitié de la normale DANS le voxel si nous supprimons (currentVoxel = 0)
  907. // ou HORS du voxel si nous ajoutons (currentVoxel > 0)
  908. const pos = intersection.position.map((v, ndx) =&gt; {
  909. return v + intersection.normal[ndx] * (voxelId &gt; 0 ? 0.5 : -0.5);
  910. });
  911. world.setVoxel(...pos, voxelId);
  912. updateVoxelGeometry(...pos);
  913. requestRenderIfNotRequested();
  914. }
  915. }
  916. const mouse = {
  917. x: 0,
  918. y: 0,
  919. };
  920. function recordStartPosition(event) {
  921. mouse.x = event.clientX;
  922. mouse.y = event.clientY;
  923. mouse.moveX = 0;
  924. mouse.moveY = 0;
  925. }
  926. function recordMovement(event) {
  927. mouse.moveX += Math.abs(mouse.x - event.clientX);
  928. mouse.moveY += Math.abs(mouse.y - event.clientY);
  929. }
  930. function placeVoxelIfNoMovement(event) {
  931. if (mouse.moveX &lt; 5 &amp;&amp; mouse.moveY &lt; 5) {
  932. placeVoxel(event);
  933. }
  934. window.removeEventListener('pointermove', recordMovement);
  935. window.removeEventListener('pointerup', placeVoxelIfNoMovement);
  936. }
  937. canvas.addEventListener('pointerdown', (event) =&gt; {
  938. event.preventDefault();
  939. recordStartPosition(event);
  940. window.addEventListener('pointermove', recordMovement);
  941. window.addEventListener('pointerup', placeVoxelIfNoMovement);
  942. }, {passive: false});
  943. canvas.addEventListener('touchstart', (event) =&gt; {
  944. // arrêter le défilement
  945. event.preventDefault();
  946. }, {passive: false});
  947. </pre>
  948. <p>Il se passe beaucoup de choses dans le code ci-dessus. En gros, la souris a une double fonction. L'une est de déplacer la caméra. L'autre est d'éditer le monde. Placer/Effacer un voxel se produit lorsque vous relâchez la souris, mais uniquement si vous n'avez pas bougé la souris depuis que vous avez appuyé pour la première fois. C'est juste une supposition que si vous avez bougé la souris, vous essayiez de déplacer la caméra, pas de placer un bloc. <code class="notranslate" translate="no">moveX</code> et <code class="notranslate" translate="no">moveY</code> sont en mouvement absolu, donc si vous vous déplacez de 10 vers la gauche puis de 10 vers la droite, vous aurez parcouru 20 unités. Dans ce cas, l'utilisateur était probablement juste en train de faire pivoter le modèle d'avant en arrière et ne voulait pas placer de bloc. Je n'ai pas fait de tests pour voir si <code class="notranslate" translate="no">5</code> est une bonne valeur ou non.</p>
  949. <p>Dans le code, nous appelons <code class="notranslate" translate="no">world.setVoxel</code> pour définir un voxel et
  950. ensuite <code class="notranslate" translate="no">updateVoxelGeometry</code> pour mettre à jour la géométrie three.js
  951. en fonction de ce qui a changé.</p>
  952. <p>Faisons cela maintenant. Si l'utilisateur clique sur un
  953. voxel au bord d'une cellule, la géométrie du voxel
  954. dans la cellule adjacente pourrait avoir besoin d'une nouvelle géométrie. Cela signifie
  955. que nous devons vérifier la cellule du voxel que nous venons d'éditer
  956. ainsi que dans les 6 directions à partir de cette cellule.</p>
  957. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const neighborOffsets = [
  958. [ 0, 0, 0], // soi-même
  959. [-1, 0, 0], // gauche
  960. [ 1, 0, 0], // droite
  961. [ 0, -1, 0], // bas
  962. [ 0, 1, 0], // haut
  963. [ 0, 0, -1], // arrière
  964. [ 0, 0, 1], // avant
  965. ];
  966. function updateVoxelGeometry(x, y, z) {
  967. const updatedCellIds = {};
  968. for (const offset of neighborOffsets) {
  969. const ox = x + offset[0];
  970. const oy = y + offset[1];
  971. const oz = z + offset[2];
  972. const cellId = world.computeCellId(ox, oy, oz);
  973. if (!updatedCellIds[cellId]) {
  974. updatedCellIds[cellId] = true;
  975. updateCellGeometry(ox, oy, oz);
  976. }
  977. }
  978. }
  979. </pre>
  980. <p>J'ai pensé à vérifier les cellules adjacentes comme </p>
  981. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  982. if (voxelX === 0) {
  983. // mettre à jour la cellule à gauche
  984. } else if (voxelX === cellSize - 1) {
  985. // mettre à jour la cellule à droite
  986. }
  987. </pre>
  988. <p>et il y aurait 4 vérifications supplémentaires pour les 4 autres directions,
  989. mais il m'est apparu que le code serait beaucoup plus simple avec
  990. juste un tableau d'offsets et en sauvegardant les cell ids des
  991. cellules que nous avons déjà mises à jour. Si le voxel mis à jour n'est pas
  992. au bord d'une cellule, le test rejettera rapidement la mise à jour
  993. de la même cellule.</p>
  994. <p>Pour <code class="notranslate" translate="no">updateCellGeometry</code>, nous allons simplement prendre le code que nous
  995. avions auparavant et qui générait la géométrie pour une cellule
  996. et le faire gérer plusieurs cellules.</p>
  997. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellIdToMesh = {};
  998. function updateCellGeometry(x, y, z) {
  999. const cellX = Math.floor(x / cellSize);
  1000. const cellY = Math.floor(y / cellSize);
  1001. const cellZ = Math.floor(z / cellSize);
  1002. const cellId = world.computeCellId(x, y, z);
  1003. let mesh = cellIdToMesh[cellId];
  1004. const geometry = mesh ? mesh.geometry : new THREE.BufferGeometry();
  1005. const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(cellX, cellY, cellZ);
  1006. const positionNumComponents = 3;
  1007. geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  1008. const normalNumComponents = 3;
  1009. geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  1010. const uvNumComponents = 2;
  1011. geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  1012. geometry.setIndex(indices);
  1013. geometry.computeBoundingSphere();
  1014. if (!mesh) {
  1015. mesh = new THREE.Mesh(geometry, material);
  1016. mesh.name = cellId;
  1017. cellIdToMesh[cellId] = mesh;
  1018. scene.add(mesh);
  1019. mesh.position.set(cellX * cellSize, cellY * cellSize, cellZ * cellSize);
  1020. }
  1021. }
  1022. </pre>
  1023. <p>Le code ci-dessus vérifie une map de cell ids vers les maillages. Si
  1024. nous demandons une cellule qui n'existe pas, un nouveau <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> est créé
  1025. et ajouté au bon endroit dans l'espace monde.
  1026. À la fin, nous mettons à jour les attributes et les indices avec les nouvelles données.</p>
  1027. <p></p><div translate="no" class="threejs_example_container notranslate">
  1028. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-ui.html"></iframe></div>
  1029. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces-ui.html" target="_blank">cliquez ici pour ouvrir dans une fenêtre séparée</a>
  1030. </div>
  1031. <p></p>
  1032. <p>Quelques notes :</p>
  1033. <p>Le <code class="notranslate" translate="no">RayCaster</code> aurait peut-être fonctionné très bien. Je n'ai pas essayé.
  1034. Au lieu de cela, j'ai trouvé <a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf">un raycaster spécifique aux voxels</a>.
  1035. qui est optimisé pour les voxels.</p>
  1036. <p>J'ai fait de <code class="notranslate" translate="no">intersectRay</code> une partie de VoxelWorld car il semblait
  1037. que si cela devenait trop lent, nous pourrions lancer des rayons contre les cellules
  1038. avant de le faire sur les voxels comme une simple accélération si cela devenait
  1039. trop lent.</p>
  1040. <p>Vous pourriez vouloir changer la longueur du raycast
  1041. car actuellement, elle va jusqu'au Z-far. Je suppose que si l'
  1042. utilisateur clique sur quelque chose de trop éloigné, il ne veut pas vraiment
  1043. placer des blocs de l'autre côté du monde qui font 1 ou 2 pixels.</p>
  1044. <p>Appeler <code class="notranslate" translate="no">geometry.computeBoundingSphere</code> pourrait être lent.
  1045. Nous pourrions simplement définir manuellement la bounding sphere pour qu'elle s'adapte
  1046. à la cellule entière.</p>
  1047. <p>Voulons-nous supprimer les cellules si tous les voxels de cette cellule sont à 0 ?
  1048. Ce serait probablement un changement raisonnable si nous voulions livrer ceci.</p>
  1049. <p>En réfléchissant à la manière dont cela fonctionne, il est clair que le
  1050. pire des cas absolu est un damier de voxels activés et désactivés. Je ne
  1051. sais pas d'emblée quelles autres stratégies utiliser
  1052. si les choses deviennent trop lentes. Peut-être que devenir trop lent
  1053. encouragerait simplement l'utilisateur à ne pas créer d'énormes zones en damier.</p>
  1054. <p>Pour simplifier, la texture atlas n'a qu'une seule colonne
  1055. par type de voxel. Il serait préférable de faire quelque chose de plus
  1056. flexible où nous aurions un tableau de types de voxels et chaque
  1057. type pourrait spécifier où se trouvent les textures de ses faces dans l'atlas.
  1058. Tel quel, beaucoup d'espace est gaspillé.</p>
  1059. <p>En regardant le vrai minecraft, il y a des tuiles qui ne sont pas
  1060. des voxels, pas des cubes. Comme une tuile de clôture ou des fleurs. Pour faire cela,
  1061. nous aurions à nouveau besoin d'un tableau de types de voxels et pour chaque
  1062. voxel, s'il s'agit d'un cube ou d'une autre géométrie. S'il ne s'agit pas d'un cube,
  1063. la vérification des voisins lors de la génération de la géométrie
  1064. devrait également changer. Un voxel de fleur à côté d'un autre
  1065. voxel ne devrait pas supprimer les faces entre eux.</p>
  1066. <p>Si vous voulez créer quelque chose de similaire à minecraft en utilisant three.js,
  1067. j'espère que cela vous a donné quelques idées pour commencer et comment
  1068. générer une géométrie quelque peu efficace.</p>
  1069. <p><canvas id="c"></canvas></p>
  1070. <script type="module" src="../resources/threejs-voxel-geometry.js"></script>
  1071. </div>
  1072. </div>
  1073. </div>
  1074. <script src="../resources/prettify.js"></script>
  1075. <script src="../resources/lesson.js"></script>
  1076. </body></html>
粤ICP备19079148号