このドキュメントは、バイナリのみが利用可能な場合は、ゲームのレンダリングを操作するためのチュートリアル(一般的にその品質を向上させる)です。 dsfix、dpfix、または私のgedosatoプラグインの多くがどのように機能するか疑問に思ったことがあるなら、これはあなたのためです。
あなたがそのようなことをすることができれば素晴らしいと思ったことがあるなら、さらに良いことに、これもあなたのためです。うまくいけば、それはあなたがこれを半10年ほど行ってきたときに第二の性質になることを理解するのに多くの時間を節約するでしょう。
このドキュメントには、「 Preamble 」(現在読んでいるもの)、「分析」、「操作」の3つの主要なセクションがあります。 「分析」は、ゲームが何をするのか、おそらく変更する必要があることを把握することに対処しますが、「操作」は実際の変更を適用する方法を説明します。
このチュートリアルでは、このソフトウェアを使用します。
基本的な知識に関しては、このチュートリアルの最大限の利益を得るために、おそらく次のことをお勧めします。
それらのいくつかがなければ、おそらくまだいくらか便利です。
Nier:Automataを扱います。これは素晴らしいゲームであり、実行したいタスクのタイプに「適度な」量の課題を提供するものを提供するからです。また、複雑な同軸なしでRenderdocでうまく機能します。もちろん、チュートリアルは他の非常に多くのゲームに等しく適用できるはずです。
チュートリアルの目的(そして、あらゆる種類の作業と改造のために)のために、非常に明確な目標を持つことが重要です。この演習の目標は、Nier:Automataの周囲閉塞効果の空間的レンダリング解決を増やすことです。私はゲームをプレイし、私が好きではないものを見て、画像品質の賢さを見ることで、この目標(ほとんどの改造の目標のように)に到着しました。
ゲームがレンダリングの実行方法を理解するために、RenderDoc内から実行してフレームをキャプチャします。これにより、その特定のフレームをレンダリングすることに関連するすべてのことを調査することができます。そのためには、RenderDocをゲーム実行可能ファイルに向けて起動する必要があります。
次に、ゲーム内では、操作したい効果を判断するのに適した場所になるはずの場所に移動し、F12を押してフレームをキャプチャします(RenderDocは、このショートカットを知らせるゲーム内のオーバーレイを表示する必要があります)。ゲームを終了した後、RenderDocは自動的にロードされ、再生モードでFramedumpを表示します。
RenderDocのすべてのUI要素について完全な説明はしません。今私たちにとって重要なのは、左側で、イベントブラウザがすべてのレンダリングイベントの時系列のタイムラインを提供し、テクスチャビューアが各レンダリングコールの出力を見ることができることです。テクスチャビューアーを開き、イベントを下方に移動することにより、最終的に生の周囲閉塞バッファーのように見えるものに到達する必要があります。上記のスクリーンショットに示されています。私たちがすぐに見るのは、レンダリング解像度が完全な2560x1440であるにもかかわらず、 800x450バッファであるということです。これはおそらく、私たちが見ている低品質の主な犯人であり、それは私たちが変更する必要があるものです。
ただし、変更する必要があるすべてを理解するためには、このAOパスが使用する入力を知る必要もあります。幸いなことに、RenderDocはこれをかなり簡単にします:
右側の[入力]タブに切り替えましたが、1つの入力しかないことがわかります。出力と同じように、800 x 450です。最初は平らな黒い領域として表示されることに注意してください。これは多くの場合、フローティングポイントバッファーの場合に当てはまります。その値が範囲に分類されない場合、肉眼で区別できます。上部の強調表示された部分でわかるように、RenderDocの範囲設定を操作してコンテンツを表示できるようにすることができます。この場合、それは明らかにZバッファー(各ピクセルの深さ値を保存する)のように見えます。これは、SSAOパスへの最小入力として予想されるものです。
興味深いことに、(下部に)10個のMIPMAPを持つテクスチャであることもわかります。ソースまたはターゲットのいずれかとして、レンダリングパスのさらなる調査は、個々のMIPマップがAOパスの直前に入力されていることを明らかにしています。ここでは、階層Zバッファーを使用して大規模なAO計算のメモリオーバーヘッドを大幅に高速化および削減する方法を説明するため、スケーラブルな周囲の不明瞭なペーパーを読むのに役立ちます。
最初のAOパスの直後にレンダリングコールを見ると、ほとんどのSSAO実装にも典型的な水平および垂直の深さ依存性ぼやけパスが表示されます。
要約すると、最初の分析では次のようになります。
R8G8B8A8_UNORM形式であり、AOカバレッジを保存し、もう1つは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ビューポート設定の両方が表示されます。最初の2つの値を解釈するには、グラフィックスの背景や経験が非常に役立ちます。私はすぐに、それらが元の入力バッファー解像度でピクセルサイズであると疑っていました - これらは多くの場合、近隣の値にアクセスできるようにシェーダーに供給されます。実際、 1.0/1600 = 0.000625であり、2番目のコンポーネントの垂直解像度にも同じことが当てはまります。
この特定のケースに関係する他のすべてのパスについては、状況は非常に似ています。ビューポートと関連するシェーダーパラメーターの両方を調整する必要があり、それらすべてについて詳しく説明することはありません。階層Zパスの場合、パラメーター設定に影響するため、RenderTargetとして使用される正しいMIPレベルが識別されることを確認することが特に重要です。
実際にビューポート、特にシェーダーパラメーターを調整するのは少し難しいです。それについて説明する方法は複数あり、私はしばらくして、パンアウトしなかった道を無駄にしました。一般的に、少なくともこれらのオプションがあります。
可能であれば、これらのオプションの最初のオプションは最もクリーンであると考えていますが、この場合はNierではうまくいきませんでした。 2つ目は、醜い国家管理を誘導します。 3番目には、バッファの寿命を管理する必要がありますが、ここではほんの数回のフロートについて話しているので、それが最も実行可能なオプションであると考えました。
2番目の決定は、実際に調整を実行する時期と、実行する必要があることを検出する方法です。ここでは、一般的にいくつかの要因を念頭に置いて報われます。
これらの原則に従って、私はこのコードに到達しました:
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のさらに800x450の深さ/ステンシルバッファーがあることを判断できます。
この問題を解決した完全なコードは、 code.cppにあります。同様の原則で動作するNier:Bloom Buffer Fixも含まれていることに注意してください。