123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- <!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="../resources/lesson.css">
- <link rel="stylesheet" href="../resources/lang.css">
- <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) => {
- return row.map((value, colNdx) => {
- 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) => {
- 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: '>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);
- }),
- });
- }
- </pre>
- <p>Теперь давайте сгенерируем пользовательский интерфейс для выбора между этими наборами данных. Для начала нам нужен HTML-интерфейс </p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
- <canvas id="c"></canvas>
- + <div id="ui"></div>
- </body>
- </pre>
- <p>и немного CSS, чтобы он появился в верхней левой области </p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#ui {
- position: absolute;
- left: 1em;
- top: 1em;
- }
- #ui>div {
- font-size: 20pt;
- padding: 1em;
- display: inline-block;
- }
- #ui>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) => {
- 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]);
- </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.mergeGeometries(
- - geometries, false);
- - const material = new THREE.MeshBasicMaterial({
- - vertexColors: true,
- - });
- - const mesh = new THREE.Mesh(mergedGeometry, material);
- - scene.add(mesh);
- - return mesh;
- + return BufferGeometryUtils.mergeGeometries(
- + 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) => {
- row.forEach((value, lonNdx) => {
- + 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) => {
- + 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;
- +});
- +baseGeometry.morphAttributes.color = geometries.map((geometry, ndx) => {
- + 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) => {
- - 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.module.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) => {
- 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();
- }
- </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 >= 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;
- }
- }
- </pre>
- <p>Чтобы использовать его, мы создадим один </p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
- const canvas = document.querySelector('#c');
- const renderer = new THREE.WebGLRenderer({antialias: true, 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) => {
- 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();
- }
- </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="../resources/prettify.js"></script>
- <script src="../resources/lesson.js"></script>
- </body></html>
|