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

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

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

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

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

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

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

    import * as THREE from 'three';
    +import {OrbitControls} from 'three/addons/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 'three';
    import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
    +import {GUI} from 'three/addons/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-редактор, генератор трехмерных графиков, каталог продуктов и т. д.