Так часто бывает, что техники оптимизации, хорошо работающие для обычной десктопной или мобильной графики, не всегда дают нужный эффект в случае WebGL. В этой статье я собрал (а точнее перевёл на русский язык и изложил в текстовом виде нашу презентацию с Verge3Day) те методы повышения производительности, которые хорошо себя зарекомендовали при создании интерактивных веб-приложений.

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

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

При работе с цилиндрической моделью постарайтесь уменьшить количество полигонов ближе к центру.

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

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

Однако карты нормалей могут создавать видимые артефакты из-за ограниченной точности 8-битного изображения. Возможные решения этой проблемы показаны на картинке ниже, но их довольно сложно осуществить: использование изображения с более высокой точностью приведет к увеличению размера загружаемого файла, в то время как второй подход довольно трудоемкий и не гарантирует чистого результата. Третий подход иногда срабатывает в случае с шероховатыми поверхностями. В этом случае мы рекомендуем добавлять шум к вашим материалам, чтобы уменьшить возможные артефакты.

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

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

Вот типичный набор текстур, используемых в современной PBR модели освещения (и не только).

Как видите, большинство из них черно-белые. Поэтому вы можете комбинировать ч/б текстуры в RGBA-каналах одного изображения (всего до 4-х карт на одно изображение).

Если у вас есть только одна ч/б текстура, вы можете объединить ее с любой существующей текстурой RGB, упаковав ее в альфа-канал. Наконец, если у вас нет изображения для объединения, вы можете преобразовать ваше черно-белое изображение в формат jpeg со сжатием 95% и включенным режимом оттенков серого.

Еще один способ уменьшить размер текстуры — оптимизировать UV-развёртку. Чем компактнее ваша развертка, тем эффективнее ваше изображение будет использовать пространство текстуры. Это позволяет вам иметь более легковесные изображения без потери качества.

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

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

Очень важно, чтобы на вашей сцене было меньше разных материалов / шейдеров. Компиляция шейдеров в WebGL приводит к длительной загрузке, что особенно заметно в Windows. Кроме того, если у вас меньше шейдеров, движок будет тратить меньше времени на переключение между ними во время рендеринга, тем самым улучшая производительность.
Если у вас есть похожие материалы, которые отличаются только текстурами, вы можете использовать только один материал и загружать / менять его текстуры во время выполнения. Для этого вы можете использовать JavaScript или взять визуальный редактор логики, имеющийся в некоторых WebGL-фреймворках. Это не только оптимизирует количество шейдеров, но и уменьшит количество изображений, загружаемых при запуске приложения.

Вот пример такой оптимизации. Все четыре разновидности одной шины представлены одним материалом, и настраиваются путем замены текстур.

Чтобы уменьшить количество шейдеров, вы можете объединить 2 или более простых материала в один более сложный. Этот метод особенно эффективен, если вы планируете переключаться между этими материалами (например, создаете приложение-конфигуратор), и не просто переключаться, а плавно и красиво анимировать переход от одного материала к другому.

Кроме того, есть еще один важный аспект — количество Draw-вызовов (они же draw calls и вызовы отрисовки). Это примерно соответствует количеству отдельных объектов, если для каждого объекта назначен только один материал, в то время как объектам с несколькими материалами требуется еще больше вызовов для их визуализации.
Поэтому вы должны стремиться объединять сетки, когда это возможно, и использовать меньше уникальных материалов, чтобы уменьшить количество вызовов отрисовки и повысить производительность.

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

Это помогает значительно улучшить производительность, если вы освещаете свою сцену только с помощью изображения HDR, без использования каких-либо источников света. Файл HDR может весить менее 1 Мб.

Используйте динамические тени только тогда, когда они помогают представить ваш объект в лучшем виде. На рисунке ниже показаны динамические тени из нашей демки Industrial Robot. Поскольку в этом приложении вращается сама модель, а не камера, тени не могут скрыть какую-либо часть объекта от пользователя и отлично подчёркивают форму робота. С другой стороны, такие же тени в демке Scooter будут затенять многие детали модели.

Отсюда мы делаем вывод: если ваш объект не перемещается в приложении, вы можете запечь карты теней и ambient occlusion и назначить их на плоскость, расположенную под объектом. Такое решение будет более производительным и выглядеть качественнее, чем при использовании динамических теней.

На этом всё. Если у вас есть ещё какие-нибудь советы, которые могут помочь с производительностью WebGL, пишите в комментариях.

Геометрия / Меши
Правильная геометрия — это ключ к производительности любого трехмерного приложения. Чтобы получить ровный шейдинг и быстрый рендеринг, вы должны держать полигональную сетку как можно более равномерной. В самом начале работы вы должны определиться с уровнем детализации вашей сцены и придерживаться его при моделировании.

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

При работе с цилиндрической моделью постарайтесь уменьшить количество полигонов ближе к центру.

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

Карты нормалей
Распространенным способом оптимизации производительности WebGL является уменьшение количества полигонов путем создания карт нормалей.

Однако карты нормалей могут создавать видимые артефакты из-за ограниченной точности 8-битного изображения. Возможные решения этой проблемы показаны на картинке ниже, но их довольно сложно осуществить: использование изображения с более высокой точностью приведет к увеличению размера загружаемого файла, в то время как второй подход довольно трудоемкий и не гарантирует чистого результата. Третий подход иногда срабатывает в случае с шероховатыми поверхностями. В этом случае мы рекомендуем добавлять шум к вашим материалам, чтобы уменьшить возможные артефакты.

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

Наконец, вот несколько случаев, когда вы можете использовать карту нормалей, а не очень подробный меш:
- Ваш объект состоит из множества различных поверхностей.
- У вас шероховатая поверхность, которая не дает видимых дефектов.
- Ваши объекты такие отдаленные или маленькие, что пользователь не в состоянии заметить какие-либо артефакты.

Текстурирование
Вот типичный набор текстур, используемых в современной PBR модели освещения (и не только).

Как видите, большинство из них черно-белые. Поэтому вы можете комбинировать ч/б текстуры в RGBA-каналах одного изображения (всего до 4-х карт на одно изображение).

Если у вас есть только одна ч/б текстура, вы можете объединить ее с любой существующей текстурой RGB, упаковав ее в альфа-канал. Наконец, если у вас нет изображения для объединения, вы можете преобразовать ваше черно-белое изображение в формат jpeg со сжатием 95% и включенным режимом оттенков серого.

Еще один способ уменьшить размер текстуры — оптимизировать UV-развёртку. Чем компактнее ваша развертка, тем эффективнее ваше изображение будет использовать пространство текстуры. Это позволяет вам иметь более легковесные изображения без потери качества.

Вертексные цвета
Использование вершинных цветов вместо изображений — это эффективный способ ускорить загрузку и повысить общую производительность ваших WebGL-приложений. Единственный недостаток — вам придётся добавить несколько дополнительных ребер в вашу модель, чтобы разделить цвета одних и тех же вершин.

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

Количество шейдеров
Очень важно, чтобы на вашей сцене было меньше разных материалов / шейдеров. Компиляция шейдеров в WebGL приводит к длительной загрузке, что особенно заметно в Windows. Кроме того, если у вас меньше шейдеров, движок будет тратить меньше времени на переключение между ними во время рендеринга, тем самым улучшая производительность.
Если у вас есть похожие материалы, которые отличаются только текстурами, вы можете использовать только один материал и загружать / менять его текстуры во время выполнения. Для этого вы можете использовать JavaScript или взять визуальный редактор логики, имеющийся в некоторых WebGL-фреймворках. Это не только оптимизирует количество шейдеров, но и уменьшит количество изображений, загружаемых при запуске приложения.

Вот пример такой оптимизации. Все четыре разновидности одной шины представлены одним материалом, и настраиваются путем замены текстур.

Чтобы уменьшить количество шейдеров, вы можете объединить 2 или более простых материала в один более сложный. Этот метод особенно эффективен, если вы планируете переключаться между этими материалами (например, создаете приложение-конфигуратор), и не просто переключаться, а плавно и красиво анимировать переход от одного материала к другому.

Draw-вызовы
Кроме того, есть еще один важный аспект — количество Draw-вызовов (они же draw calls и вызовы отрисовки). Это примерно соответствует количеству отдельных объектов, если для каждого объекта назначен только один материал, в то время как объектам с несколькими материалами требуется еще больше вызовов для их визуализации.
Поэтому вы должны стремиться объединять сетки, когда это возможно, и использовать меньше уникальных материалов, чтобы уменьшить количество вызовов отрисовки и повысить производительность.

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

HDR освещение
Это помогает значительно улучшить производительность, если вы освещаете свою сцену только с помощью изображения HDR, без использования каких-либо источников света. Файл HDR может весить менее 1 Мб.

Тени
Используйте динамические тени только тогда, когда они помогают представить ваш объект в лучшем виде. На рисунке ниже показаны динамические тени из нашей демки Industrial Robot. Поскольку в этом приложении вращается сама модель, а не камера, тени не могут скрыть какую-либо часть объекта от пользователя и отлично подчёркивают форму робота. С другой стороны, такие же тени в демке Scooter будут затенять многие детали модели.

Отсюда мы делаем вывод: если ваш объект не перемещается в приложении, вы можете запечь карты теней и ambient occlusion и назначить их на плоскость, расположенную под объектом. Такое решение будет более производительным и выглядеть качественнее, чем при использовании динамических теней.

На этом всё. Если у вас есть ещё какие-нибудь советы, которые могут помочь с производительностью WebGL, пишите в комментариях.
GCU
Хороший набор советов.
P.S. Если можно — как-то пояснить что такое Smooth Group?
«шейдинг-группы», «с гладкими вертексными группами»
Это выглядит как интерполяция нормалей, привязанных к вершинам, а в чём смысл разных групп?..
alexkowel Автор
Спасибо. Smooth Group — это отдельные категории вертексов, в пределах которых нормали интерполируются независимо от других групп. Они позволяют комбинировать плоские и объёмные элементы в одной модели и при этом не увеличивать полигонаж слишком сильно.
GCU
Всё равно непонятно.
Например кубик с фаской:
В каждой вершине задана ещё и нормаль к поверхности, поверхность гладкая. На боковых гранях все вершины (минимум 4) имеют одну и ту же нормаль, на фаске — разную. Зачем там целых 4 группы?
При рендеринге это же всё равно будут треугольники и интерполяция нормалей будет у каждого треугольника своя, независимо от других.
P.S. Кажись дошло
В самом WebGL никаких групп нету, эта сущность только в CAD нужна чтобы правильно выставить нормали в вершинах
alexkowel Автор
Именно так, а в сам движок экспортируются уже готовые нормали.