You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

476 lines
29 KiB

<!DOCTYPE html><html lang="ru"><head>
<meta charset="utf-8">
<title>Оптимизация большого количества анимированных объектов</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@threejs">
<meta name="twitter:title" content="Three.js – Оптимизация большого количества анимированных объектов">
<meta property="og:image" content="https://threejs.org/files/share.png">
<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="/manual/resources/lesson.css">
<link rel="stylesheet" href="/manual/resources/lang.css">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js"
}
}
</script>
</head>
<body>
<div class="container">
<div class="lesson-title">
<h1>Оптимизация большого количества анимированных объектов</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p></p>
<p>Эта статья является продолжением <a href="optimize-lots-of-objects.html">статьи об оптимизации множества объектов
</a>. Если вы еще не прочитали это, пожалуйста, прочитайте его, прежде чем продолжить. </p>
<p>В предыдущей статье мы объединили около 19000 кубов в одну геометрию. Это имело преимущество, заключающееся в том,
что оно оптимизировало наш рисунок из 19000 кубов, но имело тот недостаток, что затрудняло перемещение любого отдельного куба. </p>
<p>В зависимости от того, чего мы пытаемся достичь, существуют разные решения. В этом случае давайте наметим несколько наборов данных и анимируем между наборами. </p>
<p>Первое, что нам нужно сделать, это получить несколько наборов данных.
В идеале мы бы, вероятно, предварительно обрабатывали данные в автономном режиме,
но в этом случае давайте загрузим 2 набора данных и сгенерируем еще 2 </p>
<p>Вот наш старый код загрузки </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
.then(parseData)
.then(addBoxes)
.then(render);
</pre>
<p>Давайте изменим это на что-то вроде этого </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">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();
</pre>
<p>Приведенный выше код загрузит все файлы в <code class="notranslate" translate="no">fileInfos</code>, и после этого каждый объект в <code class="notranslate" translate="no">fileInfos</code>
будет иметь свойство <code class="notranslate" translate="no">file</code> с загруженным файлом. <code class="notranslate" translate="no">name</code> и <code class="notranslate" translate="no">hueRange</code> мы будем использовать позже.
<code class="notranslate" translate="no">name</code> будет для поля пользовательского интерфейса. <code class="notranslate" translate="no">hueRange</code> будет использоваться для выбора диапазона оттенков для отображения. </p>
<p>Два файла выше, по-видимому, представляют собой количество мужчин на область и число женщин на область по состоянию на 2010 год. Обратите внимание,
я не знаю, верны ли эти данные, но на самом деле это не важно. Важная часть показывает разные наборы данных. </p>
<p>Давайте сгенерируем еще 2 набора данных. Одним из них являются места,
где число мужчин превышает число женщин, и наоборот, места, где число женщин превышает число мужчин. </p>
<p>Первым делом давайте напишем функцию, которая с помощью двухмерного массива массивов,
как мы делали раньше, отобразит ее, чтобы сгенерировать новый двумерный массив массивов. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function mapValues(data, fn) {
return data.map((row, rowNdx) =&gt; {
return row.map((value, colNdx) =&gt; {
return fn(value, rowNdx, colNdx);
});
});
}
</pre>
<p>Как и обычная функция <code class="notranslate" translate="no">Array.map</code>, функция <code class="notranslate" translate="no">mapValues</code> вызывает функцию fn для каждого значения в массиве массивов.
Он передает ему значение, а также индексы строки и столбца. </p>
<p>Теперь давайте создадим некоторый код для генерации нового файла, который сравнивает 2 файла. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeDiffFile(baseFile, otherFile, compareFn) {
let min;
let max;
const baseData = baseFile.data;
const otherData = otherFile.data;
const data = mapValues(baseData, (base, rowNdx, colNdx) =&gt; {
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 {...baseFile, min, max, data};
}
</pre>
<p>Приведенный выше код использует <code class="notranslate" translate="no">mapValues</code> для генерации нового набора данных,
который представляет собой сравнение на основе переданной функции <code class="notranslate" translate="no">CompareFn</code>.
Он также отслеживает минимальные и максимальные результаты сравнения. Наконец,
он создает новый файл со всеми теми же свойствами, что и <code class="notranslate" translate="no">baseFile</code>, за исключением новых <code class="notranslate" translate="no">min</code>, <code class="notranslate" translate="no">max</code> и <code class="notranslate" translate="no">data</code>. </p>
<p>Тогда давайте использовать это, чтобы сделать 2 новых набора данных </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
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: '&gt;50%men',
hueRange: [0.6, 1.1],
file: makeDiffFile(menFile, womenFile, (men, women) =&gt; {
return amountGreaterThan(men, women);
}),
});
fileInfos.push({
name: '&gt;50% women',
hueRange: [0.0, 0.4],
file: makeDiffFile(womenFile, menFile, (women, men) =&gt; {
return amountGreaterThan(women, men);
}),
});
}
</pre>
<p>Теперь давайте сгенерируем пользовательский интерфейс для выбора между этими наборами данных. Для начала нам нужен HTML-интерфейс </p>
<pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
&lt;canvas id="c"&gt;&lt;/canvas&gt;
+ &lt;div id="ui"&gt;&lt;/div&gt;
&lt;/body&gt;
</pre>
<p>и немного CSS, чтобы он появился в верхней левой области </p>
<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#ui {
position: absolute;
left: 1em;
top: 1em;
}
#ui&gt;div {
font-size: 20pt;
padding: 1em;
display: inline-block;
}
#ui&gt;div.selected {
color: red;
}
</pre>
<p>Затем мы можем просмотреть каждый файл и сгенерировать набор объединенных блоков
для набора данных и элемент, который при наведении курсора отобразит этот набор и скроет все остальные.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
fileInfos.forEach((info) =&gt; {
const visible = fileInfo === info;
info.root.visible = visible;
info.elem.className = visible ? 'selected' : '';
});
requestRenderIfNotRequested();
}
const uiElem = document.querySelector('#ui');
fileInfos.forEach((info) =&gt; {
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', () =&gt; {
showFileInfo(fileInfos, info);
});
});
// show the first set of data
showFileInfo(fileInfos, fileInfos[0]);
</pre>
<p>Еще одно изменение, которое нам нужно из предыдущего примера, заключается в том, что мы должны заставить <code class="notranslate" translate="no">addBoxes</code> принимать <code class="notranslate" translate="no">hueRange</code> </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-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);
...
</pre>
<p>и с этим мы должны быть в состоянии показать 4 набора данных. Наведите указатель мыши на ярлыки или коснитесь их, чтобы переключать наборы </p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-multiple-data-sets.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/lots-of-objects-multiple-data-sets.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
</div>
<p></p>
<p>Обратите внимание, что есть несколько странных точек данных, которые действительно выделяются.
Интересно, что с ними? ??! В любом случае, как мы анимируем между этими 4 наборами данных. </p>
<p>Много идей. </p>
<ul>
<li><p>Просто исчезните между ними, используя <a href="/docs/#api/en/materials/Material.opacity"><code class="notranslate" translate="no">Material.opacity</code></a> </p>
<p>Проблема с этим решением заключается в том, что кубы полностью перекрываются,
что означает, что возникнут проблемы z-борьбы. Возможно, мы могли бы исправить это,
изменив функцию глубины и используя смешивание. Вероятно, мы должны изучить это. </p>
</li>
</ul>
<ul>
<li><p>Увеличьте набор, который мы хотим видеть, и уменьшите другие наборы. </p>
<p>Поскольку все коробки имеют свое происхождение в центре планеты, если мы масштабируем их ниже 1,0, они погрузятся в планету.
Сначала это звучит как хорошая идея, но проблема в том, что все поля низкой высоты исчезнут почти сразу и не будут заменены,
пока новый набор данных не масштабируется до 1,0.
Это делает переход не очень приятным. Мы могли бы исправить это с помощью необычного шейдера. </p>
</li>
<li><p>Используйте Morphtargets </p>
<p>Morphtargets - это способ, которым мы предоставляем несколько значений
для каждой вершины в геометрии и морф или lerp (линейная интерполяция)
между ними. Morphtargets чаще всего используются для лицевой анимации 3D персонажей, но это не единственное их использование. </p>
</li>
</ul>
<p>Давайте попробуем morphtargets. </p>
<p>Мы по-прежнему создадим геометрию для каждого набора данных,
но затем мы извлечем атрибут позиции из каждого из них и будем использовать их как морфтинги. </p>
<p>Сначала давайте изменим <code class="notranslate" translate="no">addBoxes</code>, чтобы просто создать и вернуть объединенную геометрию. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-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: true,
- });
- const mesh = new THREE.Mesh(mergedGeometry, material);
- scene.add(mesh);
- return mesh;
+ return BufferGeometryUtils.mergeBufferGeometries(
+ geometries, false);
}
</pre>
<p>Здесь есть еще одна вещь, которую нам нужно сделать. Morphtargets требуются, чтобы у всех было точно одинаковое количество вершин.
Вершина # 123 в одной цели должна иметь соответствующую вершину # 123 во всех других целях. Но, поскольку сейчас
разные наборы данных могут иметь некоторые точки данных без данных, поэтому для этой точки не будет сгенерировано ни одного блока,
что означало бы отсутствие соответствующих вершин для другого набора. Итак, нам нужно проверить все наборы данных и либо всегда генерировать что-либо, если
в каком-либо наборе есть данные, либо ничего не генерировать, если в каком-либо наборе отсутствуют данные. Давайте сделаем последнее. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+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) =&gt; {
row.forEach((value, lonNdx) =&gt; {
+ if (dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
+ return;
+ }
const amount = (value - min) / range;
...
</pre>
<p>Теперь мы изменим код, который вызывал <code class="notranslate" translate="no">addBoxes</code>, для использования <code class="notranslate" translate="no">makeBoxes</code> и установки morphtargets. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+// make geometry for each data set
+const geometries = fileInfos.map((info) =&gt; {
+ 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) =&gt; {
+ const attribute = geometry.getAttribute('position');
+ const name = `target${ndx}`;
+ attribute.name = name;
+ return attribute;
+});
+baseGeometry.morphAttributes.color = geometries.map((geometry, ndx) =&gt; {
+ const attribute = geometry.getAttribute('color');
+ const name = `target${ndx}`;
+ attribute.name = name;
+ return attribute;
+});
+const material = new THREE.MeshBasicMaterial({
+ vertexColors: true,
+});
+const mesh = new THREE.Mesh(baseGeometry, material);
+scene.add(mesh);
const uiElem = document.querySelector('#ui');
fileInfos.forEach((info) =&gt; {
- 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]);
</pre>
<p>Выше мы создаем геометрию для каждого набора данных,
используем первый в качестве базы, затем получаем атрибут
позиции из каждой геометрии и добавляем его в качестве морфтинга к базовой геометрии для позиции. </p>
<p>Теперь нам нужно изменить способ отображения и скрытия различных наборов данных.
Вместо того, чтобы показывать или скрывать меш, нам нужно изменить влияние морфтинга. Для набора данных,
который мы хотим видеть, нам нужно иметь влияние 1, а для всех тех, которые мы не хотим видеть, нам нужно иметь влияние 0. </p>
<p>Мы могли бы просто установить их в 0 или 1 напрямую, но если бы мы это сделали, мы бы не увидели никакой анимации,
она просто щелкала бы, что не отличалось бы от того, что у нас уже есть. Мы также могли бы написать некоторый пользовательский анимационный код, который был бы легок,
но поскольку оригинальный глобус webgl использует
<a href="https://github.com/tweenjs/tween.js/">библиотеку анимации</a> давайте используем тот же самый здесь.</p>
<p>Нам нужно включить библиотеку </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import {TWEEN} from 'three/addons/libs/tween.min.js';
</pre>
<p>А затем создайте <code class="notranslate" translate="no">Tween</code> чтобы оживить влияние.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
fileInfos.forEach((info) =&gt; {
const visible = fileInfo === info;
- info.root.visible = visible;
info.elem.className = visible ? 'selected' : '';
+ const targets = {};
+ fileInfos.forEach((info, i) =&gt; {
+ targets[i] = info === fileInfo ? 1 : 0;
+ });
+ const durationInMs = 1000;
+ new TWEEN.Tween(mesh.morphTargetInfluences)
+ .to(targets, durationInMs)
+ .start();
});
requestRenderIfNotRequested();
}
</pre>
<p>Мы также предполагаем вызывать TWEEN.update каждый кадр в нашем цикле рендеринга, но это указывает на проблему.
"tween.js" предназначен для непрерывного рендеринга, но мы делаем <a href="rendering-on-demand.html">рендеринг по требованию </a>.
Мы могли бы переключиться на непрерывный рендеринг, но иногда приятно рендерить только по требованию, так как он перестает использовать
силу пользователя, когда ничего не происходит, поэтому давайте посмотрим, сможем ли мы сделать его анимированным по запросу. </p>
<p>Мы сделаем <code class="notranslate" translate="no">TweenManager</code>, чтобы помочь. Мы будем использовать его для создания
<code class="notranslate" translate="no">Tweens</code> и отслеживания их. Он будет иметь метод <code class="notranslate" translate="no">update</code>, который будет
возвращать true, если нам нужно будет вызвать его снова, и false, если все анимации завершены. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class TweenManger {
constructor() {
this.numTweensRunning = 0;
}
_handleComplete() {
--this.numTweensRunning;
console.assert(this.numTweensRunning &gt;= 0);
}
createTween(targetObject) {
const self = this;
++this.numTweensRunning;
let userCompleteFn = () =&gt; {};
// 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) =&gt; {
userCompleteFn = fn;
return tween;
};
return tween;
}
update() {
TWEEN.update();
return this.numTweensRunning &gt; 0;
}
}
</pre>
<p>Чтобы использовать его, мы создадим один </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
+ const tweenManager = new TweenManger();
...
</pre>
<p>Мы будем использовать его для создания наших <code class="notranslate" translate="no">Tween</code>s.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
fileInfos.forEach((info) =&gt; {
const visible = fileInfo === info;
info.elem.className = visible ? 'selected' : '';
const targets = {};
fileInfos.forEach((info, i) =&gt; {
targets[i] = info === fileInfo ? 1 : 0;
});
const durationInMs = 1000;
- new TWEEN.Tween(mesh.morphTargetInfluences)
+ tweenManager.createTween(mesh.morphTargetInfluences)
.to(targets, durationInMs)
.start();
});
requestRenderIfNotRequested();
}
</pre>
<p>Затем мы обновим наш цикл рендеринга, чтобы обновить анимацию и продолжать рендеринг, если анимация все еще выполняется. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">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();
</pre>
<p>И с этим мы должны анимировать между наборами данных. </p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-morphtargets.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/lots-of-objects-morphtargets.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
</div>
<p></p>
<p>Я надеюсь, что пройти через это было полезно. Использование morphtargets либо через сервисы,
которые предоставляет three.js, либо путем написания пользовательских шейдеров -
это распространенная техника для перемещения большого количества объектов.
В качестве примера мы могли бы дать каждому кубу случайное место в другой цели и
превратить его в свои первые позиции на земном шаре. Это может быть крутой способ представить миру. </p>
<p>Далее вас может заинтересовать добавление ярлыков к глобусу, который описан в разделе.
<a href="align-html-elements-to-3d.html"> «Выравнивание элементов HTML в 3D»</a>.</p>
<p>Примечание: мы могли бы попытаться просто изобразить процент мужчин
или женщин или общую разницу, но основываясь на том, как мы отображаем
информацию, кубы, которые растут с поверхности земли, мы бы предпочли,
чтобы большинство кубов были низкими. Если бы мы использовали одно из
этих других сравнений, то большинство кубов имели бы примерно половину
их максимальной высоты, что не давало бы хорошей визуализации.
Не стесняйтесь изменить количество GreaterThan
с Math.max (a - b, 0) на что-то вроде (a - b) «сырой разницы» или a / (a ​​+ b) «процентов», и вы поймете, что я имею в виду. </p>
</div>
</div>
</div>
<script src="/manual/resources/prettify.js"></script>
<script src="/manual/resources/lesson.js"></script>
</body></html>