Browse Source

Create threejs-optimize-lots-of-objects.md

vanzo16 5 years ago
parent
commit
872a39c460
1 changed files with 522 additions and 0 deletions
  1. 522 0
      threejs/lessons/ru/threejs-optimize-lots-of-objects.md

+ 522 - 0
threejs/lessons/ru/threejs-optimize-lots-of-objects.md

@@ -0,0 +1,522 @@
+Title: Three.js Оптимизация большого количества объектов
+Description: Оптимизация путем объединения объектов
+TOC: Оптимизация большого количества объектов
+
+Эта статья является частью серии статей о three.js. Первая статья - [основы Three.js](threejs-fundamentals.html). 
+Если вы еще не читали это, и вы новичок в three.js, вы можете начать там. 
+
+Есть много способов оптимизировать вещи для three.js. Один из способов часто называют геометрией слияния.
+Каждая созданная вами сетка и three.js представляют 1 или более запросов системы на что-то нарисовать.
+Рисование 2 вещей имеет больше затрат, чем рисование 1, 
+даже если результаты одинаковы, поэтому одним из способов оптимизации является объединение сеток. 
+
+Давайте посмотрим пример, когда это хорошее решение для проблемы. Давайте заново создадим [WebGL Globe](https://globe.chromeexperiments.com/).
+
+Первое, что нам нужно сделать, это получить данные. WebGL Globe сказал, что данные, которые они используют, взяты из [SEDAC](http://sedac.ciesin.columbia.edu/gpw/). 
+Проверяя сайт, я увидел  [демографические данные в виде сетки](https://beta.sedac.ciesin.columbia.edu/data/set/gpw-v4-basic-demographic-characteristics-rev10).
+Я загрузил данные с 60-минутным разрешением. Затем я посмотрел на данные 
+
+Это выглядит так 
+
+
+```txt
+ ncols         360
+ nrows         145
+ xllcorner     -180
+ yllcorner     -60
+ cellsize      0.99999999999994
+ NODATA_value  -9999
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+ 9.241768 8.790958 2.095345 -9999 0.05114867 -9999 -9999 -9999 -9999 -999...
+ 1.287993 0.4395509 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999...
+ -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
+```
+
+Есть несколько строк, похожих на пары ключ / значение, за которыми следуют строки со значением для каждой точки сетки, по одной строке для каждой строки точек данных. 
+
+Чтобы убедиться, что мы понимаем данные, давайте попробуем построить их в 2D. 
+
+Сначала немного кода для загрузки текстового файла 
+
+```js
+async function loadFile(url) {
+  const req = await fetch(url);
+  return req.text();
+}
+```
+
+Приведенный выше код возвращает `Promise` с содержимым файла по адресу `url`; 
+
+Тогда нам нужен код для разбора файла 
+
+```js
+function parseData(text) {
+  const data = [];
+  const settings = {data};
+  let max;
+  let min;
+  // split into lines
+  text.split('\n').forEach((line) => {
+    // split the line by whitespace
+    const parts = line.trim().split(/\s+/);
+    if (parts.length === 2) {
+      // only 2 parts, must be a key/value pair
+      settings[parts[0]] = parseFloat(parts[1]);
+    } else if (parts.length > 2) {
+      // more than 2 parts, must be data
+      const values = parts.map((v) => {
+        const value = parseFloat(v);
+        if (value === settings.NODATA_value) {
+          return undefined;
+        }
+        max = Math.max(max === undefined ? value : max, value);
+        min = Math.min(min === undefined ? value : min, value);
+        return value;
+      });
+      data.push(values);
+    }
+  });
+  return Object.assign(settings, {min, max});
+}
+```
+
+Приведенный выше код возвращает объект со всеми парами ключ / значение из файла,
+а также свойство `data` со всеми данными в одном большом массиве и значениями `min` и `max`, найденными в данных. 
+
+Тогда нам нужен код для рисования этих данных 
+
+```js
+function drawData(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+  const ctx = document.querySelector('canvas').getContext('2d');
+  // make the canvas the same size as the data
+  ctx.canvas.width = ncols;
+  ctx.canvas.height = nrows;
+  // but display it double size so it's not too small
+  ctx.canvas.style.width = px(ncols * 2);
+  ctx.canvas.style.height = px(nrows * 2);
+  // fill the canvas to dark gray
+  ctx.fillStyle = '#444';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  // draw each data point
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+      const hue = 1;
+      const saturation = 1;
+      const lightness = amount;
+      ctx.fillStyle = hsl(hue, saturation, lightness);
+      ctx.fillRect(lonNdx, latNdx, 1, 1);
+    });
+  });
+}
+
+function px(v) {
+  return `${v | 0}px`;
+}
+
+function hsl(h, s, l) {
+  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+}
+```
+
+И, наконец, склеив все это 
+
+```js
+loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+  .then(drawData);
+```
+
+Дает нам этот результат 
+
+{{{example url="../gpw-data-viewer.html" }}}
+
+Так что, кажется, это работает. 
+
+Давайте попробуем это в 3D. Начиная с кода от [рендеринга по требованию](threejs-rendering-on-demand.html) мы сделаем один блок на данные в файле. 
+
+Сначала давайте сделаем простую сферу с текстурой мира. Вот текстура 
+
+<div class="threejs_center"><img src="../resources/images/world.jpg" style="width: 600px"></div>
+
+И код для его настройки. 
+
+```js
+{
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/images/world.jpg', render);
+  const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+  const material = new THREE.MeshBasicMaterial({map: texture});
+  scene.add(new THREE.Mesh(geometry, material));
+}
+```
+
+Обратите внимание на вызов для рендеринга после завершения загрузки текстуры. Нам это нужно, потому что мы выполняем  [рендеринг по требованию](threejs-rendering-on-demand.html) 
+а не постоянно, поэтому нам нужно рендерить один раз при загрузке текстуры. 
+
+Затем нам нужно изменить код, который рисует точку на точку данных выше, чтобы вместо этого создать прямоугольник для точки данных. 
+
+```js
+function addBoxes(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+
+  // make one box geometry
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+  // make it so it scales away from the positive Z axis
+  geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
+
+  // these helpers will make it easy to position the boxes
+  // We can rotate the lon helper on its Y axis to the longitude
+  const lonHelper = new THREE.Object3D();
+  scene.add(lonHelper);
+  // We rotate the latHelper on its X axis to the latitude
+  const latHelper = new THREE.Object3D();
+  lonHelper.add(latHelper);
+  // The position helper moves the object to the edge of the sphere
+  const positionHelper = new THREE.Object3D();
+  positionHelper.position.z = 1;
+  latHelper.add(positionHelper);
+
+  const lonFudge = Math.PI * .5;
+  const latFudge = Math.PI * -0.135;
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+      const material = new THREE.MeshBasicMaterial();
+      const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
+      const saturation = 1;
+      const lightness = THREE.MathUtils.lerp(0.1, 1.0, amount);
+      material.color.setHSL(hue, saturation, lightness);
+      const mesh = new THREE.Mesh(geometry, material);
+      scene.add(mesh);
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
+      latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
+
+      // use the world matrix of the position helper to
+      // position this mesh.
+      positionHelper.updateWorldMatrix(true, false);
+      mesh.applyMatrix4(positionHelper.matrixWorld);
+
+      mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+    });
+  });
+}
+```
+
+Код в основном прямо из нашего тестового кода рисования. 
+
+Мы делаем одну коробку и корректируем ее центр так, чтобы она масштабировалась от положительного Z. 
+Если бы мы этого не делали, она бы масштабировалась от центра, но мы хотим, чтобы они росли от начала координат. 
+
+
+<div class="spread">
+  <div>
+    <div data-diagram="scaleCenter" style="height: 250px"></div>
+    <div class="code">default</div>
+  </div>
+  <div>
+    <div data-diagram="scalePositiveZ" style="height: 250px"></div>
+    <div class="code">adjusted</div>
+  </div>
+</div>
+
+Конечно, мы могли бы также решить эту проблему, добавив в родительский блок больше объектов 
+THREE.Object3D, как мы покрывали в [графах сцены](threejs-scenegraph.html) 
+но чем больше узлов мы добавляем в граф сцены, тем медленнее он становится. 
+
+
+Мы также настраиваем эту небольшую иерархию узлов `lonHelper`, `latHelper` и `positionHelper`.
+Мы используем эти объекты, чтобы вычислить положение вокруг сферы, чтобы разместить коробку. 
+
+<div class="spread">
+  <div data-diagram="lonLatPos" style="width: 600px; height: 400px;"></div>
+</div>
+
+Над <span style="color: green;">зелёной полосой</span> изображен  `lonHelper` и используется для поворота в направлении долготы на экваторе.
+<span style="color: blue;">Синяя полоса</span> обозначает `latHelper` который используется для поворота на широту выше или ниже экватора. 
+<span style="color: red;">Красная сфера</span> представляет смещение, которое обеспечивает этот `positionHelper`.
+
+Мы могли бы сделать всю математику вручную, чтобы выяснить позиции на земном шаре, но, делая это таким образом,
+мы оставляем большую часть математики самой библиотеке, поэтому нам не нужно иметь с ней дело. 
+
+Для каждой точки данных мы создаем `MeshBasicMaterial` и `Mesh`, а затем запрашиваем мировую матрицу
+`positionHelper` и применяем ее к новой `Mesh`. Наконец мы масштабируем сетку в новой позиции. 
+
+Как и выше, мы могли бы также создать `latHelper`, `lonHelper` и `positionHelper` для каждого нового окна, но это было бы еще медленнее. 
+
+Мы собираемся создать до 360х145 коробок. Это до 52000 коробок. 
+Поскольку некоторые точки данных помечены как «NO_DATA», фактическое
+количество блоков, которые мы собираемся создать, составляет около 19000. 
+Если бы мы добавили 3 дополнительных вспомогательных объекта в блок,
+это было бы почти 80000 узлов графа сцены, которые THREE.js должен был бы 
+вычислить. позиции для. Вместо этого, используя один набор помощников, 
+чтобы просто расположить сетки, мы экономим около 60000 операций. 
+
+
+Примечание о `lonFudge` и `latFudge`. `lonFudge` равен π / 2, что составляет четверть оборота. 
+В этом есть смысл. Это просто означает, что текстура или координаты текстуры начинаются 
+с другого смещения по всему земному шару. `latFudge` с другой стороны, я понятия не имею, 
+почему он должен быть π * -0,135, это просто количество, которое выровняло боксы с текстурой. 
+
+
+Последнее, что нам нужно сделать, это вызвать нашего загрузчика 
+
+```
+loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+-  .then(drawData)
++  .then(addBoxes)
++  .then(render);
+```
+
+После того, как данные закончили загрузку и анализ, нам нужно выполнить рендеринг хотя бы один раз, поскольку мы выполняем 
+ [рендеринг по требованию](threejs-rendering-on-demand.html).
+
+{{{example url="../threejs-lots-of-objects-slow.html" }}}
+
+Если вы попытаетесь повернуть приведенный выше пример, перетащив образец, вы, вероятно, заметите, что он медленный. 
+
+Мы можем проверить частоту кадров, [открыв devtools](threejs-debugging-javascript.html) и включив индикатор частоты кадров браузера.
+
+<div class="threejs_center"><img src="resources/images/bring-up-fps-meter.gif"></div>
+
+На моей машине я вижу частоту кадров ниже 20 кадров в секунду. 
+
+<div class="threejs_center"><img src="resources/images/fps-meter.gif"></div>
+
+Это не очень хорошо для меня, и я подозреваю, что многие люди имеют более медленные машины, которые сделали бы это еще хуже. Нам лучше взглянуть на оптимизацию. 
+
+
+Для этой конкретной задачи мы можем объединить все блоки в одну геометрию. 
+В настоящее время мы рисуем около 19000 коробок. Объединяя их в одну геометрию, мы удалили 18999 операций. 
+
+Вот новый код для объединения блоков в одну геометрию. 
+
+```js
+function addBoxes(file) {
+  const {min, max, data} = file;
+  const range = max - min;
+
+-  // make one box geometry
+-  const boxWidth = 1;
+-  const boxHeight = 1;
+-  const boxDepth = 1;
+-  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+-  // make it so it scales away from the positive Z axis
+-  geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
+
+  // these helpers will make it easy to position the boxes
+  // We can rotate the lon helper on its Y axis to the longitude
+  const lonHelper = new THREE.Object3D();
+  scene.add(lonHelper);
+  // We rotate the latHelper on its X axis to the latitude
+  const latHelper = new THREE.Object3D();
+  lonHelper.add(latHelper);
+  // The position helper moves the object to the edge of the sphere
+  const positionHelper = new THREE.Object3D();
+  positionHelper.position.z = 1;
+  latHelper.add(positionHelper);
++  // Used to move the center of the box so it scales from the position Z axis
++  const originHelper = new THREE.Object3D();
++  originHelper.position.z = 0.5;
++  positionHelper.add(originHelper);
+
+  const lonFudge = Math.PI * .5;
+  const latFudge = Math.PI * -0.135;
++  const geometries = [];
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
+      if (value === undefined) {
+        return;
+      }
+      const amount = (value - min) / range;
+
+-      const material = new THREE.MeshBasicMaterial();
+-      const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
+-      const saturation = 1;
+-      const lightness = THREE.MathUtils.lerp(0.1, 1.0, amount);
+-      material.color.setHSL(hue, saturation, lightness);
+-      const mesh = new THREE.Mesh(geometry, material);
+-      scene.add(mesh);
+
++      const boxWidth = 1;
++      const boxHeight = 1;
++      const boxDepth = 1;
++      const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
+      latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
+
+-      // use the world matrix of the position helper to
+-      // position this mesh.
+-      positionHelper.updateWorldMatrix(true, false);
+-      mesh.applyMatrix4(positionHelper.matrixWorld);
+-
+-      mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+
++      // use the world matrix of the origin helper to
++      // position this geometry
++      positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
++      originHelper.updateWorldMatrix(true, false);
++      geometry.applyMatrix4(originHelper.matrixWorld);
++
++      geometries.push(geometry);
+    });
+  });
+
++  const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
++      geometries, false);
++  const material = new THREE.MeshBasicMaterial({color:'red'});
++  const mesh = new THREE.Mesh(mergedGeometry, material);
++  scene.add(mesh);
+
+}
+```
+
+Выше мы удалили код, который изменял центральную точку геометрии бокса,
+и вместо этого делаем это, добавляя `originHelper`. Раньше мы использовали 
+одну и ту же геометрию 19000 раз. На этот раз мы создаем новую геометрию
+для каждого отдельного блока, и, поскольку мы будем использовать `applyMatrix` 
+для перемещения вершин каждой геометрии блока, мы могли бы сделать это один раз, а не два. 
+
+
+В конце мы передаем массив всех геометрий в `BufferGeometryUtils.mergeBufferGeometries`, который объединит их все в одну сетку. 
+
+
+Нам также нужно включить `BufferGeometryUtils` 
+
+```js
+import {BufferGeometryUtils} from './resources/threejs/r119/examples/jsm/utils/BufferGeometryUtils.js';
+```
+
+И теперь, по крайней мере на моей машине, я получаю 60 кадров в секунду 
+
+{{{example url="../threejs-lots-of-objects-merged.html" }}}
+
+Так что это сработало, но поскольку это одна сетка, мы получаем только один материал, 
+что означает, что мы получаем только один цвет, тогда как раньше у нас был другой цвет 
+на каждой коробке. Мы можем исправить это, используя цвета вершин. 
+
+
+Цвета вершин добавляют цвет для каждой вершины. Установив все цвета каждой 
+вершины каждого блока на определенные цвета, каждый блок будет иметь другой цвет. 
+
+
+```js
++const color = new THREE.Color();
+
+const lonFudge = Math.PI * .5;
+const latFudge = Math.PI * -0.135;
+const geometries = [];
+data.forEach((row, latNdx) => {
+  row.forEach((value, lonNdx) => {
+    if (value === undefined) {
+      return;
+    }
+    const amount = (value - min) / range;
+
+    const boxWidth = 1;
+    const boxHeight = 1;
+    const boxDepth = 1;
+    const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+    // adjust the helpers to point to the latitude and longitude
+    lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
+    latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
+
+    // use the world matrix of the origin helper to
+    // position this geometry
+    positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+    originHelper.updateWorldMatrix(true, false);
+    geometry.applyMatrix4(originHelper.matrixWorld);
+
++    // compute a color
++    const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
++    const saturation = 1;
++    const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
++    color.setHSL(hue, saturation, lightness);
++    // get the colors as an array of values from 0 to 255
++    const rgb = color.toArray().map(v => v * 255);
++
++    // make an array to store colors for each vertex
++    const numVerts = geometry.getAttribute('position').count;
++    const itemSize = 3;  // r, g, b
++    const colors = new Uint8Array(itemSize * numVerts);
++
++    // copy the color into the colors array for each vertex
++    colors.forEach((v, ndx) => {
++      colors[ndx] = rgb[ndx % 3];
++    });
++
++    const normalized = true;
++    const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
++    geometry.setAttribute('color', colorAttrib);
+
+    geometries.push(geometry);
+  });
+});
+```
+
+Приведенный выше код ищет количество или вершины, 
+необходимые для получения атрибута `position` из геометрии. 
+Затем мы создаем `Uint8Array` для размещения цветов.
+Затем он добавляет это как атрибут, вызывая `geometry.setAttribute`. 
+
+
+Наконец, нам нужно указать three.js использовать цвета вершин. 
+
+```js
+const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
+    geometries, false);
+-const material = new THREE.MeshBasicMaterial({color:'red'});
++const material = new THREE.MeshBasicMaterial({
++  vertexColors: THREE.VertexColors,
++});
+const mesh = new THREE.Mesh(mergedGeometry, material);
+scene.add(mesh);
+```
+
+И с этим мы получаем наши цвета обратно 
+
+{{{example url="../threejs-lots-of-objects-merged-vertexcolors.html" }}}
+
+Объединение геометрии является распространенным методом оптимизации.
+Например, вместо 100 деревьев вы можете объединить деревья в одну геометрию, 
+кучу отдельных камней в одну геометрию камней, частокол из отдельных пикетов в одну сетку. 
+Другой пример в Minecraft - он не рисует каждый куб по отдельности, а создает 
+группы объединенных кубов, а также выборочно удаляет грани, которые никогда не видны. 
+
+
+Проблема создания всего одного меша состоит в том, что больше не легко перемещать какие-либо части, которые были ранее разделены.
+В зависимости от нашего варианта использования, хотя есть творческие решения. Мы рассмотрим одну в 
+[другой статье](threejs-optimize-lots-of-objects-animated.html).
+
+<canvas id="c"></canvas>
+<script type="module" src="resources/threejs-lots-of-objects.js"></script>