Dokumen ini adalah tutorial untuk memanipulasi rendering permainan (umumnya untuk meningkatkan kualitasnya) jika Anda hanya memiliki biner yang tersedia. Jika Anda pernah bertanya -tanya bagaimana sesuatu seperti DSFIX, atau DPFIX, atau banyak plugin Gedosato saya bekerja, maka ini untuk Anda.
Jika Anda pernah berpikir akan lebih baik jika Anda bisa melakukan hal seperti itu juga, maka lebih baik lagi, ini juga untuk Anda. Mudah -mudahan itu akan menghemat banyak waktu untuk mencari tahu hal -hal yang menjadi kebiasaan ketika Anda telah melakukan ini selama setengah dekade atau lebih.
Ada 3 bagian utama dalam dokumen ini: " Pembukaan " (apa yang Anda baca sekarang), " analisis ", dan " manipulasi ". "Analisis" berkaitan dengan mencari tahu apa yang dilakukan game dan apa yang mungkin perlu kita ubah, sementara "manipulasi" menjelaskan bagaimana menerapkan perubahan aktual.
Dalam tutorial ini, kami akan menggunakan perangkat lunak ini:
Dalam hal pengetahuan dasar, untuk mendapatkan manfaat penuh dari tutorial ini, mungkin akan lebih baik untuk dimiliki:
Mungkin masih agak berguna tanpa beberapa dari itu.
Kami akan berurusan dengan Nier: Automata , karena ini adalah permainan yang hebat dan karena menawarkan apa yang saya anggap sebagai tantangan "moderat" untuk jenis tugas yang ingin kami lakukan. Ini juga bermain dengan baik dengan RenderDoc tanpa coaxing yang rumit. Tentu saja, tutorial harus sama -sama berlaku untuk banyak game lainnya.
Untuk tujuan tutorial (dan untuk semua jenis pekerjaan dan modding benar -benar) penting untuk memiliki tujuan yang sangat jelas. Tujuan kami untuk latihan ini adalah untuk meningkatkan resolisi rendering spasial dari efek oklusi ambient di Nier: Automata. Saya mencapai tujuan ini (seperti kebanyakan tujuan modding saya) dengan memainkan permainan dan melihat sesuatu yang tidak saya sukai, kualitas gambar bijaksana.
Untuk mendapatkan pemahaman tentang bagaimana permainan melakukan renderingnya, kami akan menjalankannya dari dalam RenderDoc dan menangkap bingkai . Ini akan memungkinkan kami untuk menyelidiki semua yang terjadi yang terkait dengan rendering bingkai itu. Untuk melakukannya, kita perlu menunjukkan RenderDoc di game yang dapat dieksekusi dan meluncurkannya:
Kemudian, dalam game, kami pindah ke lokasi yang seharusnya menjadi tempat yang baik untuk menilai efek yang ingin kami manipulasi, dan menangkap bingkai dengan menekan F12 (RenderDoc harus menunjukkan overlay dalam game yang memberi tahu kami tentang pintasan ini). Setelah keluar dari permainan, RenderDoc akan secara otomatis memuat dan menunjukkan kepada kami FRAMEDUMP dalam mode Replay:
Saya tidak akan memberikan penjelasan lengkap tentang semua elemen UI RenderDoc. Yang penting bagi kita sekarang adalah bahwa di sebelah kiri, browser acara memberikan garis waktu kronologis dari semua acara rendering, dan bahwa penampil tekstur memungkinkan kita untuk melihat output dari setiap panggilan rendering. Dengan membuka penampil tekstur dan menavigasi ke bawah melalui peristiwa, kita pada akhirnya harus mencapai sesuatu yang terlihat seperti buffer oklusi ambien mentah. Itu ditampilkan di tangkapan layar di atas. Apa yang segera kami lihat di sana adalah bahwa itu adalah buffer 800x450 , meskipun resolusi rendering kami menjadi 2560x1440 penuh. Ini kemungkinan penyebab utama untuk kualitas rendah yang kita lihat, dan itulah yang perlu kita ubah.
Namun, untuk memahami semua yang perlu kita ubah, kita juga perlu mengetahui yang mana yang digunakan AO Pass ini. Untungnya, RenderDoc membuat ini agak mudah:
Kami telah beralih ke tab Inputs di sebelah kanan, dan sekarang melihat bahwa kami hanya memiliki satu input. Sama seperti output kami, itu 800 kali 450. Perhatikan bahwa awalnya hanya akan ditampilkan sebagai area hitam datar. Ini sering terjadi dengan buffer titik mengambang, jika nilainya tidak jatuh ke dalam rentang yang dapat kita bedakan dengan mata telanjang. Seperti yang dapat Anda lihat di bagian yang disorot di atas, kami dapat memanipulasi pengaturan rentang di RenderDoc untuk membuat konten terlihat. Dalam hal ini, itu jelas terlihat seperti buffer z (menyimpan nilai kedalaman untuk setiap piksel), yang merupakan apa yang kita harapkan sebagai input minimum ke pass SSAO.
Menariknya, kita juga melihat (di bagian bawah) bahwa itu adalah tekstur dengan 10 mipmap. Investigasi lebih lanjut dari lintasan rendering menggunakannya sebagai sumber atau target mengungkapkan bahwa peta MIP individu dihuni tepat sebelum AO Pass. Di sini sangat membantu untuk membaca kertas obsuransi ambien yang dapat diskalakan, karena menjelaskan bagaimana buffer Z hierarkis dapat digunakan untuk mempercepat dan mengurangi overhead memori perhitungan AO skala besar.
Melihat panggilan rendering tepat setelah AO Pass awal, kita melihat Blur Pass yang bergantung pada kedalaman horizontal dan vertikal, yang juga khas dari sebagian besar implementasi SSAO.
Untuk meringkas, analisis awal kami memberi tahu kami bahwa:
R8G8B8A8_UNORM dan menyimpan cakupan AO, yang lain adalah dari format R32_FLOAT dengan mipmaps dan menyimpan buffer z hierarkis.Kami akan merujuk pada penangkapan bingkai RenderDoc ini sebagai referensi awal kami, yang dapat kami gunakan untuk mencari apa yang seharusnya terjadi jika terjadi kesalahan (dan selalu terjadi) begitu kami mulai memanipulasi sesuatu.
Untuk memanipulasi rendering permainan, kita perlu menjalankan kode kita dalam prosesnya, dan kita perlu mencegat dan berpotensi mengubah panggilan API 3D -nya. Ini adalah topik yang sangat besar dan intesif tenaga kerja, dan karena tutorial ini berfokus pada bagaimana memahami dan mengubah proses rendering daripada teknis injeksi dan pengait DLL, saya tidak akan melangkah lebih jauh ke dalam hal ini.
Apa yang akan kami lakukan untuk eksperimen kami adalah menggunakan RenderDoc sebagai kendaraan injeksi kami. Ini memiliki keuntungan menjadi alat yang benar-benar solid dan teruji dengan baik, yang memungkinkan kita untuk fokus mencari tahu apa yang perlu kita lakukan daripada mengapa toolset kita tidak berfungsi untuk beberapa kasus sudut. Tentu saja, untuk benar -benar membuat versi yang dapat didistribusikan dan dapat dimainkan, kita perlu port hasil akhir kita menjadi beberapa kerangka kerja injeksi yang dirancang untuk tujuan itu.
Hal pertama yang perlu kita lakukan untuk meningkatkan resolusi rendering adalah benar -benar membuat buffer kita cukup besar untuk mendukung resolusi itu. Untuk melakukannya, kami mengubah metode WrappedID3D11Device::CreateTexture2D dalam sumber 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 = ©
}
}
// [...]
} Kode ini agak jelas, bagian [...] mengacu pada implementasi RenderDoc yang ada dari metode ini.
Biasanya, pada titik ini, saya juga akan segera melihat hal -hal lain yang relevan dan terkait yang perlu diadaptasi, seperti viewports, scissor rectanges dan parameter shader. Namun, untuk tutorial, sangat instruktif untuk melihat apa yang terjadi hanya dengan perubahan ini:

Seperti yang Anda lihat, apa yang terjadi adalah rendering kami benar -benar istirahat. Ini mungkin akan selalu menjadi hasil umum saat Anda mengembangkan sesuatu seperti ini. Jadi bagaimana kita menghadapinya? Di sinilah penangkapan bingkai referensi kami masuk dan terbukti sangat berguna.
Mengambil tangkapan lain dan membandingkannya berdampingan dengan referensi kami, kami dapat bergerak maju sampai kami melihat masalah yang terjadi. Biasanya, itu akan agak jelas, dan demikian pula dalam hal ini: 
Seperti yang Anda lihat, hanya sebagian kecil dari buffer yang dimanipulasi kami yang digunakan oleh GPU. Apa yang tidak dapat Anda lihat di tangkapan layar tetapi apa yang jelas ketika memeriksa panggilan undian berikutnya dalam file jejak adalah bahwa kesalahan ini merambat di seluruh peta MIP yang dilewati sampai pada dasarnya tidak ada yang tersisa.
Jika Anda terbiasa dengan modding semacam ini - atau setelah Anda membaca tutorial ini;) - Anda tahu bahwa ini adalah manifestasi yang sangat khas dari pengaturan viewport dan/atau parameter shader yang tidak disesuaikan dengan benar agar sesuai dengan ukuran buffer baru yang lebih besar.
Yang perlu kita selidiki adalah pengaturan viewport untuk panggilan penarikan yang relevan, serta apakah ada parameter shader yang tampaknya berasal dari atau tergantung pada resolusi buffer. Kami menemukan yang berikut untuk Pass AO utama:
Kami melihat pengaturan viewport 800x450 hardcoded serta buffer konstan pixel shader yang sangat mencurigakan yang digunakan dalam panggilan undian ini. Untuk menafsirkan dua nilai pertama, beberapa latar belakang grafik dan/atau pengalaman sekali lagi sangat membantu. Saya segera curiga bahwa mereka adalah ukuran piksel pada resolusi buffer input asli - ini sering disuplai ke shader sehingga mereka dapat mengakses nilai -nilai lingkungan. Dan memang, 1.0/1600 = 0.000625 , dan hal yang sama berlaku untuk resolusi vertikal dalam komponen kedua.
Untuk semua operan lain yang terlibat dalam kasus khusus ini situasinya sangat mirip - baik viewport maupun parameter shader terkait perlu disesuaikan, dan saya tidak akan membahas secara rinci untuk semuanya. Untuk lintasan Z hierarkis, sangat penting untuk memastikan bahwa tingkat MIP yang benar digunakan sebagai rendertarget diidentifikasi, karena itu mempengaruhi pengaturan parameter.
Sebenarnya menyesuaikan viewports dan khususnya parameter shader agak rumit. Ada beberapa cara untuk melakukannya, dan saya membuang waktu di jalan yang tidak berjalan. Secara umum, setidaknya ada opsi ini:
Jika memungkinkan, saya menganggap yang pertama dari opsi ini sebagai yang paling bersih, tetapi tidak berhasil untuk Nier dalam kasus ini. Yang kedua menginduksi beberapa manajemen negara yang buruk. Yang ketiga mengharuskan Anda untuk mengelola seumur hidup buffer, tetapi karena kita berbicara tentang hanya beberapa pelampung di sini, saya menganggap itu sebagai pilihan yang paling layak.
Keputusan kedua untuk membuat adalah kapan harus benar -benar melakukan penyesuaian, dan bagaimana mendeteksi bahwa mereka perlu dilakukan. Di sini, umumnya membuahkan hasil untuk mengingat beberapa faktor:
Mengikuti prinsip -prinsip ini, saya sampai pada kode ini:
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 );
// [...]
}Seperti yang Anda lihat, agak rumit untuk memastikan kami hanya menyentuh persis apa yang ingin kami sentuh, tetapi itu benar -benar lebih panjang daripada rumit. Pada dasarnya, untuk setiap penarikan panggilan yang sesuai dengan pola (4 simpul, 1 target, format spesifik, ukuran spesifik), kami memeriksa apakah tekstur rendertarget kami memiliki ukuran yang berbeda dari viewport yang saat ini diatur, dan jika demikian kami menyesuaikan viewport dan mengatur buffer konstan baru di slot yang sesuai.
Dengan perubahan ini, kami sekarang bekerja oklusi ambien resolusi lebih tinggi: 
Ini masih sedikit artefak, dan bisa lebih baik, tapi ini merupakan peningkatan yang sangat besar.
Namun, bingkai terakhir kami - yaitu, output permainan yang sebenarnya - masih benar -benar rusak. Untuk memperbaikinya, kita perlu kembali ke perbandingan referensi kita. Dengan begitu, kita dapat menentukan bahwa ada buffer kedalaman/stensil 800x450 lainnya dari format D24_UNORM_S8_UINT yang perlu disesuaikan agar sesuai dengan ukuran buffer AO baru kami, jika tidak API akan (secara tepat) menolak untuk melakukan rendering nanti dalam bingkai.
Kode lengkap dengan masalah ini diselesaikan dapat ditemukan di code.cpp . Perhatikan bahwa itu juga termasuk Nier: Bloom Buffer Fix, yang beroperasi pada prinsip yang sama.