Рендеринг по требованию

Эта тема может быть очевидна для многих людей, но на всякий случай ... большинство примеров Three.js отображаются непрерывно. Другими словами, они устанавливают цикл requestAnimationFrame или "цикл RAF" примерно так

function render() {
  ...
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

Для чего-то, что анимируется, это имеет смысл, но как насчет чего-то, что не анимируется? В этом случае непрерывный рендеринг является пустой тратой энергии устройств, а если пользователь находится на портативном устройстве, он расходует батарею пользователя.

Самый очевидный способ решить эту проблему - рендерить один раз в начале, а затем рендерить только тогда, когда что-то меняется. Изменения включают в себя окончательную загрузку текстур или моделей, данные, поступающие из какого-либо внешнего источника, пользователь, изменяющий настройку или камеру, или другой соответствующий ввод.

Давайте возьмем пример из статьи об отзывчивости и изменим его для отображения по требованию.

Сначала мы добавим в OrbitControls, чтобы можно было что-то изменить, что мы можем сделать в ответ.

import * as THREE from '/build/three.module.js';
+import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';

и настроить их

const fov = 75;
const aspect = 2;  // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;

+const controls = new OrbitControls(camera, canvas);
+controls.target.set(0, 0, 0);
+controls.update();

Поскольку мы больше не будем анимировать кубы, нам больше не нужно отслеживать их

-const cubes = [
-  makeInstance(geometry, 0x44aa88,  0),
-  makeInstance(geometry, 0x8844aa, -2),
-  makeInstance(geometry, 0xaa8844,  2),
-];
+makeInstance(geometry, 0x44aa88,  0);
+makeInstance(geometry, 0x8844aa, -2);
+makeInstance(geometry, 0xaa8844,  2);

Мы можем удалить код для анимации кубов и вызовы requestAnimationFrame

-function render(time) {
-  time *= 0.001;
+function render() {

  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

-  cubes.forEach((cube, ndx) => {
-    const speed = 1 + ndx * .1;
-    const rot = time * speed;
-    cube.rotation.x = rot;
-    cube.rotation.y = rot;
-  });

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

-requestAnimationFrame(render);

тогда нам нужно отрендерить один раз

render();

Нам нужно рендерить каждый раз, когда OrbitControls меняет настройки камеры. К счастью, OrbitControls отправляет событие change каждый раз, когда что-то меняется.

controls.addEventListener('change', render);

Нам также нужно обработать случай, когда пользователь изменяет размер окна. Раньше это было обработано автоматически, так как мы рендерили непрерывно, но теперь нам это не нужно, нужно рендерить, когда окно меняет размер.

window.addEventListener('resize', render);

И с этим мы получаем что-то, что рендерит по требованию.

У OrbitControls есть опции для добавления некоторой инерции, чтобы они чувствовали себя менее интенсивными. Мы можем включить это, установив для свойства enableDamping значение true.

controls.enableDamping = true;

С включенной функцией enableDamping нам нужно вызвать controls.update в нашей функции рендеринга, чтобы OrbitControls продолжал предоставлять нам новые настройки камеры, поскольку они сглаживают движение. Но это означает, что мы не можем вызвать render напрямую из события change, потому что мы окажемся в бесконечном цикле. Элементы управления отправят нам событие change и вызовут render, render вызовет controls.update. controls.update отправит еще одно событие change.

Мы можем исправить это, используя requestAnimationFrame для вызова render, но нам нужно убедиться, что мы запрашиваем новый кадр, только если он еще не был запрошен, что мы можем сделать, сохраняя переменную, которая отслеживает, если мы уже запросили кадр.

+let renderRequested = false;

function render() {
+  renderRequested = false;

  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

  renderer.render(scene, camera);
}
render();

+function requestRenderIfNotRequested() {
+  if (!renderRequested) {
+    renderRequested = true;
+    requestAnimationFrame(render);
+  }
+}

-controls.addEventListener('change', render);
+controls.addEventListener('change', requestRenderIfNotRequested);

Возможно, нам также следует использовать requestRenderIfNotRequested для изменения размера

-window.addEventListener('resize', render);
+window.addEventListener('resize', requestRenderIfNotRequested);

Может быть трудно увидеть разницу. Попробуйте нажать на приведенный ниже пример и использовать клавиши со стрелками для перемещения или перетаскивать для вращения. Затем попробуйте нажать на приведенный выше пример и сделайте то же самое, и вы сможете увидеть разницу. В приведенном примере при нажатии клавиши со стрелкой или перетаскивании мышью, кубики проскальзывают.

Давайте также добавим простой графический интерфейс lil-gui и внесем его изменения по запросу.

import * as THREE from '/build/three.module.js';
import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
+import {GUI} from '/examples/jsm/libs/lil-gui.module.min.js';

Давайте позволим установить цвет и шкалу х каждого куба. Чтобы установить цвет, мы будем использовать ColorGUIHelper, который мы создали в статье о светах.

Сначала нам нужно создать графический интерфейс

const gui = new GUI();

а затем для каждого куба мы создадим папку и добавим 2 элемента управления, один для material.color и другой для cube.scale.x.

function makeInstance(geometry, color, x) {
  const material = new THREE.MeshPhongMaterial({color});

  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  cube.position.x = x;

+  const folder = gui.addFolder(`Cube${x}`);
+  folder.addColor(new ColorGUIHelper(material, 'color'), 'value')
+      .name('color')
+      .onChange(requestRenderIfNotRequested);
+  folder.add(cube.scale, 'x', .1, 1.5)
+      .name('scale x')
+      .onChange(requestRenderIfNotRequested);
+  folder.open();

  return cube;
}

Вы можете видеть выше элементы управления lil-gui имеют метод onChange, который вы можете передать обратный вызов для вызова, когда графический интерфейс изменяет значение. В нашем случае нам просто нужно вызвать requestRenderIfNotRequested. При вызове folder.open папка запускается расширенной.

Я надеюсь, что это дает некоторое представление о том, как сделать three.js визуализированным по требованию, а не непрерывно. Приложения / страницы, которые отображают Three.js по требованию, не так часто встречаются, так как большинство страниц, использующих Three.js, являются либо играми, либо 3D-анимацией, но примеры страниц, которые могут быть лучше прорисованы по требованию, - это, скажем, просмотрщик карт, 3D-редактор, генератор трехмерных графиков, каталог продуктов и т. д.