# BufferGeometry Personnalisée

`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.

cliquez ici pour ouvrir dans une fenêtre séparée

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
    +]);

cliquez ici pour ouvrir dans une fenêtre séparée

`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
    ]);

cliquez ici pour ouvrir dans une fenêtre séparée

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.

cliquez ici pour ouvrir dans une fenêtre séparée

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`.