Этот документ является учебником для манипулирования рендерингом игры (как правило, для повышения ее качества), если у вас есть только бинар. Если вы когда -нибудь задумывались, как работает что -то вроде DSFIX, или DPFIX, или многих моих плагинов GEDOSATO, то это для вас.
Если вы когда -нибудь думали, что было бы здорово, если бы вы тоже могли сделать что -то подобное, то еще лучше, это тоже для вас. Надеемся, что это сэкономит вам много времени, выяснив вещи, которые становятся второй натурой, когда вы делаете это в течение полвеки или около того.
В этом документе есть 3 основных раздела: « Преамбула » (что вы сейчас читаете), « Анализ » и « манипуляция ». «Анализ» имеет дело с выяснением, что делает игра и что нам, вероятно, нужно изменить, в то время как «манипуляция» объясняет, как применить фактические изменения.
В этом уроке мы будем использовать это программное обеспечение:
С точки зрения основополагающих знаний, чтобы получить полную выгоду от этого урока, было бы хорошо иметь:
Это, вероятно, все еще несколько полезно без некоторых из них.
Мы будем иметь дело с Nier: Automata , потому что это отличная игра, и потому что она предлагает то, что я бы считал «умеренным» количеством проблем для типов задач, которые мы хотим выполнить. Это также хорошо играет с renderdoc без каких -либо сложных уговоров. Конечно, учебник должен быть одинаково применим к многим другим играм.
Для учебного пособия (и для любого типа работы и моддинга) важно иметь очень четкую цель. Наша цель для этого упражнения - увеличить разрешение пространственного рендеринга эффекта окклюзии окружающей среды в Nier: Automata. Я достиг этой цели (как и большинство моих моддинг), играя в игру и увидев то, что мне не нравилось, качество изображения.
Чтобы понять, как игра выполняет его рендеринг, мы запустим ее из RenderDoc и захватываем кадр . Это позволит нам исследовать все, что происходит, что связано с тем, чтобы сделать этот конкретный кадр. Для этого нам нужно указать renderdoc на исполняемом файле игры и запустить его:
Затем, в игре, мы переходим в место, которое должно быть хорошим местом, чтобы судить о эффекте, который мы хотим манипулировать, и захватить кадр, нажав F12 (renderdoc должен показывать внутриигровое наложение, которое сообщает нам об этом ярлыке). После выхода из игры renderdoc автоматически загрузит и покажет нам в рамках режима воспроизведения:
Я не дам полного объяснения всех элементов пользовательского интерфейса renderdoc. Теперь для нас важно то, что слева браузер событий дает хронологический график всех событий рендеринга, и что просмотрщик текстур позволяет нам видеть вывод каждого вызова рендеринга. Открывая просмотра текстур и навигая вниз по событиям, мы должны в конечном итоге достичь чего -то, что выглядит как необработанный окклюзионный буфер. Это показано на скриншоте выше. То, что мы сразу видим, это то, что это буфер 800x450 , несмотря на то, что наше разрешение рендеринга - полная 2560x1440. Это, вероятно, является основным виновником для низкого качества, которое мы наблюдаем, и это то, что нам нужно изменить.
Однако, чтобы понять все, что нам нужно изменить, мы также должны знать, какой вход использует этот проход AO. К счастью, renderdoc делает это довольно простым:
Мы переключились на вкладку «Входы справа», и теперь видим, что у нас есть только один вход. Как и наш выход, это 800 на 450. Обратите внимание, что изначально он будет просто показан как плоская черная область. Это часто относится к буферам с плавающей запятой, если их значения не попадают в диапазоны, мы можем различить невооруженным глазом. Как вы можете видеть в выделенной части сверху, мы можем манипулировать настройкой диапазона в renderdoc, чтобы сделать контент видимым. В этом случае он явно выглядит как Z -буфер (храня значение глубины для каждого пикселя), который мы и ожидаем в качестве минимального ввода в проход SSAO.
Интересно, что мы также видим (внизу), что это текстура с 10 MIPMAP. Дальнейшее исследование рендеринга проходит, используя его в качестве источника или цели, показывает, что отдельные карты MIP заполнены непосредственно перед проходом AO. Здесь очень помогает прочитать масштабируемую атмосферную неясную бумагу, поскольку она объясняет, как иерархический Z-буфер можно использовать для значительного ускорения и уменьшения накладных расходов на память обширных вычислений AO.
Глядя на вызовы рендеринга сразу после начального прохода AO, мы видим горизонтальный и вертикальный проход размытия, зависящий от глубины, который также типичен для большинства реализаций SSAO.
Подводя итог, наш первоначальный анализ говорит нам, что:
R8G8B8A8_UNORM и хранит покрытие AO, другой имеет формат R32_FLOAT с MIPMAPS и хранит иерархический Z -буфер.Мы будем называть этот захват кадров RenderDoc как нашу первоначальную ссылку , которую мы можем использовать, чтобы посмотреть, что действительно должно происходить, если что -то пойдет не так (и это всегда делает), как только мы начнем манипулировать вещами.
Чтобы манипулировать рендерингом игры, нам нужно запустить наш код в его процессе, и нам нужно перехватить и потенциально изменить его 3D -вызовы API. Это огромная и трудоемкая тема, и, поскольку этот урок фокусируется на том, как понять и изменить процесс рендеринга, а не технические детали инъекции и зацепления DLL, я не пойду дальше.
То, что мы будем делать для наших экспериментов, просто использовать renderdoc в качестве нашего автомобиля инъекции. Это имеет преимущество в том, что он действительно солидный и хорошо проверенный инструмент, который позволяет нам сосредоточиться на выяснении, что нам нужно делать, а не на то, почему наш инструмент не работает для какого-либо углового чехла. Конечно, чтобы фактически создать осмысленную распределимую и воспроизводимую версию, нам необходимо перенести наш окончательный результат в некоторые фреймворки впрыска, разработанную для этой цели.
Первое, что нам нужно сделать, чтобы увеличить разрешение рендеринга, - это сделать наши буферы достаточно большими, чтобы подтвердить это разрешение. Для этого мы изменяем метод WrappedID3D11Device::CreateTexture2D в источниках renderdoc:
HRESULT WrappedID3D11Device::CreateTexture2D ( const D3D11_TEXTURE2D_DESC *pDesc,
const D3D11_SUBRESOURCE_DATA *pInitialData,
ID3D11Texture2D **ppTexture2D)
{
// Desired AO resolution
static UINT aoW = 1280 ;
static UINT aoH = 720 ;
// 800x450 R8G8B8A8_UNORM is the buffer used to store the AO result and subsequently blur it
// 800x450 R32_FLOAT is used to store hierarchical Z information (individual mipmap levels are rendered to)
// and serves as input to the main AO pass
if (pDesc-> Format == DXGI_FORMAT_R8G8B8A8_UNORM || pDesc-> Format == DXGI_FORMAT_R32_FLOAT) {
if (pDesc-> Width == 800 && pDesc-> Height == 450 ) {
// set to our display resolution instead
D3D11_TEXTURE2D_DESC copy = *pDesc;
copy. Width = aoW;
copy. Height = aoH;
pDesc = ©
}
}
// [...]
} Кодекс довольно эксплуатационен, часть [...] относится к существующей реализации метода renderDoc.
Обычно, на данный момент, я также сразу же изучил бы другие соответствующие и связанные вещи, которые необходимо адаптировать, такие как Viewports, ножницы прямоугольники и параметры шейдеров. Однако для учебника поучительно посмотреть, что происходит с этим изменением:

Как видите, то, что происходит, так это то, что наш рендеринг полностью ломается. Это, вероятно, всегда будет общим результатом, пока вы разрабатываете что -то подобное. Так как же мы справимся с этим? Именно здесь приходит наш опорный захват кадров и оказывается очень полезным.
Принимая еще один захват и сравнивая его бок о бок с нашей ссылкой, мы можем двигаться вперед, пока не увидим проблему. Обычно это будет довольно очевидно, и так и в этом случае: 
Как видите, только небольшая часть нашего манипулируемого буфера используется графическим процессором. То, что вы не можете видеть на снимке экрана, но что очевидно при осмотре последующих вызовов рисования в файле трассировки, так это то, что эта ошибка распространяется на индивидуальную карту MIP, пока в основном ничего не останется.
Если вы привыкли к такому моддингу - или после того, как вы прочитали этот урок;) - вы знаете, что это очень типичное проявление настройки просмотра и/или параметров шейдеров, которые не регулируются правильно, чтобы соответствовать новым, большим размерам буферов.
Что нам нужно исследовать, так это настройки просмотра для соответствующих вызовов для розыгрыша, а также есть ли какие -либо параметры шейдеров, которые, по -видимому, получаются или зависят от разрешения буфера. Мы находим следующее для основного прохода AO:
Мы видим как настройку просмотра 800x450 800x450, так и очень подозрительный постоянный буфер Pixel Shader, используемый в этом вызове. Для интерпретации первых двух значений некоторый графический фон и/или опыт снова чрезвычайно полезны. Я сразу же подозревал, что они были размерами пикселей в исходном разрешении буфера входного буфера - их часто поставляются в шейдеры, чтобы они могли получить доступ к значениям соседства. И действительно, 1.0/1600 = 0.000625 , и то же самое относится к вертикальному разрешению во втором компоненте.
Для всех остальных проходов, связанных с этим конкретным случаем, ситуация очень похожа - как для просмотра, так и соответствующий параметр шейдера должны быть скорректированы, и я не буду вдаваться в подробности для всех из них. Для иерархических проходов Z особенно важно убедиться, что определяется правильный уровень MIP, используемый в качестве рендертаргета, поскольку он влияет на настройку параметра.
На самом деле регулировка видоубийта и, в частности, параметры шейдера немного сложно. Есть несколько способов сделать это, и я потратил некоторое время на пути, которые не вышли. Как правило, есть, по крайней мере, эти варианты:
Когда это возможно, я считаю первым из этих вариантов самым чистым, но в этом случае он не сработал для Nier. Второй вызывает некоторое уродливое управление государством. Третий требует, чтобы вы управляли жизнью буфера, но, поскольку мы говорим о нескольких поплавках здесь, я считал, что это наиболее жизнеспособным вариантом.
Второе решение - когда фактически выполнить корректировки и как обнаружить, что их необходимо выполнить. Здесь обычно окупается, чтобы помнить несколько факторов:
Следуя этим принципам, я получил этот код:
void PreDraw (WrappedID3D11DeviceContext* context) {
UINT numViewports = 0 ;
context-> RSGetViewports (&numViewports, NULL );
if (numViewports == 1 ) {
D3D11_VIEWPORT vp;
context-> RSGetViewports (&numViewports, &vp);
if ((vp. Width == 800 && vp. Height == 450 )
|| (vp. Width == 400 && vp. Height == 225 )
|| (vp. Width == 200 && vp. Height == 112 )
|| (vp. Width == 100 && vp. Height == 56 )
|| (vp. Width == 50 && vp. Height == 28 )
|| (vp. Width == 25 && vp. Height == 14 )) {
ID3D11RenderTargetView *rtView = NULL ;
context-> OMGetRenderTargets ( 1 , &rtView, NULL );
if (rtView) {
D3D11_RENDER_TARGET_VIEW_DESC desc;
rtView-> GetDesc (&desc);
if (desc. Format == desc. Format == DXGI_FORMAT_R8G8B8A8_UNORM || desc. Format == DXGI_FORMAT_R32_FLOAT) {
ID3D11Resource *rt = NULL ;
rtView-> GetResource (&rt);
if (rt) {
ID3D11Texture2D *rttex = NULL ;
rt-> QueryInterface <ID3D11Texture2D>(&rttex);
if (rttex) {
D3D11_TEXTURE2D_DESC texdesc;
rttex-> GetDesc (&texdesc);
if (texdesc. Width != vp. Width ) {
// Here we go!
// Viewport is the easy part
vp. Width = ( float )texdesc. Width ;
vp. Height = ( float )texdesc. Height ;
// if we are at mip slice N, divide by 2^N
if (desc. Texture2D . MipSlice > 0 ) {
vp. Width = ( float )(texdesc. Width >> desc. Texture2D . MipSlice );
vp. Height = ( float )(texdesc. Height >> desc. Texture2D . MipSlice );
}
context-> RSSetViewports ( 1 , &vp);
// The constant buffer is a bit more difficult
// We don't want to create a new buffer every frame,
// but we also can't use the game's because they are read-only
// this just-in-time initialized map is a rather ugly solution,
// but it works as long as the game only renders from 1 thread (which it does)
// NOTE: rather than storing them statically here (basically a global) the lifetime should probably be managed
D3D11_BUFFER_DESC buffdesc;
buffdesc. ByteWidth = 16 ;
buffdesc. Usage = D3D11_USAGE_IMMUTABLE;
buffdesc. BindFlags = D3D11_BIND_CONSTANT_BUFFER;
buffdesc. CPUAccessFlags = 0 ;
buffdesc. MiscFlags = 0 ;
buffdesc. StructureByteStride = 16 ;
D3D11_SUBRESOURCE_DATA initialdata;
ID3D11Buffer* replacementbuffer = NULL ;
ID3D11Device* dev = NULL ;
// If we are not rendering to a mip map for hierarchical Z, the format is
// [ 0.5f / W, 0.5f / H, W, H ] (half-pixel size and total dimensions)
if (desc. Texture2D . MipSlice == 0 ) {
static std::map<UINT, ID3D11Buffer*> buffers;
auto iter = buffers. find (texdesc. Width );
if (iter == buffers. cend ()) {
float constants[ 4 ] = { 0 . 5f / vp. Width , 0 . 5f / vp. Height , ( float )vp. Width , ( float )vp. Height };
initialdata. pSysMem = constants;
context-> GetDevice (&dev);
dev-> CreateBuffer (&buffdesc, &initialdata, &replacementbuffer);
buffers[texdesc. Width ] = replacementbuffer;
}
context-> PSSetConstantBuffers ( 12 , 1 , &buffers[texdesc. Width ]);
}
// For hierarchical Z mips, the format is
// [ W, H, LOD (Mip-1), 0.0f ]
else {
static std::map<UINT, ID3D11Buffer*> mipBuffers;
auto iter = mipBuffers. find (desc. Texture2D . MipSlice );
if (iter == mipBuffers. cend ()) {
float constants[ 4 ] = {vp. Width , vp. Height , ( float )desc. Texture2D . MipSlice - 1 , 0 . 0f };
initialdata. pSysMem = constants;
context-> GetDevice (&dev);
dev-> CreateBuffer (&buffdesc, &initialdata, &replacementbuffer);
mipBuffers[desc. Texture2D . MipSlice ] = replacementbuffer;
}
context-> PSSetConstantBuffers ( 8 , 1 , &mipBuffers[desc. Texture2D . MipSlice ]);
}
if (dev) dev-> Release ();
}
}
rt-> Release ();
}
}
rtView-> Release ();
}
}
}
}
void WrappedID3D11DeviceContext::Draw (UINT VertexCount, UINT StartVertexLocation)
{
if (VertexCount == 4 && StartVertexLocation == 0 ) PreDraw ( this );
// [...]
}Как вы можете видеть, немного громоздко убедиться, что мы только касаемся именно то, что хотим, но это действительно более длинный , чем сложный. По сути, для каждого вызова, подгоняющего шаблон (4 вершины, 1 цель, конкретные форматы, конкретные размеры), мы проверяем, имеет ли наша текстура рендергета отличным размером, чем в данный момент установленного вида, и если это так, мы настраиваем порт View и устанавливаем новый постоянный буфер в соответствующем слоте.
С этими изменениями теперь мы получаем окклюзию окружающей среды с более высоким разрешением: 
Это все еще немного артефакт, и может быть еще лучше, но это довольно огромное улучшение.
Тем не менее, наш последний кадр - то есть фактический выход игры - все еще полностью сломан. Чтобы исправить это, нам нужно вернуться к нашему справочному сравнению. Таким образом, мы можем определить, что существует еще один буфер глубины/трафарета 800x450 в формате D24_UNORM_S8_UINT , который необходимо регулировать, чтобы соответствовать нашему новым размерам буфера AO, в противном случае API (по праву) откажется делать какое -либо визуализацию позже в кадре.
Полный код с этой проблемой может быть найден в code.cpp . Обратите внимание, что он также включает в себя исправление Bloom Buffer, которое работает по аналогичному принципу.