เอกสารนี้เป็นบทช่วยสอนสำหรับการจัดการการแสดงผลของเกม (โดยทั่วไปเพื่อเพิ่มคุณภาพ) หากคุณมีไบนารีเท่านั้น หากคุณเคยสงสัยว่าบางอย่างเช่น DSFIX หรือ DPFIX หรือปลั๊กอิน Gedosato ของฉันหลายอย่างทำงานได้
หากคุณเคยคิดว่ามันจะดีถ้าคุณสามารถทำอะไรแบบนั้นได้เช่นกันก็ดีกว่านี่ก็สำหรับคุณเช่นกัน หวังว่ามันจะช่วยให้คุณประหยัดเวลาได้มากในการหาสิ่งต่าง ๆ ที่กลายเป็นธรรมชาติที่สองเมื่อคุณทำสิ่งนี้มาครึ่งทศวรรษหรือมากกว่านั้น
มี 3 ส่วนสำคัญในเอกสารนี้: " คำนำ " (สิ่งที่คุณกำลังอ่านอยู่ตอนนี้), " การวิเคราะห์ " และ " การจัดการ " "การวิเคราะห์" เกี่ยวข้องกับการหาสิ่งที่เกมทำและสิ่งที่เราอาจต้องเปลี่ยนแปลงในขณะที่ "การจัดการ" อธิบายวิธีการใช้การเปลี่ยนแปลงที่แท้จริง
ในบทช่วยสอนนี้เราจะใช้ซอฟต์แวร์นี้:
ในแง่ของความรู้พื้นฐานเพื่อให้ได้ประโยชน์อย่างเต็มที่จากการสอนนี้มันคงจะดีถ้ามี:
มันอาจจะยังค่อนข้างมีประโยชน์หากไม่มีบางอย่าง
เราจะจัดการกับ Nier: Automata เพราะมันเป็นเกมที่ยอดเยี่ยมและเพราะมันเสนอสิ่งที่ฉันจะพิจารณาความท้าทาย "ปานกลาง" สำหรับประเภทของงานที่เราต้องการแสดง นอกจากนี้ยังเล่นได้ดีกับ RenderDoc โดยไม่มีการเกลี้ยกล่อมที่ซับซ้อน แน่นอนว่าการสอนควรใช้กับเกมอื่น ๆ อีกมากมาย
เพื่อจุดประสงค์ของการสอน (และสำหรับงานและการดัดแปลงทุกประเภท) สิ่งสำคัญคือต้องมีเป้าหมายที่ชัดเจนมาก เป้าหมาย ของเราสำหรับแบบฝึกหัดนี้คือ การเพิ่มการแก้ไขการเรนเดอร์เชิงพื้นที่ ของเอฟเฟกต์การบดเคี้ยวโดยรอบใน Nier: Automata ฉันมาถึงเป้าหมายนี้ (เช่นเป้าหมายการดัดแปลงส่วนใหญ่ของฉัน) โดยการเล่นเกมและเห็นสิ่งที่ฉันไม่ชอบคุณภาพของภาพฉลาด
เพื่อให้เข้าใจว่าเกมดำเนินการเรนเดอร์ได้อย่างไรเราจะเรียกใช้จากภายใน RenderDoc และ จับภาพเฟรม สิ่งนี้จะช่วยให้เราตรวจสอบทุกสิ่งที่เกิดขึ้นซึ่งเกี่ยวข้องกับการแสดงผลเฟรมนั้น ในการทำเช่นนั้นเราจำเป็นต้องชี้ RenderDoc ในเกมปฏิบัติการและเปิดตัว:
จากนั้นในเกมเราย้ายไปยังสถานที่ซึ่งควรเป็นสถานที่ที่ดีในการตัดสินเอฟเฟกต์ที่เราต้องการจัดการและจับเฟรมโดยการกด F12 (RenderDoc ควรแสดงการซ้อนทับในเกมซึ่งแจ้งให้เราทราบถึงทางลัดนี้) หลังจากออกจากเกม RenderDoc จะโหลดและแสดงให้เราเห็นถึง FramedUms ในโหมด Replay:
ฉันจะไม่ให้คำอธิบายอย่างเต็มที่เกี่ยวกับองค์ประกอบ UI ทั้งหมดของ RenderDoc สิ่งที่สำคัญสำหรับเราตอนนี้คือด้านซ้าย เบราว์เซอร์เหตุการณ์ จะให้ระยะเวลาตามลำดับเหตุการณ์ของเหตุการณ์การเรนเดอร์ทั้งหมดและ ผู้ชมพื้นผิว ช่วยให้เราเห็นผลลัพธ์ของการโทรแต่ละครั้ง ด้วยการเปิดตัวชมพื้นผิวและนำทางลงไปในเหตุการณ์ในที่สุดเราควรไปถึงสิ่งที่ดูเหมือนบัฟเฟอร์การอุดตันโดยรอบดิบ มันจะแสดงในภาพหน้าจอด้านบน สิ่งที่เราเห็นได้ทันทีคือมันเป็นบัฟเฟอร์ 800x450 แม้ว่าความละเอียดการแสดงผลของเราจะเป็น 2560x1440 เต็ม นี่น่าจะเป็นผู้ร้ายหลักสำหรับคุณภาพต่ำที่เราเห็นและเป็นสิ่งที่เราต้องเปลี่ยน
อย่างไรก็ตามเพื่อที่จะเข้าใจทุกสิ่งที่เราต้องเปลี่ยนเราจำเป็นต้องรู้ว่าอินพุตใดที่ AO Pass ใช้ โชคดีที่ RenderDoc ทำให้เรื่องนี้ค่อนข้างง่าย:
เราได้เปลี่ยนไปใช้แท็บ อินพุต ทางด้านขวาและตอนนี้เราเห็นว่าเรามีอินพุตเพียงครั้งเดียว เช่นเดียวกับผลลัพธ์ของเรามันคือ 800 โดย 450 โปรดทราบว่าตอนแรกมันจะแสดงเป็นพื้นที่สีดำแบน นี่เป็นกรณีที่มีบัฟเฟอร์จุดลอยตัวหากค่าของพวกเขาไม่ตกอยู่ในช่วงเราสามารถแยกแยะได้ด้วยตาเปล่า อย่างที่คุณเห็นในส่วนที่เน้นด้านบนเราสามารถจัดการการตั้งค่า ช่วง ใน RenderDoc เพื่อให้เนื้อหามองเห็นได้ ในกรณีนี้ดูเหมือนว่าบัฟเฟอร์ Z (จัดเก็บค่าความลึกสำหรับแต่ละพิกเซล) ซึ่งเป็นสิ่งที่เราคาดหวังว่าจะเป็นอินพุตขั้นต่ำของ SSAO Pass
ที่น่าสนใจเรายังเห็น (ที่ด้านล่าง) ว่ามันเป็นพื้นผิวที่มี 10 mipmaps การตรวจสอบเพิ่มเติมของการแสดงผลผ่านโดยใช้เป็นทั้งแหล่งที่มาหรือเป้าหมายเผยให้เห็นว่าแผนที่ MIP แต่ละตัวจะถูกเติมก่อนที่จะผ่าน AO ที่นี่มันช่วยได้มากในการอ่านกระดาษที่มีความเสียหายต่อสภาพแวดล้อมที่ปรับขนาดได้เนื่องจากมันอธิบายว่า Z-buffer แบบลำดับชั้นสามารถใช้เพื่อเร่งความเร็วอย่างมากและลดความจำเหนือศีรษะของการคำนวณ AO ขนาดใหญ่
เมื่อมองไปที่การเรียกร้องการโทรทันทีหลังจากผ่าน AO ครั้งแรกเราจะเห็นการผ่านเบลอที่ขึ้นอยู่กับแนวนอนและแนวตั้งซึ่งเป็นเรื่องปกติของการใช้งาน SSAO ส่วนใหญ่
เพื่อสรุปการวิเคราะห์เบื้องต้นของเราบอกเราว่า:
R8G8B8A8_UNORM และเก็บความครอบคลุม AO อีกรูปแบบหนึ่งของรูปแบบ R32_FLOAT พร้อม mipmaps และเก็บบัฟเฟอร์ Z แบบลำดับชั้นเราจะอ้างถึงการจับเฟรม renderdoc นี้เป็นการ อ้างอิงเริ่มต้น ของเราซึ่งเราสามารถใช้เพื่อค้นหาสิ่งที่ควรเกิดขึ้นจริง ๆ ในกรณีที่มีบางอย่างผิดปกติ (และมันมักจะทำ) เมื่อเราเริ่มจัดการสิ่งต่าง ๆ
เพื่อที่จะจัดการกับการแสดงผลของเกมเราจำเป็นต้องให้รหัสของเราทำงานในกระบวนการของมันและเราจำเป็นต้องสกัดกั้นและอาจเปลี่ยนการโทร 3D API นี่เป็นหัวข้อที่มีขนาดใหญ่และมีแรงงานและเนื่องจากบทช่วยสอนนี้มุ่งเน้นไปที่วิธีการทำความเข้าใจและเปลี่ยนกระบวนการเรนเดอร์แทนที่จะเป็นเทคนิคของการฉีดและการเชื่อมต่อ DLL ฉันจะไม่ไปต่อไป
สิ่งที่เราจะทำเพื่อการทดลองของเราคือการใช้ 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 ที่มีอยู่ของวิธีการ
โดยปกติ ณ จุดนี้ฉันจะตรวจสอบสิ่งอื่น ๆ ที่เกี่ยวข้องและเกี่ยวข้องทันทีซึ่งจำเป็นต้องปรับตัวเช่นวิวพอร์ต, รูปสี่เหลี่ยมผืนผ้ากรรไกรและพารามิเตอร์ shader อย่างไรก็ตามสำหรับการสอนมันเป็นคำแนะนำที่จะดูว่าเกิดอะไรขึ้นกับการเปลี่ยนแปลงนี้เพียงแค่:

อย่างที่คุณเห็นสิ่ง ที่ เกิดขึ้นคือการแสดงผลของเราแตกอย่างสมบูรณ์ นี่อาจเป็นผลลัพธ์ที่พบบ่อยในขณะที่คุณกำลังพัฒนาสิ่งนี้ แล้วเราจะจัดการกับมันได้อย่างไร? นี่คือที่การจับเฟรมอ้างอิงของเราเข้ามาและพิสูจน์ได้ว่ามีประโยชน์มาก
การจับภาพอีกครั้งและเปรียบเทียบกับการอ้างอิงของเราเราสามารถก้าวไปข้างหน้าจนกว่าเราจะเห็นปัญหาที่เกิดขึ้น โดยปกติแล้วมันจะค่อนข้างชัดเจนดังนั้นในกรณีนี้: 
อย่างที่คุณเห็นมีเพียงส่วนเล็ก ๆ ของบัฟเฟอร์ที่จัดการของเราเท่านั้นที่ใช้โดย GPU สิ่งที่คุณไม่สามารถมองเห็นได้ในภาพหน้าจอ แต่สิ่งที่ชัดเจนเมื่อตรวจสอบการเรียกใช้การดึงที่ตามมาในไฟล์การติดตามคือข้อผิดพลาดนี้แพร่กระจายไปทั่วแผนที่ MIP แต่ละตัวจะผ่านไปจนกว่าจะไม่มีอะไรเหลืออยู่
หากคุณคุ้นเคยกับการดัดแปลงประเภทนี้ - หรือหลังจากที่คุณได้อ่านบทช่วยสอนนี้;) - คุณรู้ว่านี่เป็นการรวมตัวกันทั่วไปของการตั้งค่าวิวพอร์ตและ/หรือพารามิเตอร์ shader ที่ไม่ได้รับการปรับอย่างถูกต้อง
สิ่งที่เราต้องตรวจสอบคือการตั้งค่า Viewport สำหรับการโทรแบบวาดที่เกี่ยวข้องรวมถึงว่ามีพารามิเตอร์ shader ใด ๆ ที่ดูเหมือนจะได้มาจากหรือขึ้นอยู่กับความละเอียดบัฟเฟอร์ เราพบสิ่งต่อไปนี้สำหรับ Main AO Pass:
เราเห็นทั้งการตั้งค่า Viewport 800x450 Hardcoded รวมถึงบัฟเฟอร์ค่าคงที่พิกเซลที่น่าสงสัยมากที่ใช้ในการเรียกใช้สายนี้ สำหรับการตีความค่าสองค่าแรกพื้นหลังกราฟิกและ/หรือประสบการณ์บางอย่างมีประโยชน์อีกครั้ง ฉันสงสัยทันทีว่าพวกเขามีขนาดพิกเซลที่ความละเอียดบัฟเฟอร์อินพุตดั้งเดิม - เหล่านี้มักจะถูกส่งไปยัง shaders เพื่อให้พวกเขาสามารถเข้าถึงค่าพื้นที่ใกล้เคียง และแน่นอน 1.0/1600 = 0.000625 และเดียวกันถือเป็นจริงสำหรับความละเอียดแนวตั้งในองค์ประกอบที่สอง
สำหรับบัตรผ่านอื่น ๆ ทั้งหมดที่เกี่ยวข้องในกรณีนี้โดยเฉพาะสถานการณ์จะคล้ายกันมาก - ทั้ง Viewport และพารามิเตอร์ shader ที่เกี่ยวข้องจะต้องมีการปรับและฉันจะไม่ลงรายละเอียดสำหรับพวกเขาทั้งหมด สำหรับการผ่าน Z ลำดับชั้นเป็นสิ่งสำคัญอย่างยิ่งที่จะต้องตรวจสอบให้แน่ใจว่าระดับ MIP ที่ถูกต้องถูกใช้เป็น RENDERTARGTE นั้นถูกระบุเนื่องจากมีผลต่อการตั้งค่าพารามิเตอร์
การปรับวิวพอร์ตและโดยเฉพาะอย่างยิ่งพารามิเตอร์ shader นั้นค่อนข้างยุ่งยาก มีหลายวิธีที่จะไปเกี่ยวกับเรื่องนี้และฉันเสียเวลาไปกับช่องทางที่ไม่ได้ออกไป โดยทั่วไปมีตัวเลือกเหล่านี้อย่างน้อย:
เมื่อเป็นไปได้ฉันพิจารณาตัวเลือกแรกเหล่านี้ให้สะอาดที่สุด แต่มันก็ไม่ได้ผลสำหรับ 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 เป้าหมาย, รูปแบบเฉพาะ, ขนาดเฉพาะ) เราตรวจสอบว่าพื้นผิวการกำหนดเป้าหมายของเรามีขนาดที่แตกต่างจากวิวพัศจะเข้ามาในปัจจุบันหรือไม่และถ้าเป็นเช่นนั้นเราจะปรับวิวพอร์ต
ด้วยการเปลี่ยนแปลงเหล่านี้ตอนนี้เราได้รับการบดเคี้ยวโดยรอบความละเอียดสูงกว่า: 
มันยังคงเป็นสิ่งประดิษฐ์เล็กน้อยและอาจจะดีกว่า แต่ก็เป็นการปรับปรุงที่ค่อนข้างใหญ่
อย่างไรก็ตามเฟรมสุดท้ายของเรา - นั่นคือเอาท์พุทเกมจริง - ยังคงขาดหายไปอย่างสมบูรณ์ ในการแก้ไขปัญหานี้เราต้องกลับไปที่การเปรียบเทียบการอ้างอิงของเรา ด้วยวิธีนี้เราสามารถกำหนดได้ว่ามีบัฟเฟอร์ความลึก/stencil อีก 800x450 ของรูปแบบ D24_UNORM_S8_UINT ซึ่งจำเป็นต้องปรับเพื่อให้ตรงกับขนาดบัฟเฟอร์ AO ใหม่ของเรามิฉะนั้น API จะปฏิเสธที่จะทำการเรนเดอร์ใด ๆ ในภายหลังในเฟรม
รหัสเต็มที่มีปัญหานี้ได้รับการแก้ไขสามารถพบได้ใน code.cpp โปรดทราบว่ายังรวมถึง Nier: การแก้ไขบัฟเฟอร์บลูมซึ่งทำงานบนหลักการที่คล้ายกัน