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 = ±v(1 — X? — Y?).

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

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

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


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

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


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

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


  1. Teemon
    11.09.2016 00:33
    +2

    Спасибо, было интересно!
    А можно по-подбробнее про используемые инструменты для анализа?


    1. PatientZero
      11.09.2016 01:16
      +3

      Я перевёл текст, автор в самом начале пишет, что пользовался PIX, это дебаггер для DirectX.


  1. keydon2
    11.09.2016 01:20
    +6

    Ничего не понял, но статья шикарна. Спасибо за перевод.


  1. Mingun
    11.09.2016 11:48

    Было весьма интересно почитать, хоть и мало что понятно. Самый главный вопрос — что дает такой разбор? Ну вот увидели мы какие-то недорисованные кадры, что это дает нам в том плане, чтобы можно было повторить это у себя? Ведь такой разбор именно для этого делается.


    1. agent10
      11.09.2016 12:43
      +2

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


      1. Mingun
        11.09.2016 13:07
        -2

        Ну и какую идею можно почерпнуть из данного описания? Что нормали в текстурах хранятся? Что чтобы сделать отражение, нужно отразить камеру по оси Z? Что ландшафт из карты высот строится? Что геометрия отсекается еще на CPU и GPU уходит только то, что рисовать надо? Что для создания финальной картинки нужно несколько слоев смешать? Что?


        По-моему, все идеи давно уже избитые, используемые повсеместно и ни разу не революционные. Поэтому мне непонято, что же все-таки дает этот разбор. Просто посмотреть, в каком порядке API DirectX вызывается? Так для этого, вроде, документация существует.


        Я вот исхожу из предположения, что разбор делался по такой схеме:


        1. Ух-ты, какая картинка, как они этого добились!?
        2. Так, вызывается то-то, затем то-то, вот это да! Надо нам также!
        3. … кодим…
        4. Что-то непохоже, или тормозит слишком, может, там какая-то изюминка была?
        5. … исследуем...
        6. А, так вот оно что
        7. … кодим...
        8. PROFIT!

        Да, реализация не самое сложное, ну так ее и без этого описания можно сделать. И единственная сложность в данном алгоритме, как мне видится, в пунктах 4 и 5. Вряд ли изучая последовательность вызовов можно понять алгоритм. То есть, вероятность есть, конечно, для чего-то очень простого, но и только. Как мне кажется, это все равно, что исследовать ассемблерный листинг в попытках понять логику действий IBM Watson. Для какой-нибудь сортировки пузырьком, да даже быстрой это бы прокатило, но для систем на порядок сложнее это все равно, что исследовать слона, щупая хобот, ноги и чего-то там еще из известной притчи.


        1. realimba
          11.09.2016 22:10

          Такие разборы показывают какие трюки использовали чтобы получить красивую картинку без просадки производительности. Это интересно и нужно тем кто работает в ГД. Остальным нет. Вот только этот перевод опоздал лет на 10 :)


        1. terentjew-alexey
          11.09.2016 22:50

          Со временем специалист перестает замечать ту магию, что он делает и начинает видеть только детали, забывая о картине в целом. Вот поэтому в вашем случае вы не видите здесь ничего удивительного, но человек, мало знакомый с областью данного знания, скажет «вот это, да!» и найдет для себя что нибудь новое и интересное.


  1. V0odo0
    11.09.2016 15:20
    +1

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


  1. fp777
    11.09.2016 22:50
    -2

    Столько действий… Теперь понятно почему игра так тормозила в момент выхода.
    Спасибо за перевод, надо запустить SC и посмотреть на каждый пиксель со знанием дела:)


  1. lolipop
    12.09.2016 02:52
    -7

    пошловато
    image


  1. wiser9
    12.09.2016 16:44

    Спасибо за перевод. Приятно было вспомнить приятную игру.

    И действительно ли окупается такое разделение по слоям, с точки зрения производительности?


  1. geekmetwice
    13.09.2016 19:27
    +2

    Хочу поддержать Mingun в его недоумении по поводу назначения статьи. Для профи — слишком много общеизвестных (или просто устаревших) деталей, для чайников — вообще вся статья — «белый шум». ОК, многие из вас всё же не полные панды и понимают хотя бы простейшие вещи: сетка (НЕТ такого слова «меш»!), текстура, карта нормалей… И что, вы сможете что-то написать, прочитав статью? Я — нет.
    Статья считается хорошей, если после её прочтения возникают «умные» вопросы. А у меня почему-то только глупые:

    Ну вот для примера: «Поэтому для оптимизации 4 карты весов соединяются в единую RGBA-текстуру.» — зачем писать статью о подобных оптимизациях, если даже непонятно, ГДЕ КОНКРЕТНО происходит улучшение?? Что мы получили, соединив 4 карты? Меньше гигов на диске? Меньше считать на CPU? GPU? Я не понимаю даже примерного контекста «оптимизации», поэтому однозначно минус автору. А если переводчик всё же понимает о чём речь, БЫЛО БЫ ОЧЕНЬ ХОРОШО внести свои комментарии.

    Или вот эти замусоленные «нормали» — мы их применям по 5 штук на площадь! Вы отдаёте себе отчёт, что результирующая высота будет вообще «не там»? (по ср. с оригинальной картой) Не поэтому ли мы до сих пор ржём над «застрял в текстурах» и что техника бесконечного наложения нормалей несколько ущербна?

    А вот у меня ускоритель несёт на борту 2ГБ — может кто-то из профи объяснить, нужны ли все эти местечковые оптимизации при таких объёмах памяти? По мне даже Dune-2 была более-менее играбельной, занимая единицы мегабайт. Так может ну их нафик, эти «экономии на байтах»? Тогда и статьи по графике станут более понятны — вместо чтения 5 страниц о том, как они в одну текстуру упаковали нормали-цвет-солнце-кровь-девственниц, была бы простая схема — карта нормалей, цвет, частицы. Всё.

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


  1. Serj_By
    14.09.2016 11:51

    Отличный перевод! Спасибо!