Browse Source

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

vanzo16 5 years ago
parent
commit
09e14ae248
1 changed files with 714 additions and 0 deletions
  1. 714 0
      threejs/lessons/ru/threejs-optimize-lots-of-objects-animated.md

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

@@ -0,0 +1,714 @@
+Title: Three.js Оптимизация большого количества анимированных объектов
+Description: Анимированные объединенные объекты с морфтаргетами
+TOC: Оптимизация множества анимированных объектов
+
+Эта статья является продолжением  [статьи об оптимизации множества объектов
+](threejs-optimize-lots-of-objects.html). Если вы еще не прочитали это, пожалуйста, прочитайте его, прежде чем продолжить.  
+
+В предыдущей статье мы объединили около 19000 кубов в одну геометрию. Это имело преимущество, заключающееся в том, 
+что оно оптимизировало наш рисунок из 19000 кубов, но имело тот недостаток, что затрудняло перемещение любого отдельного куба. 
+
+В зависимости от того, чего мы пытаемся достичь, существуют разные решения. В этом случае давайте наметим несколько наборов данных и анимируем между наборами. 
+
+
+Первое, что нам нужно сделать, это получить несколько наборов данных. 
+В идеале мы бы, вероятно, предварительно обрабатывали данные в автономном режиме, 
+но в этом случае давайте загрузим 2 набора данных и сгенерируем еще 2 
+
+
+Вот наш старый код загрузки 
+
+```js
+loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
+  .then(parseData)
+  .then(addBoxes)
+  .then(render);
+```
+
+Давайте изменим это на что-то вроде этого 
+
+```js
+async function loadData(info) {
+  const text = await loadFile(info.url);
+  info.file = parseData(text);
+}
+
+async function loadAll() {
+  const fileInfos = [
+    {name: 'men',   hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
+    {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
+  ];
+
+  await Promise.all(fileInfos.map(loadData));
+
+  ...
+}
+loadAll();
+```
+
+Приведенный выше код загрузит все файлы в `fileInfos`, и после этого каждый объект в `fileInfos`
+будет иметь свойство `file` с загруженным файлом. `name` и `hueRange` мы будем использовать позже. 
+`name` будет для поля пользовательского интерфейса. `hueRange` будет использоваться для выбора диапазона оттенков для отображения. 
+
+
+Два файла выше, по-видимому, представляют собой количество мужчин на область и число женщин на область по состоянию на 2010 год. Обратите внимание,
+я не знаю, верны ли эти данные, но на самом деле это не важно. Важная часть показывает разные наборы данных. 
+
+Давайте сгенерируем еще 2 набора данных. Одним из них являются места, 
+где число мужчин превышает число женщин, и наоборот, места, где число женщин превышает число мужчин. 
+
+
+Первым делом давайте напишем функцию, которая с помощью двухмерного массива массивов, 
+как мы делали раньше, отобразит ее, чтобы сгенерировать новый двумерный массив массивов. 
+
+
+```js
+function mapValues(data, fn) {
+  return data.map((row, rowNdx) => {
+    return row.map((value, colNdx) => {
+      return fn(value, rowNdx, colNdx);
+    });
+  });
+}
+```
+
+Как и обычная функция `Array.map`, функция `mapValues` вызывает функцию fn для каждого значения в массиве массивов. 
+Он передает ему значение, а также индексы строки и столбца. 
+
+
+Теперь давайте создадим некоторый код для генерации нового файла, который сравнивает 2 файла. 
+
+
+```js
+function makeDiffFile(baseFile, otherFile, compareFn) {
+  let min;
+  let max;
+  const baseData = baseFile.data;
+  const otherData = otherFile.data;
+  const data = mapValues(baseData, (base, rowNdx, colNdx) => {
+    const other = otherData[rowNdx][colNdx];
+      if (base === undefined || other === undefined) {
+        return undefined;
+      }
+      const value = compareFn(base, other);
+      min = Math.min(min === undefined ? value : min, value);
+      max = Math.max(max === undefined ? value : max, value);
+      return value;
+  });
+  // make a copy of baseFile and replace min, max, and data
+  // with the new data
+  return Object.assign({}, baseFile, {
+    min,
+    max,
+    data,
+  });
+}
+```
+
+Приведенный выше код использует `mapValues` для генерации нового набора данных, 
+который представляет собой сравнение на основе переданной функции `CompareFn`. 
+Он также отслеживает минимальные и максимальные результаты сравнения. Наконец, 
+он создает новый файл со всеми теми же свойствами, что и `baseFile`, за исключением новых `min`, `max` и `data`. 
+
+ 
+Тогда давайте использовать это, чтобы сделать 2 новых набора данных 
+
+```js
+{
+  const menInfo = fileInfos[0];
+  const womenInfo = fileInfos[1];
+  const menFile = menInfo.file;
+  const womenFile = womenInfo.file;
+
+  function amountGreaterThan(a, b) {
+    return Math.max(a - b, 0);
+  }
+  fileInfos.push({
+    name: '>50%men',
+    hueRange: [0.6, 1.1],
+    file: makeDiffFile(menFile, womenFile, (men, women) => {
+      return amountGreaterThan(men, women);
+    }),
+  });
+  fileInfos.push({
+    name: '>50% women', 
+    hueRange: [0.0, 0.4],
+    file: makeDiffFile(womenFile, menFile, (women, men) => {
+      return amountGreaterThan(women, men);
+    }),
+  });
+}
+```
+
+Теперь давайте сгенерируем пользовательский интерфейс для выбора между этими наборами данных. Для начала нам нужен HTML-интерфейс 
+
+
+```html
+<body>
+  <canvas id="c"></canvas>
++  <div id="ui"></div>
+</body>
+```
+
+и немного CSS, чтобы он появился в верхней левой области 
+
+```css
+#ui {
+  position: absolute;
+  left: 1em;
+  top: 1em;
+}
+#ui>div {
+  font-size: 20pt;
+  padding: 1em;
+  display: inline-block;
+}
+#ui>div.selected {
+  color: red;
+}
+```
+
+Затем мы можем просмотреть каждый файл и сгенерировать набор объединенных блоков
+для набора данных и элемент, который при наведении курсора отобразит этот набор и скроет все остальные.
+
+```js
+// show the selected data, hide the rest
+function showFileInfo(fileInfos, fileInfo) {
+  fileInfos.forEach((info) => {
+    const visible = fileInfo === info;
+    info.root.visible = visible;
+    info.elem.className = visible ? 'selected' : '';
+  });
+  requestRenderIfNotRequested();
+}
+
+const uiElem = document.querySelector('#ui');
+fileInfos.forEach((info) => {
+  const boxes = addBoxes(info.file, info.hueRange);
+  info.root = boxes;
+  const div = document.createElement('div');
+  info.elem = div;
+  div.textContent = info.name;
+  uiElem.appendChild(div);
+  div.addEventListener('mouseover', () => {
+    showFileInfo(fileInfos, info);
+  });
+});
+// show the first set of data
+showFileInfo(fileInfos, fileInfos[0]);
+```
+
+Еще одно изменение, которое нам нужно из предыдущего примера, заключается в том, что мы должны заставить `addBoxes` принимать `hueRange` 
+
+
+```js
+-function addBoxes(file) {
++function addBoxes(file, hueRange) {
+
+  ...
+
+    // compute a color
+-    const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
++    const hue = THREE.MathUtils.lerp(...hueRange, amount);
+
+  ...
+```
+
+и с этим мы должны быть в состоянии показать 4 набора данных. Наведите указатель мыши на ярлыки или коснитесь их, чтобы переключать наборы 
+
+
+{{{example url="../threejs-lots-of-objects-multiple-data-sets.html" }}}
+
+Обратите внимание, что есть несколько странных точек данных, которые действительно выделяются. 
+Интересно, что с ними? ??! В любом случае, как мы анимируем между этими 4 наборами данных. 
+
+
+Много идей. 
+
+*  Просто исчезните между ними, используя `Material.opacity` 
+
+   Проблема с этим решением заключается в том, что кубы полностью перекрываются, 
+   что означает, что возникнут проблемы z-борьбы. Возможно, мы могли бы исправить это, 
+   изменив функцию глубины и используя смешивание. Вероятно, мы должны изучить это. 
+
+
+*  Увеличьте набор, который мы хотим видеть, и уменьшите другие наборы. 
+
+   Поскольку все коробки имеют свое происхождение в центре планеты, если мы масштабируем их ниже 1,0, они погрузятся в планету. 
+   Сначала это звучит как хорошая идея, но проблема в том, что все поля низкой высоты исчезнут почти сразу и не будут заменены, 
+   пока новый набор данных не масштабируется до 1,0. 
+   Это делает переход не очень приятным. Мы могли бы исправить это с помощью необычного шейдера. 
+
+*  Используйте Morphtargets 
+
+   Morphtargets - это способ, которым мы предоставляем несколько значений 
+   для каждой вершины в геометрии и морф или lerp (линейная интерполяция) 
+   между ними. Morphtargets чаще всего используются для лицевой анимации 3D персонажей, но это не единственное их использование. 
+
+
+Давайте попробуем morphtargets. 
+
+Мы по-прежнему создадим геометрию для каждого набора данных, 
+но затем мы извлечем атрибут позиции из каждого из них и будем использовать их как морфтинги. 
+
+
+Сначала давайте изменим `addBoxes`, чтобы просто создать и вернуть объединенную геометрию. 
+
+```js
+-function addBoxes(file, hueRange) {
++function makeBoxes(file, hueRange) {
+  const {min, max, data} = file;
+  const range = max - min;
+  
+  ...
+
+-  const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
+-      geometries, false);
+-  const material = new THREE.MeshBasicMaterial({
+-    vertexColors: THREE.VertexColors,
+-  });
+-  const mesh = new THREE.Mesh(mergedGeometry, material);
+-  scene.add(mesh);
+-  return mesh;
++  return BufferGeometryUtils.mergeBufferGeometries(
++     geometries, false);
+}
+```
+
+Здесь есть еще одна вещь, которую нам нужно сделать. Morphtargets требуются, чтобы у всех было точно одинаковое количество вершин.
+Вершина # 123 в одной цели должна иметь соответствующую вершину # 123 во всех других целях. Но, поскольку сейчас 
+разные наборы данных могут иметь некоторые точки данных без данных, поэтому для этой точки не будет сгенерировано ни одного блока, 
+что означало бы отсутствие соответствующих вершин для другого набора. Итак, нам нужно проверить все наборы данных и либо всегда генерировать что-либо, если
+в каком-либо наборе есть данные, либо ничего не генерировать, если в каком-либо наборе отсутствуют данные. Давайте сделаем последнее. 
+
+```js
++function dataMissingInAnySet(fileInfos, latNdx, lonNdx) {
++  for (const fileInfo of fileInfos) {
++    if (fileInfo.file.data[latNdx][lonNdx] === undefined) {
++      return true;
++    }
++  }
++  return false;
++}
+
+-function makeBoxes(file, hueRange) {
++function makeBoxes(file, hueRange, fileInfos) {
+  const {min, max, data} = file;
+  const range = max - min;
+
+  ...
+
+  const geometries = [];
+  data.forEach((row, latNdx) => {
+    row.forEach((value, lonNdx) => {
++      if (dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
++        return;
++      }
+      const amount = (value - min) / range;
+
+  ...
+```
+
+Теперь мы изменим код, который вызывал `addBoxes`, для использования `makeBoxes` и установки morphtargets. 
+
+```js
++// make geometry for each data set
++const geometries = fileInfos.map((info) => {
++  return makeBoxes(info.file, info.hueRange, fileInfos);
++});
++
++// use the first geometry as the base
++// and add all the geometries as morphtargets
++const baseGeometry = geometries[0];
++baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
++  const attribute = geometry.getAttribute('position');
++  const name = `target${ndx}`;
++  attribute.name = name;
++  return attribute;
++});
++const material = new THREE.MeshBasicMaterial({
++  vertexColors: THREE.VertexColors,
++  morphTargets: true,
++});
++const mesh = new THREE.Mesh(baseGeometry, material);
++scene.add(mesh);
+
+const uiElem = document.querySelector('#ui');
+fileInfos.forEach((info) => {
+-  const boxes = addBoxes(info.file, info.hueRange);
+-  info.root = boxes;
+  const div = document.createElement('div');
+  info.elem = div;
+  div.textContent = info.name;
+  uiElem.appendChild(div);
+  function show() {
+    showFileInfo(fileInfos, info);
+  }
+  div.addEventListener('mouseover', show);
+  div.addEventListener('touchstart', show);
+});
+// show the first set of data
+showFileInfo(fileInfos, fileInfos[0]);
+```
+
+Выше мы создаем геометрию для каждого набора данных, 
+используем первый в качестве базы, затем получаем атрибут 
+позиции из каждой геометрии и добавляем его в качестве морфтинга к базовой геометрии для позиции. 
+
+
+Теперь нам нужно изменить способ отображения и скрытия различных наборов данных. 
+Вместо того, чтобы показывать или скрывать меш, нам нужно изменить влияние морфтинга. Для набора данных, 
+который мы хотим видеть, нам нужно иметь влияние 1, а для всех тех, которые мы не хотим видеть, нам нужно иметь влияние 0. 
+
+Мы могли бы просто установить их в 0 или 1 напрямую, но если бы мы это сделали, мы бы не увидели никакой анимации,
+она просто щелкала бы, что не отличалось бы от того, что у нас уже есть. Мы также могли бы написать некоторый пользовательский анимационный код, который был бы легок, 
+но поскольку оригинальный глобус webgl использует
+[библиотеку анимации](https://github.com/tweenjs/tween.js/) давайте используем тот же самый здесь.
+
+Нам нужно включить библиотеку 
+
+```js
+import * as THREE from './resources/three/r119/build/three.module.js';
+import {BufferGeometryUtils} from './resources/threejs/r119/examples/jsm/utils/BufferGeometryUtils.js';
+import {OrbitControls} from './resources/threejs/r119/examples/jsm/controls/OrbitControls.js';
++import {TWEEN} from './resources/threejs/r119/examples/jsm/libs/tween.min.js';
+```
+
+А затем создайте `Tween` чтобы оживить влияние.
+
+```js
+// show the selected data, hide the rest
+function showFileInfo(fileInfos, fileInfo) {
+  fileInfos.forEach((info) => {
+    const visible = fileInfo === info;
+-    info.root.visible = visible;
+    info.elem.className = visible ? 'selected' : '';
++    const targets = {};
++    fileInfos.forEach((info, i) => {
++      targets[i] = info === fileInfo ? 1 : 0;
++    });
++    const durationInMs = 1000;
++    new TWEEN.Tween(mesh.morphTargetInfluences)
++      .to(targets, durationInMs)
++      .start();
+  });
+  requestRenderIfNotRequested();
+}
+```
+
+Мы также предполагаем вызывать TWEEN.update каждый кадр в нашем цикле рендеринга, но это указывает на проблему. 
+"tween.js" предназначен для непрерывного рендеринга, но мы делаем [рендеринг по требованию ](threejs-rendering-on-demand.html). 
+Мы могли бы переключиться на непрерывный рендеринг, но иногда приятно рендерить только по требованию, так как он перестает использовать 
+силу пользователя, когда ничего не происходит, поэтому давайте посмотрим, сможем ли мы сделать его анимированным по запросу. 
+
+Мы сделаем `TweenManager`, чтобы помочь. Мы будем использовать его для создания 
+`Tweens` и отслеживания их. Он будет иметь метод `update`, который будет 
+возвращать true, если нам нужно будет вызвать его снова, и false, если все анимации завершены. 
+
+
+```js
+class TweenManger {
+  constructor() {
+    this.numTweensRunning = 0;
+  }
+  _handleComplete() {
+    --this.numTweensRunning;
+    console.assert(this.numTweensRunning >= 0);
+  }
+  createTween(targetObject) {
+    const self = this;
+    ++this.numTweensRunning;
+    let userCompleteFn = () => {};
+    // create a new tween and install our own onComplete callback
+    const tween = new TWEEN.Tween(targetObject).onComplete(function(...args) {
+      self._handleComplete();
+      userCompleteFn.call(this, ...args);
+    });
+    // replace the tween's onComplete function with our own
+    // so we can call the user's callback if they supply one.
+    tween.onComplete = (fn) => {
+      userCompleteFn = fn;
+      return tween;
+    };
+    return tween;
+  }
+  update() {
+    TWEEN.update();
+    return this.numTweensRunning > 0;
+  }
+}
+```
+
+Чтобы использовать его, мы создадим один 
+
+```js
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
++  const tweenManager = new TweenManger();
+
+  ...
+```
+
+Мы будем использовать его для создания наших `Tween`s.
+
+```js
+// show the selected data, hide the rest
+function showFileInfo(fileInfos, fileInfo) {
+  fileInfos.forEach((info) => {
+    const visible = fileInfo === info;
+    info.elem.className = visible ? 'selected' : '';
+    const targets = {};
+    fileInfos.forEach((info, i) => {
+      targets[i] = info === fileInfo ? 1 : 0;
+    });
+    const durationInMs = 1000;
+-    new TWEEN.Tween(mesh.morphTargetInfluences)
++    tweenManager.createTween(mesh.morphTargetInfluences)
+      .to(targets, durationInMs)
+      .start();
+  });
+  requestRenderIfNotRequested();
+}
+```
+
+Затем мы обновим наш цикл рендеринга, чтобы обновить анимацию и продолжать рендеринг, если анимация все еще выполняется. 
+```js
+function render() {
+  renderRequested = false;
+
+  if (resizeRendererToDisplaySize(renderer)) {
+    const canvas = renderer.domElement;
+    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.updateProjectionMatrix();
+  }
+
++  if (tweenManager.update()) {
++    requestRenderIfNotRequested();
++  }
+
+  controls.update();
+  renderer.render(scene, camera);
+}
+render();
+```
+
+И с этим мы должны анимировать между наборами данных. 
+
+{{{example url="../threejs-lots-of-objects-morphtargets.html" }}}
+
+Кажется, это работает, но, к сожалению, мы потеряли цвета. 
+
+Three.js не поддерживает цвета morphtarget, и на самом деле это проблема оригинального [шара webgl](https://github.com/dataarts/webgl-globe).
+В основном это просто делает цвета для первого набора данных. Любые другие наборы данных используют те же цвета, даже если они сильно различаются. 
+
+Давайте посмотрим, сможем ли мы добавить поддержку для изменения цвета.
+Это может быть хрупким. Наименее хрупким способом, вероятно, было бы на 100% 
+писать наши собственные шейдеры, но я думаю, что было бы полезно посмотреть, 
+как модифицировать встроенные шейдеры. 
+
+
+Первое, что нам нужно сделать, это сделать цвет выделения кода `BufferAttribute` из геометрии каждого набора данных. 
+
+
+```js
+// use the first geometry as the base
+// and add all the geometries as morphtargets
+const baseGeometry = geometries[0];
+baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
+  const attribute = geometry.getAttribute('position');
+  const name = `target${ndx}`;
+  attribute.name = name;
+  return attribute;
+});
++const colorAttributes = geometries.map((geometry, ndx) => {
++  const attribute = geometry.getAttribute('color');
++  const name = `morphColor${ndx}`;
++  attribute.name = `color${ndx}`;  // just for debugging
++  return {name, attribute};
++});
+const material = new THREE.MeshBasicMaterial({
+  vertexColors: THREE.VertexColors,
+  morphTargets: true,
+});
+```
+
+Затем нам нужно изменить шейдер three.js. Материалы Three.js имеют свойство `Material.onBeforeCompile`, 
+которое мы можем назначить функции. Это дает нам возможность изменить шейдер материала до его передачи в WebGL. 
+На самом деле предоставленный шейдер - это на самом деле специальный синтаксис с тремя шейдерами,
+который содержит только три блока шейдеров, которые три.js заменит реальным кодом GLSL для каждого блока. 
+Вот как выглядит код неизмененного вершинного шейдера, передаваемый в `onBeforeCompile`. 
+
+
+```glsl
+#include <common>
+#include <uv_pars_vertex>
+#include <uv2_pars_vertex>
+#include <envmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+void main() {
+	#include <uv_vertex>
+	#include <uv2_vertex>
+	#include <color_vertex>
+	#include <skinbase_vertex>
+	#ifdef USE_ENVMAP
+	#include <beginnormal_vertex>
+	#include <morphnormal_vertex>
+	#include <skinnormal_vertex>
+	#include <defaultnormal_vertex>
+	#endif
+	#include <begin_vertex>
+	#include <morphtarget_vertex>
+	#include <skinning_vertex>
+	#include <project_vertex>
+	#include <logdepthbuf_vertex>
+	#include <worldpos_vertex>
+	#include <clipping_planes_vertex>
+	#include <envmap_vertex>
+	#include <fog_vertex>
+}
+```
+
+Перебирая различные фрагменты, мы хотим заменить 
+[`morphtarget_pars_vertex` блок](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js)
+[`morphnormal_vertex` блок](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js)
+[`morphtarget_vertex` блок](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js)
+[`color_pars_vertex` блок](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_pars_vertex.glsl.js)
+и [`color_vertex` блок](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_vertex.glsl.js)
+
+Для этого мы сделаем простой массив замен и применим их в `Material.onBeforeCompile`. 
+
+```js
+const material = new THREE.MeshBasicMaterial({
+  vertexColors: THREE.VertexColors,
+  morphTargets: true,
+});
++const vertexShaderReplacements = [
++  {
++    from: '#include <morphtarget_pars_vertex>',
++    to: `
++      uniform float morphTargetInfluences[8];
++    `,
++  },
++  {
++    from: '#include <morphnormal_vertex>',
++    to: `
++    `,
++  },
++  {
++    from: '#include <morphtarget_vertex>',
++    to: `
++      transformed += (morphTarget0 - position) * morphTargetInfluences[0];
++      transformed += (morphTarget1 - position) * morphTargetInfluences[1];
++      transformed += (morphTarget2 - position) * morphTargetInfluences[2];
++      transformed += (morphTarget3 - position) * morphTargetInfluences[3];
++    `,
++  },
++  {
++    from: '#include <color_pars_vertex>',
++    to: `
++      varying vec3 vColor;
++      attribute vec3 morphColor0;
++      attribute vec3 morphColor1;
++      attribute vec3 morphColor2;
++      attribute vec3 morphColor3;
++    `,
++  },
++  {
++    from: '#include <color_vertex>',
++    to: `
++      vColor.xyz = morphColor0 * morphTargetInfluences[0] +
++                   morphColor1 * morphTargetInfluences[1] +
++                   morphColor2 * morphTargetInfluences[2] +
++                   morphColor3 * morphTargetInfluences[3];
++    `,
++  },
++];
++material.onBeforeCompile = (shader) => {
++  vertexShaderReplacements.forEach((rep) => {
++    shader.vertexShader = shader.vertexShader.replace(rep.from, rep.to);
++  });
++};
+```
+
+Three.js также сортирует `morphtargets` и применяет только самые высокие влияния. 
+Это позволяет разрешить гораздо больше целей морфинга, если одновременно используется только несколько.
+Нам нужно выяснить, как он сортировал морф-цели, а затем установить соответствие наших цветовых атрибутов.
+Мы можем сделать это, сначала удалив все наши цветовые атрибуты, а затем проверив атрибуты `morphTarget` и увидев,
+какой `BufferAttribute` был назначен. Используя имя `BufferAttribute`, мы можем сказать, какой соответствующий атрибут цвета необходим. 
+
+
+Сначала мы изменим имена атрибутов морфтаргет `BufferAttributes`, чтобы их было легче разобрать позже. 
+
+
+```js
+// use the first geometry as the base
+// and add all the geometries as morphtargets
+const baseGeometry = geometries[0];
+baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
+  const attribute = geometry.getAttribute('position');
+-  const name = `target${ndx}`;
++  // put the number in front so we can more easily parse it later
++  const name = `${ndx}target`;
+  attribute.name = name;
+  return attribute;
+});
+
+```
+
+Затем мы можем установить соответствующие атрибуты цвета в `Object3D.onBeforeRender`, 
+который является свойством нашей сетки. Three.js вызовет его непосредственно перед рендерингом, 
+что даст нам возможность исправить ситуацию. 
+
+
+```js
+const mesh = new THREE.Mesh(baseGeometry, material);
+scene.add(mesh);
++mesh.onBeforeRender = function(renderer, scene, camera, geometry) {
++  // remove all the color attributes
++  for (const {name} of colorAttributes) {
++    geometry.deleteAttribute(name);
++  }
++
++  for (let i = 0; i < colorAttributes.length; ++i) {
++    const attrib = geometry.getAttribute(`morphTarget${i}`);
++    if (!attrib) {
++      break;
++    }
++    // The name will be something like "2target" as we named it above
++    // where 2 is the index of the data set
++    const ndx = parseInt(attrib.name);
++    const name = `morphColor${i}`;
++    geometry.setAttribute(name, colorAttributes[ndx].attribute);
++  }
++};
+```
+
+И с этим у нас должны быть оживляющие цвета так же как коробки. 
+
+{{{example url="../threejs-lots-of-objects-morphtargets-w-colors.html" }}}
+
+Я надеюсь, что пройти через это было полезно. Использование morphtargets либо через сервисы,
+которые предоставляет three.js, либо путем написания пользовательских шейдеров - 
+это распространенная техника для перемещения большого количества объектов. 
+В качестве примера мы могли бы дать каждому кубу случайное место в другой цели и 
+превратить его в свои первые позиции на земном шаре. Это может быть крутой способ представить миру. 
+
+
+Далее вас может заинтересовать добавление ярлыков к глобусу, который описан в разделе. 
+ [ «Выравнивание элементов HTML в 3D»](threejs-align-html-elements-to-3d.html).
+
+Примечание: мы могли бы попытаться просто изобразить процент мужчин
+или женщин или общую разницу, но основываясь на том, как мы отображаем 
+информацию, кубы, которые растут с поверхности земли, мы бы предпочли, 
+чтобы большинство кубов были низкими. Если бы мы использовали одно из 
+этих других сравнений, то большинство кубов имели бы примерно половину
+их максимальной высоты, что не давало бы хорошей визуализации. 
+Не стесняйтесь изменить количество GreaterThan
+с Math.max (a - b, 0) на что-то вроде (a - b) «сырой разницы» или a / (a ​​+ b) «процентов», и вы поймете, что я имею в виду. 
+