Dieses Dokument ist ein Tutorial zur Manipulation des Renders eines Spiels (im Allgemeinen, um seine Qualität zu erhöhen), wenn Sie nur eine Binärdatum zur Verfügung haben. Wenn Sie sich jemals gefragt haben, wie so etwas wie DSFIX, DPFIX oder viele meiner Gedosato -Plugins funktionieren, dann ist dies für Sie.
Wenn Sie jemals gedacht haben, es wäre großartig, wenn Sie so etwas auch tun könnten, dann ist dies auch für Sie. Hoffentlich sparen Sie viel Zeit, um Dinge herauszufinden, die zu einer zweiten Natur werden, wenn Sie dies seit einem halben Jahrzehnt oder so tun.
In diesem Dokument gibt es 3 Hauptabschnitte: " Präamble " (was Sie gerade lesen), " Analyse " und " Manipulation ". "Analysis" befasst sich damit, herauszufinden, was das Spiel tut und was wir wahrscheinlich ändern müssen, während "Manipulation" erklärt, wie die tatsächlichen Änderungen angewendet werden können.
In diesem Tutorial werden wir diese Software verwenden:
In Bezug auf grundlegende Kenntnisse wäre es wahrscheinlich gut zu haben, den vollen Nutzen dieses Tutorials zu erzielen:
Ohne einige davon ist es wahrscheinlich immer noch etwas nützlich.
Wir werden es mit Nier: Automata zu tun haben, weil es ein großartiges Spiel ist und weil es eine "moderate" Herausforderung für die Arten von Aufgaben bietet, die wir ausführen möchten. Es spielt auch gut mit Renderdoc ohne komplizierte Überregung. Natürlich sollte das Tutorial gleichermaßen für viele andere Spiele anwendbar sein.
Für die Zwecke eines Tutorials (und für jede Art von Arbeit und Modding) ist es wichtig, ein sehr klares Ziel zu haben. Unser Ziel für diese Übung ist es, die räumliche Rendering -Aufklärung des Umgebungsverschlussseffekts in Nier: Automata zu erhöhen . Ich kam zu diesem Ziel (wie in den meisten meinen Modding -Toren), indem ich das Spiel spielte und etwas sah, das mir nicht gefiel, und die Bildqualität.
Um ein Verständnis dafür zu erlangen, wie das Spiel seine Darstellung durchführt, werden wir es von Renderdoc ausführen und einen Rahmen erfassen . Auf diese Weise können wir alles untersuchen, was geschieht, was mit der Wiedergabe dieses bestimmten Rahmens zusammenhängt. Dazu müssen wir Renderdoc auf das ausführbare Spiel zeigen und starten:
Dann, im Spiel, bewegen wir uns zu einem Ort, an dem wir den Effekt beurteilen sollten, den wir manipulieren möchten, und einen Rahmen durch Drücken von F12 erfassen (Renderdoc sollte eine Überlagerung im Spiel zeigen, die uns über diese Abkürzung informiert). Nach dem Verlassen des Spiels lädt Renderdoc automatisch und zeigt uns den Framedump im Wiederholungsmodus:
Ich werde keine vollständige Erklärung für alle UI -Elemente von Renderdoc geben. Was uns jetzt wichtig ist, ist, dass der Ereignisbrowser auf der linken Seite eine chronologische Zeitleiste aller Rendering -Ereignisse anbietet und dass der Texturbieter es uns ermöglicht, die Ausgabe jedes Rendering -Anrufs zu sehen. Indem wir den Texturbetrachter öffnen und durch die Ereignisse nach unten navigieren, sollten wir irgendwann etwas erreichen, das wie ein Verschlusspuffer von rohem Umgebungsbetrag aussieht. Es wird im obigen Screenshot gezeigt. Was wir sofort sehen, ist, dass es sich um einen 800x450 -Puffer handelt, obwohl wir eine vollständige 2560x1440 -Auflösung haben. Dies ist wahrscheinlich der Hauptschuldige für die geringe Qualität, die wir sehen, und es ist das, was wir uns ändern müssen.
Um jedoch alles zu verstehen, was wir ändern müssen, müssen wir auch wissen, welche Eingaben dieser AO -Pass verwendet. Zum Glück macht Renderdoc dies ziemlich einfach:
Wir haben rechts auf die Registerkarte Eingänge gewechselt und sehen nun, dass wir nur eine einzelne Eingabe haben. Genau wie unsere Ausgabe ist es 800 mal 450. Beachten Sie, dass es zunächst nur als flacher schwarzer Bereich angezeigt wird. Dies ist oft bei schwimmenden Punktpuffern der Fall, wenn ihre Werte nicht in die Bereiche fallen, können wir mit dem bloßen Auge unterscheiden. Wie Sie im hervorgehobenen Teil oben sehen können, können wir die Reichweite in Renderdoc manipulieren, um den Inhalt sichtbar zu machen. In diesem Fall sieht es eindeutig aus wie ein Z -Puffer (speichern Sie einen Tiefenwert für jedes Pixel).
Interessanterweise sehen wir auch (unten), dass es sich um eine Textur mit 10 MIPMaps handelt. Eine weitere Untersuchung des Rendering -Durchgangs als Quelle oder Ziel zeigt, dass die einzelnen MIP -Karten kurz vor dem AO -Pass besiedelt sind. Hier hilft es sehr, das skalierbare Umgebungsdachsackpapier gelesen zu haben, da es erklärt, wie ein hierarchischer Z-Puffer verwendet werden kann, um den Speicheraufwand von großräumigen AO-Berechnungen erheblich zu beschleunigen und zu reduzieren.
Wenn wir uns die Rendering-Anrufe direkt nach dem anfänglichen AO-Pass ansehen, sehen wir einen horizontalen und vertikalen Tiefenpass, der auch für die meisten SSAO-Implementierungen typisch ist.
Zusammenfassend zeigt unsere erste Analyse::
R8G8B8A8_UNORM und speichert die AO -Abdeckung, das andere ist des R32_FLOAT -Formats mit MIPMAPS und speichert einen hierarchischen Z -Puffer.Wir werden diese Renderdoc -Frame -Erfassung als unsere anfängliche Referenz bezeichnen, mit der wir nachsehen können, was wirklich geschehen sollte, falls etwas schief geht (und immer es immer tut), sobald wir mit der Manipulation von Dingen beginnen.
Um die Darstellung eines Spiels zu manipulieren, müssen wir unseren Code in seinem Prozess laufen lassen, und wir müssen seine 3D -API -Aufrufe abfangen und potenziell ändern. Dies ist ein riesiges und arbeitsunfähiges Thema, und da sich dieses Tutorial darauf konzentriert, wie man den Rendering-Prozess und nicht die technische geltende DLL-Injektion und -Kazidierung verändert, werde ich mich nicht weiter in diese Weise eingehen.
Was wir für unsere Experimente tun werden, ist, Renderdoc einfach als Injektionsfahrzeug zu verwenden. Dies hat den Vorteil, ein wirklich solides und gut getestetes Werkzeug zu sein, mit dem wir uns darauf konzentrieren können, herauszufinden, was wir tun müssen, und warum unser Toolset für einen Eckfall nicht funktioniert. Um tatsächlich eine sinnvoll verteilbare und spielbare Version zu erstellen, müssen wir unser Endergebnis in ein für diesen Zweck entwickeltes Injektionsrahmen portieren.
Das erste, was wir tun müssen, um die Rendering -Lösung zu erhöhen, ist, unsere Puffer tatsächlich groß genug zu machen, um diese Auflösung zu unterstützen. Dazu ändern wir die WrappedID3D11Device::CreateTexture2D -Methode in den RenderDOC -Quellen:
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 = ©
}
}
// [...]
} Der Code ist eher selbsterklärend, der Teil [...] bezieht sich auf die vorhandene Renderdoc-Implementierung der Methode.
Normalerweise würde ich zu diesem Zeitpunkt auch auch andere relevante und verwandte Dinge untersuchen, die angepasst werden müssen, z. B. Ansichtsfenster, Scherenrechte und Shader -Parameter. Für das Tutorial ist es jedoch lehrreich zu sehen, was mit dieser Änderung passiert:

Wie Sie sehen können, ist das passiert, dass unsere Rendering vollständig bricht. Dies wird wahrscheinlich immer ein gemeinsames Ergebnis sein, wenn Sie so etwas entwickeln. Wie gehen wir damit um? Hier kommt unsere Referenzrahmenaufnahme ein und erweist sich als sehr nützlich.
Wenn wir eine weitere Erfassung und den Vergleich von es neben uns mit unserer Referenz nehmen, können wir vorwärts gehen, bis wir ein Problem sehen, das auftritt. Normalerweise ist es ziemlich offensichtlich, und so ist es in diesem Fall: 
Wie Sie sehen können, wird von der GPU nur ein kleiner Teil unseres manipulierten Puffers verwendet. Was Sie im Screenshot nicht sehen können, aber was ist offensichtlich, wenn Sie die nachfolgenden Ziehaufrufe in der Trace -Datei inspizieren, ist, dass sich dieser Fehler über die einzelnen MIP -Karte ausbreitet, bis im Grunde genommen nichts mehr übrig ist.
Wenn Sie an diese Art von Modding gewöhnt sind - oder nachdem Sie dieses Tutorial gelesen haben;) - wissen Sie, dass dies eine sehr typische Manifestation einer Ansichtsfensterung und/oder Shader -Parameter ist, die nicht korrekt an die neuen, größeren Puffergrößen angepasst werden.
Wir müssen untersuchen Wir finden Folgendes für den Haupt -AO -Pass:
Wir sehen sowohl eine hartcodierte 800x450 Ansichtsfenstereinstellung als auch einen sehr verdächtigen Pixel -Shader -Konstantpuffer, der in diesem Ziehaufruf verwendet wird. Für die Interpretation der ersten beiden Werte sind einige Grafikhintergrund und/oder Erfahrung wieder äußerst hilfreich. Ich vermutete sofort, dass es sich um Pixelgrößen an der ursprünglichen Eingangspufferauflösung handelte - diese werden häufig an Shader geliefert, damit sie auf Nachbarschaftswerte zugreifen können. Und in der Tat 1.0/1600 = 0.000625 , und das gleiche gilt für die vertikale Auflösung in der zweiten Komponente.
Für alle anderen Pässe, die in diesen speziellen Fall beteiligt sind, ist die Situation sehr ähnlich - sowohl das Ansichtsfenster als auch ein verwandter Shader -Parameter müssen angepasst werden, und ich werde für alle nicht detailliert eingehen. Für die hierarchischen Z -Durchgänge ist es besonders wichtig sicherzustellen, dass die korrekte MIP -Ebene als Rendertarget identifiziert wird, da sie die Parametereinstellung beeinflusst.
Das Einstellen der Ansichtsfenster und insbesondere die Shader -Parameter sind etwas schwierig. Es gibt mehrere Möglichkeiten, es zu erledigen, und ich habe einige Zeit in Wegen verschwendet, die nicht herausgekommen sind. Im Allgemeinen gibt es mindestens diese Optionen:
Wenn möglich, betrachte ich die erste dieser Optionen als die sauberste, aber es hat in diesem Fall nicht für Nier geklappt. Der zweite induziert ein hässliches Staatsmanagement. Der dritte erfordert, dass Sie die Buffer -Lebensdauer verwalten, aber da wir hier nur über ein paar Schwimmkörper sprechen, hielt ich das als die praktikabelste Option an.
Die zweite Entscheidung ist, wann die Anpassungen tatsächlich vorgenommen werden sollen und wie sie feststellen, dass sie durchgeführt werden müssen. Hier zahlt es sich im Allgemeinen aus, um einige Faktoren im Auge zu behalten:
Nach diesen Prinzipien kam ich zu diesem Code an:
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 );
// [...]
}Wie Sie sehen können, ist es ein bisschen umständlich, sicherzustellen, dass wir nur genau das berühren, was wir berühren wollen, aber es ist sehr langwieriger als kompliziert. Grundsätzlich prüfen wir für jeden Zeichnungsaufruf, der das Muster anpasst (4 Scheitelpunkte, 1 Ziel, bestimmte Formate, bestimmte Größen).
Mit diesen Veränderungen erhalten wir jetzt Arbeiten mit höherer Auflösung am Ambient Occlusion: 
Es ist immer noch ein bisschen artifaktiert und könnte sogar noch besser sein, aber es ist eine ziemlich große Verbesserung.
Unser letzter Frame - dh die tatsächliche Spielausgabe - ist jedoch immer noch vollständig kaputt. Um dies zu beheben, müssen wir zu unserem Referenzvergleich zurückkehren. Auf diese Weise können wir feststellen, dass es einen weiteren 800x450 -Tiefen-/Schablonenpuffer des Formats D24_UNORM_S8_UINT gibt, der so eingestellt werden muss, dass er unseren neuen AO -Puffergrößen entspricht, ansonsten wird sich die API (zu Recht) abweist, später im Rahmen des Renders zu werden.
Der vollständige Code mit diesem Problem ist in code.cpp gefunden. Beachten Sie, dass es auch den Nier: eine Bloom -Puffer -Fix enthält, die nach einem ähnlichen Prinzip arbeitet.