Бывает так, что ваше приложение обрабатывает графику и нуждается в высокой производительности – например, если это сервис для бронирования мест в зале, который мы показали в одной из прошлых статей. При этом зачастую нужны плавный зум или скроллинг элементов, а также поддержка различных библиотек. Рассмотрим, как сохранить производительность и скорость, на примере работы с библиотекой react-three-fiber.

React-three-fiber, можно с вами познакомиться?

Если вы еще не работали с react-three-fiber, то вероятно, слышали о библиотеках React и three.js. Указанная библиотека, в свою очередь, дает возможность использовать при работе с three.js подходы React, такие как virtual dom и jsx, при этом не снижая производительность готового приложения. 

При взаимодействии с react-three-fiber, как мы отметили выше, может возникнуть необходимость в плавной анимации большого количества элементов. В чем же трудность? 

Для начала необходимо задуматься о корне данной проблемы. Для того чтобы создать объект на сцене, мы используем geometry, material и оборачиваем их в mesh. Это выглядит вот так:

<mesh>
    <planeBufferGeometry attach="geometry" />
    <meshBasicMaterial attach="material" color="#0c60d8" />
</mesh>

Разобрав любой базовый пример использования библиотеки react-three-fiber, вы сможете реализовать что-то подобное. Однако, такой подход применим только в том случае, если вы анимируете небольшое количество элементов. Ограничение во многом зависит от вашего устройства, поэтому возьмем некоторое абстрактное значение. Допустим, n – максимальное число элементов, которые вы можете анимировать с частотой в 60 кадров в секунду.

А если ограничения вызывают негативные чувства?

Как же быть, если элементов больше, чем n? При таком подходе у вас будут возникать проблемы с производительностью, и это будет заметно, так как анимация станет не такой плавной, как мы задумывали. Тут возникает вопрос, как быть дальше. Неужели ничего нельзя сделать и нет выхода?

К счастью, это не так! 

Новый взгляд на вещи

Что же мешает нам? Основная проблема – это создание собственного mesh для каждого объекта на сцене. Из-за этого компьютеру приходится отображать по очереди все наши элементы, один за другим. Но давайте рассмотрим изображение как одну сущность, которая состоит из множества объектов. Представьте, вместо того, чтобы переносить с места на место предметы по отдельности, мы поместим их в какое-то “хранилище” и перенесем за раз. 

Именно такой подход и помогает решить проблему с производительностью. Единственное, что нас ограничивает в данной ситуации – для достижения результата  необходимо, чтобы все объекты использовали единые geometry и material. Но с таким ограничением можно мириться при решении множества задач.

Вместо тысячи слов

Давайте рассмотрим способы реализации подобного подхода. Для этих целей существует instancedMesh. Что это такое? По сути, это тот же mesh, только он состоит из множества объектов, которые имеют одни и те же geometry и material. Выглядит это вот так:

<instancedMesh
    ref={ref}
    args={[null, null, countOfElements]}
>
    <planeBufferGeometry attach="geometry" />
    <meshBasicMaterial attach="material" color="#0c60d8" />
</instancedMesh>

С виду обычный mesh, но есть ряд особенностей.

Во-первых, наличие ref тут неспроста. Именно с помощью него будет происходить добавление элементов в наш instancedMesh, так как на данном этапе внутри него нет ни одного элемента.

Во-вторых, countOfElements – это число элементов, которое будет добавлено в наш instancedMesh.

Ну и сам процесс добавления:

const tempObject = new THREE.Object3D();
 
coordinates.forEach(({x, y, id}) => {
  tempObject.position.set(x - width / 2, y - height / 2, -20);
 
  tempObject.updateMatrix();
 
  ref.current.setMatrixAt(id, tempObject.matrix);
});
 
ref.current.instanceMatrix.needsUpdate = true;

Давайте пройдем по шагам.

Мы создадим объект tempObject. Он будет нужен нам для формирования матрицы преобразований и всё. Мы берём оттуда матрицу, а объект отображать на сцене не будем.

Далее пройдем по нашему массиву элементов, которые послужат для создания объектов, добавляемых на сцену.

Для каждого элемента получим  его координаты x и y, а также уникальные идентификаторы id, которые  дадут нам возможность обращаться к элементам при необходимости.

Далее зададим позиционирование для объекта tempObject и обновим ему матрицу трансформаций. Такие трансформации, как смещение, поворот и тому подобные, можно записать в матричном виде и применить их к каждой из координат какого-либо элемента на сцене (подробнее с этим знакомит линейная алгебра).

Затем зададим матрицу для каждого из элементов, к которым планируем обращаться через id. Причём если объекта с данным идентификатором не существует, то он будет создан.

После добавления всех интересующих нас элементов обновим матрицу нашего instancedMesh и увидим изменения. Обратите внимание, что число элементов, которые мы добавляем в наш instancedMesh, должно быть равно тому значению, которое указали в countOfElements.

Подводя итоги

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

Спасибо за внимание! Надеемся, что этот опыт был вам полезен. 

Комментарии (5)


  1. nin-jin
    15.12.2021 21:49

    Так и не понял зачем тут jQuery React.


    1. halfcupgreentea
      16.12.2021 16:48

      https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#enable-concurrency

      Удобный дешевый шедулинг всего приложения включая и webgl и DOM


      1. nin-jin
        16.12.2021 17:36

        А вы запускали эту демку? Она дико течёт по памяти и каждые пару секунд зависает на секунду.


  1. john_samilin
    16.12.2021 15:25

    Это же справедливо для массы статичных объектов. Если у вас, скажем, 150 анимированных моделек, то instancedMesh не поможет


  1. fimashagal
    16.12.2021 20:41

    Немного непонятно получается. InstancedMesh это класс THREE.js, при чем тут fiber?