Internals of "Crysis" game rendering engine (ru)
Crysis... Как много в этом слове! :) Это было сложно и невероятно увлекательно. Каждый эффект - новое открытие. Одно дело прочитать 1-2 абзаца описания реализации в документации, и совсем другое самостоятельно пощупать и поиграться с эффектом. Сначала, зачастую, ничего не понятно. Дальше по частицам начинает вырисовываться технология и эффект приобретает новую красоту. Обычно заканчивается тем, что находишь самое яркое его проявление и любуешься со всех сторон. :)
Да, это реверс рендера Кризиса. С самого начала хочу сделать 2 ремарки:
- Существует документация по технологиям Кризиса, где в сжатом виде рассказаны те или иные подходы к реализации. Мне известно 3 документа. Первый это доклад Мартина Миттринга (Martin Mittring) на Siggraph 2007 (p97-mittring.pdf). Второй - доклад Карстена Вензеля (Carsten Wenzel) на GDC 2007 (D3DTutorial_Crytek.pdf) И последний - статья Тиаго Сусы (Tiago Sousa) в GPU Gems 3. Все эти источники информации мне сильно помогли, так что советую их просмотреть. Я не буду акцентировать большое внимание на момментах, которые неплохо описаны в этих документах.
- Я очень уважаю команду разработчиков. Это команда экстра класса. Естественно, что вряд ли я один смогу вот так, наскоком, постичь все премудрости движка. Поэтому вполне вероятно, что в процессе анализа я что-то упустил (возможно даже существенное), либо не так понял.
Итак, Crysis, Windows Vista, DX10. Компьютер Core2 Duo E6550, 3GB RAM, GF 8600GTS. Все настройки Ultra High. Наверное, у всех на слуху знаменитый Screen Space Ambient Occlusion (SSAO). С него и начну. Основной ингредиент для приготовления данного эффекта является camera space normalized scene depth, или текстура с глубиной сцены в пространстве камеры, если по-русски. Рисуется этот эффект в отдельный буфер, и результат выглядит вот так:
Это полностью реалтаймовый эффект, который считается на видеокарте. Как он реализован? Для каждого пикселя изображения берутся 8 векторов, которые можно представить внутри кубика. Эти вектора идут от центра кубика до его углов. Как и в классическом алгоритме просчета AO, это и есть лучи с помощью которых мы проверяем заслонённость данной точки каким-то объектом (правда тут проверка производится не по полусфере, а по полной сфере, так как нормаль в точке недоступна). Затем, чтобы избавиться от регулярности, нужно как-то сэмулировать произвольное распределение лучей. Это сделано с помощью операции вычисления отраженного вектора из данного (полный аналог интринсика reflect в DX HLSL). Нормаль, относительно которой выполняется отражение берется из вот такой текстурки (увеличено в 50 раз):
Это текстура размером 4х4 текселя, она проецируется так, каждый тексель этой текстурки соответсвует каждому пикселю выходного изображения. Достигается это тайлингом (текстурным повторением) размер_экрана/4 раз. Т.о., для каждого пикселя экрана мы получили псевдопроизвольную нормаль. Повернув все 8 исходных векторов относительно полученной псевдопроизвольной нормали, мы получим 8 псевдопроизвольных лучей для тестов. Лучи у нас единичной длины, так что их длина скейлится в зависимости от отдаленности обрабатываемой точки от near plane (ближние пиксели получают меньшую длину лучей, дальние большую). Затем полученные текстурные координаты и Z смещаются по каждому лучу и производится проверка на Z на конце вектора-луча. Если луч приникнет в какую-либо геометрию, то Z в буфере глубины камеры будет меньше Z на конце луча. Данная ситуация означает, что процент загороженных лучей будет увеличен, что в конце повлияет на общую освещенность точки. К сожалению, малое количество лучей-тестов и недостаточная произвольность выборок приводит к ярко выраженной, зернистой, структуре изображения:
Для нивелирования этого побочного эффекта, применятся проход полноэкранного сглаживания c color leak correction и используется scene camera z. В результате чего, получается картинка, которая приведена в начале. Кому интересно посмотреть этот эффект вживую, вот вам ссылочка на RenderMonkey проект, сделанный мной. В нём содержится практически полная реплика эффекта из Кризиса. Crysis.zip на upload.com.ua
Далее эфффект, который, на сколько мне помнится, не описан нигде. Dynamic Terrain AO, или динамический амбиент окклюжен для террейна (земли). Рендеринг производится в G канал текстуры с SSAO (в то время как сам SSAO хранится в R). Данный эффект предназначен для аттенюации освещения объектов под влиянием террейна:
Создается он хитро. Террейн разбит на прямоугольные патчи различного размера. Для каждого такого патча имеется прямоугольный параллелепипед выровненный по самой нижней точке (т.е. самая нижня точка патча земли совпадает с нижней плоскостью параллелепипеда). По сути это ААBB, только вытянутый немного по вертикали. Данный бокс, рендерится 2 раза используя предрассчитанный ранее depth buffer. z-test on, z-write off и color write off. Первый раз с front face culling, второй с back face culling. Эти 2 прохода являют собой stencil fill pass. В обоих случаях StencilFunc = Always, ZFail = Replace, остальные операции Keep. Отличия между этими двумя проходами в StencilRefl. Первым идет front face culling, и тут StencilRef = 6 (не знаю чего именно 6. Cчитайте что просто больше нуля). После отрисовки бокса, стенсил как бы "подсвечивает" не нулями то место в кадре, которое соответствует данному патчу земли. Но эта информация для шейдинга пока неверна, так как все объекты в кадре, которые ближе к камере, чем задняя стенка бокса, оказываются помеченными. Для исправления ситуации используется второй рендер бокса, но с back face cull, и StencilRef = 0. Данным проходом мы снимаем пометку с пикселей, которые не попадают в пространство внутри куба. Т.е. если игрок, например, вне этого патча земли, то оружие, которое он держит в руках, будет исключено из сгенерированной маски. Чтобы проще было понять вот картиночка с кубиком. Фиолетовый цвет это Terrain AO в G канале текстуры. Вернее это уменьшение яркости канала, так как Terrain AO расчитвает только затенение.
После этого, в пространстве экрана рендерится прямоугольник, который описан над ААBB упомянутым ранее. В параметрах рендеринга устанавливается SetncilFunc = Equal, StencilRef = 6. Таким образом, обработке подвергнутся только пиксели объектов, которые лежат внутри баундинг бокса патча террейна. Для картинки приведенной выше он выглядит так:
Дальше - интереснее. Рисуем то мы всё в пространстве экрана. Как вообще можно что-то сделать полезное? Оказывается можно. Для каждого патча террейна, есть пара текстурок, которые используются при его рендеринге. Часть информации из них нужна при генерации Terrain AO. Вот они:
В терминах Crytek, они называются незамысловато: TerrainInfo0, TerrainInfo1. :) Из нулевой текстуры нам нужна предрасчитанная грубая степень освещенности террейна от солнца (я так понимаю это что-то сродни статическому амбиент окклюжену). Она в R канале:
В текстуре с индексом 1, нам потребуется хейтмапа и карта высоты растительности на данном клочке земли (А и G каналы соответсвенно).
Как же использовать эти текстуры? Естественно необходимо посчитать текстурные координаты для проецирования этих текстур на поверхность земли. Сделано просто шикарно. Первым шагом вычисляется мировые координаты обрабатываемого пикселя. У нас есть уже буфер глубины сцены. Формула проста: мировая позиция камеры + вектор направления взгляда на пиксель * Z. 1 mad инструкция! Как узнать вектор направления взгляда, спросите вы? Очень просто. Можно посчитать его для каждого вертекса в вершинном шейдере для уголов Screen Aligned Quad. Интерполированные значения будут соответствовать искомым. Но команда Crytek пошла ещё дальше по пути оптимизации. Соответствующий вектор передаётся как per-vertex info отдельным стримом. Вот что значит оптимизация! :) Вторым шагом нам нужно перевести world space координаты в локальные координаты патча земли. Это делается простым вычитанием координат ближайшего к мировому 0 угла ААBB патча из мировых координат пикселя. Перевод в стандартный [0..1] uv тривиален. Полученные локальные, относительно патча, координаты делим на его ширину и длину. Текстурные координаты для проективного текстурирования получены. Далее чистый арт. Зная высоту обрабатываемого пикселя, высоту террейна под этой точкой, высоту растительности и предрасчитанный статический АО, всё это смешивается в хитрых формулах, с кучей коэффициентов. В результате получается амбиент тень от деревьев снизу, затенения в арках и гротах и т.д. Fade out эффекта также реализован по расстоянию. Результат на изображении в начале описания эффекта.
Каустика от воды. Красивый эффект. Делается достаточно просто. На основе вектора направления солнечного света строим проективную матрицу. Эта матрица используется для вычисления текстурных координат для карты нормалей. Правдами и неправдами, мешая высокочастотные семплы и низкочастотные, добиваемся отсутствия регулярной структуры нормалей. Нормаль после всех пертрубаций преломляем с помощью refrаct относительно направления света от солнца. При этом немножко меняем угол преломления для каждого компонента цвета (R, G и B). Это даст в результате сдвиг спектра в результирующем изображении. Затем с помощью полученных нормалей семплим текстуру вида:
Реузльтат сеплирования возносится в степень, чтобы увеличить четкость изображения каустики. Степень видимости каустики определяется скалярным произведением направления света солнца и пертрубированной нормали. Есть также операции по предотвращению появления каустики на поверхностях под углом > 90 градусов к солнышку.
Облака. Бывают разные. Например, самые верхние состоят из 1 полигона параллельного земле. Шейдинг их состоит на определении влияния солнца и неба на цвет. Для расчета плотности применяется texture space tracing. Вектор взгляда на солнышко переводится в текстурное пространство облака и делается 8 семплов и текстуры плотности. Шаг семплирования фиксирован. Низкие же облака щейдятся с учетом atmospheric light scattering. Рендеринг большого облака, состоящего из множества маленьких, выполняется в отдельную тестуру. Затем полученное изображение уже переносится на основную картинку. Пример:
Целью данного подхода, думаю, было создание уникальной облачности, без сильной вычислительной нагрузки, ведь отрендериный кластер облака можно использовать несколько кадров подряд. А так как облако это медленно изменяющийся объект, то такой метод себя оправдывает.
Океан. Поверхность океана представляется высокотесселированным параллелепипедом нижняя плоскость которого лежит на уровне поверхности воды, а верхняя на уровне камеры (глаз) игрока. Всё как писал Мартин Миттринг.
Я не совсем понимаю как они манипулируют координатами вертексов так, чтобы при растеризации экранная Z координата совпадала с water plane. Несколько часов ломания головы и пересмотра шейдеров мне не помогло. :) Ну да ладно. При помощи vertex texture fetch генерируются волны. В пиксельном шейдере всё как обычно: reflection, refraction, отражение солнышка. Также для рефракшена реализована хроматическая аберация (подробно тут), т.е. различные углы преломления световых волн разной длины. Это достигается путем небольшого смещения текстурных координат для каждого из цветовых каналов. Прямо как при генерации каустики.
Теперь рассмотрим рендер типичного кадра по порядку. Отражения для воды. Упрощенные шейдеры. Далее подготавливаются shadow maps для теней от нужных источников. Я не буду останавливаться на тенях подробно, так как они описаны в первом документе, который я упомянул первым в начале. Скажу лишь, что всё что там написано - правда. :) Для directional light sources - cascaded shadow maps. Для точечных кубемапа в обычной 2d текстуре (хотя по шейдерам похоже, что нативные кубемапы тоже поддерживаются). Для террейна variance shadow maps. Для большего заблуривания применяется не summed area tables, a простой проход блура VSM карты. Вы уже, наверное, заметили что мало какой эффект обходится без camera space depth buffer. Он используется практически повсеместно. На этом шаге подготавливаем его. Запись ведется в текстуру формата R32F, параллельно с заполнением depth buffer, чтобы далее снизить depth complexity до 0. Следующим эффектом идет SSAO + Terrain AO описанные выше. После подготовки этой текстуры она блурится. Теперь настал черёд создания shadow mask для т.н. deffered shadowing. Тени мы рисуем в отдельный буфер, чтобы сэкономить инструкции в шедерах в основном проходе. Основной проход выполняется в текстуру формата RGBA16F, так как HDR - наше всё. Буфер глубины настроен только на тест, так что перетирания пикселей нет. Это очень важно с учётом применения огромных шейдеров. Дополнительным бонусом является отсутствие необходимости сортировки объектов по дальности. В основном проходе также применены интересные решения. Например для металлов применяются модели освещения Schlick и Сook-Torrance (для люознательных: демка RenderMonkey и небольшая доканебольшая дока. Подробное описание можно найти в книге Kelly Dempski, Emmanuel Viale. 2004. "Advanced Lighting andMaterials with Shaders". Wordware Publishing, Inc.). Для облегчения вычислительной нагрузки, части функций предрассчитаны и вынесены в текстуры. Все системы частиц, и облака, используют depth buffer для удаления артефактов пересечения геометрии. Отличный батчинг растительности. Ближние деревья и кусты рисуются маленькими батчами и детализированно, дальние же вырождены в screen aligned quads и выводятся большущими батчами:
Рендерингу растительности посвящена статья в GPU Gems 3. Советую обратить на неё внимание. Если вкратце, то анимация реализуется на вертексном шейдере с применением различных карт жесткости. Освещение листвы двусторонне, с применением subsurface scattering. Небо с абсолютно честным atmospheric light scattering. Есть вот такая симпатичная текстурка луны:
Террейн и некоторые элементы сцены рисуются в 2 прохода. Первым проходом идет базовый цвет, вторым же накладываются детальный рельеф, каустика, etc. В конце volumetric fog, опять же с использованием scene camera z текстурки. Получилось так:
После основного прохода из текстуры со сценой мы выделяем яркие фрагметны, которые мы блурим, для получения блум текстуры. Считаем среднюю яркость сцены для перевода HDR в LDR в 1x1 текстурку. Теперь пора применить всякие пост-процесс эффекты. Motion blur: Блурим картинку по направлению движения камеры с учетом дальности пикселя (опять используем scene z) Ближние будут блурится сильнее. Применяется коррекция ликов цвета с оружия.
Depth of field: Даунсемпл и блур изображения сцены. Затем простое смешивание с учётом глубины. И ещё всяких много: chroma shift, и radial blur когда колбасят; эффект мокрого экрана, когда из воды вылазишь, и т.д. Далее идёт tonemap resolve, т.е. перевод из HDR в LDR.
Но это ещё не всё! :) Есть ещё 1 красивый эффект - геренация light shafts. Метод похож на описанный в статье "Volumetric Light Scattering as a Post-Process" в GPU Gems 3. В текстурном пространстве пускаем луч в направлении солнышка. Двигаясь по лучу проверяем загораживет ли слонце какоё-то предмет, сравнивая Z с 1 (0 - near plane, 1 - far). Количество загороженых семплов и есть степень загороженности солнца предметами. Аттенюация по дальности также пристутсвует.
После 3-х таких проходов с различным шагом (чтобы убрать артефакты дискретного семплинга), получаем таку. картиночку:
Финальная композиция выполняется с применением т.н. Soft Light Blend Mode, аналогичного фотошоповскому. Данный метод позволяет осветлить одни участки изображения и затемнить другие. Почитать про различные режимы блендинга и их формулы можно тут: http://www.pegtop.net/delphi/articles/blendmodes/softlight.htm Финальное изображение:
Гуи. По элементику, по буковке ~100 draw calls. Рендер завершен.
А, там же ещё снег есть. :) Сильно его не ковырял. Он адванснутый. Основная идея: отталкиваемся от отношения вертикальной оси и нормали к поверхности для создания слоя. Остальное это создание нерегулярной его структуры и вида.
В кадре 2М триугольников, размер вертексов выравнивать не сильно старались. Draw calls доходит до 2000. Почти все текстуры в DXT сжатии. Нормали в BC5 (ATI2N). Собственно всё. Что хочется отметить, так это повсеместное использование camera z буфера. Почти во всех эффектах.
Хочется поблагадорить команду Кризиса за замечательный экспириенс, и Серегу kss за апдейт мегатулзы, без которой данное увлекательное приключение не состоялось бы. :)