|
|
|
|
<!DOCTYPE html><html lang="fr"><head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<title>Géométrie Voxel (type Minecraft)</title>
|
|
|
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
|
|
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
|
|
|
<meta name="twitter:site" content="@threejs">
|
|
|
|
|
<meta name="twitter:title" content="Three.js – Géométrie Voxel (type Minecraft)">
|
|
|
|
|
<meta property="og:image" content="https://threejs.org/files/share.png">
|
|
|
|
|
<link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
|
|
|
|
|
<link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="../resources/lesson.css">
|
|
|
|
|
<link rel="stylesheet" href="../resources/lang.css">
|
|
|
|
|
<script type="importmap">
|
|
|
|
|
{
|
|
|
|
|
"imports": {
|
|
|
|
|
"three": "../../build/three.module.js"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="lesson-title">
|
|
|
|
|
<h1>Géométrie Voxel (type Minecraft)</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="lesson">
|
|
|
|
|
<div class="lesson-main">
|
|
|
|
|
<p>J'ai vu ce sujet revenir plus d'une fois à divers endroits.
|
|
|
|
|
C'est fondamentalement, "Comment faire un affichage de voxels comme Minecraft".</p>
|
|
|
|
|
<p>La plupart des gens essaient d'abord en créant une géométrie de cube, puis
|
|
|
|
|
en faisant un maillage à chaque position de voxel. Juste pour le plaisir, j'ai essayé
|
|
|
|
|
cela. J'ai créé un <code class="notranslate" translate="no">Uint8Array</code> de 16777216 éléments pour représenter
|
|
|
|
|
un cube de voxels de 256x256x256.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 256;
|
|
|
|
|
const cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
</pre>
|
|
|
|
|
<p>J'ai ensuite fait une seule couche avec une sorte de collines de
|
|
|
|
|
vagues sinusoïdales comme ceci</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
|
|
|
|
|
if (height > y && height < y + 1) {
|
|
|
|
|
const offset = y * cellSize * cellSize +
|
|
|
|
|
z * cellSize +
|
|
|
|
|
x;
|
|
|
|
|
cell[offset] = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>J'ai ensuite parcouru toutes les cellules et si elles n'étaient pas
|
|
|
|
|
à 0, j'ai créé un maillage avec un cube.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
|
|
|
const material = new THREE.MeshPhongMaterial({color: 'green'});
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const offset = y * cellSize * cellSize +
|
|
|
|
|
z * cellSize +
|
|
|
|
|
x;
|
|
|
|
|
const block = cell[offset];
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
mesh.position.set(x, y, z);
|
|
|
|
|
scene.add(mesh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Le reste du code est basé sur l'exemple de
|
|
|
|
|
<a href="rendering-on-demand.html">l'article sur le rendu à la demande</a>.</p>
|
|
|
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
|
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-separate-cubes.html"></iframe></div>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p></p>
|
|
|
|
|
<p>Cela prend un certain temps pour démarrer et si vous essayez de bouger la caméra
|
|
|
|
|
c'est probablement trop lent. Comme dans <a href="optimize-lots-of-objects.html">l'article sur l'optimisation de nombreux objets</a>
|
|
|
|
|
le problème est qu'il y a juste beaucoup trop d'objets. 256x256
|
|
|
|
|
fait 65536 boîtes !</p>
|
|
|
|
|
<p>L'utilisation de <a href="rendering-on-demand.html">la technique de fusion de la géométrie</a>
|
|
|
|
|
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 ?
|
|
|
|
|
En d'autres termes, changez la boucle qui remplit les voxels comme ceci :</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
|
|
|
|
|
- if (height > y && height < y + 1) {
|
|
|
|
|
+ if (height < y + 1) {
|
|
|
|
|
const offset = y * cellSize * cellSize +
|
|
|
|
|
z * cellSize +
|
|
|
|
|
x;
|
|
|
|
|
cell[offset] = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>J'ai essayé une fois juste pour voir les résultats. Ça a mouliné pendant
|
|
|
|
|
environ une minute, puis ça a planté avec un message <em>manque de mémoire</em> 😅</p>
|
|
|
|
|
<p>Il y a plusieurs problèmes, mais le plus important est
|
|
|
|
|
que nous créons toutes ces faces à l'intérieur des cubes que
|
|
|
|
|
nous ne pouvons en fait jamais voir.</p>
|
|
|
|
|
<p>En d'autres termes, disons que nous avons une boîte de voxels
|
|
|
|
|
3x2x2. En fusionnant les cubes, nous obtenons ceci :</p>
|
|
|
|
|
<div class="spread">
|
|
|
|
|
<div data-diagram="mergedCubes" style="height: 300px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p>mais nous voulons vraiment ceci</p>
|
|
|
|
|
<div class="spread">
|
|
|
|
|
<div data-diagram="culledCubes" style="height: 300px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p>Dans la boîte du haut, il y a des faces entre les voxels. Des faces
|
|
|
|
|
qui sont un gâchis car elles ne peuvent pas être vues. Ce n'est pas seulement
|
|
|
|
|
une face entre chaque voxel, il y a 2 faces, une pour
|
|
|
|
|
chaque voxel faisant face à son voisin qui sont un gâchis. Toutes ces faces supplémentaires,
|
|
|
|
|
surtout pour un grand volume de voxels, tueront les performances.</p>
|
|
|
|
|
<p>Il devrait être clair que nous ne pouvons pas simplement fusionner la géométrie.
|
|
|
|
|
Nous devons la construire nous-mêmes, en tenant compte du fait
|
|
|
|
|
que si un voxel a un voisin adjacent, il n'a pas besoin de la
|
|
|
|
|
face qui fait face à ce voisin.</p>
|
|
|
|
|
<p>Le problème suivant est que 256x256x256 est tout simplement trop grand. 16 Mo représentent beaucoup de mémoire et
|
|
|
|
|
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 à
|
|
|
|
|
considérer d'un coup.</p>
|
|
|
|
|
<p>Une solution consiste à diviser la zone en zones plus petites.
|
|
|
|
|
Toute zone qui ne contient rien n'a pas besoin de stockage. Utilisons
|
|
|
|
|
des zones de 32x32x32 (soit 32k) et ne créons une zone que si elle contient quelque chose.
|
|
|
|
|
Nous appellerons l'une de ces zones plus grandes de 32x32x32 une "cellule".</p>
|
|
|
|
|
<p>Découpons cela en morceaux. Tout d'abord, créons une classe pour gérer les données de voxel.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Créons la fonction qui génère la géométrie pour une cellule.
|
|
|
|
|
Supposons que vous passiez une position de cellule.
|
|
|
|
|
En d'autres termes, si vous voulez la géométrie pour la cellule qui couvre les voxels (0-31x, 0-31y, 0-31z)
|
|
|
|
|
alors vous passerez 0,0,0. Pour la cellule qui couvre les voxels (32-63x, 0-31y, 0-31z), vous passerez
|
|
|
|
|
1,0,0.</p>
|
|
|
|
|
<p>Nous devons pouvoir vérifier les voxels voisins, alors supposons que notre classe
|
|
|
|
|
dispose d'une fonction <code class="notranslate" translate="no">getVoxel</code> qui, étant donné une position de voxel, renvoie la valeur
|
|
|
|
|
du voxel à cet endroit. En d'autres termes, si vous lui passez 35,0,0 et que la cellSize est de 32,
|
|
|
|
|
elle regardera la cellule 1,0,0 et dans cette cellule, elle regardera le voxel 3,0,0.
|
|
|
|
|
En utilisant cette fonction, nous pouvons regarder les voxels voisins d'un voxel, même s'ils
|
|
|
|
|
se trouvent dans des cellules voisines.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
}
|
|
|
|
|
+ generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ const startX = cellX * cellSize;
|
|
|
|
|
+ const startY = cellY * cellSize;
|
|
|
|
|
+ const startZ = cellZ * cellSize;
|
|
|
|
|
+
|
|
|
|
|
+ for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
+ const voxelY = startY + y;
|
|
|
|
|
+ for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
+ const voxelZ = startZ + z;
|
|
|
|
|
+ for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
+ const voxelX = startX + x;
|
|
|
|
|
+ const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
|
|
|
|
|
+ if (voxel) {
|
|
|
|
|
+ for (const {dir} of VoxelWorld.faces) {
|
|
|
|
|
+ const neighbor = this.getVoxel(
|
|
|
|
|
+ voxelX + dir[0],
|
|
|
|
|
+ voxelY + dir[1],
|
|
|
|
|
+ voxelZ + dir[2]);
|
|
|
|
|
+ if (!neighbor) {
|
|
|
|
|
+ // ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face ici.
|
|
|
|
|
+ // here.
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+VoxelWorld.faces = [
|
|
|
|
|
+ { // gauche
|
|
|
|
|
+ dir: [ -1, 0, 0, ],
|
|
|
|
|
+ },
|
|
|
|
|
+ { // droite
|
|
|
|
|
+ dir: [ 1, 0, 0, ],
|
|
|
|
|
+ },
|
|
|
|
|
+ { // bas
|
|
|
|
|
+ dir: [ 0, -1, 0, ],
|
|
|
|
|
+ },
|
|
|
|
|
+ { // haut
|
|
|
|
|
+ dir: [ 0, 1, 0, ],
|
|
|
|
|
+ },
|
|
|
|
|
+ { // arrière
|
|
|
|
|
+ dir: [ 0, 0, -1, ],
|
|
|
|
|
+ },
|
|
|
|
|
+ { // avant
|
|
|
|
|
+ dir: [ 0, 0, 1, ],
|
|
|
|
|
+ },
|
|
|
|
|
+];
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Donc, en utilisant le code ci-dessus, nous savons quand nous avons besoin d'une face. Générons les faces.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
}
|
|
|
|
|
generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
const {cellSize} = this;
|
|
|
|
|
+ const positions = [];
|
|
|
|
|
+ const normals = [];
|
|
|
|
|
+ const indices = [];
|
|
|
|
|
const startX = cellX * cellSize;
|
|
|
|
|
const startY = cellY * cellSize;
|
|
|
|
|
const startZ = cellZ * cellSize;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
const voxelY = startY + y;
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
const voxelZ = startZ + z;
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const voxelX = startX + x;
|
|
|
|
|
const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
|
|
|
|
|
if (voxel) {
|
|
|
|
|
- for (const {dir} of VoxelWorld.faces) {
|
|
|
|
|
+ for (const {dir, corners} of VoxelWorld.faces) {
|
|
|
|
|
const neighbor = this.getVoxel(
|
|
|
|
|
voxelX + dir[0],
|
|
|
|
|
voxelY + dir[1],
|
|
|
|
|
voxelZ + dir[2]);
|
|
|
|
|
if (!neighbor) {
|
|
|
|
|
// ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face.
|
|
|
|
|
+ const ndx = positions.length / 3;
|
|
|
|
|
+ for (const pos of corners) {
|
|
|
|
|
+ positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
|
|
|
|
|
+ normals.push(...dir);
|
|
|
|
|
+ }
|
|
|
|
|
+ indices.push(
|
|
|
|
|
+ ndx, ndx + 1, ndx + 2,
|
|
|
|
|
+ ndx + 2, ndx + 1, ndx + 3,
|
|
|
|
|
+ );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
+ return {
|
|
|
|
|
+ positions,
|
|
|
|
|
+ normals,
|
|
|
|
|
+ indices,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VoxelWorld.faces = [
|
|
|
|
|
{ // gauche
|
|
|
|
|
dir: [ -1, 0, 0, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 0, 1, 0 ],
|
|
|
|
|
+ [ 0, 0, 0 ],
|
|
|
|
|
+ [ 0, 1, 1 ],
|
|
|
|
|
+ [ 0, 0, 1 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // droite
|
|
|
|
|
dir: [ 1, 0, 0, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 1, 1, 1 ],
|
|
|
|
|
+ [ 1, 0, 1 ],
|
|
|
|
|
+ [ 1, 1, 0 ],
|
|
|
|
|
+ [ 1, 0, 0 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // bas
|
|
|
|
|
dir: [ 0, -1, 0, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 1, 0, 1 ],
|
|
|
|
|
+ [ 0, 0, 1 ],
|
|
|
|
|
+ [ 1, 0, 0 ],
|
|
|
|
|
+ [ 0, 0, 0 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // haut
|
|
|
|
|
dir: [ 0, 1, 0, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 0, 1, 1 ],
|
|
|
|
|
+ [ 1, 1, 1 ],
|
|
|
|
|
+ [ 0, 1, 0 ],
|
|
|
|
|
+ [ 1, 1, 0 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // arrière
|
|
|
|
|
dir: [ 0, 0, -1, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 1, 0, 0 ],
|
|
|
|
|
+ [ 0, 0, 0 ],
|
|
|
|
|
+ [ 1, 1, 0 ],
|
|
|
|
|
+ [ 0, 1, 0 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // avant
|
|
|
|
|
dir: [ 0, 0, 1, ],
|
|
|
|
|
+ corners: [
|
|
|
|
|
+ [ 0, 0, 1 ],
|
|
|
|
|
+ [ 1, 0, 1 ],
|
|
|
|
|
+ [ 0, 1, 1 ],
|
|
|
|
|
+ [ 1, 1, 1 ],
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Le code ci-dessus générerait des données de géométrie de base pour nous. Il suffit de fournir
|
|
|
|
|
la fonction <code class="notranslate" translate="no">getVoxel</code>. Commençons par une seule cellule codée en dur.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
+ this.cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
}
|
|
|
|
|
+ getCellForVoxel(x, y, z) {
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ const cellX = Math.floor(x / cellSize);
|
|
|
|
|
+ const cellY = Math.floor(y / cellSize);
|
|
|
|
|
+ const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
+ if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.cell;
|
|
|
|
|
+ }
|
|
|
|
|
+ getVoxel(x, y, z) {
|
|
|
|
|
+ const cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
+ if (!cell) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
+ const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
+ const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
+ const voxelOffset = voxelY * cellSize * cellSize +
|
|
|
|
|
+ voxelZ * cellSize +
|
|
|
|
|
+ voxelX;
|
|
|
|
|
+ return cell[voxelOffset];
|
|
|
|
|
+ }
|
|
|
|
|
generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Cela semble fonctionner. Créons une fonction <code class="notranslate" translate="no">setVoxel</code>
|
|
|
|
|
pour pouvoir définir des données.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
this.cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
}
|
|
|
|
|
getCellForVoxel(x, y, z) {
|
|
|
|
|
const {cellSize} = this;
|
|
|
|
|
const cellX = Math.floor(x / cellSize);
|
|
|
|
|
const cellY = Math.floor(y / cellSize);
|
|
|
|
|
const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return this.cell;
|
|
|
|
|
}
|
|
|
|
|
+ setVoxel(x, y, z, v) {
|
|
|
|
|
+ let cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
+ if (!cell) {
|
|
|
|
|
+ return; // TODO : ajouter une nouvelle cellule ?
|
|
|
|
|
+ }
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
+ const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
+ const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
+ const voxelOffset = voxelY * cellSize * cellSize +
|
|
|
|
|
+ voxelZ * cellSize +
|
|
|
|
|
+ voxelX;
|
|
|
|
|
+ cell[voxelOffset] = v;
|
|
|
|
|
+ }
|
|
|
|
|
getVoxel(x, y, z) {
|
|
|
|
|
const cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
if (!cell) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const {cellSize} = this;
|
|
|
|
|
const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
const voxelOffset = voxelY * cellSize * cellSize +
|
|
|
|
|
voxelZ * cellSize +
|
|
|
|
|
voxelX;
|
|
|
|
|
return cell[voxelOffset];
|
|
|
|
|
}
|
|
|
|
|
generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Hmmm, je vois beaucoup de code répété. Arrangeons ça</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(cellSize) {
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
+ this.cellSliceSize = cellSize * cellSize;
|
|
|
|
|
this.cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
}
|
|
|
|
|
getCellForVoxel(x, y, z) {
|
|
|
|
|
const {cellSize} = this;
|
|
|
|
|
const cellX = Math.floor(x / cellSize);
|
|
|
|
|
const cellY = Math.floor(y / cellSize);
|
|
|
|
|
const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return this.cell;
|
|
|
|
|
}
|
|
|
|
|
+ computeVoxelOffset(x, y, z) {
|
|
|
|
|
+ const {cellSize, cellSliceSize} = this;
|
|
|
|
|
+ const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
+ const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
+ const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
+ return voxelY * cellSliceSize +
|
|
|
|
|
+ voxelZ * cellSize +
|
|
|
|
|
+ voxelX;
|
|
|
|
|
+ }
|
|
|
|
|
setVoxel(x, y, z, v) {
|
|
|
|
|
const cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
if (!cell) {
|
|
|
|
|
return; // TODO : ajouter une nouvelle cellule ?
|
|
|
|
|
}
|
|
|
|
|
- const {cellSize} = this;
|
|
|
|
|
- const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
- const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
- const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
- const voxelOffset = voxelY * cellSize * cellSize +
|
|
|
|
|
- voxelZ * cellSize +
|
|
|
|
|
- voxelX;
|
|
|
|
|
+ const voxelOffset = this.computeVoxelOffset(x, y, z);
|
|
|
|
|
cell[voxelOffset] = v;
|
|
|
|
|
}
|
|
|
|
|
getVoxel(x, y, z) {
|
|
|
|
|
const cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
if (!cell) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
- const {cellSize} = this;
|
|
|
|
|
- const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
- const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
|
|
|
|
|
- const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
|
|
|
|
|
- const voxelOffset = voxelY * cellSize * cellSize +
|
|
|
|
|
- voxelZ * cellSize +
|
|
|
|
|
- voxelX;
|
|
|
|
|
+ const voxelOffset = this.computeVoxelOffset(x, y, z);
|
|
|
|
|
return cell[voxelOffset];
|
|
|
|
|
}
|
|
|
|
|
generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Maintenant, créons du code pour remplir la première cellule avec des voxels.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 32;
|
|
|
|
|
|
|
|
|
|
const world = new VoxelWorld(cellSize);
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
|
|
|
|
|
if (y < height) {
|
|
|
|
|
world.setVoxel(x, y, z, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>et du code pour effectivement générer la géométrie comme nous l'avons vu dans
|
|
|
|
|
<a href="custom-buffergeometry.html">l'article sur BufferGeometry personnalisé</a>.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
|
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
|
|
|
const material = new THREE.MeshLambertMaterial({color: 'green'});
|
|
|
|
|
|
|
|
|
|
const positionNumComponents = 3;
|
|
|
|
|
const normalNumComponents = 3;
|
|
|
|
|
geometry.setAttribute(
|
|
|
|
|
'position',
|
|
|
|
|
new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
|
|
|
|
|
geometry.setAttribute(
|
|
|
|
|
'normal',
|
|
|
|
|
new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
|
|
|
|
|
geometry.setIndex(indices);
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
scene.add(mesh);
|
|
|
|
|
</pre>
|
|
|
|
|
<p>essayons</p>
|
|
|
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
|
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces.html"></iframe></div>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p></p>
|
|
|
|
|
<p>Cela semble fonctionner ! D'accord, ajoutons des textures.</p>
|
|
|
|
|
<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>
|
|
|
|
|
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>.
|
|
|
|
|
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>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<p>Pour simplifier les choses, elles sont arrangées un type de voxel par colonne,
|
|
|
|
|
où la rangée supérieure est le côté d'un voxel. La 2ème rangée est
|
|
|
|
|
le dessus du voxel, et la 3ème rangée est le dessous du voxel.</p>
|
|
|
|
|
<p>Sachant cela, nous pouvons ajouter des informations à nos données <code class="notranslate" translate="no">VoxelWorld.faces</code>
|
|
|
|
|
pour spécifier pour chaque face quelle rangée utiliser et les UVs à utiliser
|
|
|
|
|
pour cette face.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">VoxelWorld.faces = [
|
|
|
|
|
{ // gauche
|
|
|
|
|
+ uvRow: 0,
|
|
|
|
|
dir: [ -1, 0, 0, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 0, 1, 0 ],
|
|
|
|
|
- [ 0, 0, 0 ],
|
|
|
|
|
- [ 0, 1, 1 ],
|
|
|
|
|
- [ 0, 0, 1 ],
|
|
|
|
|
+ { pos: [ 0, 1, 0 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ { pos: [ 0, 0, 0 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ { pos: [ 0, 0, 1 ], uv: [ 1, 0 ], },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{ // droite
|
|
|
|
|
+ uvRow: 0,
|
|
|
|
|
dir: [ 1, 0, 0, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 1, 1, 1 ],
|
|
|
|
|
- [ 1, 0, 1 ],
|
|
|
|
|
- [ 1, 1, 0 ],
|
|
|
|
|
- [ 1, 0, 0 ],
|
|
|
|
|
+ { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ { pos: [ 1, 0, 1 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ { pos: [ 1, 1, 0 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ { pos: [ 1, 0, 0 ], uv: [ 1, 0 ], },
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // bas
|
|
|
|
|
+ uvRow: 1,
|
|
|
|
|
dir: [ 0, -1, 0, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 1, 0, 1 ],
|
|
|
|
|
- [ 0, 0, 1 ],
|
|
|
|
|
- [ 1, 0, 0 ],
|
|
|
|
|
- [ 0, 0, 0 ],
|
|
|
|
|
+ { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
|
|
|
|
|
+ { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ { pos: [ 1, 0, 0 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ { pos: [ 0, 0, 0 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // haut
|
|
|
|
|
+ uvRow: 2,
|
|
|
|
|
dir: [ 0, 1, 0, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 0, 1, 1 ],
|
|
|
|
|
- [ 1, 1, 1 ],
|
|
|
|
|
- [ 0, 1, 0 ],
|
|
|
|
|
- [ 1, 1, 0 ],
|
|
|
|
|
+ { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ { pos: [ 0, 1, 0 ], uv: [ 1, 0 ], },
|
|
|
|
|
+ { pos: [ 1, 1, 0 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // arrière
|
|
|
|
|
+ uvRow: 0,
|
|
|
|
|
dir: [ 0, 0, -1, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 1, 0, 0 ],
|
|
|
|
|
- [ 0, 0, 0 ],
|
|
|
|
|
- [ 1, 1, 0 ],
|
|
|
|
|
- [ 0, 1, 0 ],
|
|
|
|
|
+ { pos: [ 1, 0, 0 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ { pos: [ 0, 0, 0 ], uv: [ 1, 0 ], },
|
|
|
|
|
+ { pos: [ 1, 1, 0 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ { pos: [ 0, 1, 0 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
{ // avant
|
|
|
|
|
+ uvRow: 0,
|
|
|
|
|
dir: [ 0, 0, 1, ],
|
|
|
|
|
corners: [
|
|
|
|
|
- [ 0, 0, 1 ],
|
|
|
|
|
- [ 1, 0, 1 ],
|
|
|
|
|
- [ 0, 1, 1 ],
|
|
|
|
|
- [ 1, 1, 1 ],
|
|
|
|
|
+ { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
|
|
|
|
|
+ { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
|
|
|
|
|
+ { pos: [ 0, 1, 1 ], uv: [ 0, 1 ], },
|
|
|
|
|
+ { pos: [ 1, 1, 1 ], uv: [ 1, 1 ], },
|
|
|
|
|
+ ],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Et nous pouvons mettre à jour le code pour utiliser ces données. Nous devons
|
|
|
|
|
connaître la taille d'une tuile dans la texture atlas et les dimensions
|
|
|
|
|
de la texture.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
- constructor(cellSize) {
|
|
|
|
|
- this.cellSize = cellSize;
|
|
|
|
|
+ constructor(options) {
|
|
|
|
|
+ this.cellSize = options.cellSize;
|
|
|
|
|
+ this.tileSize = options.tileSize;
|
|
|
|
|
+ this.tileTextureWidth = options.tileTextureWidth;
|
|
|
|
|
+ this.tileTextureHeight = options.tileTextureHeight;
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ this.cellSliceSize = cellSize * cellSize;
|
|
|
|
|
+ this.cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
generateGeometryDataForCell(cellX, cellY, cellZ) {
|
|
|
|
|
- const {cellSize} = this;
|
|
|
|
|
+ const {cellSize, tileSize, tileTextureWidth, tileTextureHeight} = this;
|
|
|
|
|
const positions = [];
|
|
|
|
|
const normals = [];
|
|
|
|
|
+ const uvs = [];
|
|
|
|
|
const indices = [];
|
|
|
|
|
const startX = cellX * cellSize;
|
|
|
|
|
const startY = cellY * cellSize;
|
|
|
|
|
const startZ = cellZ * cellSize;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
const voxelY = startY + y;
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
const voxelZ = startZ + z;
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const voxelX = startX + x;
|
|
|
|
|
const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
|
|
|
|
|
if (voxel) {
|
|
|
|
|
const uvVoxel = voxel - 1; // le voxel 0 est le ciel, donc pour les UVs nous commençons à 0
|
|
|
|
|
// There is a voxel here but do we need faces for it?
|
|
|
|
|
- for (const {dir, corners} of VoxelWorld.faces) {
|
|
|
|
|
+ for (const {dir, corners, uvRow} of VoxelWorld.faces) {
|
|
|
|
|
const neighbor = this.getVoxel(
|
|
|
|
|
voxelX + dir[0],
|
|
|
|
|
voxelY + dir[1],
|
|
|
|
|
voxelZ + dir[2]);
|
|
|
|
|
if (!neighbor) {
|
|
|
|
|
// ce voxel n'a pas de voisin dans cette direction, nous avons donc besoin d'une face.
|
|
|
|
|
const ndx = positions.length / 3;
|
|
|
|
|
- for (const pos of corners) {
|
|
|
|
|
+ for (const {pos, uv} of corners) {
|
|
|
|
|
positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
|
|
|
|
|
normals.push(...dir);
|
|
|
|
|
+ uvs.push(
|
|
|
|
|
+ (uvVoxel + uv[0]) * tileSize / tileTextureWidth,
|
|
|
|
|
+ 1 - (uvRow + 1 - uv[1]) * tileSize / tileTextureHeight);
|
|
|
|
|
}
|
|
|
|
|
indices.push(
|
|
|
|
|
ndx, ndx + 1, ndx + 2,
|
|
|
|
|
ndx + 2, ndx + 1, ndx + 3,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
positions,
|
|
|
|
|
normals,
|
|
|
|
|
uvs,
|
|
|
|
|
indices,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Nous devons ensuite <a href="textures.html">charger la texture</a></p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
|
|
|
|
|
const texture = loader.load('resources/images/minecraft/flourish-cc-by-nc-sa.png', render);
|
|
|
|
|
texture.magFilter = THREE.NearestFilter;
|
|
|
|
|
texture.minFilter = THREE.NearestFilter;
|
|
|
|
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
|
|
|
</pre>
|
|
|
|
|
<p>et passer les paramètres à la classe <code class="notranslate" translate="no">VoxelWorld</code></p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const tileSize = 16;
|
|
|
|
|
+const tileTextureWidth = 256;
|
|
|
|
|
+const tileTextureHeight = 64;
|
|
|
|
|
-const world = new VoxelWorld(cellSize);
|
|
|
|
|
+const world = new VoxelWorld({
|
|
|
|
|
+ cellSize,
|
|
|
|
|
+ tileSize,
|
|
|
|
|
+ tileTextureWidth,
|
|
|
|
|
+ tileTextureHeight,
|
|
|
|
|
+});
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Utilisons réellement les UVs lors de la création de la géométrie
|
|
|
|
|
et la texture lorsque nous fabriquons le matériau</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
|
|
|
|
|
+const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(0, 0, 0);
|
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
|
|
|
-const material = new THREE.MeshLambertMaterial({color: 'green'});
|
|
|
|
|
+const material = new THREE.MeshLambertMaterial({
|
|
|
|
|
+ map: texture,
|
|
|
|
|
+ side: THREE.DoubleSide,
|
|
|
|
|
+ alphaTest: 0.1,
|
|
|
|
|
+ transparent: true,
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
|
|
const positionNumComponents = 3;
|
|
|
|
|
const normalNumComponents = 3;
|
|
|
|
|
+const uvNumComponents = 2;
|
|
|
|
|
geometry.setAttribute(
|
|
|
|
|
'position',
|
|
|
|
|
new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
|
|
|
|
|
geometry.setAttribute(
|
|
|
|
|
'normal',
|
|
|
|
|
new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
|
|
|
|
|
+geometry.setAttribute(
|
|
|
|
|
+ 'uv',
|
|
|
|
|
+ new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
|
|
|
|
|
geometry.setIndex(indices);
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
scene.add(mesh);
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Une dernière chose, nous devons réellement définir certains voxels
|
|
|
|
|
pour utiliser différentes textures.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y < cellSize; ++y) {
|
|
|
|
|
for (let z = 0; z < cellSize; ++z) {
|
|
|
|
|
for (let x = 0; x < cellSize; ++x) {
|
|
|
|
|
const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
|
|
|
|
|
if (y < height) {
|
|
|
|
|
- world.setVoxel(x, y, z, 1);
|
|
|
|
|
+ world.setVoxel(x, y, z, randInt(1, 17));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function randInt(min, max) {
|
|
|
|
|
+ return Math.floor(Math.random() * (max - min) + min);
|
|
|
|
|
+}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>et avec cela, nous obtenons des textures !</p>
|
|
|
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
|
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-with-textures.html"></iframe></div>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p></p>
|
|
|
|
|
<p>Supportons maintenant plus d'une cellule.</p>
|
|
|
|
|
<p>Pour ce faire, stockons les cellules dans un objet en utilisant des cell ids.
|
|
|
|
|
Un cell id sera simplement les coordonnées d'une cellule séparées par
|
|
|
|
|
une virgule. En d'autres termes, si nous demandons le voxel 35,0,0,
|
|
|
|
|
qui est dans la cellule 1,0,0, son id est donc <code class="notranslate" translate="no">"1,0,0"</code>.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
|
|
|
|
|
constructor(options) {
|
|
|
|
|
this.cellSize = options.cellSize;
|
|
|
|
|
this.tileSize = options.tileSize;
|
|
|
|
|
this.tileTextureWidth = options.tileTextureWidth;
|
|
|
|
|
this.tileTextureHeight = options.tileTextureHeight;
|
|
|
|
|
const {cellSize} = this;
|
|
|
|
|
this.cellSliceSize = cellSize * cellSize;
|
|
|
|
|
- this.cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
+ this.cells = {};
|
|
|
|
|
}
|
|
|
|
|
+ computeCellId(x, y, z) {
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ const cellX = Math.floor(x / cellSize);
|
|
|
|
|
+ const cellY = Math.floor(y / cellSize);
|
|
|
|
|
+ const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
+ return `${cellX},${cellY},${cellZ}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ getCellForVoxel(x, y, z) {
|
|
|
|
|
- const cellX = Math.floor(x / cellSize);
|
|
|
|
|
- const cellY = Math.floor(y / cellSize);
|
|
|
|
|
- const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
- if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- return this.cell;
|
|
|
|
|
+ return this.cells[this.computeCellId(x, y, z)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>et maintenant nous pouvons faire en sorte que <code class="notranslate" translate="no">setVoxel</code> ajoute de nouvelles cellules si
|
|
|
|
|
nous essayons de définir un voxel dans une cellule qui n'existe pas encore</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no"> setVoxel(x, y, z, v) {
|
|
|
|
|
- const cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
+ let cell = this.getCellForVoxel(x, y, z);
|
|
|
|
|
if (!cell) {
|
|
|
|
|
- return 0;
|
|
|
|
|
+ cell = this.addCellForVoxel(x, y, z);
|
|
|
|
|
}
|
|
|
|
|
const voxelOffset = this.computeVoxelOffset(x, y, z);
|
|
|
|
|
cell[voxelOffset] = v;
|
|
|
|
|
}
|
|
|
|
|
+ addCellForVoxel(x, y, z) {
|
|
|
|
|
+ const cellId = this.computeCellId(x, y, z);
|
|
|
|
|
+ let cell = this.cells[cellId];
|
|
|
|
|
+ if (!cell) {
|
|
|
|
|
+ const {cellSize} = this;
|
|
|
|
|
+ cell = new Uint8Array(cellSize * cellSize * cellSize);
|
|
|
|
|
+ this.cells[cellId] = cell;
|
|
|
|
|
+ }
|
|
|
|
|
+ return cell;
|
|
|
|
|
+ }
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Rendons cela modifiable.</p>
|
|
|
|
|
<p>Tout d'abord, nous ajouterons une UI. En utilisant des boutons radio, nous pouvons créer un tableau de tuiles 8x2</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
|
|
|
|
|
<canvas id="c"></canvas>
|
|
|
|
|
+ <div id="ui">
|
|
|
|
|
+ <div class="tiles">
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel1" value="1"><label for="voxel1" style="background-position: -0% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel2" value="2"><label for="voxel2" style="background-position: -100% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel3" value="3"><label for="voxel3" style="background-position: -200% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel4" value="4"><label for="voxel4" style="background-position: -300% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel5" value="5"><label for="voxel5" style="background-position: -400% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel6" value="6"><label for="voxel6" style="background-position: -500% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel7" value="7"><label for="voxel7" style="background-position: -600% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel8" value="8"><label for="voxel8" style="background-position: -700% -0%"></label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tiles">
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel9" value="9" ><label for="voxel9" style="background-position: -800% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel10" value="10"><label for="voxel10" style="background-position: -900% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel11" value="11"><label for="voxel11" style="background-position: -1000% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel12" value="12"><label for="voxel12" style="background-position: -1100% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel13" value="13"><label for="voxel13" style="background-position: -1200% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel14" value="14"><label for="voxel14" style="background-position: -1300% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel15" value="15"><label for="voxel15" style="background-position: -1400% -0%"></label>
|
|
|
|
|
+ <input type="radio" name="voxel" id="voxel16" value="16"><label for="voxel16" style="background-position: -1500% -0%"></label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
</body>
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Et ajouter du CSS pour le styliser, afficher les tuiles et mettre en évidence
|
|
|
|
|
la sélection actuelle</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-css" translate="no">body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
#c {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
+#ui {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 10px;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
+ padding: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+#ui input[type=radio] {
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+}
|
|
|
|
|
+#ui input[type=radio] + label {
|
|
|
|
|
+ background-image: url('resources/images/minecraft/flourish-cc-by-nc-sa.png');
|
|
|
|
|
+ background-size: 1600% 400%;
|
|
|
|
|
+ image-rendering: pixelated;
|
|
|
|
|
+ width: 64px;
|
|
|
|
|
+ height: 64px;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+}
|
|
|
|
|
+#ui input[type=radio]:checked + label {
|
|
|
|
|
+ outline: 3px solid red;
|
|
|
|
|
+}
|
|
|
|
|
+@media (max-width: 600px), (max-height: 600px) {
|
|
|
|
|
+ #ui input[type=radio] + label {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
</pre>
|
|
|
|
|
<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>
|
|
|
|
|
<p>Ce code permettra à l'utilisateur de désélectionner le
|
|
|
|
|
bouton radio surligné.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">let currentVoxel = 0;
|
|
|
|
|
let currentId;
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('#ui .tiles input[type=radio][name=voxel]').forEach((elem) => {
|
|
|
|
|
elem.addEventListener('click', allowUncheck);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function allowUncheck() {
|
|
|
|
|
if (this.id === currentId) {
|
|
|
|
|
this.checked = false;
|
|
|
|
|
currentId = undefined;
|
|
|
|
|
currentVoxel = 0;
|
|
|
|
|
} else {
|
|
|
|
|
currentId = this.id;
|
|
|
|
|
currentVoxel = parseInt(this.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Et le code ci-dessous nous permettra de définir un voxel en fonction de l'endroit
|
|
|
|
|
où l'utilisateur clique. Il utilise un code similaire à celui que nous avons
|
|
|
|
|
fait dans <a href="picking.html">l'article sur la sélection</a>
|
|
|
|
|
mais il n'utilise pas le <code class="notranslate" translate="no">RayCaster</code> intégré. Au lieu de cela,
|
|
|
|
|
il utilise <code class="notranslate" translate="no">VoxelWorld.intersectRay</code> qui renvoie
|
|
|
|
|
la position d'intersection et la normale de la face
|
|
|
|
|
touchée.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
|
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
return {
|
|
|
|
|
x: (event.clientX - rect.left) * canvas.width / rect.width,
|
|
|
|
|
y: (event.clientY - rect.top ) * canvas.height / rect.height,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function placeVoxel(event) {
|
|
|
|
|
const pos = getCanvasRelativePosition(event);
|
|
|
|
|
const x = (pos.x / canvas.width ) * 2 - 1;
|
|
|
|
|
const y = (pos.y / canvas.height) * -2 + 1; // notez que nous inversons Y
|
|
|
|
|
|
|
|
|
|
const start = new THREE.Vector3();
|
|
|
|
|
const end = new THREE.Vector3();
|
|
|
|
|
start.setFromMatrixPosition(camera.matrixWorld);
|
|
|
|
|
end.set(x, y, 1).unproject(camera);
|
|
|
|
|
|
|
|
|
|
const intersection = world.intersectRay(start, end);
|
|
|
|
|
if (intersection) {
|
|
|
|
|
const voxelId = event.shiftKey ? 0 : currentVoxel;
|
|
|
|
|
// le point d'intersection est sur la face. Cela signifie
|
|
|
|
|
// que l'imprécision mathématique pourrait nous placer de chaque côté de la face.
|
|
|
|
|
// alors allons à la moitié de la normale DANS le voxel si nous supprimons (currentVoxel = 0)
|
|
|
|
|
// ou HORS du voxel si nous ajoutons (currentVoxel > 0)
|
|
|
|
|
const pos = intersection.position.map((v, ndx) => {
|
|
|
|
|
return v + intersection.normal[ndx] * (voxelId > 0 ? 0.5 : -0.5);
|
|
|
|
|
});
|
|
|
|
|
world.setVoxel(...pos, voxelId);
|
|
|
|
|
updateVoxelGeometry(...pos);
|
|
|
|
|
requestRenderIfNotRequested();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mouse = {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function recordStartPosition(event) {
|
|
|
|
|
mouse.x = event.clientX;
|
|
|
|
|
mouse.y = event.clientY;
|
|
|
|
|
mouse.moveX = 0;
|
|
|
|
|
mouse.moveY = 0;
|
|
|
|
|
}
|
|
|
|
|
function recordMovement(event) {
|
|
|
|
|
mouse.moveX += Math.abs(mouse.x - event.clientX);
|
|
|
|
|
mouse.moveY += Math.abs(mouse.y - event.clientY);
|
|
|
|
|
}
|
|
|
|
|
function placeVoxelIfNoMovement(event) {
|
|
|
|
|
if (mouse.moveX < 5 && mouse.moveY < 5) {
|
|
|
|
|
placeVoxel(event);
|
|
|
|
|
}
|
|
|
|
|
window.removeEventListener('pointermove', recordMovement);
|
|
|
|
|
window.removeEventListener('pointerup', placeVoxelIfNoMovement);
|
|
|
|
|
}
|
|
|
|
|
canvas.addEventListener('pointerdown', (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
recordStartPosition(event);
|
|
|
|
|
window.addEventListener('pointermove', recordMovement);
|
|
|
|
|
window.addEventListener('pointerup', placeVoxelIfNoMovement);
|
|
|
|
|
}, {passive: false});
|
|
|
|
|
canvas.addEventListener('touchstart', (event) => {
|
|
|
|
|
// arrêter le défilement
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}, {passive: false});
|
|
|
|
|
</pre>
|
|
|
|
|
<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>
|
|
|
|
|
<p>Dans le code, nous appelons <code class="notranslate" translate="no">world.setVoxel</code> pour définir un voxel et
|
|
|
|
|
ensuite <code class="notranslate" translate="no">updateVoxelGeometry</code> pour mettre à jour la géométrie three.js
|
|
|
|
|
en fonction de ce qui a changé.</p>
|
|
|
|
|
<p>Faisons cela maintenant. Si l'utilisateur clique sur un
|
|
|
|
|
voxel au bord d'une cellule, la géométrie du voxel
|
|
|
|
|
dans la cellule adjacente pourrait avoir besoin d'une nouvelle géométrie. Cela signifie
|
|
|
|
|
que nous devons vérifier la cellule du voxel que nous venons d'éditer
|
|
|
|
|
ainsi que dans les 6 directions à partir de cette cellule.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const neighborOffsets = [
|
|
|
|
|
[ 0, 0, 0], // soi-même
|
|
|
|
|
[-1, 0, 0], // gauche
|
|
|
|
|
[ 1, 0, 0], // droite
|
|
|
|
|
[ 0, -1, 0], // bas
|
|
|
|
|
[ 0, 1, 0], // haut
|
|
|
|
|
[ 0, 0, -1], // arrière
|
|
|
|
|
[ 0, 0, 1], // avant
|
|
|
|
|
];
|
|
|
|
|
function updateVoxelGeometry(x, y, z) {
|
|
|
|
|
const updatedCellIds = {};
|
|
|
|
|
for (const offset of neighborOffsets) {
|
|
|
|
|
const ox = x + offset[0];
|
|
|
|
|
const oy = y + offset[1];
|
|
|
|
|
const oz = z + offset[2];
|
|
|
|
|
const cellId = world.computeCellId(ox, oy, oz);
|
|
|
|
|
if (!updatedCellIds[cellId]) {
|
|
|
|
|
updatedCellIds[cellId] = true;
|
|
|
|
|
updateCellGeometry(ox, oy, oz);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>J'ai pensé à vérifier les cellules adjacentes comme </p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
|
|
|
|
|
if (voxelX === 0) {
|
|
|
|
|
// mettre à jour la cellule à gauche
|
|
|
|
|
} else if (voxelX === cellSize - 1) {
|
|
|
|
|
// mettre à jour la cellule à droite
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>et il y aurait 4 vérifications supplémentaires pour les 4 autres directions,
|
|
|
|
|
mais il m'est apparu que le code serait beaucoup plus simple avec
|
|
|
|
|
juste un tableau d'offsets et en sauvegardant les cell ids des
|
|
|
|
|
cellules que nous avons déjà mises à jour. Si le voxel mis à jour n'est pas
|
|
|
|
|
au bord d'une cellule, le test rejettera rapidement la mise à jour
|
|
|
|
|
de la même cellule.</p>
|
|
|
|
|
<p>Pour <code class="notranslate" translate="no">updateCellGeometry</code>, nous allons simplement prendre le code que nous
|
|
|
|
|
avions auparavant et qui générait la géométrie pour une cellule
|
|
|
|
|
et le faire gérer plusieurs cellules.</p>
|
|
|
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellIdToMesh = {};
|
|
|
|
|
function updateCellGeometry(x, y, z) {
|
|
|
|
|
const cellX = Math.floor(x / cellSize);
|
|
|
|
|
const cellY = Math.floor(y / cellSize);
|
|
|
|
|
const cellZ = Math.floor(z / cellSize);
|
|
|
|
|
const cellId = world.computeCellId(x, y, z);
|
|
|
|
|
let mesh = cellIdToMesh[cellId];
|
|
|
|
|
const geometry = mesh ? mesh.geometry : new THREE.BufferGeometry();
|
|
|
|
|
|
|
|
|
|
const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(cellX, cellY, cellZ);
|
|
|
|
|
const positionNumComponents = 3;
|
|
|
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
|
|
|
|
|
const normalNumComponents = 3;
|
|
|
|
|
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
|
|
|
|
|
const uvNumComponents = 2;
|
|
|
|
|
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
|
|
|
|
|
geometry.setIndex(indices);
|
|
|
|
|
geometry.computeBoundingSphere();
|
|
|
|
|
|
|
|
|
|
if (!mesh) {
|
|
|
|
|
mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
mesh.name = cellId;
|
|
|
|
|
cellIdToMesh[cellId] = mesh;
|
|
|
|
|
scene.add(mesh);
|
|
|
|
|
mesh.position.set(cellX * cellSize, cellY * cellSize, cellZ * cellSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</pre>
|
|
|
|
|
<p>Le code ci-dessus vérifie une map de cell ids vers les maillages. Si
|
|
|
|
|
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éé
|
|
|
|
|
et ajouté au bon endroit dans l'espace monde.
|
|
|
|
|
À la fin, nous mettons à jour les attributes et les indices avec les nouvelles données.</p>
|
|
|
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
|
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-ui.html"></iframe></div>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p></p>
|
|
|
|
|
<p>Quelques notes :</p>
|
|
|
|
|
<p>Le <code class="notranslate" translate="no">RayCaster</code> aurait peut-être fonctionné très bien. Je n'ai pas essayé.
|
|
|
|
|
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>.
|
|
|
|
|
qui est optimisé pour les voxels.</p>
|
|
|
|
|
<p>J'ai fait de <code class="notranslate" translate="no">intersectRay</code> une partie de VoxelWorld car il semblait
|
|
|
|
|
que si cela devenait trop lent, nous pourrions lancer des rayons contre les cellules
|
|
|
|
|
avant de le faire sur les voxels comme une simple accélération si cela devenait
|
|
|
|
|
trop lent.</p>
|
|
|
|
|
<p>Vous pourriez vouloir changer la longueur du raycast
|
|
|
|
|
car actuellement, elle va jusqu'au Z-far. Je suppose que si l'
|
|
|
|
|
utilisateur clique sur quelque chose de trop éloigné, il ne veut pas vraiment
|
|
|
|
|
placer des blocs de l'autre côté du monde qui font 1 ou 2 pixels.</p>
|
|
|
|
|
<p>Appeler <code class="notranslate" translate="no">geometry.computeBoundingSphere</code> pourrait être lent.
|
|
|
|
|
Nous pourrions simplement définir manuellement la bounding sphere pour qu'elle s'adapte
|
|
|
|
|
à la cellule entière.</p>
|
|
|
|
|
<p>Voulons-nous supprimer les cellules si tous les voxels de cette cellule sont à 0 ?
|
|
|
|
|
Ce serait probablement un changement raisonnable si nous voulions livrer ceci.</p>
|
|
|
|
|
<p>En réfléchissant à la manière dont cela fonctionne, il est clair que le
|
|
|
|
|
pire des cas absolu est un damier de voxels activés et désactivés. Je ne
|
|
|
|
|
sais pas d'emblée quelles autres stratégies utiliser
|
|
|
|
|
si les choses deviennent trop lentes. Peut-être que devenir trop lent
|
|
|
|
|
encouragerait simplement l'utilisateur à ne pas créer d'énormes zones en damier.</p>
|
|
|
|
|
<p>Pour simplifier, la texture atlas n'a qu'une seule colonne
|
|
|
|
|
par type de voxel. Il serait préférable de faire quelque chose de plus
|
|
|
|
|
flexible où nous aurions un tableau de types de voxels et chaque
|
|
|
|
|
type pourrait spécifier où se trouvent les textures de ses faces dans l'atlas.
|
|
|
|
|
Tel quel, beaucoup d'espace est gaspillé.</p>
|
|
|
|
|
<p>En regardant le vrai minecraft, il y a des tuiles qui ne sont pas
|
|
|
|
|
des voxels, pas des cubes. Comme une tuile de clôture ou des fleurs. Pour faire cela,
|
|
|
|
|
nous aurions à nouveau besoin d'un tableau de types de voxels et pour chaque
|
|
|
|
|
voxel, s'il s'agit d'un cube ou d'une autre géométrie. S'il ne s'agit pas d'un cube,
|
|
|
|
|
la vérification des voisins lors de la génération de la géométrie
|
|
|
|
|
devrait également changer. Un voxel de fleur à côté d'un autre
|
|
|
|
|
voxel ne devrait pas supprimer les faces entre eux.</p>
|
|
|
|
|
<p>Si vous voulez créer quelque chose de similaire à minecraft en utilisant three.js,
|
|
|
|
|
j'espère que cela vous a donné quelques idées pour commencer et comment
|
|
|
|
|
générer une géométrie quelque peu efficace.</p>
|
|
|
|
|
<p><canvas id="c"></canvas></p>
|
|
|
|
|
<script type="module" src="../resources/threejs-voxel-geometry.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="../resources/prettify.js"></script>
|
|
|
|
|
<script src="../resources/lesson.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</body></html>
|