После выпуска God of War на PC я был просто обязан попробовать её. Я играю с 90-х годов, поэтому совершенно не против снова и снова проходить игру с линейным сюжетом. В этом есть большая ценность. На данный момент моей самой переигрываемой линейной игрой является Max Payne (первая часть), я целиком проходил её на разных платформах более 15 раз. Но всё равно чаще всего я играю на PC. Когда я впервые играл в GoW на PS4, то надеялся приобрести её для PC, мне нравится коллекционировать игры и ачивки Steam, поэтому благодаря выпуску игры для компьютера я решил снова к ней вернуться. Но на этот раз я решил прихватить с собой рабочие инструменты, ведь мне было очень любопытно, как устроен движок/рендерер этой игры.
В этом нет ничего нового для меня, я переигрываю в любимые игры и изучаю их технологии; насколько я помню, этот синдром появился с Max Payne 3, и с тех пор я много раз я пробовал делать это с другими играми, но, к сожалению, так никогда и не завершил ни одной статьи. Я выполняю захваты кадров, рассматриваю их, делаю какие-то заметки, начинаю сохранять скриншоты, а потом бах! — и начинаю заниматься чем-то другим. Последними двумя жертвами моей лени стали The Medium и Halo Infinite, возможно, я когда-нибудь их закончу, выпущу черновик поста и освобожу кучу места, которую занимают их файлы! Однако с GoW получилась совершенно иная история: как только я получил письмо со Steam о выпуске игры, я решил переиграть её, по крайней мере, ради ачивок, а также на этот раз завершить анализ рендеринга.
Возможно, потом выйдет вторая часть этой статьи, потому что из первого прохождения на PS4 мне запомнились несколько интересных моментов, но они слишком далеко по сюжету, и чтобы добраться до них, потребуется время. Сначала (в этой статье) мы проанализируем и обсудим игру в целом, общие идеи и функции, используемые и применяемые во всей игре, то, что встречается практически в каждом кадре во время примерно двадцатичасового прохождения. А затем (во второй части, которая неизвестно когда выйдет) мы разберём уникальные элменты, которые используются только в некоторых частях игры.
Конфигурации
У меня есть пара PC, и захваты кадров были выполнены с RTX 3070 и RTX 3080, а все параметры графики установлены на Ultra. Тем не менее, захваты на PC с процессором AMD всегда давались с трудом, по какой-то причине GoW часто вылетала, если я пытался выполнять захваты GPU любыми способами на PC с AMD, а на PC с Intel i9 всё было совершенно стабильно. Однако на всякий случай я хотел выполнить несколько захватов отдельных частей игры. Но главное здесь то, что я играл на параметрах Ultra, что может привести к отличающимся результатам. Кроме того, игра запускалась с разрешением 1920*1200 и отключенным HDR.
Введение
Как ни удивительно, я выяснил, что GoW на PC работает на основе D3D11, что мне показалось очень странным и совершенно неожиданным выбором. Я понимаю, что изначально игра была выпущена в 2018 году, и в то время ни D3D12, ни VK не были распространённой целевой платформой, но для игры на основе GNM/GNMX это не имело значения. Да, игру выпустили 4 года назад, но саму разработку игры начали, вероятно, 4-5 годами ранее, и, как я всегда говорю, у любого консольного эксклюзива есть версия, работающая на PC, потому что именно на PC она разрабатывалась и писалась! Возможно, редактор консольной версии GoW PS4 2018 года был основан на D3D11 в Windows, а потому было принято решение оставить всё как есть и использовать эту версию как основу для порта под Windows. В конечном итоге, разработка на D3D12 или VK потребовала бы в три раза больше времени, если не больше. Однако, если честно, я всё равно ожидал, что порт на PC 2022 года будет работать на VK API, но имеем то, что имеем. Надеюсь, на ближайшей GDC разработчики подробно расскажут о том труде, который Santa Monica Studio вложила в этот порт. Посмотрим…
За кадром
Показанные в статье скриншоты сделаны примерно по трём разным кадрам, а не по одному. Все эти кадры идентичны с точки зрения внутреннего устройства, но я решил сделать так, чтобы всё было понятнее при их объяснении. Например, при обсуждении частиц я буду ссылаться на кадр, в котором сделан упор на частицы, а при обсуждении снега я буду ссылаться на кадр, в котором сделан упор на снег. Но в большинстве случаев я буду ссылаться на любимый мной кадр, вынесенный в начало статьи. Вот как рендерится типичный кадр GoW в порядке выполнения.
1.Ветер (Compute)
Похоже, ребята из Santa Monica очарованы вычислительными шейдерами, и я не могу их винить, учитывая ту магию, которой очень быстро можно добиться с их помощью. Я не удивлён, что в такой крайне красивой игре активно и с умом используются вычислительные шейдеры. На самом деле, мне кажется, что не только Santa Monica, но и все остальные студии Sony первыми начали использовать вычислительные шейдеры (и стали в этом лучшими), добиваясь на домашних консолях самых потрясающих результатов; Naughty Dog, Santa Monica, Insomniac, Sucker Punch и т. п. Все они в последние годы, с момента выпуска PS4, демонстрировали великолепные техники на основе вычислительных шейдеров.
Вызов первого вычислительного шейдера в типичном кадре GoW выполняется до любых команд отрисовки, да и вообще до всего остального. Кадр в буквальном смысле начинается с вызова вычислительного шейдера, и этот первый шейдер всегда выполняет симуляцию ветра, как будто всё остальное в кадре будет учитывать текущее состояние кадра для симуляции ветра, и, по моему мнению, это действительно так. В дальнейшем мы увидим это по порядку выполнения действий. Волосы, частицы, растительность/листва, ткани, источники звука и другие мелкие элементы геймплея учитываю результат симуляции ветра, а ветер в GoW очень сложен. Рекомендую посмотреть доклад на эту тему Руперта Ренарда (Rupert Renard) с GDC 2019. Хоть я и не буду подробно рассказывать о том, как симулируется ветер, но объяснение того, как используются данные для симуляции ветра, может помочь вам разобраться в основах этой техники. Эта симуляция в вычислительном шейдере манипулирует следующими данными ветра:
Данные | Тип | Размер/габариты | Формат/компоненты |
---|---|---|---|
Скорость | 3D-текстура | 32*32*16 (16 слайсов) | R16G16B16A16_FLOAT |
Турбулентность | 3D-текстура | 64*32*64 (64 слайса) | R10G10B10A2_UNORM |
Экземпляры листьев | Структурированный буфер | Переменный размер | struct WindLeaveInst { float3 m_vPosWS; uint m_paramIdx; float3 m_vVelWS; float m_fGeoScale; } |
Параметры листьев | Структурированный буфер | Переменный размер | struct WindLeaveParam { float m_fHighSpeed; float m_fLowSpeedDensity; float m_fHighSpeedDensity; float m_fWeightScale; float m_fBend; uint m_eRotationMode; float m_fStretchiness; float m_fUseWindAtPivot; float m_fSwaySpring; float m_fSwayDamping; uint m_eTreeMode; float m_fTreeBend; float m_fTreeWeightScale; float m_fTreeHeight; float m_fTreeLeafLag; float _unused0; } |
Состояние листьев | Структурированный буфер | Переменный размер | struct WindLeaveState { float3 m_vOffsetA; float m_fPhaseLerp; float3 m_vOffsetB; uint m_uIsReturnPhase; float3 m_vLeafSway; float3 m_vLeafSwayMomentum; float3 m_vTreeSway; float3 m_vTreeSwayMomentum; float3 m_vWindVecSmooth; float m_fPad0; } |
Данные турбулентности | Структурированный буфер | Переменный размер | struct TurbulenceData { float3 leafSwayLag; float bend; float3 offsetA; float useWindAtPivot; float3 offsetB; float phaseLerp; float3 densityLevel; float stretchiness; float3 densityLevelBlend; float treeBend; float3 rotationPivotMask; float treeHeight; float3 treeSway; float treeWeightScale; float3 leafSway; float leafWiggleScale; float windAdjustScale; float swaySpring; float swayDamping; float pad0; int _debugNoNoise; float _debugUserParam1; float _debugUserParam2; float padDebug; } |
Ядром симуляции ветра являются 3D-текстуры, описывающие 3D-объём/сетку в игровом мире. Один слайс (срез) такой 3D-текстуры выглядит примерно так:
Каждый пиксель этого среза задаёт float3, которые являются или значением турбулентности, или значением скорости в соответствующей ячейке сетки мира. Ячейка сетки или её описание как пикселя в 3D-текстуре имеет в игровом мире размер 1 м3. Выше показано, как текстура выглядит в GPU и как её считывает игра, но чтобы вам было видно её чётче, я покажу этот срез 3D-текстуры в увеличенном виде:
Но в целом, почти каждый ресурс для этого вычислительного шейдера создаётся со включенными флагами D3D11_BIND_SHADER_RESOURCE и D3D11_BIND_UNORDERED_ACCESS. Более подробную информацию о том, как работает математика симуляции ветра в вычислительном шейдере и, возможно, фрагменты кода, см. в докладах GDC, ссылки на которые я привёл в конце статьи.
2. Частицы на GPU (Compute)
Вторая группа вызовов вычислительных шейдеров тоже выполняется до команд отрисовки (как и в случае с ветром) и сразу после вычислительного шейдера симуляции ветра (что логично, ведь всё остальное зависит от результатов симуляции ветра). Эта группа занимается симуляцией частиц на GPU, и похоже, что почти все частицы, которые мне удалось найти в 100 ГБ захватов GPU были эмиттерами GPU. Вычислительный шейдер GPU для симуляции систем частиц зависит от сложности текущего кадра с точки зрения систем/эмиттеров частиц и вариаций их типов. Пока мне удалось найти от одного до максимум трёх разных вызовов групп вычислительных шейдеров для частиц на GPU. Обычно в GoW на экране всегда присутствуют частицы, даже если вы их не видите! Даже в главном меню (что логично, ведь его UI рендерится поверх 3d-карты) или в геймплейном меню модификации брони или апгрейдов дерева навыков вызовы вычислительных шейдеров частиц на GPU выполняются ВСЕГДА!
Частицы в этих вычислительных шейдерах почти всегда берут данные из предыдущего кадра, как будто им недостаточно уже имеющейся информации. Весь GBuffer предыдущего кадра используется как входящие данные для вычислительного шейдера частиц на GPU, и это значит, что у нас есть доступ к глубине, базовому цвету, нормалям, свойствам (AO, metallic, roughness/gloss), рассеянию, излучению предыдущего кадра. Не совсем понимаю, зачем всё это передаётся вычислительному шейдеру, наверно, стоило бы ждать, что для выполнения симуляции частиц на GPU достаточно глубины и, возможно, нормалей, но могут быть и другие способы применения, требующие информации о цвете и свойствах.
GBuffer предыдущего кадра, полностью переданный вычислительному шейдеру
Ещё одним элементом, необходимым для частиц на GPU, являются данные ветра. Вычислительный шейдер ветра, выполнявшийся ранее, выводит текстуру объёма, которая тоже требуется частицам. Ниже показаны изображения всех 16 слайсов текстуры объёма 32*32*16 из вычислительного шейдера ветра.
Наряду с GBuffer и ветром вычислительному шейдеру передаются и другие структуры, содержащие все свойства систем частицы, а также описание задания по испусканию частиц на GPU. В этой статье мы не будем рассматривать, что происходит с этими значениями в вычислительном шейдере, но я приведу краткое описание задаваемых данных и того, что необходимо для выполнения симуляций.
Данные частиц
Данные | Тип | Размер/габариты | Формат/компоненты |
---|---|---|---|
Описание задания по испусканию частиц на GPU | Массив структурированного буфера Максимум 5000 элементов |
Переменный размер | struct ParticleGPUEmissionJob { uint batchIndex; uint systemIndex; uint count; uint dstOffset; uint srcOffset; uint emitterType; uint interpType; float emitStart; float emitInterval; float batchAge; float timeStepInv; float quadInterpStart; float initialDistance; float frameDistance; uint decayEmitRandomStart; uint decayMaxCount; uint decayEmissionBufferOffset; uint decayAtomicCounterIndex; float decaySampleSpread; uint dupBatchIndex; uint dupCount; float dupAgeDiff; uint cFlags; float3 cDirection; float cSpeed; float cSpeedRandom; float cSpread; float cSpreadRandom; float cDistanceMin; float cDistanceRange; float cVelAwayFromCenter; float cVelAwayFromAxis; float cVelAlongAxis; float cVelDirectionalSpeed; float cVelAroundAxis; float cVelRandomDirection; float cVolumeSweep; float cSectionRadius; float3 emitPosition0; float3 emitPosition1; float3 emitPosition2; float3x3 emitOrientation0; float3x3 emitOrientation1; float3x3 emitOrientation2; float3 emitVelocity0; float3 emitVelocity1; uint _pad0; uint _pad1; } |
Данные системы частиц | Массив структурированного буфера Максимум 3000 элементов |
Переменный размер | struct ParticleGPUFieldData { float3 position; float attenuation; float3 direction; float maxDistance; float magnitude; float _pad0; float _pad1; float _pad2; } struct ParticleGPUAttributeData { float start; float end; uint type; float alpha; float jitter; xint pad[3]; } struct ParticleGPUSystemData { float4x4 cSystemToWorld; uint cameraID; uint cDrawArgStart; float4 textureFlipbookVecs[10]; float3 cSpriteCenter; float3 cSpriteSize; float3 cSpriteSizeRandom; float2 cUVScale; float2 cUVOffset; float2 cUVScroll; uint cRibbonUVMode; float cTimeStep; uint cFlags; float cLifeTime; float cLifeTimeRandom; float cFogDensityStrength; float cAge; uint systemRandom; uint cAlignMode; uint cSecondaryAlignMode; uint cBlendMode; float cAngleFadeRate; float cSoftDistance; float cSoftDistanceInverse; float cVelocityScale; float cVelocityScaleClamp; float cExternalScale; float cSpriteRotation; float cSpriteCameraOffset; float cStaticTwist; float cTwistSpeed; uint cTwistDirection; float cMaxExtent; float cColorModulate; float cColorRandom; ParticleGPUAttributeData cParticleColorBlend; float4 cColorTintModulate; ParticleGPUAttributeData cNormalMapIntensity; ParticleGPUAttributeData cOpacity; ParticleGPUAttributeData cOpacityTexturesExponent; ParticleGPUAttributeData cScale; float cCameraScaleStartDistance; float cCameraScaleStartValue; float cCameraScaleEndDistance; float cCameraScaleEndValue; float cCameraOneOverEndMinusStart; uint cFlipbookPageCount; float cFlipbookPageCycle; float cNearCull; float cFarCull; float cNearFade; float cFarFade; ParticleGPUAttributeData cDrag; float3 cGravity; ParticleGPUAttributeData cGravityIntensity; ParticleGPUAttributeData cWindInfluenceOverLifeTime; float cMinDistance; float cMaxDistance; float3 cTurbulenceFrequency; float3 cTurbulenceAmplitude; float3 cTurbulencePhase; float3 cTurbulenceScrollSpeed; ParticleGPUAttributeData cTurbulenceIntensity; uint cActiveFields; ParticleGPUFieldData cFields[4]; float cNewtonMinDistance; float cCollisionBounciness; float cCollisionBouncinessRandom; float cCollisionRadius; float cDepthCollisionAgeThreshold; float cDepthCollisionThreshold; float cPlaneCollisionHeight; float3 emitterPosition; float3 emitterVelocity; float3 velocityDirectionWorld; float3x3 emitterOrientation; float emitterDistance; float cMFXEventProbability; uint4 ramp1Indices[2]; uint4 ramp3Indices; uint4 ramp1Constants[2]; float4 ramp3Constants; } |
Данные линейных изменений | Массив структурированного буфера Максимум 4096 элементов (не точно) |
Переменный размер | struct ParticleGPURamp1Data { float multiplier; float offset; } |
Частицы целиком обрабатываются GPU, а вся симуляция выполняется в вычислительном шейдере, но задействована только небольшая часть систем частиц, поэтому они называются CPU-эмиттерами. CPU-эмиттер — это глобальные данные, которые могут передаваться между системой частиц GPU и остальной частью игры (которая необязательно получает доступ к данным GPU). Этот небольшой фрагмент данных CPU систем частиц выглядит следующим образом:
Данные CPU-эмиттера
Данные | Тип | Размер/габариты | Формат/компоненты |
---|---|---|---|
Частицы CPU-эмиттера | Массив структурированного буфера Максимальное количество неизвестно! |
Переменный размер | struct ParticleGPUData { float3 position; float3 velocity; float3 axis; uint _pad0; uint _pad1; uint _pad2; } |
После того, как совершится магия вычислительного шейдера, мы получаем набор позиций частиц/данных преобразований. С наибольшей вероятностью они хранятся в виде ramp-текстуры, в которой каждый пиксель содержит информацию одной текстуры (по моим предположениям). Эта текстура выглядит примерно так:
В конце, когда приходит время отрисовки прохода цвета (раздел под номером 5 ниже), эта ramp-текстура данных преобразований частиц используется для отрисовки частиц/четырёхугольников (DrawIndexedInstancedIndirect) в соответствующих позициях, переданных вычислительным шейдером.
Хотя я сказал, что отрисовка выполняется в разделе 5, это не единственный момент, когда мы отрисовываем частицы. Похоже, дополнительные частицы отрисовываются почти в конце кадра (перед постобработкой), но эта поздняя отрисовка выполняется для частиц со значениями излучения. То есть это можно назвать проходом излучения.
Как говорилось выше, процесс отрисовки выполняется в виде четырёхугольников, но эти четырёхугольники обычно хорошо текстурированы с красивыми нормалями, пороговыми значениями альфы, текстурами искажений (разные облачные текстуры для разных вариаций), благодаря чему они выглядят живыми и кажутся трёхмерными.
Примеры пороговых значений альфы
Примеры нормалей
Более подробно узнать о том, как вычислительный шейдер использует эти данные для симуляции частиц на GPU, или о том, какие новые функции создала Santa Monica Studio, можно из доклада на GDC, ссылка на который есть в конце статьи.
3. Меш снега
Обычно в подобных играх первая команда отрисовки занимается рельефом, но в данном это немного не так. Похоже, первые несколько команд отрисовки обрабатывают слой покрытия снегом рельефа, а не сам рельеф. Чтобы вам проще было понять, я использовал для этого другой захват. Я пытался двигать дядьку Кратоса вперёд и назад, чтобы получилась буква M, которая оказалась больше похожей на E.
Отрисовка меша снега
Сначала отрисовывается меш снега, он выглядит как заранее созданный фрагмент меша, представляющий область, которая будет покрыта деформируемым снегом. В виде сверху меш снега выглядит примерно так, как показано ниже. Разумеется, для каждой области есть свой собственный фрагмент меша. Думаю, дырка в центре — это место, где находится дом Кратоса.
В этом меше для высот (height) и карт потоков (flowmap) используется набор текстур BC5_UNORM и BC6_UFLOAT, напоминающих процедурные облака.
Примечание: текстура Flowmap 1*1 используется много раз; вероятно, здесь она применяется как кисть для показанного ниже процесса рисования, но позже точно такая же текстура повторно используется для множества других вещей, например, теней и глобального освещения (GI). То есть она не применяется исключительно для затенения меша снега.
Внеэкранная отрисовка (смешение фрагментного и вычислительного шейдера)
После того, как меш готов, начинаются деформации снега, выполняемые в нескольких последовательных проходах рендеринга. Render target деформации снега отрисовывается в зависимости от позиции игрока (если он находится на покрытой снегом земле). Отрисовки выполняются в render target R16G16B16A16_FLOAT странного размера, почему-то 384*384!
А затем эта текстура 384*384 (на изображении со сплошным чёрным цветом) передаётся вычислительному шейдеру для применения высот к пути, нарисованному Кратосом (на основании центра отрисовываемых штрихов), поэтому мы можем использовать эти высоты для правильной деформации меша, не создавая слишком резких краёв. Окончательный render target деформации (на изображении с жёлтым цветом), созданный вычислительным шейдером — это R16G16_FLOAT размера 1024*1024.
Это вызвало у меня вопрос: почему бы не использовать одинаковый формат для отрисовки render target, зачем применять 4 канала, если в конечном итоге требуется только два? На самом деле отрисовка render target заполняет только канал R, оставляя GBA полностью чёрными. Я бы сказал, что это подходящая область для оптимизации.
Ранее, когда мы говорили о текстуре flowmap 1*1, я сказал, что она «вероятно» используется для RT деформации снега. «Вероятно», потому что я заметил, что существует целый проход цвета с набором отрисовок простых сфер (сжатых и растянутых), применяемых к позициям Кратоса и они могут использоваться для отрисовки этих RT деформаций (первое красно-чёрное изображение). Но в то же время, согласно одному из докладов на GDC, Santa Monica помещала сферы на пути Кратоса чтобы влиять на растительность. Так что возможно, эти сферы имеют двойное применение.
После завершения вычислительного шейдера всё остальное — это обычная отрисовка в меш с использованием набора текстур для добавления деталей и случайности к пути игрока по мешу снега.
Как говорилось в начале раздела, деформация снега выполняется за несколько последовательных проходов рендеринга. Это три прохода цвета. Чтобы не путать, вот как они применяются по порядку исполнения:
– первый проход цвета выполняет отрисовку пути игрока на RT деформаций
– второй проход цвета выполняет обновление RT деформаций добавлением значений «высот»
– третий проход цвета используется для отрисовки этой текстуры высот RT (её проецирования) в экранное пространство в меше деформации снега. Вот как выглядят результаты этих трёх проходов:
Стоит упомянуть, что процесс деформации снега задействует адаптивную тесселяцию этого участка геометрии. И в процессе выполнения тесселяции точность меша становится выше, если мы видим или используем соответствующую область. Вот пример двух захватов одной и той же области, в каждом из случаев применяется разное количество тесселяции меша снега на основании расстояния до камеры на момент захвата.
И прежде чем мы завершим обсуждение снега, надо сказать, что в любой другой игре со снегом он будет реализован так. Однако мне удалось найти ядро техники, делающей снег GoW действительно отличным — это использование параллакса в экранном пространстве (Screen Space Parallax) (с использованием результата третьего, жёлтого, прохода рендеринга цвета).
Именно секретный ингредиент параллакса делает снег таким красивым! Вы с лёгкостью можете найти визуальные артефакты/проблемы параллакса, если повернуть камеру под странными углами, которые обычно можно получить в режиме фотографии, но не при прохождении.
4. Глубина
Стандартная отрисовка всей геометрии в D24S8_TYPELESS. Ничего выдающегося.
И, разумеется, элементы наподобие частиц не включаются в проход только глубин и пока мы только вычислили симуляцию частиц GPU, но ещё их не отрисовывали. Частицы будут отрисовываться на очень поздних этапах создания кадра и для их отрисовки всё равно нужна глубина, поэтому вполне логично, что они отсутствуют в буфере глубин/стенсил-буфере. Из другого захвата, в котором есть частицы GPU, отображение только глубины выглядит так:
5. Цвет
Рендеринг в GoW выполняется по схеме Deferred+, то есть рендеринг будет проходить несколько известных фаз. У нас есть два основных прохода цвета, а также пара промежуточных вычислительных шейдеров.
Основной проход цвета (5 RT + глубина)
Раздел ещё не доработан. В конце этого прохода мы отрисовываем системы частиц, как сказано выше в разделе 2. Это неизлучающие частицы, а также частицы-меши.
Векторы движения (вычислительный шейдер)
Раздел ещё не доработан.
Освещение Deferred+ (вычислительный шейдер)
Раздел ещё не доработан.
Декали (4 RT + глубина)
Для этого прохода цвета мы вносим незначительные модификации в то, что получили от предыдущего прохода. Наверно, самое примечательное здесь (я использую другой захват, чтобы показать это) заключается в том, что мы выполняем ещё один слой деформаций снега, но на этот раз он не такой сложный и не задействует вычислительные шейдеры, только декали. Здесь декали снега имеют множество разных видов, некоторые используются для добавления оттенка (цвета) снега по краям объектов.
Есть и другие, проецируемые на нормали мира при помощи текстуры карт нормалей, например, следы ног (они бывают разные, наверно, в зависимости от размеров персонажа) на проходимых заснеженных поверхностях.
Разные текстуры декалей для разных персонажей и случаев применения:
Есть декали, располагаемые художниками и дизайнерами уровней при загрузке уровня/области. Например, это следы оленей, которые сразу есть в игре. Но есть и декали, размещаемые в процессе игры, например, следы Кратоса и Атрея. А в целом все декали проецируются при помощи простых кубических мешей и отрисовываются под одному на результатах предыдущих проходов (цвета, нормалей и т. д.).
Где мой СЫН?
Прежде чем закончить с декалями, нужно упомянуть ещё одну вещь. Следы оленя отрисовываются как DrawIndexedInstanced, а декали Кратоса и Атрея — просто как DrawIndexed, и так происходит со многими декалями, что не имеет никакого смысла, ведь у оленя есть только одна декаль, может быть несколько на всей карте, но следы игрока и NPC встречаются повсеместно, разве не было бы лучше тоже создавать их инстансы?
6. SSAO + GI (вычислительный шейдер)
Раздел ещё не доработан.
7. Излучение (и поздние частицы)
Ближе к концу кадра всегда есть три прохода рендеринга цвета. Первый используется для глобального излучения, а два других отрисовывают дополнительные (излучающие) частицы. Каждый из этих трёх проходов состоит из 1 Target + глубины. Находитесь ли вы в главном меню, бродите по миру или сражаетесь, этих проходов всегда три и они всегда ближе к концу кадра. (Возможно, я изменю своё мнение, когда доберусь до Хельхейма и сделаю захваты там.)
Глобальное излучение (1 проход рендеринга)
Раздел ещё не доработан.
Излучающие частицы (2 прохода рендеринга)
Раздел ещё не доработан.
8. Постобработка
Цветокоррекция, тональная коррекция, Bloom и размытие (фрагментный шейдер)
Этот шейдер получает версию кадра в половинном разрешении и применяет размытие с 16 сэмплами, а также bloom.
Раздел ещё не доработан.
Временное сглаживание (вычислительный шейдер)
Раздел ещё не доработан.
Чтобы понять, какая постобработка применяется, кроме описанной выше, или даже разобраться с подробностями упомянутой мной обработки, можете взглянуть на данные, передаваемые фрагментному шейдеру.
Данные постобработки
Глобальные данные | Тональная коррекция | DoF | TAA |
---|---|---|---|
Константы { float MotionBlurEnabled, float DebugBlur, float DebugBlurEffect, float NoOpaquePass, uint DebugTakingCalibrationScreenshot, float DebugMotionVectors, float BloomEnabled, float2 screenSize, float2 screenSizeRcp, float2 screenHalfSize, float2 screenHalfSizeRcp, float2 screenSizeResolved, float2 screenSizeResolvedRcp, float2 screenResolvedToUnresolved, float2 screenUnresolvedToResolved, float2 motionVectorTemporalRescaleCompress, float2 motionVectorTemporalRescaleDecompress, float2 dlssScreenSize, float2 screenScale, float2 screenOffset, float4 scaleOffsetR, float4 scaleOffsetG, float4 scaleOffsetB, float3 Vignette_Color, float Vignette_Brightness, float Vignette_Falloff, float Vignette_Scale, float lensVignetteExposureSq, float scale,”0.96799″,float float rcpScreenLensRadius2, float2 screenLensCenter, int filmGrainOffset, float4 filmGrainUVTransform, float filmGrainEffectShadows, float filmGrainEffectMids, float filmGrainEffectBrights, float doUnsharp, float unsharpStrength, bool DebugNoLUT, bool passThrough, bool passThrough_Exposure, bool passThrough_WhiteBalance, bool passThrough_SLog } |
toneMappingConstants { float3 WBMatrixR, float3 WBMatrixG, float3 WBMatrixB, float EnableLocalAdaptation, float Adaptation_Exposure, float ExposureEV, float ExposureEVClampMin, float ExposureEVClampMax, float LocalAdaptationShadows, float LocalAdaptationHighlights, int Tonemapping_Curve, float Contrast, float HDRMax, float HDRWhite, float ContrastTimesShoulder, float ChannelCrossTalk, float PrecomputedBMult, float PrecomputedCAdd, float SceneToScreenPower, float IsHDRRendering, float DebugHDRRendering, float DebugHDRRenderingPhase, float DebugNoHDRRendering, float ShowWaveform } |
dofConstants { float m_Enabled, float m_DebugDOF, float m_NearDistanceOfAcceptableSharpness, float m_FarDistanceOfAcceptableSharpness, float m_CircleOfConfusionMultiplier, float m_CameraApertureHeightRelativeRcp, float m_HyperFocalDistance, float m_FocusDistance, float m_FStop, float m_DepthReprojectScale, float m_DepthReprojectBias } |
temporalConsts { float TAAConvergenceLimit, float TAAHP, float TAALP, float TAAColorExtent, float TAACheckerboardFramePhase, float TAAMotionRejection, float TAAEnable, float HalfResTAAEnable, float HalfResTAAConvergence, uint TAAForceSmooth, float TAATransparentsContributionRejectionMultiply, float TAAVarianceBlurIncrease, float TAAVarianceTransparentDecrease, float TAAEncodeRange, float TAAInvEncodeRange, uint DebugSimilarity, float VarianceWindow, float2 UpsampleOffset } |
9. Постобработка 2 (на проходе рендеринга UI)
Хитрый момент: постобработка выполнятся в паре проходов. Хотя большой объём постобработки выполняется на предыдущем этапе, в этой фазе и непосредственно перед добавлением UI в кадр начинается новый проход рендеринга; этот проход рендеринга может выполняться и для UI, и для небольшой доли постобработки. С выходными данными TAA работает небольшой фрагментный шейдер, добавляющий эффект зерна плёнки, виньетирование и т. п. Также если применяется DoF, то это самый подходящий для него момент, насколько я могу понять из данных, передаваемых на этом этапе шейдеру.
Если бы мы добавили такие элементы, как эффект зерна плёнки до TAA, то результат TAA ослабил бы эффект зерна плёнки, и поэтому его нужно применять после TAA, и после завершения TAA в вычислительном шейдере нам нужен ещё один проход рендеринга цвета, чтобы использовать результат TAA для применения оставшейся постобработки, например, зерна плёнки. В то же время может быть неоптимально использовать для этого целый новый проход рендеринга, поэтому, насколько я понял, разработчики решили применить оставшуюся постобработку в том же проходе, когда отрисовывается UI. Это сделано просто чтобы снизить затраты на запуск и завершение отдельного прохода рендеринга, предназначенного только для зерна плёнки.
Увеличим, чтобы эффект был заметнее:
И, разумеется, это реализуется при помощи простейшей в мире одноканальной текстуры зерна 512*512 BC4_UNORM.
В конце этой статьи я привёл две ссылки на интересные и глубокие статьи Тимоти Лоттса и Барта Вронски, в которых описываются техники тональной коррекции, по моему мнению, использованные в GoW.
10. UI
В UI этой игры мне нравится то, что почти всё (за исключением, пожалуй, только логотипа игры) сделано так, как я люблю. Я всегда любил, чтобы элементы UI состояли из оттенков серого, это упрощает их настройку и подгонку под вкусы арт-директора. Я видел, что в некоторых играх для конкретных элементов UI используются конкретные цвета, что мне не особо нравится. Почти каждый элемент UI в GoW является BC4_UNORM (то есть единственным каналом R).
Отрисовка UI — это последовательность DrawIndexedInstanced для прямоугольников во всех размерах в соответствии с требованиями UI. К концу этого прохода рендеринга в кадр будет добавлен весь UI; тот же проход рендеринга используется для описанной выше постобработки, то есть один проход используется для двух целей, постобработки + UI.
11. Гамма-коррекция
В этом последнем вызове до перехода к цепочке буферов есть простой фрагментный шейдер применяющий нужную гамму (в данном случае 1.6f).
12. Готовый кадр
Готовым кадром является цепочка буферов (большинство захватов сделано на моём основном PC с разрешением 1920*1200, но для этого примера я использовал другой PC с разрешением 1920*1080) с форматом DXGI_FORMAT_R10G10B10A2_UNORM и использованием D3D11_USAGE_DEFAULT, флагами D3D11_BIND_SHADER_RESOURCE и D3D11_BIND_RENDER_TARGET. Любопытно здесь то, что цепочка буферов имеет флаг DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING, это значит, что интервал синхронизации, вероятно, равен 0, то есть vsync отключен. И это действительно так, по умолчанию он отключен в GoW, если только пользователь его не включит. В отключении vsync есть свои плюсы и минусы, но важнее всего здесь упомянуть, что он влияет на включение переменной частоты обновления (если не ошибаюсь, эта функция есть только в Windows 10). Подробнее об этой функции можно прочитать по ссылкам в конце статьи.
Теперь я покажу нечто интересное, причину, по которой я хотел подчеркнуть использование vsync: скриншоты ниже взяты из игры, работающей в режиме отладки, вы видите, что когда значение Vsync в опциях игры равно ON, оборудование NVIDIA на самом деле сообщает, что оно Forced-Off (принудительно отключено), а FPS ограничен приблизительно 60 кадрами. С другой стороны, когда в игровых опциях задано значение OFF, оборудование NVIDIA ничего об этом не сообщает, значит ли это, что оно считает параметр включенным? Более того, мне удалось достичь примерно 160 FPS, хотя понятно, что FPS Limit установлен на 120FPS, но оборудование не обращает на это внимания. Интересно!
Значение ON, но отображается как Forced-OFF, значения FPS выглядят реалистично, но не соответствуют Limit
Значение OFF, но отображается как ON, однако значения FPS выглядят реалистично
То есть NVIDIA отображает значения неверно? Ожидается, что при OFF можно достичь высоких FPS (но опять-таки, почему не это не соответствует значению 120 из настроек игры?), а при ON частота должна быть ограничена 30 или 60 кадрами. Я думал, что это может быть какой-то баг с инструментами, но нет, ниже показано видео, снятое при экспериментах с параметрами vsync, на этот раз состояние vsync с текущим FPS отображает уже Intel HUD.
Также стоит заметить, что в цепочке буферов SwapEffect имеет значение FLIP_DISCARD; использование этого типа SwapEffect означает, что нам не нужно копировать (разумеется, копирование было бы лишней тратой ресурсов) содержимого заднего буфера в готовый кадр с каждым вызовом. Вместо этого весь задний буфер является общим для Desktop Window Manager (DWM) без гарантий того, что содержимое каждого заднего буфера сохранится. В конечном итоге, это значение FLIP_DISCARD является очень хорошим выбором, делающим работу чуть более эффективной. Это мелкое значение может быть очень хитрым, потому что каждый тип имеет свои плюсы и минусы, обеспечивая доступность различных функций. Подробнее об этом можно почитать по ссылкам в конце этой статьи.
Жизнь кадра
Соединив всё это почти отвечающий последовательности вычислений визуальный формат, мы получим видео, демонстрирующее всю жизнь кадра игры (этот кадр состоял из 19219 событий). Задумайтесь: это ускоренное шестиминутное видео всех этапов рендеринга, а рендерер GoW может создавать его 60 или более раз в СЕКУНДУ! В какую замечательную для GPU эпоху мы живём!
Вполне нормально, что некоторые вещи повторяются — дети становятся взрослыми, выходные результаты становятся входящими данными, такова жизнь!
Помните, что когда вы видите сплошное чёрное или белое изображение, это с большой вероятностью означает, что в фоновом режиме выполняется вычислительный шейдер. К сожалению, в видео мы не можем видеть прогресса шейдера, только сплошной цвет. Кроме того, если вы долгое время видите буфер глубин или что-то иное, не думайте, что игра зависла, на самом деле, в фоновом режиме добавляется многое, например, небольшой кусок травы или куст за Кратосом. Посмотрите на другой кадр, в нём используется другой способ максимально возможного отображения происходящего.
Интересные факты
- GoW рендерится в Double Buffering (кажется, это актуально для большинства игр на D3D11).
- Отображаемые в готовом кадре проходы цвета рендерятся в треугольнике, который больше размера дисплея, а не в полноэкранном четырёхугольнике, и это мне нравится, я так делал для Mirage. Поэтому хвалю GoW за это!
- Атрей (а может и другие NPC, необходимо дополнительное изучение) полностью рендерится, даже когда находится за пределами камеры. Я ожидал, что существуют только его сущность и геймплейные данные без полного рендеринга, но это не так: во многих захватах, когда его нет в кадре, он отрисовывается.
- Борода Кратоса эпична, потому что состоит примерно из шести слоёв (может и больше, не помню конкретного числа). Но в общем случае с волосами/мехом ситуация похожая, например, ремень на груди Кратоса имеет примерно такое же количество слоёв геометрии. С другой стороны, существо с мехом, например, красивый голубоглазый олень, на которого охотился Атрей, имеет примерно 14 слоёв геометрии волос на всём своём теле.
- Стоит также упомянуть постоянно встречающуюся странность. В том же проходе цвета, где отрисовываются сферы в render target деформации снега (это происходит вне экрана), ВСЕГДА присутствует отрисовка меша топора Левиафан тоже вне экрана. Почему? Понятия не имею! Возможно, это связано с функциональностью топора и тем, что он влияет на окружения? Не знаю, но этот случай требует более глубокого исследования в более подходящих захватах.
- Есть несколько дополнений в проходе рендеринга 10, которые не используются, а просто присутствуют. Они не всегда выполняются в полном конечном разрешении (в моём случае 1920*1200), и иногда уменьшены вдвое (в моём случае 960*600). Если они не используются и для их существования нет причин, то это подходящая область для оптимизации. Но я могу ошибаться, никто не может судить выбор разработчиков кроме те, кто его сделал, поэтому у него может быть какая-то причина.
- Почти вся внеэкранная работа выполняется засчёт двух render target не в размере дисплея или уменьшенных до размера дисплея. Но почти вся работа, а не вся. Существует множество внеэкранных render target, имеющих или размер дисплея, или даже очень странные размеры, не являющиеся степенью двойки.
- При сбросе render target они обычно заполняются чёрным цветом, но в GoW есть несколько render target, сбрасываемых в оранжевый и зелёный. Я люблю оранжевый, люблю фиолетовый и серый, но я никогда не думал сбрасывать render target, заполняя их этими цветами. Но теперь у меня есть источник для вдохновения.
Эпилог
God of War — не только отличная игра с отличным сюжетом, но и с самого своего выпуска оставалась техническим шедевром и великим чудом для любого поклонника игровых движков и/или рендеринга. Всё сказанное выше является моей точкой зрения после изучения множества захватов GPU, анализа и реверс-инжиниринга кадров.
Я люблю франшизу GoW и Кратоса с того момента, как их создал Дэвид Яффе; пусть у меня не было шанса поработать над этими играми, но, по крайней мере, теперь я получаю удовольствие, отдавая игре должное так, как могу: изучая самое её ядро.
Материалы для чтения и просмотра
- GDC Vault – Wind Simulation in ‘God of War’
- GDC Vault – Interactive Wind and Vegetation in ‘God of War’
- GDC Vault – The Future of Scene Description on ‘God of War’
- GDC Vault – The Indirect Lighting Pipeline of ‘God of War’
- GDC Vault – Disintegrating Meshes with Particles in ‘God of War’
- Advanced Techniques and Optimization of HDR VDR Color Pipelines
- Localized tonemapping – is global exposure and global tonemapping operator enough for video games?
- Variable refresh rate displays
- DXGI_SWAP_EFFECT enumeration
- Block Compression (Direct3D 10)