이 문서는 이진을 사용할 수있는 경우 게임 렌더링 (일반적으로 품질을 높이기 위해)을 조작하기위한 튜토리얼입니다. DSFIX, DPFIX 또는 많은 Gedosato 플러그인이 어떻게 작동하는지 궁금한 적이 있다면 이것이 당신을위한 것입니다.
당신이 그런 일을 할 수 있다면 좋을 것이라고 생각했다면, 더 나은 것은 당신을위한 것입니다. 바라건대 그것은 당신이 반 10 년 정도 그렇게했을 때 두 번째 자연이되는 것들을 알아내는 데 많은 시간을 절약 할 수 있기를 바랍니다.
이 문서에는 " Preamble "(지금 읽고있는 내용), " 분석 "및 " 조작 "의 3 가지 주요 섹션이 있습니다. "분석"은 게임이 무엇을하는지, 아마도 변화해야 할 것인지 알아내는 반면, "조작"은 실제 변경 사항을 적용하는 방법을 설명합니다.
이 튜토리얼에서는이 소프트웨어를 사용할 것입니다.
기본 지식의 관점 에서이 튜토리얼의 모든 이점을 얻으려면 다음과 같은 것이 좋습니다.
아마도 그 중 일부 없이는 여전히 다소 유용 할 것입니다.
우리는 Nier : Automata를 다룰 것입니다. 왜냐하면 그것은 훌륭한 게임이기 때문에 우리가 수행하고자하는 작업 유형에 대해 "적당한"양의 도전을 제공하기 때문입니다. 또한 복잡한 동축없이 RenderDoc과 잘 어울립니다. 물론, 자습서는 다른 많은 게임에도 동일하게 적용되어야합니다.
튜토리얼의 목적 (그리고 모든 유형의 작업 및 모딩에 대해 실제로는 매우 명확한 목표를 달성하는 것이 중요합니다. 이 연습의 목표는 Nier : Automata에서 주변 폐색 효과의 공간 렌더링 해결을 증가시키는 것 입니다. 나는 게임을하고 내가 좋아하지 않는 것을보고 이미지 품질을 현명하게 보면서이 목표 (대부분의 모딩 목표와 같이)에 도달했습니다.
게임이 어떻게 렌더링을 수행하는지 이해하기 위해 RenderDoc 내에서 실행하고 프레임을 캡처합니다 . 이를 통해 특정 프레임을 렌더링하는 것과 관련된 모든 것을 조사 할 수 있습니다. 그렇게하려면 게임 실행 파일에서 RenderDoc을 가리키고 시작해야합니다.
그런 다음 게임 내에서 우리는 조작하려는 효과를 판단하기에 좋은 장소로 이동하고 F12를 눌러 프레임을 캡처합니다 (RenderDoc은이 단축키를 알려주는 게임 내 오버레이를 표시해야합니다). 게임을 종료 한 후 RenderDoc은 자동으로로드하고 리플레이 모드에서 프레임 덤프를 표시합니다.
나는 RenderDoc의 모든 UI 요소에 대한 자세한 설명을하지 않을 것입니다. 우리에게 중요한 것은 왼쪽에서 이벤트 브라우저가 모든 렌더링 이벤트의 연대순 타임 라인을 제공하고 텍스처 뷰어를 통해 각 렌더링 호출의 출력을 볼 수 있다는 것입니다. 텍스처 뷰어를 열고 이벤트를 통해 아래쪽으로 탐색함으로써 결국 원시 앰비언트 폐색 버퍼처럼 보이는 것에 도달해야합니다. 위의 스크린 샷에 나와 있습니다. 우리가 즉시 보는 것은 렌더링 해상도가 전체 2560x1440 임에도 불구하고 800x450 버퍼라는 것입니다. 이것은 우리가보고있는 저 품질의 주요 원인 일 것입니다.
그러나 변경해야 할 모든 것을 이해하려면 AO 패스가 사용하는 입력을 알아야합니다. 운 좋게도 RenderDoc은 이것을 쉽게 만듭니다.
오른쪽의 입력 탭으로 전환했으며 이제 단일 입력 만 있음을 알 수 있습니다. 우리의 출력과 마찬가지로 800 x 450입니다. 처음에는 평평한 검은 영역으로 표시됩니다. 값이 육안으로 구별 할 수있는 범위에 빠지지 않으면 부동 소수점 버퍼의 경우가 종종 있습니다. 위에 강조 표시된 부분에서 볼 수 있듯이 RenderDoc의 범위 설정을 조작하여 컨텐츠를 가시적으로 만들 수 있습니다. 이 경우 z 버퍼 (각 픽셀의 깊이 값 저장)처럼 보이며, 이는 SSAO 패스에 대한 최소 입력으로 기대할 수있는 것입니다.
흥미롭게도, 우리는 또한 (아래에서) 10 마일 맵이있는 질감임을 알 수 있습니다. 소스 또는 대상으로 사용하는 렌더링 패스에 대한 추가 조사는 개별 MIP 맵이 AO 패스 직전에 채워진다는 것을 보여줍니다. 여기서는 확장 가능한 주변 홀 홀비 페이퍼를 읽는 데 많은 도움이됩니다. 계층 적 Z- 버퍼를 사용하여 대규모 AO 계산의 메모리 오버 헤드를 크게 높이고 줄이는 방법을 설명하기 때문입니다.
초기 AO 패스 직후 렌더링 호출을 살펴보면 수평 및 수직 깊이 의존적 블러 패스가 보이며, 이는 대부분의 SSAO 구현의 전형적인 것입니다.
요약하면, 우리의 초기 분석은 다음을 알려줍니다.
R8G8B8A8_UNORM 형식이며 AO 범위를 저장하고 다른 하나는 MIPMAPS가있는 R32_FLOAT 형식이며 계층 적 Z 버퍼를 저장합니다.우리는이 RenderDoc 프레임 캡처를 우리의 초기 참조 라고 언급 할 것입니다.이 참조는 무언가 잘못 될 경우를 대비하여 실제로 일어나야 할 일을 찾는 데 사용할 수 있습니다.
게임 렌더링을 조작하려면 프로세스에서 코드를 실행해야하며 3D API 호출을 가로 채고 잠재적으로 변경해야합니다. 이것은 거대하고 노동 집약적 인 주제이며,이 튜토리얼은 DLL 주입 및 고리의 기술보다는 렌더링 프로세스를 이해하고 변경하는 방법에 중점을두기 때문에 더 이상 들어 가지 않을 것입니다.
실험을 위해 우리가 할 일은 단순히 RenderDoc을 주사 차량으로 사용하는 것입니다. 이것은 정말 견고하고 잘 테스트 된 도구라는 장점이있어 툴셋이 코너 케이스에서 작동하지 않는 이유보다는 우리가해야 할 일을 파악하는 데 집중할 수 있습니다. 물론, 실제로 의미있게 배포 할 수 있고 재생 가능한 버전을 만들려면 최종 결과를 해당 목적을 위해 설계된 일부 주입 프레임 워크로 포팅해야합니다.
렌더링 해상도를 높이기 위해 가장 먼저해야 할 일은 실제로 버퍼를 해당 해상도를 지원하기에 충분히 크게 만드는 것입니다. 이를 위해 RenderDoc 소스에서 WrappedID3D11Device::CreateTexture2D 메소드를 변경합니다.
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 구현을 나타냅니다.
일반적 으로이 시점에서 뷰포트, 가위 사각형 및 셰이더 매개 변수와 같이 적응 해야하는 다른 관련 및 관련 사항을 즉시 살펴 봅니다. 그러나 튜토리얼의 경우이 변경 사항만으로 어떤 일이 발생하는지 보는 것이 도움이됩니다.

보시다시피, 우리의 렌더링은 완전히 깨지는 것 입니다 . 이와 같은 것을 개발하는 동안 이것은 항상 일반적인 결과 일 것입니다. 그래서 우리는 어떻게 처리합니까? 이곳은 참조 프레임 캡처가 들어오고 매우 유용한 곳입니다.
또 다른 캡처를하고 참조와 나란히 비교하면 문제가 발생할 때까지 앞으로 나아갈 수 있습니다. 일반적으로 다소 분명 할 것이므로이 경우에 다음과 같습니다. 
보시다시피, 조작 버퍼의 작은 부분 만 GPU에서 사용하고 있습니다. 스크린 샷에서 볼 수없는 것은 추적 파일에서 후속 드로우 호출을 검사 할 때 명백한 것은이 오류가 기본적으로 아무것도 남지 않을 때까지 개별 MIP 맵에 걸쳐 전파된다는 것입니다.
이런 종류의 모딩에 익숙 하거나이 튜토리얼을 읽은 후에;) - 이것은 새로운 버퍼 크기와 일치하도록 뷰포트 설정 및/또는 셰이더 매개 변수의 매우 일반적인 표현이라는 것을 알고 있습니다.
우리가 조사해야 할 것은 관련 드로우 호출의 뷰포트 설정과 버퍼 해상도에서 파생되거나 의존하는 것처럼 보이는 셰이더 매개 변수가 있는지 여부입니다. 메인 AO 패스의 경우 다음을 찾습니다.
우리는 하드 코드 800x450 뷰포트 설정 과이 드로우 호출에 사용되는 매우 의심스러운 픽셀 셰이더 상수 버퍼를 볼 수 있습니다. 처음 두 값을 해석하기 위해 일부 그래픽 배경 및/또는 경험이 다시 매우 유용합니다. 나는 원래 입력 버퍼 해상도에서 픽셀 크기라고 의심했다. 이들은 종종 셰이더에 공급되므로 이웃 값에 접근 할 수있다. 그리고 실제로, 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 개의 대상, 특정 형식, 특정 크기)에 맞는 모든 드로우 콜에 대해 rendertarget 텍스처가 현재 설정된 뷰포트와 크기가 다른지 여부를 확인하고 해당 슬롯에서 뷰포트를 조정하고 새로운 상수 버퍼를 설정합니다.
이러한 변화를 통해 이제 우리는 이제 고해상도 주변 폐색을 일으킨다. 
그것은 여전히 약간 아티팩트이며 더 나아질 수 있지만 꽤 큰 개선입니다.
그러나 우리의 최종 프레임, 즉 실제 게임 출력은 여전히 완전히 깨졌습니다. 이 문제를 해결하려면 참조 비교로 돌아 가야합니다. 이렇게하면 새로운 AO 버퍼 크기와 일치하도록 조정 해야하는 형식의 D24_UNORM_S8_UINT 깊이/스텐실 버퍼가있는 것으로 판단 할 수 있습니다. 그렇지 않으면 API는 나중에 프레임의 렌더링을 거부합니다.
이 문제가 해결 된 전체 코드는 code.cpp 에서 찾을 수 있습니다. 비슷한 원칙으로 작동하는 Nier : A Bloom 버퍼 수정도 포함됩니다.