[Перевод] Разбор графики Supreme Commander

e91d370d746c4e2690383d072859eac7.jpg

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».

Это вид сверху всей карты, такой она выглядит в игре на миникарте:

b7026d42889b4fba83f7dcab951d68fc.jpg

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

8b5a8a5fef0749be9d8a2b3bf921a749.jpg

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

b16e93f3345b4f3e9bae44758084c28e.jpg

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

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

Рельеф
ecc52d30c6f847dfbd1c0bd9b60cf339.jpg

5e3895409e844d86ae3a81c236039d92.jpg

fdda37ddde7b41199dc72878e1aaab53.jpg


Ну хорошо, текстурирование с привязкой к высоте — это неплохо, но оно быстро исчерпывает свои пределы.
Как можно добавить больше деталей и вариаций в карту?
У нас уже есть слой 0: рельеф с исходными текстурами альбедо + цвета.
Для использования нового слоя нам нужна дополнительная информация: карта весов, сообщающая нам, где нужно рисовать новые альбедо+нормали, и что более важно, где их не рисовать! Без такой карты весов, также называемой альфа-картой при использовании нового слоя мы полностью перекроем наш предыдущий слой. При нанесении на меш текстуры альбедо и нормалей имеют собственный коэффициент масштабирования.
Добавление слоёв
b679afaac22f4538bd0a8c3b5330ec6e.jpg

605f418f831640a7bf03b118e5230e8d.jpg

ba9239d38c8341779a0d7b88ead5d702.jpg

3b692d73d4074b5e9622e83a7669d25f.jpg

5498ac4fca394e9eb286426dcc387e29.jpg

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

44f903b987c84a84a376ea8fb55d8995.jpg

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

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

Декали
9e0b2a8e0b76495395aa84d5e488c448.jpg

ae84de58e6684eb9bca036245677291f.jpg

1f1eec39e9794e98b30190113c426555.jpg

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

Пропсы
49b996cd0db549db88fca2c52ecdee54.jpg

2b6d32fb886041b98a2dc3a4e09ef38c.jpg

6f8122cb4da74d8b874f9e0ee5bca88e.jpg

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

Поверхность моря
7602285989754b5586a950b67a8ccf16.jpg

1101eb336df64e4bad0c2cad2c1dd43b.jpg


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

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

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

b3e560445dfe45f4bd17e12b596ee5a9.jpg

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


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

099a7eea6c9441249b97947be9c2a637.jpg

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


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

b44334ad36ea4eafb4900ed72f428fb7.jpg

Расчёты выполняются за один вызов отрисовки с 6 вызовами текстур. Вы можете заметить, что результат выглядит желтовато, в отличие от других карт нормалей, которые обычно имеют синий оттенок. И действительно: здесь синий канал совсем не используется, есть только красный и зелёный.
Но постойте, нормаль — это трёхкомпонентный вектор, как он может храниться всего в двух компонентах? На самом деле применяется техника сжатия (она рассмотрена в конце поста).
Так что давайте пока примем, что красный и зелёный каналы содержат всю необходимую информацию о нормалях.

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

Декали
639e38d25e7749a2a622590229654cf5.jpg

c0aa7d7d5327473296d9c426478b6e3b.jpg

0f615f096e8a4c2dac28183ac3c64bcc.jpg


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

bc2af69ff0524fec9f31c9360abd5187.jpg

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

0e1e69c54fca4afdbfbcb4e672c01566.jpg

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

colorRGB * 2.0 - 1.0
также можно проверить, что норма вектора равна 1.

Карта теней


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

87a58cd0371e46919de81715f564adc0.png

a38b1b6f83694b489f5781c64bc31c87.png

5b2261e7227242d3b879848cdd124109.png

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

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

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

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

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

1eae2babd573442d96a270f4520c70b7.jpg

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

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

07560959f99343bba1264957a910dce6.jpg

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


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

0e719d11d3104cb89a3ac01bcdfffe8a.jpg

Декали


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

4b304772e70a499a99ed5eca2366a142.jpg

2ca92aeda76b41cca7aa2ada6707d87f.jpg

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


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

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

907e5f0ca6f24ac7a117ffce03e95f74.jpg

258310198c154cceb3a7cfec8646fe8f.jpg

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


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

6d5f77741d7c456499b3d35fc00b8bc9.jpg

e9d83846462d487abef4a2fa5aae1d68.jpg

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

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


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

24988017fbea4d9db3ce6a22441e2353.jpg

Частицы


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

296e7e91753c49cd9f2083a1b6c6bbe0.jpg

6329cbabb404470e9c976fa7761977f7.jpg

Bloom


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

7c82835baae741229e1b05834effb4bd.png

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

Bloom
ba7671606bb74c5bb220d4bd75f65446.jpg

92ae5e70b88d40ff8fb785642ebe1227.jpg

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

038096276a0a4c44a4ade5b2701920a2.jpg

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

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

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

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


Так как SupCom поддерживает множество вариаций уровня зума, он активно применяет уровни детализации (level of detail, LOD).
Если игрок отдаляет камеру от карты, количество видимых юнитов быстро увеличивается; чтобы справиться с возросшей нагрузкой на видеопроцессор приходится рендерить упрощённую геометрию и текстуры меньшего размера. Поскольку юниты находятся очень далеко, движок может отбросить их: модели заменяются низкополигональными версиями с пониженной детализацией, но они рендерятся на экране такими маленькими, что игрок едва ли заметит различия от высокополигональных моделей.
Различия в LOD
11e2ccfd0f0d4e30812b54b757c047df.jpg

429866928242491484f05c9f368ea378.jpg

c6b6da0bed2348218cf4adca098ef0d8.jpg

24ee4f2d349c47a2b96d9436709ba390.jpg

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

Туман войны


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

fc406e7d827c4aabbe192e4c97d088bd.pnga2e450c8d7934136a9b4304bf4fc318a.png6f93cfa627b2421badb1c57692f6d684.png

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


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

1bcefed1c01a477ca9fb759737d959e9.png

Если мы примем, что нормаль — это единичный вектор, то его длина равна единице: X² + Y² + Z² = 1.
Если значения X и Y известны, то Z может иметь только два возможных значения: Z = ±√(1 — X² — Y²).
Но поскольку Z всегда направлена от поверхности, она должна быть положительной, т.е. Z = √(1 — X² — Y²).
Именно поэтому достаточно хранить в красном и зелёном каналах значения X и Y, значение Z может быть получено из них. Более подробное (и лучшее) объяснение можно прочитать в этой статье (на английском).

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


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

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

  • В блоге Джонатана Мейвора есть множество технических идей и очень интересный пост о графическом движке TA.
  • История разработки TA. Очень интересное чтиво из 1998 года, архивированное в Wayback Machine.
  • Подробности о редактировании карт и моддинге SupCom.

Подробное обсуждение темы этой статьи: Slashdot, Hacker News, Reddit.

Комментарии (0)

© Habrahabr.ru