Как стать автором
Обновить

Разбор графики Supreme Commander

Время на прочтение 9 мин
Количество просмотров 27K
Автор оригинала: Adrian Courrèges

Total Annihilation занимает в моём сердце особое место, потому что это была моя первая RTS; вместе с Command & Conquer и Starcraft это одна из самых лучших RTS, выпущенных во второй половине 90-х.

Через десять лет, в 2007 году, был выпущена её наследница: Supreme Commander. Благодаря тому, что над игрой работали одни из основных создателей Total Annihilation (дизайнер Крис Тейлор, программист движка Джонатан Мейвор и композитор Джереми Соул), ожидания фанатов были очень высокими.

Supreme Commander была тепло принята критиками и игроками благодаря своим интересным особенностям, таким как «стратегический зум» и физически реалистичная баллистика.

Давайте посмотрим, как движок SupCom под названием Moho рендерит кадр игры. RenderDoc не поддерживает игры под DirectX 9, поэтому реверс-инжиниринг выполнялся при помощи старого доброго PIX.

Структура рельефа


Прежде чем углубляться в вопрос рендеринга кадра, важно сначала поговорить о том, как в SupCom создаётся рельеф и какая техника при этом используется.

Вот как выглядит карта для боёв 1 на 1 «Finn’s Revenge». Это вид сверху всей карты, такой она выглядит в игре на миникарте:


Ниже представлена та же самая карта с другого угла:



Сначала геометрия рельефа рассчитывается с помощью карты высот. Карта высот описывает высоту рельефа. Белый цвет обозначает высокий уровень, а тёмный — более низкий. Для нашей карты использовано одноканальное изображение размером 513x513, оно представляет собой в игре площадь 10x10 км. SupCom поддерживает гораздо более масштабные карты размером до 81x81 км.



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

(Примечание переводчика: более наглядно благодаря анимации изменения здесь и ниже видны в оригинале статьи.)

Рельеф






Ну хорошо, текстурирование с привязкой к высоте — это неплохо, но оно быстро исчерпывает свои пределы.

Как можно добавить больше деталей и вариаций в карту?

Здесь используется техника, называющаяся Texture splatting: игра отрисовывает наборы допольнительных текстур альбедо+нормалей. Каждый этап добавляет на рельеф новый «слой».
У нас уже есть слой 0: рельеф с исходными текстурами альбедо + цвета.
Для использования нового слоя нам нужна дополнительная информация: карта весов, сообщающая нам, где нужно рисовать новые альбедо+нормали, и что более важно, где их не рисовать! Без такой карты весов, также называемой альфа-картой при использовании нового слоя мы полностью перекроем наш предыдущий слой. При нанесении на меш текстуры альбедо и нормалей имеют собственный коэффициент масштабирования.

Добавление слоёв









Итак, мы применили слои 1, 2, 3 и 4, каждый из которых основан на 3 отдельных текстурах. Текстуры альбедо и нормалей используют по 3 канала (RGB), а карта весов — только один. Поэтому для оптимизации 4 карты весов соединяются в единую RGBA-текстуру.



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

Поэтому в дело вступают декали: это небольшие спрайты, локально изменяющие цвет альбедо и нормаль пикселя. На этом рельефе есть 861 копий 21 уникальной декали.

Декали





Так уже намного лучше, но как насчёт растительности? Следующим шагом будет добавление на рельеф того, что движок называет «пропсами» (Props): моделей деревьев или камней. На этой карте существует 6026 копий 23 уникальных моделей.

Пропсы





И теперь финальный штрих: поверхность моря. Это сочетание нескольких карт нормалей со скроллингом UV-развёртки в различных направлениях, карты окружения (environment map) для отражений и спрайтами для волн на береговой линии.

Поверхность моря




После этого рельеф готов. Создание хороших карт высот и карт весов может стать проблемой для дизайнеров карт, но, к счастью, существуют инструменты, помогающие в этой работе: есть официальный редактор карт «Supcom Map Editor» и World Machine с ещё более широкими возможностями.

Итак, теперь вы знаете теорию разработки рельефа SupCom, давайте перейдём к самому кадру игры.

Разбивка кадра


Вот кадр, который мы будем разбирать:



Отсечение по пирамиде видимости


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

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

Выделение субмеша



Карта нормалей


Сначала рассчитываются только нормали. При первом проходе вычисляются нормали, полученные при сочетании 5 слоёв (5 карт нормалей и 4 карт весов). Разные карты нормалей смешиваются вместе, все операции выполняются в касательном пространстве.



Расчёты выполняются за один вызов отрисовки с 6 вызовами текстур. Вы можете заметить, что результат выглядит желтовато, в отличие от других карт нормалей, которые обычно имеют синий оттенок. И действительно: здесь синий канал совсем не используется, есть только красный и зелёный.

Но постойте, нормаль — это трёхкомпонентный вектор, как он может храниться всего в двух компонентах? На самом деле применяется техника сжатия (она рассмотрена в конце поста).

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

Со слоями мы закончили, настало время декалей: добавляются декали рельефа и зданий для изменения нормалей слоя.

Декали






Мы всё ещё не использовали синий канал и альфа-канал нашего рендера.

Итак, игра выполняет считывание из текстуры 512x512, представляющей все нормали рельефа (запечённые из исходной карты высот), и рассчитывает для каждого пикселя его нормаль с помощью бикубической интерполяции. Результаты сохраняются в синем и альфа-канале.



Затем игра комбинирует эти два множества нормалей (нормали слоёв/декалей и нормали рельефа) в финальные нормали, используемые для расчёта освещения.



В этом случае сжатие не выполняется: нормали используют 3 канала RGB, по одному на каждый компонент.

Карта может выглядеть очень зелёной, но это потому, что сцена довольно плоская, так что результат правильный: можно взять любой пиксель и рассчитать вектор его нормали с помощью формулы colorRGB * 2.0 - 1.0, также можно проверить, что норма вектора равна 1.

Карта теней


Техника, используемая для рендера теней, называется Light Space Perspective Shadow Maps (LiSPSM). Здесь в качестве источника направленного освещения у нас есть только солнце. Каждый меш сцены рендерится, а расстояние от него до солнца сохраняется в красном канале текстуры 1024x1024. Техника LiSPSM рассчитывает наилучшее проектируемое пространство для максимизации точности карты теней.




Если мы остановимся на этом, мы сможет отрисовывать только жёсткие тени. На самом деле при рендеринге юнитов игра пытается сгладить края теней с помощью PCF-сэмплинга.

Но даже при помощи PCF у нас всё равно не получится достичь таких красивых сглаженных теней, которые мы видим на скриншоте, в особенности сглаженных силуэтов зданий на земле… Как же их получить?

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

Тени на этих скриншотах не будут соответствовать финальной версии, и мы пока продолжаем работать над ними.
[…]
На данный момент мы не закончили работу над графикой игры.
Джонатан Мейвор, 24 февраля 2006 года

Всего через месяц после этого заявления появилась новая потрясающая техника создания карт теней: Variance Shadow Maps (VSM). Она была способна очень эффективно рендерить замечательные мягкие тени.

Похоже, что разработчики SupCom пытались экспериментировать с этой новой техникой: при декомпиляции байт-кода D3D обнаружилась ссылка на функцию DepthToVariancePS(), вычисляющую версию карты теней с размытием. До изобретения VSM для карт теней невозможно было выполнить размытие.

Здесь SupCom выполняет гауссово размытие 5x5 (горизонтальный и вертикальный проход) для карты теней.



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

Заметьте, что псевдо-VSM-карта использовалась только для создания мягких теней на земле.
Когда тень нужно отрисовать на юните, это делается с помощью карты LiSPSM с PCF-сэмплингом. Можно увидеть разницу на скриншоте ниже (PCF имеет сильные артефакты на границе тени):



Рельеф с тенями


Благодаря сгенерированным картам нормалей и теней можно наконец начать рендерить рельеф: текстурированный меш с освещением и тенями.



Декали


После вычисления с помощью информации о нормалях уравнения освещения отрисовываются компоненты альбедо декалей.

Декали





Отражения на воде


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

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




Рендеринг мешей


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

Рендеринг мешей







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

Во время игры UI скрывает эти артефакты за тонкой рамкой, перекрывающей края окна просмотра.

Структура мешей


Каждый юнит в SupCom рендерится за один вызов отрисовки. Модель определяется набором текстур:

  • картой альбедо
  • картой нормалей
  • «картой отражений», которая на самом деле содержит больше информации, чем просто отражения. Это RGBA-текстура, содержащая следующую информацию:

    • Красный: количество отражения карты окружения (Reflection).
    • Зелёный: отражения солнечного света (Specular).
    • Синий: яркость (Brightness). Используется позже для управления блумом (bloom).
    • Альфа: цвет команды (Team Color). Изменяет альбедо юнита в зависимости от цвета команды.




Частицы


Затем рендерятся все частицы, а также полоски здоровья.

Рендеринг частиц и индикаторов здоровья





Bloom


Настало время добавить блеска! Но как нам получить «информацию о яркости», если мы работаем с LDR-буферами? На самом деле карта яркости содержится в альфа-канале, он создаётся в то же время, когда отрисовываются предыдущие меши. Создаётся копия кадра пониженного качества, применяется альфа-канал для выделения только ярких областей, затем последовательно выполняются гауссова размытия.



Буфер размытия затем отрисовывается поверх исходной сцены с дополнительным смешиванием.

Bloom



Пользовательский интерфейс


Мы закончили с основной сценой. В конце рендерится UI, который замечательно оптимизирован: единственный вызов отрисовки для рендеринга всего интерфейса. 1158 треугольников одновременно передаются в GPU.

UI



Пиксельный шейдер выполняет считывание из единой текстуры 1024x1024, использующейся в роли текстурного атласа. При выборе другого юнита UI изменяется и текстурный атлас повторно генерируется «на лету» для упаковки нового набора спрайтов.

И на этом мы завершили разбор кадра!

Дополнительная информация


Уровень детализации


Так как SupCom поддерживает множество вариаций уровня зума, он активно применяет уровни детализации (level of detail, LOD).

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

Различия в LOD







LOD применяется не только для юнитов: после определённого предела тени, декали и пропсы перестают рендерится.

Туман войны


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

Игра хранит информацию о тумане в одноканальной текстуре 128x128, определяющей плотность тумана: 1 означает отсутствие видимости, а 0 — полную видимость.



Сжатие нормалей


Как я и обещал, вот краткое объяснение трюка, использованного в SupCom для сжатия нормалей. Обычно нормаль — это трёхкомпонентный вектор, однако в касательном пространстве вектор выражается относительно касательной к поверхности: X и Y находятся на касательной плоскости, а компонента Z всегда направлена от поверхности. По умолчанию нормаль равна (0, 0, 1); именно поэтому большинство карт нормалей имеют синий цвет, если направления нормалей не изменены.


Если мы примем, что нормаль — это единичный вектор, то его длина равна единице: X² + Y² + Z² = 1.

Если значения X и Y известны, то Z может иметь только два возможных значения: Z = ±√(1 — X² — Y²).

Но поскольку Z всегда направлена от поверхности, она должна быть положительной, т.е. Z = √(1 — X² — Y²).

Именно поэтому достаточно хранить в красном и зелёном каналах значения X и Y, значение Z может быть получено из них. Более подробное (и лучшее) объяснение можно прочитать в этой статье (на английском).

Смешивание нормалей


Если уж мы говорим о нормалях: SupCom выполняет какую-то lerp между картами нормалей, используя карты весов в качестве коэффициентов. На самом деле есть несколько способов смешивания двух карт нормалей, которые дают различные результаты; как объясняется в этой статье (на английском), это не такая простая проблема.

Дополнительные ссылки


Подробное обсуждение темы этой статьи: Slashdot, Hacker News, Reddit.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+73
Комментарии 14
Комментарии Комментарии 14

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн