本文檔是一個教程,用於操縱遊戲的渲染(通常是為了提高質量),如果您只有二進製文件。如果您想知道諸如DSFIX,DPFIX或我的許多Gedosato插件之類的東西如何工作,那麼這是給您的。
如果您曾經以為如果您也可以做類似的事情,那就太好了,那就更好了,這也適合您。希望您能為您節省大量時間來弄清楚當您做這件事五年左右時成為第二天性的事情。
本文檔中有3個主要部分:“序言”(您現在正在閱讀的內容),“分析”和“操縱”。 “分析”涉及弄清遊戲的作用以及我們可能需要更改的內容,而“操縱”解釋瞭如何應用實際更改。
在本教程中,我們將使用此軟件:
就基本知識而言,為了獲得本教程的全部利益,擁有:
沒有其中一些,它可能仍然有用。
我們將與Nier:Automata打交道,因為這是一款出色的遊戲,並且因為它為我們希望執行的任務類型提供了“適度”的挑戰。它也可以與Renderdoc一起發揮不錯的作用,而沒有任何復雜的哄騙。當然,本教程應同樣適用於許多其他遊戲。
出於教程的目的(實際上,對於任何類型的工作和改裝),實現非常明確的目標很重要。我們的這項練習的目標是增加NIER:自動機中環境閉塞效應的空間渲染分解。我通過玩遊戲並看到我不喜歡的東西來達到這個目標(就像大多數改裝目標一樣)。
為了了解遊戲的執行方式,我們將從renderdoc內部運行它並捕獲框架。這將使我們能夠調查與渲染該特定框架有關的一切。為此,我們需要將RenderDoc指向遊戲可執行文件並啟動它:
然後,在遊戲中,我們移至一個位置,應該是判斷我們要操縱的效果的好地方,並通過按F12來捕獲框架(RenderDoc應該顯示一個遊戲中的覆蓋層,以告知我們此快捷方式)。退出遊戲後,RenderDoc將自動加載並在重播模式下向我們展示FramedUmp:
我不會對RenderDoc的所有UI元素做出完整的解釋。現在對我們來說重要的是,在左側,事件瀏覽器給出了所有渲染事件的時間表,並且紋理查看器允許我們看到每個渲染調用的輸出。通過打開紋理查看器並通過事件向下導航,我們最終應該達到看起來像原始環境遮擋緩衝液的東西。它顯示在上面的屏幕截圖中。我們立即看到的是它是800x450緩衝區,儘管我們的渲染分辨率是完整的2560x1440。這可能是我們所看到的低品質的主要罪魁禍首,這是我們需要改變的罪魁禍首。
但是,為了了解我們需要更改的所有內容,我們還需要知道該AO Pass使用的哪些輸入。幸運的是,Renderdoc使它變得很容易:
我們已經切換到右側的“輸入”選項卡,現在看到我們只有一個輸入。就像我們的輸出一樣,它是800 x 450。請注意,最初它將僅顯示為平坦的黑色區域。浮點緩衝區通常是這種情況,如果它們的值不屬於範圍,我們可以用肉眼區分。正如您在頂部的突出顯示部分中看到的那樣,我們可以操縱RenderDoc中的範圍設置以使內容可見。在這種情況下,顯然看起來像Z緩衝區(為每個像素存儲一個深度值),這是我們期望的作為SSAO Pass的最小輸入。
有趣的是,我們還看到(底部)它是帶有10個MIPMAP的紋理。用它作為源或目標,對渲染通行證的進一步研究表明,在AO通行證之前,單個MIP映射是填充的。在這裡,它有助於閱讀可擴展的環境晦澀的紙張,因為它解釋瞭如何使用層次結構Z-buffer大大加速和減少大規模AO計算的內存開銷。
查看初始AO通行證後的渲染調用,我們看到了水平和垂直深度依賴性模糊通行證,這也是大多數SSAO實現的典型代表。
總而言之,我們的初步分析告訴我們:
R8G8B8A8_UNORM格式的,並存儲AO覆蓋範圍,另一個是帶有MIPMAP的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級別用作renderTarget的正確級別尤其重要,因為它會影響參數設置。
實際調整視口,尤其是著色器參數有點棘手。有多種方法可以解決,我浪費了一些時間,這些途徑沒有淘汰。通常,至少有這些選擇:
如果可能的話,我認為這些選項中的第一個是最乾淨的,但是在這種情況下,它對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紋理是否具有與當前設置的視口不同的大小,如果這樣,我們調整了視口並在相應插槽中設置新的常數緩衝區。
通過這些更改,我們現在開始使用更高分辨率的環境遮擋: 
它仍然有些偽裝,甚至可以更好,但這是一個相當大的進步。
但是,我們的最後幀(即實際的遊戲輸出)仍然完全破壞。為了解決這個問題,我們需要回到參考比較。這樣,我們可以確定格式D24_UNORM_S8_UINT的另一個800x450深度/模板緩衝區需要進行調整以匹配我們的新AO緩衝區大小,否則API(正確地)將拒絕以後在框架後來進行任何渲染。
解決此問題的完整代碼可以在code.cpp中找到。請注意,它還包括NIER:Bloom Buffer Fix,以類似的原理運行。