Предисловие
Как и в других моих исследованиях, давайте начнём с введения. Сегодня мы рассмотрим последнюю игру французского разработчика Asobo Studio. Впервые я увидел видео этой игры в прошлом году, когда коллега поделился со мной 16-минутным геймплейным трейлером. Моё внимание привлекла механика «крысы против света», но мне не особо захотелось играть в эту игру. Однако после её выхода многие стали говорить, что она выглядит так, как будто сделана на движке Unreal, но это не так. Мне стало любопытно увидеть, как работает рендеринг и насколько вообще разработчики вдохновлялись Unreal. Ещё меня заинтересовал процесс рендеринга стаи крыс, потому то в игре она выглядела очень убедительно и к тому же является одним из ключевых элементов геймплея.
Когда я начал попытки выполнить захват игры, то подумал, что придётся сдаться, потому что ничего не срабатывало. Хотя игра использует DX11, который сейчас поддерживают практически все инструменты анализа, мне не удалось заставить работать ни один из них. Когда я пытался использовать RenderDoc, игра вылетала при запуске, и то же самое происходило с PIX. Я по-прежнему не знаю, почему так происходит, но к счастью, мне удалось выполнить несколько захватов с помощью NSight Graphics. Как обычно, я поднял все параметры до максимальных и начал искать подходящие для анализа кадры.
Разбивка кадра
Сделав пару захватов, я решил использовать для анализа кадра один из самого начала игры. Между захватами нет особой разницы, и к тому же я смогу избежать при этом спойлеров.
Как обычно, давайте начнём с окончательного кадра:
Первым, что я заметил, был совершенно иной баланс в этой игре событий рендеринга по сравнению с тем, что я видел в других играх ранее. Здесь множество вызовов отрисовки, что нормально, но на удивление лишь немногие из них используются для постобработки. В других играх после рендеринга цветов для получения окончательного результата кадр проходит ещё много этапов, но в A Plague Tale: Innocence стек постобработки очень мал и оптимизирован всего до нескольких событий отрисовки/вычислений.
Игра начинает построение кадра с рендеринга GBuffer с шестью render targets. Интересно, что это все render targets имеют 32-битный беззнаковый целочисленный формат (за исключением одного) вместо цветов RGBA8 или других специфичных для таких данных форматов. Это представляло сложность, потому что мне приходилось декодировать каждый канал вручную с помощью функции Custom Shader из NSight. Я потратил много времени на выяснение того, какие значения закодированы в 32-битные targets, но не исключено, что всё равно что-то упустил.
GBuffer 0
Первый target содержит в 24 битах некие значения шейдинга, а в 8 битах — какие-то другие значения для волос.
GBuffer 1
Второй target выглядит как традиционный RGBA8-target с разными значениями управления материалом в каждом канале. Насколько я понимаю, красный канал — это metalness (не совсем понятно, почему ею помечены некоторые листья), зелёный канал выглядит как значение roughness, а синий канал — это маска главного персонажа. Ни в одном из сделанных мной захватов альфа-канал не использовался.
GBuffer 2
Третий target тоже выглядит как RGBA8 с albedo в каналах RGB, а альфа-канал в каждом сделанном мной захвате был полностью белым, так что я не совсем понимаю, что эти данные должны делать.
GBuffer3
Четвёртый target любопытен, потому что на всех моих захватах почти полностью чёрный. Значения выглядят как маска части растительности и всех волос/меха. Возможно, это как-то связано с просвечиванием (translucency).
GBuffer 4
Пятый target — это, вероятно, некая кодировка нормалей, потому что я не видел их нигде больше, а шейдер, похоже, сэмплирует карты нормалей, а затем выполняет вывод в этот target. С учётом этого, я не разобрался, как их правильно визуализировать.
Глубина из GBuffer 5
Маска из GBuffer 5
Последний target является исключением, потому что он использует 32-битный формат с плавающей запятой. Причина этого заключается в том, что он содержит линейную глубину изображения, а знаковый бит кодирует какую-то другую маску, снова маскирующую волосы и часть растительности.
После завершения создания GBuffer разрешение карты глубин снижается в вычислительном шейдере, а затем рендерятся карты теней (направленные каскадные карты теней от солнца и кубические карты глубин от точечных источников освещения).
Сумеречные лучи
После завершения карт теней можно вычислить освещение, но прежде в отдельный target рендерятся сумеречные лучи (god rays).
SSAO
На этапе вычисления освещения выполняется вычислительный шейдер для расчёта SSAO.
Освещённая непрозрачная геометрия
Освещение добавляется из кубических карт и локальных источников освещения. Все эти разные источники освещения в сочетании с отрендеренными выше targets в результате формируют освещённое HDR-изображение.
Элементы, отрисовываемые упреждающим рендерингом
Отрисовываемые упреждающим рендерингом элементы добавляются поверх освещённой непрозрачной геометрии, но в этой сцене они не особо заметны.
После накопления всего цвета мы почти закончили, осталось только несколько операций постобработки и UI.
Разрешение цвета снижается в вычислительном шейдере, а затем увеличивается для создания очень красивого и мягкого эффекта bloom.
После композитинга всех предыдущих результатов, добавления грязи камеры, цветокоррекции и наконец тональной коррекции изображения мы получаем цвета сцены. Наложение UI даёт нам изображение из начала статьи.
Стоит упомянуть пару интересных вещей, касающихся рендеринга:
- Instancing (дублирование геометрии) используется только для отдельных мешей (похоже, что только для растительности). Все другие объекты рендерятся в отдельных вызовах отрисовки.
- Похоже, объекты приблизительно сортируются спереди назад, за некоторыми исключениями.
- Кажется, разработчики не прикладывали никаких усилий для группирования вызовов отрисовки с точки зрения параметров материалов.
Крысы
Как я говорил в начале статьи, одной из причин, по которым я хотел исследовать эту игру, был способ рендеринга стаи крыс. Решение меня в чём-то разочаровало: похоже, оно сделано методом грубой силы. Здесь я использую скриншоты из другой сцены игры, но, надеюсь, в ней нет никаких спойлеров.
Как и в случае с другими объектами, для крыс, похоже, не выполняется никакого дублирования геометрии, за исключением случая, когда мы достигнем расстояния, на котором переключаемся на последний уровень детализации меша (LOD). Давайте посмотрим, как это работает.
LOD0
LOD1
LOD2
LOD3
У крыс есть 4 уровня LOD. Интересно, что на третьем уровне хвост загнут к телу, а у четвёртого хвоста вовсе нет. Вероятно это означает, что анимации активны только для первых двух уровней. К сожалению, у NSight Graphics, похоже, не хватает инструментов, чтобы это проверить.
Без дублирования (instancing) крыс.
С дублированием.
В сцене, захват которой показан выше, отрендерено следующее количество крыс:
- LOD0 – 200
- LOD1 – 200
- LOD2 – 1258
- LOD3 – 3500 (с дублированием геометрии)
Это даёт нам понять, что существует жёсткое ограничение на количество крыс, которых можно отрендерить на первых двух LOD.
В сделанном мной захвате я не смог выявить никакой логики, привязывающей крыс к отдельным LOD. Иногда крысы, расположенные ближе к камере, не очень детализированы, а иногда едва видимые крысы имеют высокую детализацию.
В заключение
Plague Tale: Innocence очень интересна с точки зрения рендеринга. Его результаты без сомнений меня впечатлили, они очень хорошо служат геймплею. Как и в случае с любым проприетарным движком, было бы здорово услышать более подробный анализ из уст самих разработчиков, особенно потому, что мне не удалось подтвердить некоторые из моих теорий. Надеюсь, моя статья когда-нибудь доберётся до кого-нибудь из Asobo Studio и они увидят, что у людей есть к этому интерес.
Автор: PatientZero