BufferGeometry
est la manière dont three.js représente toute la géométrie. Une BufferGeometry
est essentiellement une collection nommée de BufferAttribute
s.
Chaque BufferAttribute
représente un tableau d'un type de données : positions,
normales, couleurs, uv, etc... Ensemble, les BufferAttribute
s nommées représentent
des tableaux parallèles de toutes les données pour chaque sommet.
Ci-dessus, vous pouvez voir que nous avons 4 attributs : position
, normal
, color
, uv
.
Ils représentent des tableaux parallèles, ce qui signifie que le N-ième ensemble de données de chaque
attribut appartient au même sommet. Le sommet à l'index = 4 est mis en évidence
pour montrer que les données parallèles à travers tous les attributs définissent un seul sommet.
Cela soulève un point, voici un diagramme d'un cube avec un coin mis en évidence.
En y réfléchissant, ce coin unique nécessite une normale différente pour chaque face du cube. Une normale est une information sur la direction vers laquelle quelque chose fait face. Dans le diagramme, les normales sont représentées par les flèches autour du sommet d'angle, montrant que chaque face qui partage cette position de sommet a besoin d'une normale qui pointe dans une direction différente.
Ce coin a également besoin d'UV différents pour chaque face. Les UV sont des coordonnées de texture qui spécifient quelle partie d'une texture dessinée sur un triangle correspond à cette position de sommet. Vous pouvez voir que la face verte a besoin que ce sommet ait une UV qui corresponde au coin supérieur droit de la texture F, la face bleue a besoin d'une UV qui corresponde au coin supérieur gauche de la texture F, et la face rouge a besoin d'une UV qui corresponde au coin inférieur gauche de la texture F.
Un seul sommet est la combinaison de toutes ses parties. Si un sommet a besoin d'une partie différente, alors il doit s'agir d'un sommet différent.
Comme exemple simple, créons un cube en utilisant BufferGeometry
. Un cube est intéressant
car il semble partager des sommets aux coins mais ce n'est pas le cas en réalité. Pour notre exemple, nous allons lister tous les sommets avec toutes leurs données,
puis convertir ces données en tableaux parallèles et enfin les utiliser pour créer des
BufferAttribute
s et les ajouter à une BufferGeometry
.
Nous commençons par une liste de toutes les données nécessaires pour le cube. Rappelez-vous encore une fois que si un sommet a des parties uniques, il doit s'agir d'un sommet distinct. En conséquence, pour faire un cube, il faut 36 sommets. 2 triangles par face, 3 sommets par triangle, 6 faces = 36 sommets.
const vertices = [ // avant { pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], }, { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], }, // droite { pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], }, { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, { pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], }, // arrière { pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], }, { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, { pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], }, // gauche { pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], }, { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, { pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], }, // haut { pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], }, { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, { pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], }, // bas { pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], }, { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, { pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], }, ];
Nous pouvons ensuite traduire tout cela en 3 tableaux parallèles
const positions = []; const normals = []; const uvs = []; for (const vertex of vertices) { positions.push(...vertex.pos); normals.push(...vertex.norm); uvs.push(...vertex.uv); }
Enfin, nous pouvons créer une BufferGeometry
, puis une BufferAttribute
pour chaque tableau
et l'ajouter à la BufferGeometry
.
const geometry = new THREE.BufferGeometry(); 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));
Notez que les noms sont importants. Vous devez nommer vos attributs avec les noms
que three.js attend (sauf si vous créez un shader personnalisé).
Dans ce cas : position
, normal
, et uv
. Si vous voulez des couleurs de sommet,
nommez votre attribut color
.
Ci-dessus, nous avons créé 3 tableaux natifs JavaScript, positions
, normals
et uvs
.
Nous les avons ensuite convertis en
TypedArrays
de type Float32Array
. Une BufferAttribute
nécessite un TypedArray, pas un tableau natif.
Une BufferAttribute
exige également que vous lui indiquiez combien de composants il y a
par sommet. Pour les positions et les normales, nous avons 3 composants par sommet,
x, y et z. Pour les UV, nous en avons 2, u et v.
C'est beaucoup de données. Une petite chose que nous pouvons faire est d'utiliser des indices pour référencer les sommets. En revenant à nos données de cube, chaque face est composée de 2 triangles avec 3 sommets chacun, soit 6 sommets au total, mais 2 de ces sommets sont exactement les mêmes ; La même position, la même normale et la même uv. Nous pouvons donc supprimer les sommets correspondants et les référencer par index. Nous commençons par supprimer les sommets correspondants.
const vertices = [ // avant { pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], }, // 0 { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, // 1 { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, // 2 - - { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, - { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], }, // 3 // droite { pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], }, // 4 { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, // 5 - - { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, - { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, // 6 { pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], }, // 7 // arrière { pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], }, // 8 { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, // 9 - - { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, - { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, // 10 { pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], }, // 11 // gauche { pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], }, // 12 { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, // 13 - - { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, - { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, // 14 { pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], }, // 15 // haut { pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], }, // 16 { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, // 17 - - { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, - { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, // 18 { pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], }, // 19 // bas { pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], }, // 20 { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, // 21 - - { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, - { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, // 22 { pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], }, // 23 ];
Nous avons donc maintenant 24 sommets uniques. Ensuite, nous spécifions 36 indices
pour les 36 sommets que nous devons dessiner pour faire 12 triangles en appelant BufferGeometry.setIndex
avec un tableau d'indices.
geometry.setAttribute( 'position', new THREE.BufferAttribute(positions, positionNumComponents)); geometry.setAttribute( 'normal', new THREE.BufferAttribute(normals, normalNumComponents)); geometry.setAttribute( 'uv', new THREE.BufferAttribute(uvs, uvNumComponents)); +geometry.setIndex([ + 0, 1, 2, 2, 1, 3, // avant + 4, 5, 6, 6, 5, 7, // droite + 8, 9, 10, 10, 9, 11, // arrière + 12, 13, 14, 14, 13, 15, // gauche + 16, 17, 18, 18, 17, 19, // haut + 20, 21, 22, 22, 21, 23, // bas +]);
BufferGeometry
a une méthode computeVertexNormals
pour calculer les normales si vous
ne les fournissez pas. Malheureusement,
étant donné que les positions ne peuvent pas être partagées si une autre partie d'un sommet est différente,
les résultats de l'appel à computeVertexNormals
généreront des coutures si votre
géométrie est censée se connecter à elle-même comme une sphère ou un cylindre.
Pour le cylindre ci-dessus, les normales ont été créées à l'aide de computeVertexNormals
.
Si vous regardez attentivement, il y a une couture sur le cylindre. Cela est dû au fait qu'il
n'y a aucun moyen de partager les sommets au début et à la fin du cylindre, car ils
nécessitent des UV différents, de sorte que la fonction pour les calculer n'a aucune idée qu'il
s'agit des mêmes sommets à lisser. Juste une petite chose dont il faut être conscient.
La solution est de fournir vos propres normales.
Nous pouvons également utiliser des TypedArrays dès le début au lieu de tableaux JavaScript natifs.
L'inconvénient des TypedArrays est que vous devez spécifier leur taille à l'avance. Bien sûr,
ce n'est pas une si grande contrainte, mais avec les tableaux natifs, nous pouvons simplement
push
des valeurs et voir la taille finale en vérifiant leur
length
à la fin. Avec les TypedArrays, il n'y a pas de fonction push, nous devons donc
faire notre propre gestion des données lorsque nous y ajoutons des valeurs.
Dans cet exemple, connaître la longueur à l'avance est assez facile, car nous utilisons un grand bloc de données statiques pour commencer.
-const positions = []; -const normals = []; -const uvs = []; +const numVertices = vertices.length; +const positionNumComponents = 3; +const normalNumComponents = 3; +const uvNumComponents = 2; +const positions = new Float32Array(numVertices * positionNumComponents); +const normals = new Float32Array(numVertices * normalNumComponents); +const uvs = new Float32Array(numVertices * uvNumComponents); +let posNdx = 0; +let nrmNdx = 0; +let uvNdx = 0; for (const vertex of vertices) { - positions.push(...vertex.pos); - normals.push(...vertex.norm); - uvs.push(...vertex.uv); + positions.set(vertex.pos, posNdx); + normals.set(vertex.norm, nrmNdx); + uvs.set(vertex.uv, uvNdx); + posNdx += positionNumComponents; + nrmNdx += normalNumComponents; + uvNdx += uvNumComponents; } geometry.setAttribute( 'position', - new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents)); + new THREE.BufferAttribute(positions, positionNumComponents)); geometry.setAttribute( 'normal', - new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents)); + new THREE.BufferAttribute(normals, normalNumComponents)); geometry.setAttribute( 'uv', - new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents)); + new THREE.BufferAttribute(uvs, uvNumComponents)); geometry.setIndex([ 0, 1, 2, 2, 1, 3, // avant 4, 5, 6, 6, 5, 7, // droite 8, 9, 10, 10, 9, 11, // arrière 12, 13, 14, 14, 13, 15, // gauche 16, 17, 18, 18, 17, 19, // haut 20, 21, 22, 22, 21, 23, // bas ]);
Une bonne raison d'utiliser des typedarrays est si vous souhaitez mettre à jour dynamiquement une partie des sommets.
Je n'ai pas pu trouver un très bon exemple de mise à jour dynamique des sommets, alors j'ai décidé de faire une sphère et de déplacer chaque quadricône vers l'intérieur et l'extérieur depuis le centre. J'espère que c'est un exemple utile.
Voici le code pour générer les positions et les indices d'une sphère. Le code partage les sommets au sein d'un quadricône, mais ne partage pas les sommets entre les quadricônes, car nous voulons pouvoir déplacer chaque quadricône séparément.
Comme je suis paresseux, j'ai utilisé une petite hiérarchie de 3 objets Object3D
pour calculer
les points de la sphère. La façon dont cela fonctionne est expliquée dans l'article sur l'optimisation de nombreux objets.
function makeSpherePositions(segmentsAround, segmentsDown) { const numVertices = segmentsAround * segmentsDown * 6; const numComponents = 3; const positions = new Float32Array(numVertices * numComponents); const indices = []; const longHelper = new THREE.Object3D(); const latHelper = new THREE.Object3D(); const pointHelper = new THREE.Object3D(); longHelper.add(latHelper); latHelper.add(pointHelper); pointHelper.position.z = 1; const temp = new THREE.Vector3(); function getPoint(lat, long) { latHelper.rotation.x = lat; longHelper.rotation.y = long; longHelper.updateMatrixWorld(true); return pointHelper.getWorldPosition(temp).toArray(); } let posNdx = 0; let ndx = 0; for (let down = 0; down < segmentsDown; ++down) { const v0 = down / segmentsDown; const v1 = (down + 1) / segmentsDown; const lat0 = (v0 - 0.5) * Math.PI; const lat1 = (v1 - 0.5) * Math.PI; for (let across = 0; across < segmentsAround; ++across) { const u0 = across / segmentsAround; const u1 = (across + 1) / segmentsAround; const long0 = u0 * Math.PI * 2; const long1 = u1 * Math.PI * 2; positions.set(getPoint(lat0, long0), posNdx); posNdx += numComponents; positions.set(getPoint(lat1, long0), posNdx); posNdx += numComponents; positions.set(getPoint(lat0, long1), posNdx); posNdx += numComponents; positions.set(getPoint(lat1, long1), posNdx); posNdx += numComponents; indices.push( ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3, ); ndx += 4; } } return {positions, indices}; }
Nous pouvons ensuite l'appeler comme ceci
const segmentsAround = 24; const segmentsDown = 16; const {positions, indices} = makeSpherePositions(segmentsAround, segmentsDown);
Comme les positions retournées sont des positions de sphère unitaire, ce sont exactement les mêmes valeurs dont nous avons besoin pour les normales, nous pouvons donc simplement les dupliquer pour les normales.
const normals = positions.slice();
Et ensuite nous configurons les attributs comme auparavant
const geometry = new THREE.BufferGeometry(); const positionNumComponents = 3; const normalNumComponents = 3; +const positionAttribute = new THREE.BufferAttribute(positions, positionNumComponents); +positionAttribute.setUsage(THREE.DynamicDrawUsage); geometry.setAttribute( 'position', + positionAttribute); geometry.setAttribute( 'normal', new THREE.BufferAttribute(normals, normalNumComponents)); geometry.setIndex(indices);
J'ai mis en évidence quelques différences. Nous sauvegardons une référence à l'attribut de position. Nous le marquons également comme dynamique. C'est une indication pour THREE.js que nous allons modifier souvent le contenu de l'attribut.
Dans notre boucle de rendu, nous mettons à jour les positions en fonction de leurs normales à chaque image.
const temp = new THREE.Vector3(); ... for (let i = 0; i < positions.length; i += 3) { const quad = (i / 12 | 0); const ringId = quad / segmentsAround | 0; const ringQuadId = quad % segmentsAround; const ringU = ringQuadId / segmentsAround; const angle = ringU * Math.PI * 2; temp.fromArray(normals, i); temp.multiplyScalar(THREE.MathUtils.lerp(1, 1.4, Math.sin(time + ringId + angle) * .5 + .5)); temp.toArray(positions, i); } positionAttribute.needsUpdate = true;
Et nous réglons positionAttribute.needsUpdate
à true pour dire à THREE.js d'utiliser nos changements.
J'espère que ces exemples vous ont été utiles pour comprendre comment utiliser BufferGeometry
directement pour
créer votre propre géométrie et comment mettre à jour dynamiquement le contenu d'un
BufferAttribute
.