Как оптимизируют графику в играх
Доклад программиста игровой графики Васифа Абдуллаева.
Иллюстрация: Катя Павловская для Skillbox Media
В 2021 году в рамках онлайн-сессии From Zero to Game на азербайджанском портале Gamepons программист Васиф Абдуллаев затронул тему оптимизации графики в видеоиграх с позиции разработчика. В рамках своего выступления он объяснил, что такое оптимизация, как разработчики оптимизируют графику в играх, зачем они это делают и каких усилий им это стоит.
Редакция «Геймдев» Skillbox Media делится основными тезисами этого доклада.
Васиф Абдуллаев
Более пяти лет занимается разработкой мобильных и десктопных игр в небольших инди-командах и крупных студиях. Он специализируется на программировании геймплея, шейдеров компьютерной графики и мультиплеера в движках Unity и Unreal Engine. Сейчас сотрудничает с турецкой студией Coconut Game.
Что такое оптимизация
Оптимизация кода или ПО — это процесс модификации системы для повышения эффективности некоторых элементов программы, а также для экономии ресурсов. В контексте оптимизации игр к ресурсам относятся графический процессор (GPU), центральный процессор (CPU), батарея смартфона, оперативная память и прочее железо.
В разработке игр выделяют две категории оптимизации:
- Оптимизация кода, которая по большей части влияет на CPU. Ею занимаются программисты, ответственные за геймплей, UI, боевую систему и прочие важные элементы игры;
- Оптимизация графики — более сложный процесс, в котором задействованы оба процессора. За этот вид оптимизации отвечают программисты игровой графики и технические художники.
Зачем оптимизировать графику в играх
- Разработчик всегда заинтересован в том, чтобы его игра хорошо продавалась. Но далеко не каждый игрок может похвастаться наличием современного железа. Согласно статистике Steam, на март 2023 года доля систем с видеокартой NVIDIA GeForce GTX 1060 составляет 7,85% от общего числа сборок (для сравнения: у лидера NVIDIA GeForce RTX 3060 — 10,67%). Проект с современной, но неоптимизированной графикой будет сильно тормозить на системе с относительно слабой видеокартой или не запустится вовсе. А это значит, что в игру смогут сыграть далеко не все. Таким образом, оптимизация увеличивает охват аудитории.
- У каждой платформы — консолей, портативных устройств, смартфонов, планшетов — своё железо. Если разработчик планирует мультиплатформенный релиз, игра должна идеально работать на всех заявленных устройствах. Поэтому её нужно оптимизировать.
- И наконец, оптимизированная графика — залог плавного геймплея без просадки кадров, даже если в сцене много детализированных объектов.
Примеры игр с хорошей оптимизацией:
- Metal Gear Solid 5: The Phantom Pain — стелс-экшен с открытым миром и динамическим освещением выдаёт 60 кадров в секунду на PS4 и Xbox One, при этом качество картинки на PS3 и Xbox 360, где игра тоже доступна, ничуть не хуже (пусть и частота кадров на этих платформах ниже).
- Marvel’s Spider-Man — плавный геймплей в сочетании с детализированным открытым миром реализован как на PS4, так и на портированной ПК-версии.
Важно помнить, что оптимизация графики в большей степени зависит не от навыков программиста или возможностей технологии, а от того, как работают с движком программисты игровой графики и 2D/3D-художники.
Графические API
Чтобы понять, как работает оптимизация графики, сперва необходимо ознакомиться с работой графических API. Это специальные интерфейсы, которые помогают разработчикам отрисовать картинку на экране. Также они задействованы в процессе рендеринга в реальном времени.
Примеры графических API:
- ПК — Vulkan, DirectX 11/12, OpenGL, Metal.
- Мобильные платформы — Vulkan, OpenGL, Metal.
- консоли — DirectX, PSGL, GNM.
У каждого графического API свой синтаксис кода, язык программирования шейдеров и совместимость между устройствами. Из списка выше можно увидеть, что некоторые интерфейсы поддерживают кросс-платформенность. Но основная их логика сводится к выводу изображения на экран методом растеризации или трассировки лучей в реальном времени. Современные игровые движки поддерживают сразу несколько графических API.
Vulkan и DirectX 12 можно отнести к «современной» категории интерфейсов. Мы привыкли считать, что понятие «современный» подразумевает более упрощённый подход. Однако код рендеринга для простого треугольника в Vulkan занимает около 1000 строк, а в OpenGL — менее 100.
Всё дело в том, что такой тип API не прячет все данные в функции, а даёт разработчику гибкость для оптимизации, особенно в отношении CPU.
Примечание
Более подробно о преимуществах API Vulkan можно узнать из спецификации на сайте NVIDIA.
Из кода Vulkan, представленного по ссылке выше, можно увидеть обилие команд. В графическом API их называют дроуколлами (от англ. DrawCall). Они отправляют центральному процессору информацию о текстурах, буферах вершин, усечении геометрии, шейдерах и так далее.
К слову, шейдер — это тоже тип программы для рендеринга, которая определяет итоговый вид поверхности объекта в сцене: наложение текстур, рельефность, взаимодействие со светом (поглощение, рассеивание, отражение, преломление и так далее). Шейдеры взаимодействуют с графическим ускорителем и написаны в основном на языках HLSL или GLSL. Но в большинстве случаев разработчики не взаимодействуют с кодом шейдеров напрямую, а настраивают их в движке. Например, шейдеры в Unreal Engine можно собрать с помощью нодов во встроенном редакторе.
Нюансы работы с игровыми движками
Существует очень много игровых движков со своими достоинствами и недостатками. Каждый из них имеет уникальную архитектуру и свой подход к обработке графики. Выбор в пользу той или иной технологии зависит от бюджета, платформы, жанра, геймплея и прочих составляющих.
Стоит учитывать, что сцены в игре также имеют свою специфику в плане оптимизации. Например, рендеринг густого леса и симуляция толпы людей — это разные технические процессы. Поэтому программисты игровой графики должны иметь доступ к исходному коду движка, чтобы понять, как в рамках выбранной технологии реализовать различные операции. В противном случае оптимизировать игру будет сложно.
Следует помнить, что идеального игрового движка не существует. Даже проекты на нашумевшем Unreal Engine 5 нуждаются в оптимизации по нескольким причинам:
- Многие до сих пор играют на слабом железе.
- Инновационная технология Nanite с возможностью отрисовки миллионов полигонов не работает на мелких объектах вроде растений.
- Трассировка лучей или различные сценарии освещения могут негативно влиять на производительность.
Как происходит рендеринг
Рендеринг, или отрисовка изображения, производится с помощью вычислений центрального процессора. В результате игрок видит на экране набор кадров или анимацию 3D-объектов с учётом их расположения, текстурирования, освещения и других характеристик. Различают однопоточный рендеринг с синхронным воспроизведением вычислений и многопоточный, во время которого задействовано несколько ядер процессора. Во втором случае нагрузка распределяется равномерно.
Есть два метода рендеринга на графическом процессоре — с помощью трассировки лучей и с помощью растеризации 3D-моделей. Последний более распространён, поэтому его стоит рассмотреть подробнее.
Известно, что сцены в игре состоят из 3D-объектов. В свою очередь, каждый из них состоит из примитивов: точек, линий и треугольников (иногда квадратов). Задача программы, ответственной за растеризацию, — получить из этих исходных примитивов фрагменты (пиксели) итогового изображения.
Краткое описание стадий, обозначенных на схеме выше:
- Сборщик входных данных (Input Assembler) — чтение данных примитивов (точек, линий и треугольников) из заполненных буферов и сбор данных в примитивы, которые будут использоваться на следующих этапах. Также на этой стадии прикрепляются значения, созданные системой для повышения эффективности шейдеров.
- Этап шейдеров вершин (Vertex Shader Stage) — обработка отдельных вершин шейдером для получения преобразованного атрибута.
- Этап тесселяции (Tesselation Stage) — преобразование геометрии и формирование набора небольших объектов (треугольников, точек и линий).
- Этап шейдера геометрии (Geometry Shader Stage) — обработка целых примитивов: треугольников, линий, точек и смежных с ними вершин.
- Этап средства программной прорисовки (Rasterizer) — урезание примитивов, которых нет в представлении, и их подготовка для следующего этапа шейдера пикселей. Векторные данные (фигуры или примитивы) преобразуются в растровое изображение из пикселей для отображения трёхмерной графики в режиме реального времени.
- Этап шейдера пикселей (Pixel Shader Stage) — приём интерполированных данных для примитива и генерация данных для пикселей (например, данных о цвете). На этой стадии доступны расширенные возможности, такие как попиксельное освещение и постобработка.
- Этап слияния и вывода (Output Merger) — выходные данные (значения пиксельного шейдера, информация о глубине и наборе элементов) объединяются с содержимым целевого объекта отрисовки, буферами глубины и набором элементов для окончательного результата.
Примечание
Этапы растеризации могут варьироваться в зависимости от архитектуры графического процессора. Более подробно обо всех стадиях (в контексте Direct3D) можно прочесть в официальной справке Microsoft.
Теперь, когда есть представление о том, как графический и центральный процессоры обрабатывают графику, можно перейти к вопросам оптимизации.
Профилирование
Один из важных аспектов оптимизации — использование профайлеров. Это инструменты, с помощью которых можно получить информацию об элементах, снижающих производительность в конкретных сценах. Также профайлер отображает работу низкоуровневых модулей API. В отличие от общепринятых стандартов, инструмент измеряет производительность в миллисекундах (ms), а не в частоте кадров в секунду.
Профилировать графику можно непосредственно в движке (Unreal Insights для Unreal Engine, Unity Profiler для Unity) или с помощью сторонних утилит. Среди последних наиболее известны RenderDoc, NVIDIA Nsight, Radeon GPU Profiler и Pix.
Работая с профайлером, следует обращать внимание на несколько моментов:
- Профайлер необходимо запускать на целевой платформе — не стоит оптимизировать мобильную игру на ПК.
- Профилируйте готовый билд, а не проект: редакторы игровых движков используют очень много ресурсов.
- Старайтесь профилировать игру в разных условиях, ведь тактовая частота CPU и GPU зависит от нагрева и уровня заряда устройства. В этом могут помочь утилиты для оверклокинга — например, GPU Boost от NVIDIA или PowerTune от AMD.
Способы оптимизации графики в играх
Использование уровней детализации (LOD)
Нет смысла размещать объекты с высокой детализацией на дальнем расстоянии, так как игрок их всё равно не увидит. К тому же это приведёт к снижению производительности из-за овершейдинга мелких деталей. Модели с низким уровнем детализации не нуждаются в освещении — для них достаточно unlit-материалов. Для текстурирования в этом случае нужно использовать только карту альбедо в низком разрешении.
Оптимизация текстур
Всегда подключайте MIP-мэппинг в настройках текстур. МIP-мэппинг создаёт несколько копий одной текстуры с разной детализацией. Такой подход сводит полосы от швов на моделях к минимуму и предотвращает возможные проблемы в результате сглаживания.
Старайтесь комбинировать несколько текстур в одну, подключая отдельные каналы карты к разным слотам шейдеров.
Используйте текстурные атласы для похожих мешей. Этот подход нередко применяют в игровой индустрии, так как он уменьшает количество дроуколлов.
Выбор между упреждающим и отложенным рендерингом
Упреждающий рендеринг (Forward Rendering) — стандартный метод рендеринга «из коробки», который используется в большинстве движков. GPU получает данные о геометрии, проецирует её и разбивает на вершины. Затем происходит преобразование вершин в геометрию и дальнейшее её разделение на фрагменты или пиксели. Они подвергаются финальной отрисовке, после чего передаются на экран. При этом освещение в сцене вычисляется отдельно для каждой вершины и каждого фрагмента в зоне видимости с учётом количества источников света.
При отложенном рендеринге (Deferred Rendering) сначала отрисовывается вся геометрия. Затем на полученный результат накладывают фрагментарные шейдеры с освещением, и только после этого происходит вывод картинки на экран.
Иногда под отложенным рендерингом подразумевают отложенное освещение (Deferred Shading / Lighting) — модификацию отложенного рендеринга, которая уменьшает размер G-буфера за счёт большого количества источников света в кадре.
Почему важно обращать внимание на метод отрисовки при оптимизации? Допустим, в сцене 100 объектов, в каждом из них порядка 1000 вершин. В таком случае в кадре будет 100 000 полигонов, которые легко обработает видеокарта. Но когда эти полигоны попадают на стадию фрагментного шейдера, запускается процесс вычисления освещения в сцене. А так как при стандартном методе расчёты освещения ведутся по отдельности для каждого сегмента, при выводе изображения не исключены задержки.
В случае со сложными сценами с динамическим освещением или множеством источников света лучше обратить внимание на отложенный рендеринг, так как в этом случае освещение вычисляется для каждого пикселя всего один раз, что улучшает производительность. Например, в Unreal Engine 5 этот тип рендеринга используется по умолчанию. Однако у него есть свои недостатки — в частности, несовместимость с некоторыми платформами. Поэтому выбор метода рендеринга в пользу оптимизации зависит от задач разработчика и специфики игрового движка.
Усечение геометрии (Occlusion Culling)
Если в сцене много статических объектов, геометрию, которая находится вне зоны видимости, отсекают. Это можно сделать как в самом игровом движке, так и с помощью динамических систем на базе GPU.
При настройках усечения обращайте внимание на минимальный размер окклюдера. Окклюдеры — это объекты, позволяющие отсекать геометрию, которая находится за ними. Например, на скриншоте ниже выставлено значение 5. Это значит, что все объекты выше или шире 5 метров не будут отображаться позади окклюдера таких объёмов, что экономит время рендеринга. Размер окклюдера во многом зависит от геймдизайна игры.
Если речь идёт об усечении динамических мешей, в этом случае стоит использовать пул объектов, то есть повторно применять сущности вместо их создания и уничтожения.
Игрок не сможет детально рассмотреть объекты малого размера, расположенные на дальнем расстоянии. Поэтому в движке настраивают дистанцию усечения, исходя из размеров мешей. В Unity это делают с помощью кода, а в Unreal Engine — с помощью инструмента Cull Distance Volumes.
GPU-подход к спецэффектам
Многие анимации и визуальные эффекты на основе физики нагружают процессор. Вместо этого можно воспользоваться менее ресурсоёмкими техниками. Например, при вертексной анимации, известной как Vertex Animation Texture, используются только текстуры и шейдеры, поэтому основная нагрузка идёт на видеокарту, а не на процессор.
Работа с шейдерами
Старайтесь избегать этапов тесселяции и шейдера геометрии, так как они занимают много времени. Если нужно подчеркнуть рельефность, можно сделать следующее:
- Рассмотрите текстурирование с использованием карты параллакса в качестве альтернативы.
- Уменьшайте тесселяцию в зависимости от расстояния камеры.
Также следует воздержаться от большого количества конструкций ветвления (if) в коде шейдеров. GPU лучше воспринимает параллельный код.
Кроме того, старайтесь сократить количество входных параметров шейдера. Оставьте только те, что можно изменить во время выполнения. Используйте минимальную точность шейдеров, особенно при разработке мобильных проектов. И обращайте внимание на занятость шейдеров.
Дополнительные рекомендации
И ещё несколько общих рекомендаций, которые помогут улучшить оптимизацию игры.
- Проверяйте и профилируйте эффекты постпроцессинга. Если они сильно влияют на задержку — рассмотрите другие варианты.
- Отслеживайте производительность при наличии теней и используйте каскадные теневые карты.
- Помните, что запекание освещения менее ресурсоёмкое, чем освещение в реальном времени.
- Существует несколько способов создания определённых спецэффектов — выберите самый эффективный метод, подходящий вашему проекту.
- Не забывайте, что использование системы частиц может негативно повлиять на производительность.
- Если есть возможность, измените значение счётчика цепочки буферов и проверьте результат.
- Используйте динамическое разрешение.
Помните, что оптимизация графики в играх зависит от общего визуального стиля проекта, жанра, целевой платформы и специфики игрового движка. Рекомендации, приведённые в этом материале, а также углублённое изучение и понимание внутренних процессов рендеринга помогут улучшить производительность вашего проекта.