Советы: как оптимизировать 3D-игры

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

Автор: Владимир Андреев, арт-директор Playgendary.

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

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

Теперь, когда мы уже обсудили внутренние процессы арт-отдела в распределённой команде, я расскажу, что конкретно можно сделать для оптимизации 3D-игр на Unity: как подготовить ассеты, настроить сцены, использовать батчинг и так далее.

Введение

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

Как ни странно, процесс оптимизации начинается задолго до появления того, что потребуется улучшить. И первый шаг — определиться с целевой платформой, нижней границей по устройствам (например, iPhone 6 и его аналог на Android) и минимальными требованиями к графике.

Ещё на этапе пре-продакшна поможет создание фейкшотов (имитация скриншота игры со всеми элементами). Они покажут, какие ассеты могут присутствовать в кадре и их количество. Далее на основе фейкшотов создаются тестовые 3D-сцены в Unity для проверки нагрузки.

Для экономия времени и быстрого прототипирования достаточно использовать бесплатные ассеты из Unity Asset Store: модели персонажей со скинами и анимациями, модели для окружения, эффекты, UI.

На этом этапе уже можно оценить:

  • Примерное количество полигонов.

  • Загрузку текстурной памяти.
  • Влияние эффектов.
  • Анимации.

Что на этом этапе нельзя полноценно ценить:

  • Влияние игровой логики.
  • Влияние физического движка (если будет).

  • Влияние соединения с сетью (если будет).
  • Загрузку памяти атласами (UI, шрифты и другое).

Не надо сильно ограничиваться на тестовых сценах. Главное — заниматься оптимизацией с самого начала, а не пытаться что-то переделать в готовой игре.

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

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

Оптимизация ради оптимизации — пустая трата времени.

Дальше — основные шаги, которые помогут увеличить производительность.

1. Организуем проект

Плохо организованный проект в Unity сложно оптимизировать и избавлять от проблем. Особенно, если над ним работает большая команда. А в нашем случае ещё и распределённая.

Это приводит к повторным материалам, текстурам, ненужному дублированию ассетов и запутанности. Появляются узкие места в памяти, увеличиваются требования к устройствам и общее время разработки.

Гораздо проще заранее определить структуру проекта, чтобы строго её придерживаться. Опишите всё в документации и отметьте, где искать конкретные материалы и ассеты.

Каждому элементу дайте понятные имена, чтобы не встречать бесконечные Material (1) или сотни объектов с именем Cube.

Создайте отдельные папки в проекте для всего — плагинов, материалов, моделей, сцен, UI, текстур и так далее. В имени каждого ассета укажите, для чего он используется.

Например:

– UI/Common/Icons/IconAbilityFreeze.png

– VFX/Textures/FXAmbientFog01.png

– Models/Characters/Enemies/Boss.fbx

2. Находим узкие места

Unity Profiler — отличный инструмент в Unity Pro, который позволяет анализировать проблемные места в производительности и видеть загрузку доступных ресурсов (CPU/GPU/RAM).

Достаточно запустить билд в режиме разработчика и профилировщик начнёт выкачивать данные о производительности. Чтобы подключиться к плееру, он должен быть запущен с флажком Development Build (находится в диалоговом окне Build Settings).

Unity Profiler показывает график загрузки процессора во время игры — просто держите устройство подключенным к компьютеру. Вы увидите все падения и взлёты в режиме реального времени: рендеринг, сценарии, физика, сборщик мусора, VSync и так далее. Если на мобильных проектах есть падения ниже 30 кадров/с, то пора заняться оптимизацией.

Обычно проблемы мобильных игр связаны с рендерингом. Графическая часть нагружает в первую очередь GPU (графический процессор) и CPU (центральный процессор). Стратегии оптимизации для GPU и CPU сильно отличаются — иногда даже возникают ситуации, когда с GPU нагрузка начинает ложиться на CPU и наоборот.

GPU ограничен филлрейтом (fillrate) или пропускной способностью памяти. Если более низкое разрешение экрана увеличило производительность — проблема в филлрейте.

CPU ограничен количеством Draw Calls (вызов отрисовки). Проверьте его в окне Rendering Statistics — если он больше нескольких сотен (для мобильных устройств) или тысяч (для PC), то придется уменьшать количество объектов.

Менее типичные проблемы могут быть, например, в скриптах или физике — чтобы найти их источник, используйте Profiler.

3. Снижаем нагрузку

Немного теории. Чтобы визуализировать любой объект на экране, CPU должен:

  • Выяснить, какие источники света влияют на объект.
  • Настроить шейдер и его параметры.
  • Послать команды отрисовки (Draw Calls) графическому драйверу, который подготовит их для отправки на видеокарту.

Это будет довольно ресурсозатратно, если у вас много видимых объектов.

Команды Draw Calls можно сравнить с тем, как художнику приходится каждый раз мыть свою кисть, прежде чем продолжать рисовать другим цветом. Цвет краски в нашем случае — это разные материалы и модели, которые двигаются независимо друг от друга.

Объединяйте близко расположенные объекты: вручную или используя инструмент Draw Call Batching (о нём поговорим отдельно). Но помните, что объединение двух объектов с разными материалами не увеличит производительность — убедитесь, что они используют один материал.

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

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

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

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

Пример. Если на объект падает тень, то она «запекается» на плоскости. Движок быстрее отобразит готовую текстуру, чем будет просчитывать отражение и прочие вещи. Эта фича позволит использовать несколько источников света и создавать сложные сцены освещения, не задумываясь о производительности — всё будет запекаться в лайтмапах.

Оптимизируйте рендер. Попробуйте отключить реалтайм-тени или сглаживание. И не экспериментируйте с пост-эффектами: bloom, blur, depth of field или ambient occlusion. На Android они съедают особенно много ресурсов.

4. Оптимизируем 3D-модели

Высокодетализированные объекты сильно нагружают железо. Старайтесь не использовать полигонов больше, чем нужно, а также уменьшайте количество швов на UV-карте и жёстких рёбер, которые удваивают вершины. Поговорим про некоторые техники оптимизации 3D-моделей.

Редукция полигонов. Это упрощение 3D-модели с помощью уменьшения количества полигонов. Принцип простой: вы заменяете группу полигонов одним. Большинство 3D-редакторов умеют регулировать интенсивность, чтобы искать баланс между детализации и производительностью.

Удаление невидимых граней. Есть несколько алгоритмов для удаления линий, поверхностей, граней и рёбер, которые не видны игроку. Делят их на три класса:

  1. Алгоритмы, работающие в пространстве объекта. Они оценивают, видно ли конкретную поверхность из позиции, где находится наблюдатель в конкретный момент времени.

  2. Алгоритмы, работающие в пространстве объектов. У 3D-моделей часто есть грани, которые находятся внутри или на стороне скрытой от наблюдателя. Для вычисления видимых поверхностей используется сравнение и вычисление расположения всех объектов в сцене с удалением всех невидимых граней.
  3. Алгоритмы, формирующие список приоритетов. Попеременно комбинируют работу первых двух типов.

Работу алгоритмов можно увидеть на примере спидрана Half-Life 2, когда игрок летает над картой — с некоторых ракурсов у зданий нет стен, крыш и тому подобного.

Низкополигональные модели (low-poly). Их стоит использовать, когда детализации можно добиться с помощью текстур или различных визуальных эффектов. Цель в том, чтобы создать модель, похожую на референс, за минимальное количество рёбер и вершин.

Такие модели сильно экономят вычислительные ресурсы и обладают хорошим качеством, но требуют довольно много времени на разработку.

Levels Of Detail (LOD). Когда вы отдаляете камеру от объекта, то видите меньше деталей. Но при этом движок во время отрисовки по-прежнему будет использовать то же количество треугольников, зря нагружая устройство.

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

LOD 0 — объект находится близко к игроку, LOD 2 — тот же объект вдалеке.

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

5. Объединяем объекты — используем батчинг

При визуализации каждого объекта выполняется Draw Call, который отправляется графическому API (например, OpenGL или Direct3D). Тот в свою очередь обрабатывает каждый DC, что в итоге требует много ресурсов CPU. Но есть решение.

Unity может объединять объекты во время исполнения, чтобы уменьшить количество Draw Call — эта называется батчингом (batching). Другими словами: это группировка нескольких мешей в один с последующим вызовом отрисовки.

Тут тоже есть свои нюансы и ограничения:

  • У объектов должен быть один и тот же материал. Если два одинаковых материала отличаются только текстурами, то объедините эти текстуры в одну большую — создайте текстурный атлас (о нём мы поговорим ниже).
  • Общее количество вершин ограничивается числом 900.
  • Нельзя менять позицию, масштаб и поворот объектов (параметр transform).

Батчинг в свою очередь бывает двух видов: статический и динамический.

Статический батчинг (Static Batching)

Статический батчинг даёт большую производительность, чем динамический. Для этого метода старайтесь использовать как можно меньше разных материалов и не забудьте поставить галочку Static в Inspector. Обязательно убедитесь, что объекты в сцене статичны, не двигаются, не вращаются и не масштабируются.

Динамический батчинг (Dynamic Batching)

В Unity он работает автоматически и не требует никаких манипуляций от разработчика. Главное условие — объекты должны использовать общий материал. Результаты его работы можно проверить в окне Statistics.

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

6. Оптимизируем текстуры

POT-текстуры (Power Of Two). В идеале размеры текстуры и в высоту, и в ширину должны быть кратны двум: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 и так далее. При этом они не обязаны быть именно квадратными. Например, 2048×256 — это тоже POT.

Объединяйте текстуры в атласы. Текстурный атлас — это изображение, содержащее набор других изображений, каждое из которых является текстурой какого-либо объекта. Под-текстуры можно использовать, указывая координаты на атласе — то есть выбирая определённую часть изображения.

Текстурный атлас​

Среди основных преимуществ использования атласа:

  • Занимает всего один текстурный слот.

  • Минимизирует фрагментацию видеопамяти.
  • Даёт возможность использовать NPOT-текстуры.

NPOT-текстуры (Non Power Of Two). В Unity можно использовать текстуры с размерами, не равными степени двойки. Они называются NPOT, требуют больше памяти и медленнее считываются GPU — старайтесь обходиться без них, если хотите увеличить производительность. В основном они нужны только при создании интерфейсов.

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

Пример. Размеры NPOT всегда ниже рекомендуемых значений POT. То есть текстура 515×515 — это не чуть больше 512×512, а сильно меньше 1024×1024. Поэтому при ее распаковке в память вы получите второй вариант, который занимает намного больше памяти.

NPOT-текстуры можно масштабировать при импорте, используя опцию Non-Power-Of-Two (если в Texture Type установлен Advanced).

Используйте сжатые и 8-битные-текстуры — они быстрее загружаются и занимают меньше памяти. Также стоит свести к минимуму общее количество текстур.

Mip Map (мипмап или мип-карта) — последовательность текстур, которая представляет собой одно и то же изображение с последовательно снижающимся разрешением. Их используют при работе с 3D-движками для оптимизации производительности в реальном времени.

Мипмап​

Процесс работы прост: объекты, которые находятся далеко от камеры, будут использовать уменьшенные версии своих текстур. Мипмапы требуют больше памяти, но зато увеличивают производительность.

Всегда используйте мипмапы для внутриигровых текстур. Единственные исключения — текстуры, которые никогда не будут уменьшаться на экране (например, GUI).

7. Настраиваем камеры

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

  • Для пазла — статичная камера, охватывающая весь уровень.

  • Для шутера от первого лица — камера на уровне глаз персонажа.
  • Для гоночный игры — камера позади автомобиля, которая следует за ним.

Near и Far Clip Plane. Это две плоскости, которые определяют начало и конец область отрисовки камеры. Камера показывает только то, что находится между ними.

​Near Clip Plane — ближайшая позиция, Far Clip Plane — дальняя позиция

Поле зрения камеры, которое определяют две плоскости, называется фрустум (Frustrum). Объекты, полностью находящиеся за пределами фрустума, отображаться не будут — это называется Frustum Culling.

Причём это работает независимо от Occlusion Culling— функции, отключающей рендеринг объектов, которые находятся вне поля зрения камеры.

​Occlusion Culling включена (слева) и выключена (справа)

Frustum и Occlusion не заменяют друг друга — от них одновременно можно получить профит. Frustrum отключает рендеринг только тех объектов, которые находятся вне зоны обзора камеры (то, что находится между Near и Far Clip Plane). И не отключает ничего, что скрыто, например, за какой-нибудь преградой — это поможет оптимизировать Occlusion.

Culling Mask. Маска отрезания позволяет выбирать, слои которые отображает камера и выборочно рендерить определенные объекты.

Например, можно поместить весь пользовательский интерфейс в отдельный слой — его будет отрисовывать отдельная камера, игнорируя все остальные слои. Чтобы интерфейс отрисовывался поверх других изображений, нужно установить Clear Flags в значение Depth Only для камеры интерфейса и выставить значение Depth выше, чем у других камер.

8. Работаем со светом и тенями

Рендеринг освещения бывает двух видов: vertex и pixel.

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

​Вертексное освещение

Пиксельное освещение вычисляет свет для каждого отображаемого объекта. Этот метод забирает больше ресурсов, но и выдаёт более качественный результат.

​Пиксельное освещение

Пиксельного освещение также добавляет некоторые эффекты, которые нельзя использовать при вертексном — например, рельефное текстурирование, light cookie и динамические тени.

Режим освещения сильно влияет на скорость рендеринга, поэтому приходится искать компромисс между качеством и производительностью. По умолчанию Unity автоматически его выбирает, основываясь на количестве объектов под данным источником света. Но режим всегда можно изменить в пункте Render Mode.

Полезные советы списком

  • Помните, что производительность GPU на мобильных устройствах скорее всего будет намного ниже, чем на PC, который вы используете для разработки.
  • Используйте в кадре не более 200 тысяч вершин, если целевая платформа — мобильная. Это примерная оценка для iPhone 7, для современных смартфонов показатель будет выше.

  • Используйте Occlusion Culling для снижения количества видимой геометрии и количества Draw Calls.
  • Используйте скайбоксы для имитации далеко расположенной геометрии.
  • Уменьшайте до минимума количество материалов и текстур.
  • Используйте батчинг.
  • Не используйте пиксельное освещение без необходимости.
  • Не используйте динамические источники света без необходимости — лучше запекайте в лайтмапы.
  • Используйте пиксельные шейдеры или инструменты для совмещения текстур, чтобы смешивать текстуры вместо многопроходной визуализации.
  • Некоторые встроенные в Unity шейдеры имеют «мобильные» эквиваленты, которые работают намного быстрее за счёт своих упрощений.
  • Также в помощь доступно множество ресурсов, которые помогут лучше понять оптимизацию игр. Например, гайд от Unity.
0
6 комментариев
Написать комментарий...
Denis Bystruev

Спасибо за ликбез.

Кстати, у Apple на прошлогоднем WWDC была хорошая презентация про оптимизацию: https://developer.apple.com/videos/play/wwdc2019/606/

Ответить
Развернуть ветку
Михаил Бобков

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

Ответить
Развернуть ветку
Nikita Guk

На хабре аналогично ведем ;)

Ответить
Развернуть ветку
Дмитрий Бобковский

А что за сайт? Для начинающих))

Ответить
Развернуть ветку
Михаил Бобков

Хабр) там все прогерское и не только комьюнити сидит

Ответить
Развернуть ветку
vic buynoff

К чему здесь эти азбучные истины игрового 3D моделинга?
К чему здесь Unity - движок для школоты и студентов?

Ответить
Развернуть ветку

Комментарий удален модератором

Развернуть ветку

Комментарий удален модератором

Развернуть ветку

Комментарий удален модератором

Развернуть ветку
3 комментария
Раскрывать всегда