Skip to main content

Internals of "StarCraft 2" game rendering engine. Part 1 (ru)

· 8 min read

Мой последний реверс датирован 4 июля 2008 года. Оправдываться не буду, что обещал чаще, а получилось вот так. Наконец кривые желания и возможности пересеклись, и вашему вниманию предлагается реверс инжиниринг рендера всем известной игры Старкрафт 2, от не менее известной компании Близзард.

Технологии шагнули далеко вперед. Уже можно играть в игры с поддержкой DX11. Есть игры которые изначально разрабатывались на DX10 и не поддерживают DX9 железо. Но, игры компании Близзард всегда славилась поддержкой широкого спектра аппаратуры (взять тот же ВоВ, который и на ФФП нормально бегает). Не исключением стала и SC2. Рендер стратегии разработан с использованием D3D версии 9 (для windows). DX10 не поддерживается. Я думаю после нескольких лет разработки было накладно переводить рендер на новый АПИ, поэтому имеем только DX9 (и Open GL для Mac).

Первое с чем я столкнулся после того как я сел за дело были проблемы с тулзами. СК2 использует хитрую систему запуска, через всякие лоадеры, так что под PerfHUD’ом запустить не удалось. Можно было бы поковыряться в исходниках лоадера (который форсит PerfHUD девайс), чтобы скипал несколько процессов, но из-за того, что стар молча выходил, при попытках тестовых запусков, я отложил вариант с PerfHUD’ом. Плюс некоторый функционал этого профайлера перестал работать на новой Win7 OS (не показывает перфоманс графики). Новый профайлер Nvidia Parallel Insight показал себя с лучшей стороны. Он быстрый, удобный и практически не глючный. Один недостаток – DX9 не поддерживается. Так что у меня оставался всего один вариант – MS PIX for Windows. За два года данную тулзу немного улучшили (особенно в плане производительности), так что пользовался я исключительно ей.

Вторая неприятность возникла, когда я взглянул на шейдеры. Обычно, когда шейдер собирается из эффект (*.fx) файла, то в бинарном байткоде остается немного дебаговой информации. Это информация включает комментарии в начале шейдера с блоком прешейдера (если используется) и, что самое главное, названия переменных и их соответствия константным регистрам.

Вот как выглядит шейдер который я ожидал: http://www.everfall.com/paste/id.php?fgnlr6ffcx0y Вот так выглядят шейдера от Близзард: http://www.everfall.com/paste/id.php?njt5do17c4bi Что в первом, что во втором случае разобрать по асму что там происходит сложно, но в первом примере есть имена переменных, по которым можно сориентироваться. В общем, я понял, что так я далеко не уеду, и полез ковырять данные игры. Распаковал пару MPQшек, и нашел исходники шейдеров. Зачастую мне было сходу понятно, что делается в данном draw call. Но иногда я заходил в тупик. Помогало медитирование над входными текстурами, вертекс форматом и константами объявленными в шейдере. Если вижу, например, операцию вычитания двух регистров, один из которых константный со значением 1.5, я ищу по всем hlsl шейдерам текст -1.5 (или просто 1.5) и часто с первого раза попадаю в нужный. Если шейдер небольшой, то разбирал прямо по ассемблеру.

По рендеру СК2 есть замечательная дока: http://developer.amd.com/documentation/presentations/legacy/Chapter05-Filion-StarCraftII.pdf В ней описаны многие аспекты рендера, и я ей активно пользовался. Рекомендую к ознакомлению.

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

Хочется остановиться на системе материалов используемой в СК2. Применяется техника убершейдера, с плотной интеграцией в С++ код. Вот пример главной функции пиксельного шейдера и вычисления цвета материала: http://www.everfall.com/paste/id.php?bjpc19uks7he. Как видно, в исходном коде имеются множественные проверки булевых констант, которые определяют разветвления комбинаций опций шейдинга. Система пиксельной обработки построена на т.н. слоях (layers) каждый из которых состоит из одной текстуры (вернее самплера) и параметров-свойств этого слоя. Вот как оно реализовано: http://www.everfall.com/paste/id.php?5ekjmezzcllx. В шейдере, перед телом, идет декларация нескольких слоев:

DECLARE_LAYER(Diffuse);
DECLARE_LAYER(Decal);
DECLARE_LAYER(Specular);
DECLARE_LAYER(Emissive);
DECLARE_LAYER(Emissive2);
DECLARE_LAYER(Envio);
DECLARE_LAYER(EnvioMask);
DECLARE_LAYER(AlphaMask);
DECLARE_LAYER(AlphaMask2);
DECLARE_LAYER(Normal);
DECLARE_LAYER(Heightmap);
DECLARE_LAYER(Lightmap);
DECLARE_LAYER(AmbientOcclusion);

Настройка слоя происходит внутри тела шейдера с помощью макроса SETUP_LAYER (пример можно посмотреть в предыдущей ссылке с кодом главной функции пиксельного шейдера). DX Effect System не используется. В доке указанной выше, сказано, что система материалов разрабатывалась для максимального удобства программистов. Плюсовая часть ее мне не видна, к сожалению, но шейдерная сделана довольно интересно.

Рендер сцены начинается с рендера карты тени (shadowmap) размером 2048х2048 пикселов. Трик с нулевым рендер таргетом не используется. Соответствующий буфер цвета используется для рендера полупрозрачных объектов с позиции источника света для создания цветных теней. Также для полупрозрачный объектов используется отдельная карта теней с тем же размером. Искажающие перспективу и каскадные техники не используются по понятным причинам. Камера никогда не смотрит вдаль, и в нормальном игровом процессе расположена под углом 60-70 градусов к поверхности земли. В данной ситуации ни каскадные тени ни перспективное искажение возле положения камеры не дадут сколь видимого эффекта.

Пример шадоумапы:

SM

Пример буфера цвета для цветных теней:

Color SM

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

Оригинальная текстура шума:

Creep Noise

Полученная карта высот:

Creep HM

Карта нормалей полученная из карты высот:

Creep NM

Заметьте, что оригинальная карта высот сделана тайлящейся по всем направлениям. Т.о. мы имеем тайлящуюся карту нормалей.

Дальше начинается основной рендеринг. Устанавливается 3 рендертаргета формата ARGB16F в первые 3 MRT слота. Первый RT содержит освещенную глобальными источниками света картинку. Второй нормали в пространстве камеры. Третий только диффузный цвет.

MRT0 MRT1 MRT2

Система освещения в СК2 комбинированная. Глобальные источники, как то солнце и те что занимают много места на экране, применяются сразу в первом проходе (т.е. forward shading). Множество мелких источников применяются после, с помощью аддитивного блендинга (т.е. deferred shading). Deferred источники используют стенсил для минимизации обрабатываемых пикселей. Стенсил буфер очищается, и перед отрисовкой deferred источника, рендерится его футпринт (т.е. шар для точечных, конус для спот источников). Сначала помечаем всю поверхность источника света, а затем исключаем из стенсил маски те участки, где прошел тест глубины для бэк-фейс полигонов. Т.о. получается маска пикселов которые буду затронуты данным источником освещения. Forward источников может быть не много, упираемся в шейдерные константы:

#define MAX_DIRECTIONAL_LIGHTS 3
#define MAX_POINT_LIGHTS 5
#define MAX_SPOTLIGHTS 5

Deferred источников может быть бесконечное множество.

Перед рендером геометрии с шейдингом идет depth prepass, в котором заполняется буфер глубины патчами земли. Почему только земля? Потому что, неровности земли часто скрывают элементы ландшафта, а остальные объекты небольшие. Таким образом, экономим еще немного производительности на пиксельной обработке.

Шейдеры главного прохода очень большие. Наложение теней, полупрозрачных теней, параллакс маппинга (в коде шейдера реализованы методы relief mapping и parallax occlusion mapping. Хорошо видно после взрыва протосовского здания – котлован взрыва), тумана войны, наложение крипа, того же направленного освещения. Блендинг кучи текстур. Сплаттинг для террейна часто не помещается по инструкциям в один большой шейдер. Слоя земли смешиваются в отдельном RT, а затем этот RT используется как диффузная текстура в рендере патча земли. Все движения выполняются посредством применения скелетной анимации.

Обычные тени размываются посредством percentage closer filtering’а. Для рандомизации семплов используется текстура с 2D rotation векторами: L канал:

RotL

А канал:

RotA

Туман войны считается на CPU и хрантися в небольшой текстурке:

FoW

Эта текстура используется для всех эффектов в игре.

Полупрозрачные тени детально описаны в документе от разработчиков. Опишу вкратце. У нас есть карта тени основная, карта тени в которой только глубины полупрозрачных объектов, и цветная текстура отрисованная с позиции источника света с полупрозрачными объектами. Когда мы делаем тест на то в тени ли пиксель или нет, при отрицательном результате теста (не в тени) мы тестируем его еще раз, но уже используя shadowmap с глубинами полупрозрачных объектов. Если тест положительный (в тени от полупрозрачного объекта, но не в тени от непрозрачного), то берем цвет из цветной текстуры и окрашиваем тень в этот цвет. Эффект хорошо рядом с кристаллами и под прозрачными юнитами.

Color Shadow

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

После рендера всей геометрии идет проход deferred источников света, о которых я писал выше.

Далее идет отрисовка рамок UI, маркеров юнитов и портрета текущего выбранного юнита. К портрету юнита применятся depth of field.

Before Bright Pass

Затем идет брайтпасс, и блур. Получаем блум эффект. После этого комбинируем все с тонемаппингом. Далее отрисовка миникарты со всем маркерами юнитов (одним вызовом), туманом войны и крипом.

Minimap

Поэлементно дорисовывается весь интерфейс. Текст рендерится пачками – применяется рендер скейлформа, который пакует все нужные буквы в кадре в текстуру. Получается эффективно.

Text

Результат:

Final

Формат вершин моделей сделан хорошо, зачастую 32 байта. Данные упакованы хорошо. Количество DIPов тоже неплохое. В средней сцене – 800-1200. Лимит зерглингов на экране с основной базой показал 1800-1900 DIPов. Все карты нормалей хранятся в DXT5 формате со сжатием и распределением по каналам для большего качества. Реверс инжиниринг проходил на компьютере Core 2 Duo, GF460GTX, все настройки ультра. Качество текстур – высокое (при попытка захвата кадра пиксом, на настройках текстур ультра, старкрафт падал с out of memory).