Skip to main content

Internals of "Lost Planet Extreme Condition" game rendering engine (ru)

· 9 min read

Что-то я обленился в последнее время. :) Весна наверное. Сорри, за долгий перерыв.

Итак, сегодня мы будем оперировать продукт японско-консольного мира, который намеревается вот-вот появиться на РС. Это игра Lost Planet: Extreme Condition. Полной версии игры у меня, естественно, нет, но есть публично доступная демка. Саму игру я впервые увидел на Xbox360, и она меня технологически впечатлила. Когда я решил побороть свою лень, и пореверсить что-нибудь, демо-версия была уже довольно долгое время доступна, и я выбрал её.

Конфигурация моей машины со времени последних обзоров не изменилась: A64 X2 4200+, GF8800GTX, 2GB RAM. Все настройки на максимум.

Сразу оговорюсь, что ревресилась DX9 версия. О DX10 я напишу ниже.

Общие впечатления по движку очень хорошие. Куча cutting-edge технологий, плюс виден очень грамотный подход к реализации. Весь SM3.0 only. На двух уровнях ~500DIPs на пустой сцене (персонаж + террейн), и до 1500 в бою с несколькими противниками. Вся геометрия, кроме совсем уж отдельных случаев, таких как интерфейс, имеет один и тот же вертекс формат:

0 SHORT4N POSITION 0
8 UBYTE4 BLENDINDICES 0
12 UBYTE4N BLENDWEIGHT 0
16 UBYTE4N NORMAL 0
20 UBYTE4N TANGENT 0
24 FLOAT16_2 TEXCOORD 0
28 FLOAT16_2 TEXCOORD 1

Итого 32 байта. Что бросается в глаза:

  1. Очень vertex cache friendly.
  2. Много входных данных, которые хорошо соптимизированны. • Позиция вместо 12 байт (float3) – 8 байт. • Веса костей вместо 16 байт (float4) – 4 байта. • Нормаль и тангент вместо 12 байт каждый (float3) - по 4 байта. • Текстурные координаты вместо 8 байт каждый (float2) – по 2 байта. В этом нет ничего экстраординарного, я для сравнения привёл то, как делают обычно. Просто видна аккуратность подхода. Все данные должны быть аккуратно созданы, чтобы не вылезти за допустимые границы.

Благодаря красивому вертексу, а ещё и высокому быстродействию vertex processor’a современных видечипов, практически вся игровая геометрия (кроме террейна) рисуется шейдером со скиннингом. У статичных моделек значащая матрица всего одна – она является world матрицей объекта.

Множество мешей содержатся в одном VB и IB, которые рисуются с помощью оффсетов.

В игре используются динамически генерируемые кубемапы. Схема работы алгоритма хитрая. Изображение шотится в cubemap в позиции главного персонажа, раз в несколько кадров (возможно генерация фейсов размазана по кадрам, я, к сожалению, этот момент не поймал). Вместе с текущим отражением хранится ещё и предыдущее в отдельной текстуре. В начале каждого кадра в третью кубемапу блендятся предыдущее и текущее изображение, с учётом весов. Таким образом, производится плавный переход одной environment map’ы к другой. Чтобы лучше проиллюстрировать процесс, приведу несколько картинок:

Фейс кубемапы с текущим отражением:

Cube Face Cur

Фейс кубемапы с предыдущим отражением:

Cube Face Prev

Скомбинированное отражение (обратите внимание на видимый ghost-image здания):

Cube Face Combined

Кубемапы размером 128х128 A8R8G8B8. Бледнятся все мип-уровни. Визуально полезность данного технического решения определить я не смог, так как на уровнях, где можно побегать, хороших отражающих объектов я не заметил. На воде (о ней ниже), где сгенерированная envmap’a тоже используется, ничего толком не видно.

Далее генерируются шадовмапы. Используется технология т.н. Cascaded Shadow Maps, когда пространство, покрываемое view frustum-ом, разбивается на несколько регионов, по мере удалённости от позиции камеры, и каждой области назначается своя shadow-текстура. В LT:EC используются 3 текстуры формата R32F размером 2Кх2К. Соответственно hardware shadow map хаки от NV и ATI не используются. Кажется, даже применяются перспективные искажения кусков (визуально понять сложно). В текстуру идет z/w, ничего необычного.

Следующим пунктом идёт сохранение post-perspective z, в текстуру. Формат A8R8G8B8, размер экрана. Floating point упаковывается по в 3 канала по формуле.

R = frc(z * 65535)
G = frc(z * 255)
A = z – R – G

Всё хорошо, только непонятно зачем было геморроится с упаковкой (9 инструкций на упаковку, 2 на распаковку), если можно было воспользоваться R32F.

Симпатичная картиночка с упакованными данными:

Scene Depth

Далее идёт рендер основного изображения. Всё в нём рисуется в RT формата A16R16G16F.

Первый проход для моделек сцены. Огромные и ужасные шейдеры. vs - 60 инструкций, ps 260 инструкций. Все в принципе одинаковые, с небольшими вариациями. Про тотальный хардварный скиннинг (4 кости) я уже говорил. Используется исключительно попиксельное освещение. 8 динамических точечных источников (другие, возможно, поддерживаются, но в коде я их не увидел). 4 важных, 4 не очень. Первая четвёрка отличается от второй исключительно наличием спекуляра. Источники включаются/выключаются посредством булевых регистров. Расчет стандартный (как в FFP), за исключением формулы аттенюации. Как по мне, в FFP она слишком хитромудрая. Здесь проще (нечто подобное я сам использовал):

lightIntensity = 1 - saturate((distance - startFadeDist)/fadeDist)

Пояснительная картиночка:

Scene Depth

Если освещаемая точка будет внутри внутреннего круга, то интенсивность источника будет максимальная. В кольце от внутреннего круга до внешнего интенсивность падает линейно от 1 до 0. Ближние объекты освещаются с использованием текстур нормалей, для дальних берутся нормали из вертексов. Нормал-мапы в DXT5, в котором используются только green и alpha каналы для xy компонент вектора соответсвенно. Z = 1 – x^2 – y^2. Особенности запаковки карт нормалей в сжатые форматы можно почитать тут: http://developer.nvidia.com/object/bump_map_compression.html Статические источники запакованы в 7 коэффициентов, которые используются в Spherical Harmonics Lighting (подробности про этот метод освещения в предыдущем реверсе). Учитывается также туманчик.

Есть интересный момент в учёте ambient lighting. Он не константный для всей сцены, а запакован в текстурку. Таким образом нужные части моделек подсвечиваются ярче, чем другие.

Все шейдеры имеют несколько вариаций. Например, крупные объекты используют ambient occlusion map, мелкие нет. Совсем статические объекты (terrain, здания) используют лайтмапы вместо SH (правильно, 1 текстурная выборка быстрее десятка арифметических инструкций). Также применяется динамически сгенерированная environment cube map, о которой я писал в самом начале.

Диффузная текстурка персонажа

Diffuse

Амбиент текстура

Ambient

Ambient occlusion для террейна (оригинал был 2Kx2K)

Terrain AO

Лайтмапа для террейна (оригинал был 2Kx2K). Она такая цветастая, т.к. каналы используются отдельно

Terrain LM

Альфа-канал лайтмапы (оригинал был 2Kx2K)

Terrain LM Alpha

Второй проход для моделек сцены, в котором рисуются тени. Используются 3 R32F текстуры, о которых было выше. PCF в шейдере.

Скрин до расчёта теней:

Only Lighting

Скрин после наложения теней:

With Shadows

В игре есть технология визуализации меха. Как оказалось она очень простая. Рисуем несколько раз меш, на котором должен быть мех, увеличивая его каждый раз в размерах. С помощью альфа-канала на текстуре модели получается, что во все стороны торчат как бы ворсинки и волокна. Если сильно присмотреться, то артефакты видны, и то, в близи. А так в глаза не бросается и выглядит симпатично.

На первом уровне есть вода. Она простая. 1 скроллящаяся в разные стороны нормал-мапа. По нормали выбираем тексели из динамической envmap’ы. Может быть полупрозрачная. Далее soft партиклы. Они не освещаются, а для фейда по альфе на границах с геометрией, используется текстурка с post-prerspective z, подготовленная ранее.

После того как основной рендеринг сцены закончен, начинается перевод HDR image в LDR. Технология стандартная:

  1. Пинг-понгом посчитали усредненную scene luminance.
  2. Bright-pass – выделили яркие области.
  3. Поблурили картинку после bright-pass’a несколько раз.
  4. Скомбинировали все ингредиенты с применением tone-mapping’а.

HDR image (на ней нет очень ярких частей, к сожалению)

HDR

LDR image

LDR

Следующий эффект – depth of field. Да, на статике его практически не видно, но он есть. :) Возможно в движении эффект более заметен (без него игру не запускал, если его вообще можно отключить). Реализован просто: берем downsampled (даже не заблуренную) картинку, оригинал и текстуру с z. Используя фокусное расстояние, рассчитываем степень заблуривания пикселя по формулам из статьи «Improved Depth of Field Rendering» из ShaderX3. По этому коэффициенту берем либо оригинал, либо уменьшенную картинку. Учитывается также дальность удаления пикселя от камеры.

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

DoF

Предпоследним идет motion-blur. Алгоритм эффекта таков: Сначала копируется текущее изображения сцены, в дополнительный RT, в альфа-канал которого записывается дальность удаления пикселя от камеры.

MB Scale

Далее берется A8R8G8B8 текстурка, и рисуются все анимированные объекты (персонажи, крутящееся оружие на земле, и т.д.) со специальным шейдером. В него передаются предыдущие и текущие матрицы костей для скелетной анимации, предыдущая и текущая матрица view и projection. Вычисляются 2 позиции объекта (предыдущая и текущая), координаты переводятся в screen-space, а затем вычитаются (естественно с учетом множества коэффициентов и т.п.). Т.о. мы получили двумерный вектор смещения текущего пикселя со времени предыдущего кадра. В RT рисуются объекты в текущей и предыдущей позиции, чтобы захватить все пространство занимаемое моделью за эти 2 кадра.

Вот такая картиночка:

MB Offset Vec

Для статики всё проще. Т.к. её позиция определяется исключительно view-projection матрицей, и у нас есть координаты каждого пикселя (z - в текстуре, а xy – текстурные координаты), то можно получить позицию пикселя в предыдущем кадре. Для этого достаточно умножить текущее post-perspective положение на инверсную матрицу проекции, затем перевести координаты в пространство предыдущего кадра, используя cохранённые view и projection матрицы. Так получается вектор смещения для статики. Для того чтобы не перетирались данные рассчитанные для динамических объектов, используется stencil буфер.

Render Target с векторами смещения

MB Offset Map

Финальная композиция использует полученные выше данные, чтобы заблурить пиксели учитывая вектор смещения пикселя.

Получили (эффект хорошо наблюдать, если взять картинку после depth of field и эту, и быстро переключать их в просмотрщике):

After MB

На статической картинке артефакты довольно заметны, особенно при резком движении камеры, но в динамике их практически незаметно.

И, напоследок, делается что-то типа гамма- коррекции с каким-то хитрым color remap’ом, чтобы «подсинить» картинку.

Финальное изображение:

After Gamma

Ну и UI (блоками, не поэлементно).

Что можно сказать напоследок? Присутствует хорошая сортировка по материалам/текстурам/вертекс буферам. А вообще очень наворочено и технологично. Естественно всё это хозяйство жрёт немеряно памяти и вычислительных ресурсов. Вот такой он, консольный пришелец. :) З.Ы. Обещанные 2 слова о DX10 версии. Я было хотел реверсить её, но по каким-то причинам под PixWin на Висте игра ужаснейшим образом тормозит. Кадр в секунд 30 где-то. Причём, даже в заставочном видео (тут соврал, в видео 1 кадр - секунды 3). Я кое-как добрался до загрузки уровня, терпеливо прождал около часа, вырубил игру нафиг, и пошёл реверсить DX9 версию. :)