Browse Source

Create threejs-custom-buffergeometry.md

vanzo16 5 years ago
parent
commit
94ee21342c
1 changed files with 434 additions and 0 deletions
  1. 434 0
      threejs/lessons/ru/threejs-custom-buffergeometry.md

+ 434 - 0
threejs/lessons/ru/threejs-custom-buffergeometry.md

@@ -0,0 +1,434 @@
+Title: Three.js Custom BufferGeometry
+Description: Создание собственной геометрии BufferGeometry.
+TOC: Custom BufferGeometry
+
+В [предыдущей статье](threejs-custom-geometry.html) рассказывалось, как использовать `Geometry`. Эта статья о `BufferGeometry`. `BufferGeometry` обычно быстрее запускается и использует меньше памяти, но может быть сложнее в настройке. 
+
+В [статье о Geometry](threejs-custom-geometry.html) мы рассказали, что для использования `Geometry` вы предоставляете массив вершин `Vector3` (позиций). 
+Затем вы создаете объекты `Face3`, определяя по индексу 3 вершины, которые составляют каждый треугольник фигуры, которую вы делаете.
+Для каждого `Face3` вы можете указать либо нормаль грани, либо нормали для каждой отдельной вершины грани. 
+Вы также можете указать цвет грани или отдельные цвета вершин. Наконец, вы можете создать параллельный массив массивов текстурных координат (UV), 
+один массив для каждой грани, содержащий массив UV, один для каждой вершины грани. 
+
+<div class="threejs_center"><img src="resources/threejs-geometry.svg" style="width: 700px"></div>
+
+`BufferGeometry` с другой стороны использует названный `BufferAttributes`. 
+Каждый атрибут `BufferAttribute` представляет собой массив данных одного типа: позиции, нормали, цвета и ультрафиолетовые лучи. 
+Вместе добавленные атрибуты `BufferAttributes` представляют параллельные массивы всех данных для каждой вершины. 
+
+<div class="threejs_center"><img src="resources/threejs-attributes.svg" style="width: 700px"></div>
+
+Вы можете видеть, что у нас есть 4 атрибута: `position`, `normal`, `color`, `uv`. 
+Они представляют параллельные массивы, что означает, что N-й набор данных в каждом атрибуте принадлежит одной и той же вершине. 
+Вершина с индексом = 4 подсвечивается, чтобы показать, что параллельные данные по всем атрибутам определяют одну вершину. 
+
+Это поднимает точку, вот схема куба с одним выделенным углом. 
+
+<div class="threejs_center"><img src="resources/cube-faces-vertex.svg" style="width: 500px"></div>
+
+Думая об этом, один угол нуждается в разной нормали для каждой грани куба. 
+Для каждой стороны тоже нужны разные ультрафиолеты. Это указывает на самую большую разницу между `Geometry` и `BufferGeometry`. Ничего общего с `BufferGeometry`.
+Одна вершина - это комбинация всех ее частей. Если вершина нуждается в какой-либо части, то она должна быть другой. 
+
+Правда в том, что когда вы используете `Geometry` three.js преобразует его в этот формат. 
+Вот откуда появляется дополнительная память и время при использовании `Geometry`. 
+Дополнительная память для всех объектов `Vector3s`, `Vector2s`, `Face3s` и массива, а затем дополнительное время для преобразования всех этих данных
+в параллельные массивы в форме атрибутов `BufferAttributes`, как указано выше. 
+Иногда это облегчает использование Geometry. С `BufferGeometry` мы можем предоставить данные, уже преобразованные в этот формат. 
+
+В качестве простого примера давайте сделаем куб, используя `BufferGeometry`.
+Куб интересен тем, что кажется, что он разделяет вершины в углах, но на самом деле это не так. В нашем примере мы перечислим все вершины со всеми их данными,
+а затем преобразуем эти данные в параллельные массивы и, наконец, используем их для создания атрибутов `Buffer` и добавления их в `BufferGeometry`. 
+
+Начиная с примера координат текстуры из предыдущей статьи, мы удалили весь код, связанный с настройкой `Geometry`. 
+Затем мы перечисляем все данные, необходимые для куба. Помните еще раз, что если вершина имеет какие-либо уникальные части, она должна быть отдельной вершиной. 
+Для создания куба необходимо 36 вершин. 2 треугольника на грань, 3 вершины на треугольник, 6 граней = 36 вершин. 
+
+```js
+const vertices = [
+  // front
+  { pos: [-1, -1,  1], norm: [ 0,  0,  1], uv: [0, 1], },
+  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
+  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
+
+  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
+  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 0,  0,  1], uv: [1, 0], },
+  // right
+  { pos: [ 1, -1,  1], norm: [ 1,  0,  0], uv: [0, 1], },
+  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
+
+  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
+  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
+  { pos: [ 1,  1, -1], norm: [ 1,  0,  0], uv: [1, 0], },
+  // back
+  { pos: [ 1, -1, -1], norm: [ 0,  0, -1], uv: [0, 1], },
+  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
+  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
+
+  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
+  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
+  { pos: [-1,  1, -1], norm: [ 0,  0, -1], uv: [1, 0], },
+  // left
+  { pos: [-1, -1, -1], norm: [-1,  0,  0], uv: [0, 1], },
+  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], },
+  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], },
+
+  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], },
+  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], },
+  { pos: [-1,  1,  1], norm: [-1,  0,  0], uv: [1, 0], },
+  // top
+  { pos: [ 1,  1, -1], norm: [ 0,  1,  0], uv: [0, 1], },
+  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], },
+
+  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], },
+  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], },
+  { pos: [-1,  1,  1], norm: [ 0,  1,  0], uv: [1, 0], },
+  // bottom
+  { pos: [ 1, -1,  1], norm: [ 0, -1,  0], uv: [0, 1], },
+  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], },
+  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], },
+
+  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], },
+  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], },
+  { pos: [-1, -1, -1], norm: [ 0, -1,  0], uv: [1, 0], },
+];
+```
+
+Затем мы можем перевести все это в 3 параллельных массива 
+```js
+const positions = [];
+const normals = [];
+const uvs = [];
+for (const vertex of vertices) {
+  positions.push(...vertex.pos);
+  normals.push(...vertex.norm);
+  uvs.push(...vertex.uv);
+}
+```
+
+Наконец, мы можем создать `BufferGeometry`, а затем `BufferAttribute` для каждого массива и добавить его в `BufferGeometry`.
+
+```js
+  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));
+```
+
+Обратите внимание, что имена являются значительными. Вы должны назвать свои атрибуты именами, которые соответствуют ожиданиям three.js 
+(если вы не создаете пользовательский шейдер). В этом случае `position`, `normal` и `uv`. Если вы хотите цвета вершин, назовите свой атрибут `color`. 
+
+Выше мы создали 3 собственных массива JavaScript, `positions`, `normals`  и `uvs` . Затем мы конвертируем их в 
+[TypedArrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
+типа Float32Array. Атрибут `BufferAttribute` требует TypedArray, а не собственного массива. Атрибут `BufferAttribute` также требует, чтобы вы указали,
+сколько компонентов в каждой вершине. Для позиций и нормалей у нас есть 3 компонента на вершину, x, y и z. Для UV у нас есть 2, u и v. 
+
+{{{example url="../threejs-custom-buffergeometry-cube.html"}}}
+
+Это много данных. Небольшая вещь, которую мы можем сделать, это использовать индексы для ссылки на вершины. 
+Оглядываясь назад на данные нашего куба, каждая грань состоит из 2 треугольников с 3 вершинами в каждом, всего 6 вершин, но 2 из этих вершин абсолютно одинаковы; 
+Та же самая position, та же самая normal, и та же самая uv. 
+Таким образом, мы можем удалить совпадающие вершины и затем ссылаться на них по индексу. Сначала мы удаляем совпадающие вершины. 
+
+```js
+const vertices = [
+  // front
+  { pos: [-1, -1,  1], norm: [ 0,  0,  1], uv: [0, 1], }, // 0
+  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], }, // 1
+  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], }, // 2
+-
+-  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
+-  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 0,  0,  1], uv: [1, 0], }, // 3
+  // right
+  { pos: [ 1, -1,  1], norm: [ 1,  0,  0], uv: [0, 1], }, // 4
+  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], }, // 5
+-
+-  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
+-  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], }, // 6
+  { pos: [ 1,  1, -1], norm: [ 1,  0,  0], uv: [1, 0], }, // 7
+  // back
+  { pos: [ 1, -1, -1], norm: [ 0,  0, -1], uv: [0, 1], }, // 8
+  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], }, // 9
+-
+-  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
+-  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
+  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], }, // 10
+  { pos: [-1,  1, -1], norm: [ 0,  0, -1], uv: [1, 0], }, // 11
+  // left
+  { pos: [-1, -1, -1], norm: [-1,  0,  0], uv: [0, 1], }, // 12
+  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], }, // 13
+-
+-  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], },
+-  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], },
+  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], }, // 14
+  { pos: [-1,  1,  1], norm: [-1,  0,  0], uv: [1, 0], }, // 15
+  // top
+  { pos: [ 1,  1, -1], norm: [ 0,  1,  0], uv: [0, 1], }, // 16
+  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], }, // 17
+-
+-  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], },
+-  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], },
+  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], }, // 18
+  { pos: [-1,  1,  1], norm: [ 0,  1,  0], uv: [1, 0], }, // 19
+  // bottom
+  { pos: [ 1, -1,  1], norm: [ 0, -1,  0], uv: [0, 1], }, // 20
+  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], }, // 21
+-
+-  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], },
+-  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], },
+  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], }, // 22
+  { pos: [-1, -1, -1], norm: [ 0, -1,  0], uv: [1, 0], }, // 23
+];
+```
+
+Итак, теперь у нас есть 24 уникальные вершины. 
+Затем мы указываем 36 индексов для 36 вершин, которые нам нужно нарисовать, чтобы сделать 12 треугольников, вызывая `BufferGeometry.setIndex` с массивом индексов. 
+
+```js
+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,  // front
++   4,  5,  6,   6,  5,  7,  // right
++   8,  9, 10,  10,  9, 11,  // back
++  12, 13, 14,  14, 13, 15,  // left
++  16, 17, 18,  18, 17, 19,  // top
++  20, 21, 22,  22, 21, 23,  // bottom
++]);
+```
+
+{{{example url="../threejs-custom-buffergeometry-cube-indexed.html"}}}
+
+Как и в `Geometry`, в `BufferGeometry` есть метод [`computeVertexNormals`](BufferGeometry.computeVertexNormals) для вычисления нормалей,
+если вы их не предоставляете. В отличие от версии `Geometry` той же функции, 
+поскольку позиции не могут быть общими, если любая другая часть вершины отличается, результаты вызова `computeVertexNormals` будут другими. 
+
+<div class="spread">
+  <div>
+    <div data-diagram="bufferGeometryCylinder"></div>
+    <div class="code">BufferGeometry</div>
+  </div>
+  <div>
+    <div data-diagram="geometryCylinder"></div>
+    <div class="code">Geometry</div>
+  </div>
+</div>
+
+Вот 2 цилиндра, где нормали были созданы с использованием `computeVertexNormals`. 
+Если вы посмотрите внимательно, на левом цилиндре есть шов. Это связано с тем, 
+что нет возможности совместно использовать вершины в начале и конце цилиндра, так как они требуют разных UV.
+Просто небольшая вещь, чтобы быть в курсе. 
+Решение состоит в том, чтобы предоставить свои собственные normals. 
+
+Мы также можем использовать [TypedArrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
+с самого начала вместо собственных массивов JavaScript. Недостатком TypedArrays является то, что вы должны указать их размер заранее.
+Конечно, это не так уж сложно, но с помощью собственных массивов мы можем просто `push` значения в них и посмотреть, 
+какого размера они заканчиваются, проверив их `length` в конце.
+В TypedArrays нет функции push, поэтому нам нужно вести собственную бухгалтерию при добавлении значений к ним. 
+
+В этом примере узнать длину заранее довольно просто, так как для начала мы используем большой блок статических данных. 
+
+```js
+-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,  // front
+   4,  5,  6,   6,  5,  7,  // right
+   8,  9, 10,  10,  9, 11,  // back
+  12, 13, 14,  14, 13, 15,  // left
+  16, 17, 18,  18, 17, 19,  // top
+  20, 21, 22,  22, 21, 23,  // bottom
+]);
+```
+
+{{{example url="../threejs-custom-buffergeometry-cube-typedarrays.html"}}}
+
+Хорошая причина использовать typedarrays - если вы хотите динамически обновлять любую часть вершин. 
+
+Я не мог придумать действительно хороший пример динамического обновления вершин,
+поэтому я решил создать сферу и переместить каждый четырехугольник внутрь и наружу от центра. Надеюсь, это полезный пример. 
+
+Вот код для генерации позиций и индексов для сферы. Код разделяет вершины внутри четырехугольника, 
+но не разделяет вершины между четырьмя, потому что мы хотим иметь возможность перемещать каждый четырёхугольник по отдельности. 
+
+Поскольку я ленивый, я использовал небольшую иерархию из 3 объектов Object3D для вычисления точек сферы. Как это работает, объясняется в
+[статье об оптимизации множества объектов. ](threejs-optimize-lots-of-objects.html).
+
+```js
+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};
+}
+```
+
+Затем мы можем вызвать это так
+
+```js
+const segmentsAround = 24;
+const segmentsDown = 16;
+const {positions, indices} = makeSpherePositions(segmentsAround, segmentsDown);
+```
+
+Поскольку возвращаемые позиции являются позициями единичных сфер, 
+они являются точно такими же значениями, которые нам нужны для нормалей, поэтому мы можем просто дублировать их для нормалей. 
+
+```js
+const normals = positions.slice();
+```
+
+И тогда мы устанавливаем атрибуты, как раньше
+
+```js
+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);
+```
+
+Я выделил несколько различий. Мы сохраняем ссылку на атрибут позиции.
+Мы также отмечаем его как динамический. Это намек на THREE.js, что мы будем часто менять содержимое атрибута. 
+
+В нашем цикле рендеринга мы обновляем позиции на основе их нормалей каждый кадр.
+
+```js
+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;
+```
+
+И мы устанавливаем `positionAttribute.needsUpdate`, чтобы THREE.js указывал использовать наши изменения.
+
+{{{example url="../threejs-custom-buffergeometry-dynamic.html"}}}
+
+Я надеюсь, что это были полезные примеры того, как использовать
+`BufferGeometry` напрямую для создания собственной геометрии и как динамически 
+обновлять содержимое `BufferAttribute`. То, что вы используете, `Geometry` или `BufferGeometry`, 
+действительно зависит от ваших потребностей. 
+
+<canvas id="c"></canvas>
+<script type="module" src="resources/threejs-custom-buffergeometry.js"></script>
+