هذا المستند هو برنامج تعليمي لمعالجة عرض اللعبة (بشكل عام لزيادة جودتها) إذا كان لديك ثنائي متاح فقط. إذا كنت تساءلت يومًا كيف يعمل شيء مثل DSFIX أو DPFIX أو العديد من مكونات Gedosato الخاصة بي ، فهذا مناسب لك.
إذا كنت تعتقد أنه سيكون من الرائع أن تفعل شيئًا كهذا أيضًا ، فهذا أفضل ، فهذا أمر لك أيضًا. نأمل أن يوفر لك الكثير من الوقت في معرفة الأشياء التي تصبح الطبيعة الثانية عندما كنت تفعل ذلك لمدة نصف عقد أو نحو ذلك.
هناك 3 أقسام رئيسية في هذا المستند: " الديباجة " (ما تقرأه الآن) ، " التحليل " ، و " التلاعب ". يتناول "التحليل" معرفة ما تفعله اللعبة وربما نحتاج إلى تغييره ، بينما يشرح "التلاعب" كيفية تطبيق التغييرات الفعلية.
في هذا البرنامج التعليمي ، سنستخدم هذا البرنامج:
فيما يتعلق بالمعرفة التأسيسية ، للحصول على الفائدة الكاملة لهذا البرنامج التعليمي ، من المحتمل أن يكون من الجيد أن يكون ذلك:
ربما لا يزال مفيدًا إلى حد ما بدون بعض هؤلاء.
سنتعامل مع Nier: Automata ، لأنها لعبة رائعة ولأنها تقدم ما أعتبره قدرًا "معتدلًا" من التحدي لأنواع المهام التي نرغب في القيام بها. كما أنه يلعب بشكل جيد مع RenderDoc دون أي اقناع معقد. بالطبع ، يجب أن يكون البرنامج التعليمي قابلاً للتطبيق على قدم المساواة على العديد من الألعاب الأخرى.
لغرض البرنامج التعليمي (ولأي نوع من العمل والتعديل حقًا) ، من المهم أن يكون لديك هدف واضح للغاية. هدفنا في هذا التمرين هو زيادة الحكم على التقديم المكاني لتأثير انسداد المحيط في Nier: Automata. وصلت إلى هذا الهدف (مثل معظم أهداف التعديل الخاصة بي) من خلال لعب اللعبة ورؤية شيء لم يعجبني ، جودة الصورة.
لاكتساب فهم لكيفية أداء اللعبة ، سنقوم بتشغيلها من داخل RenderDoc والتقاط إطار . سيسمح لنا ذلك بالتحقيق في كل ما يحدث مرتبطًا بتقديم هذا الإطار المحدد. للقيام بذلك ، نحتاج إلى توجيه RenderDoc إلى اللعبة القابلة للتنفيذ وإطلاقها:
بعد ذلك ، في اللعبة ، ننتقل إلى موقع يجب أن يكون مكانًا جيدًا للحكم على التأثير الذي نريد معالجته ، والتقاط إطار عن طريق الضغط على F12 (يجب أن يظهر RenderDoc تراكبًا في اللعبة الذي يخبرنا بهذا الاختصار). بعد الخروج من اللعبة ، سيتم تحميل RenderDoc تلقائيًا وإظهار Framedump في وضع إعادة التشغيل:
لن أقدم شرحًا كاملاً لجميع عناصر واجهة المستخدم في RenderDoc. المهم بالنسبة لنا الآن هو أنه على اليسار ، يعطي متصفح الحدث جدولًا زمنيًا زمنيًا لجميع أحداث العرض ، وأن عارض الملمس يسمح لنا برؤية إخراج كل مكالمة عرض. من خلال فتح عارض الملمس والتنقل إلى الأسفل خلال الأحداث ، يجب أن نصل في النهاية إلى شيء يبدو وكأنه مخزن انسداد المحيط الخام. يظهر في لقطة الشاشة أعلاه. ما نراه على الفور هو أنه هو عازلة 800 × 450 ، على الرغم من أن قرار العرض لدينا هو 2560 × 1440 الكامل. من المحتمل أن يكون هذا هو الجاني الأساسي للجودة المنخفضة التي نراها ، وهذا ما نحتاج إلى تغييره.
ومع ذلك ، من أجل فهم كل ما نحتاج إلى تغييره ، نحتاج أيضًا إلى معرفة المدخلات التي يستخدمها AO Pass. لحسن الحظ ، فإن RenderDoc يجعل هذا الأمر سهلاً إلى حد ما:
لقد تحولنا إلى علامة تبويب المدخلات على اليمين ، ونرى الآن أن لدينا فقط إدخال واحد. تمامًا مثل ناتجنا ، فهو 800 × 450. لاحظ أنه في البداية سيتم عرضه كمنطقة سوداء مسطحة. غالبًا ما يكون هذا هو الحال مع المخازن المؤقتة للنقطة العائمة ، إذا لم تقع قيمها في النطاقات ، فيمكننا التمييز مع العين المجردة. كما ترون في الجزء المميز في الأعلى ، يمكننا معالجة إعداد النطاق في RenderDoc لجعل المحتوى مرئيًا. في هذه الحالة ، يبدو من الواضح أنه عازلة Z (تخزين قيمة عمق لكل بكسل) ، وهو ما نتوقعه كحد أدنى مدخلات لتمريرة SSAO.
ومن المثير للاهتمام ، نرى أيضًا (في الأسفل) أنه ملمس يحتوي على 10 MIPMaps. يكشف مزيد من التحقيق في تمريرات العرض باستخدامه كمصدر أو هدف أن خرائط MIP الفردية يتم ملؤها قبل ممر AO. يساعد هنا كثيرًا على قراءة ورقة الغموض المحيطة القابلة للتطوير ، لأنه يفسر كيف يمكن استخدام Z-buffer الهرمي لتسريع بشكل كبير وتقليل ذاكرة الحسابات AO واسعة النطاق.
بالنظر إلى المكالمات التقديم مباشرة بعد تمريرة AO الأولية ، نرى تمريرة طيبة أفقية وعمودية تعتمد على العمق ، والتي هي أيضًا نموذجية لمعظم تطبيقات SSAO.
لتلخيص ، يخبرنا تحليلنا الأولي أن:
R8G8B8A8_UNORM ويخزن تغطية AO ، والآخر من تنسيق R32_FLOAT مع MIPMAPS ويخزن المخزن المؤقت Z هرمي.سنشير إلى التقاط إطار RenderDoc هذا باعتباره مرجعنا الأولي ، والذي يمكننا استخدامه للبحث عن ما يجب أن يحدث حقًا في حالة حدوث خطأ ما (وهو ما يحدث دائمًا) بمجرد أن نبدأ في معالجة الأشياء.
من أجل معالجة تقديم اللعبة ، نحتاج إلى تشغيل الكود لدينا في عمليتها ، ونحن بحاجة إلى اعتراض مكالمات API ثلاثية الأبعاد. هذا موضوع ضخم وعملي ، وبما أن هذا البرنامج التعليمي يركز على كيفية فهم وتغيير عملية التقديم بدلاً من الجوانب الفنية لحقن DLL والتواصل ، فلن أذهب إلى هذا الحد.
ما سنفعله لتجاربنا هو ببساطة استخدام RenderDoc كأداة للحقن. هذا يتمتع بميزة أن تكون أداة صلبة ومختبرة جيدًا ، والتي تسمح لنا بالتركيز على معرفة ما نحتاج إلى القيام به بدلاً من سبب عدم عمل مجموعة أدواتنا في بعض حالات الزاوية. بالطبع ، لإنشاء نسخة قابلة للتوزيع وقابلة للعب بشكل مفيد ، نحتاج إلى نقل نتائجنا النهائية إلى بعض إطار الحقن المصمم لهذا الغرض.
أول ما نحتاج إلى القيام به من أجل زيادة دقة التقديم هو جعل المخازن المؤقتة لدينا كبيرة بما يكفي لدعم هذا القرار. للقيام بذلك ، نقوم بتغيير طريقة WrappedID3D11Device::CreateTexture2D في مصادر 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 = ©
}
}
// [...]
} الرمز هو إلى حد ما ، فإن الجزء [...] يشير إلى تنفيذ RenderDoc الحالي للطريقة.
عادةً ، في هذه المرحلة ، سأبحث على الفور في الأشياء الأخرى ذات الصلة والذات ذات الصلة والتي تحتاج إلى تكييفها ، مثل موانئ العرض ومستطيلات المقص ومعلمات التظليل. ومع ذلك ، بالنسبة للبرنامج التعليمي ، من المفيد معرفة ما يحدث مع هذا التغيير فقط:

كما ترون ، ما الذي يحدث هو أن استراحات العرض لدينا تماما. من المحتمل أن تكون هذه نتيجة شائعة أثناء تطوير شيء من هذا القبيل. فكيف نتعامل معها؟ هذا هو المكان الذي يأتي فيه التقاط الإطار المرجعي الخاص بنا ويثبت أنه مفيد للغاية.
أخذ التقاط آخر ومقارنته جنبًا إلى جنب مع مرجعنا ، يمكننا المضي قدمًا حتى نرى مشكلة تحدث. عادة ، سيكون من الواضح إلى حد ما ، وهكذا هو في هذه الحالة: 
كما ترون ، يتم استخدام جزء صغير فقط من المخزن المؤقت المعالج لدينا من قبل وحدة معالجة الرسومات. ما لا يمكنك رؤيته في لقطة الشاشة ولكن ما هو واضح عند فحص مكالمات السحب اللاحقة في ملف التتبع هو أن هذا الخطأ ينتشر عبر خريطة MIP الفردية حتى لا يتم ترك أي شيء.
إذا كنت معتادًا على هذه الأنواع من التعديل - أو بعد قراءة هذا البرنامج التعليمي ؛) - فأنت تعلم أن هذا مظهر نموذجي للغاية لإعداد إطار العرض و/أو معلمات التظليل التي لا يتم تعديلها بشكل صحيح لتتناسب مع أحجام المخزن المؤقت الجديدة والأكبر.
ما نحتاج إلى التحقيق فيه هو إعدادات منفذ العرض لمكالمات السحب ذات الصلة ، وكذلك ما إذا كانت هناك أي معلمات تظليل يبدو أنها مستمدة من دقة المخزن المؤقت أو تعتمد عليها. نجد ما يلي لتمريرة AO الرئيسية:
نرى كلاً من إعدادات عرض 800 × 450 متشددة بالإضافة إلى مخزن مؤقت للتظليل البكسل المشبوه للغاية المستخدم في مكالمة السحب هذه. لتفسير القيمتين الأولين ، فإن بعض خلفية الرسومات و/أو الخبرة مفيدة للغاية مرة أخرى. اشتبهت على الفور في أنها كانت أحجام البكسل بدقة الإدخال الأصلية للمخزن المؤقت - غالبًا ما يتم توفيرها إلى التظليلات حتى يتمكنوا من الوصول إلى قيم الحي. وبالفعل ، 1.0/1600 = 0.000625 ، وينطبق الشيء نفسه على الدقة الرأسية في المكون الثاني.
بالنسبة لجميع التمريرات الأخرى التي تشارك في هذه الحالة بالذات ، فإن الموقف متشابه جدًا - يجب تعديل كل من معلمة العرض والمعلمة ذات الصلة ، ولن أخوض في التفاصيل بالنسبة لهم جميعًا. بالنسبة لتمريرات Z التسلسل الهرمي ، من المهم بشكل خاص التأكد من تحديد مستوى MIP الصحيح الذي يتم استخدامه كإحالة ، لأنه يؤثر على إعداد المعلمات.
في الواقع ، يعد تعديل منصات العرض وخاصة معلمات التظليل أمرًا صعبًا بعض الشيء. هناك طرق متعددة للقيام بذلك ، وقد أهدرت بعض الوقت في طرق لم تخرج. بشكل عام ، هناك على الأقل هذه الخيارات:
عندما يكون ذلك ممكنًا ، أعتبر أن أول هذه الخيارات هو أنظف ، لكنه لم ينجح في 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 رؤوس ، هدف واحد ، تنسيقات محددة ، أحجام محددة) ، نتحقق مما إذا كان نسيج RenderTarget لدينا يحتوي على حجم مختلف عن مُعرفة العرض حاليًا ، وإذا كان الأمر كذلك ، فسنقوم بضبط منفذ العرض وضبط مخزنًا مؤقتًا جديدًا في الفتحة المقابلة.
مع هذه التغييرات ، نحصل الآن على انسداد المحيط عالي الدقة: 
لا يزال هناك بعض القطع الأثرية ، ويمكن أن يكون أفضل ، لكنه تحسن كبير للغاية.
ومع ذلك ، فإن إطارنا النهائي - أي إخراج اللعبة الفعلي - لا يزال مكسورًا تمامًا. لإصلاح هذا ، نحتاج إلى العودة إلى المقارنة المرجعية لدينا. وبهذه الطريقة ، يمكننا تحديد أن هناك 800 × 450 من العازلة/الاستنسل من التنسيق D24_UNORM_S8_UINT الذي يحتاج إلى تعديله لمطابقة أحجامنا المخزن المؤقت AO الجديدة ، وإلا فإن واجهة برمجة التطبيقات (بحق) سترفض أي عرض لاحقًا في الإطار.
يمكن العثور على الرمز الكامل مع هذه المشكلة في code.cpp . لاحظ أنه يتضمن أيضًا Nier: A Bloom Buffer Fix ، الذي يعمل بمبدأ مماثل.