في الآونة الأخيرة ، بدأت التعامل مع عرض The Witcher 3. هذه اللعبة لديها تقنيات تقديم مذهلة. بالإضافة إلى ذلك ، فهي رائعة من حيث المؤامرة / الموسيقى / اللعب.
في هذه المقالة ، سأتحدث عن الحلول المستخدمة لتقديم The Witcher 3. لن تكون شاملة مثل تحليل رسومات
GTA V بواسطة Adrian Correger ، على الأقل في الوقت الحالي.
سنبدأ بهندسة عكسية لتصحيح النغمة.
الجزء 1: تصحيح النغمة
في معظم ألعاب AAA الحديثة ، فإن إحدى خطوات العرض هي بالضرورة تصحيح النغمة.
دعني أذكرك أنه في الحياة الواقعية هناك نطاق واسع من السطوع ، بينما على شاشات الكمبيوتر يكون محدودًا جدًا (8 بت لكل بكسل ، مما يمنحنا 0-255). هذا هو المكان الذي يأتي فيه التنغيم في الإنقاذ ، مما يسمح لك بتناسب نطاق أوسع في فترة إضاءة محدودة. عادة ، هناك مصدران للبيانات في هذه العملية: صورة HDR بنقطة عائمة ، تتجاوز قيم ألوانها 1.0 ومتوسط إضاءة المشهد (يمكن حساب الأخير بعدة طرق ، حتى مع مراعاة تكيف العين لمحاكاة سلوك العين البشرية ، ولكن لا يهم هنا).
الخطوة التالية (والأخيرة) هي الحصول على سرعة الغالق ، وحساب اللون بسرعة الغالق ومعالجته باستخدام منحنى تصحيح النغمة. وهنا يصبح كل شيء مربكًا تمامًا ، لأن المفاهيم الجديدة تظهر ، مثل "النقطة البيضاء" (النقطة البيضاء) و "الرمادي الأوسط" (الرمادي الأوسط). هناك على الأقل عدد قليل من المنحنيات الشائعة ، وبعضها مغطى في
نظرة أقرب إلى مات بيتينيو
على رسم خرائط النغمات .
بصراحة ، كانت لدي دائمًا مشاكل في التنفيذ الصحيح لتصحيح النغمة في الكود الخاص بي. هناك على الأقل
بعض الأمثلة المختلفة على الإنترنت التي كانت مفيدة لي ... إلى حد ما. البعض منهم يأخذ في الاعتبار سطوع HDR / النقطة البيضاء / الرمادي المتوسط ، والبعض الآخر لا يفعل ذلك - وبالتالي لا يساعدون حقًا. كنت أرغب في العثور على تنفيذ "تم اختباره في المعركة".
سنعمل في RenderDoc مع التقاط هذا الإطار لأحد المهام الرئيسية لـ Novigrad. جميع الإعدادات كحد أقصى:
بعد أن بحثت قليلاً ، وجدت دعوة سحب لتصحيح النغمة! كما ذكرت أعلاه ، هناك مخزن مؤقت لألوان HDR (رقم المادة 0 ، دقة كاملة) ومتوسط سطوع المشهد (رقم المادة 1 ، 1x1 ، النقطة العائمة ، المحسوبة سابقًا بواسطة جهاز تظليل الحوسبة).
دعونا نلقي نظرة على رمز المجمّع لتظليل البكسل:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret
هناك عدة نقاط جديرة بالملاحظة. أولاً ، لا يجب أن يساوي السطوع المحمل الاستخدام المستخدم ، لأنه محدود (مكالمات بحد أقصى / دقيقة) ضمن القيم التي اختارها الفنانون (من المخزن المؤقت المستمر). هذا مناسب لأنه يسمح لك بتجنب سرعة الغالق العالية أو المنخفضة جدًا للمشهد. تبدو هذه الخطوة شائعة جدًا ، لكنني لم أفعلها من قبل. ثانيًا ، سيتعرف شخص على دراية بمنحنيات تصحيح النغمة على الفور على قيمة "11.2" ، لأن هذه هي في الواقع قيمة النقطة البيضاء من منحنى
تصحيح النغمة Uncharted2 الخاص بـ John Hable.
يتم تحميل معلمات AF من cbuffer.
لذا ، لدينا ثلاث معلمات أخرى: cb3_v16.x ، cb3_v16.y ، cb3_v16.z. يمكننا فحص معانيها:
حدسي:
أعتقد أن "x" هو نوع من "المقياس الأبيض" أو الرمادي المتوسط ، لأنه مضروب في 11.2 (السطر 4) ، وبعد ذلك يتم استخدامه كبسط في حساب إعداد سرعة الغالق (السطر 10).
"Y" - أسميتها "عامل البسط u2" ، وسرعان ما سترى السبب.
"Z" هي "معلمة الأسي" ، لأنها تُستخدم في log / mul / exp الثلاثي (في الواقع ، في الأسي).
لكن تعامل مع هذه الأسماء المتغيرة بدرجة من الشك!
أيضًا:
cb3_v4.yz - قيم الحد الأدنى / الأقصى للسطوع المقبول ،
cb3_v7.xyz - معلمات AC لمنحنى Uncharted2 ،
cb3_v8.xyz - معلمات DF لمنحنى Uncharted2.
الآن دعنا ننتقل إلى الجزء الصعب - سنكتب تظليل HLSL الذي سيعطينا بالضبط نفس رمز المجمع.
قد يكون هذا صعبًا جدًا ، وكلما طال التظليل ، زادت صعوبة المهمة. لحسن الحظ ، كتبت منذ وقت طويل أداة لتصفح hlsl-> asm بسرعة.
سيداتي وسادتي ... نرحب D3DShaderDisassembler!
بعد أن جربت الشفرة ، حصلت على
تصحيح نغمة HLSL الجاهز
The Witcher 3 :
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); }
لقطة شاشة من الأداة المساعدة الخاصة بي لتأكيد ذلك:
فويلا!
أعتقد أن هذا تنفيذ دقيق إلى حد ما لتصحيح نغمة TW3 ، على الأقل من حيث رمز المجمع. لقد قمت بتطبيقه بالفعل في إطار العمل الخاص بي ويعمل بشكل رائع!
قلت "كفاية" لأنني لا أعلم لماذا يصبح المقام في ToneMapU2Func كحد أقصى عند الصفر. عند القسمة على 0 ، يجب أن تحصل على غير محدد؟
يمكن الانتهاء من هذا ، ولكن عن طريق الصدفة تقريبًا وجدت في هذا الإطار نسخة أخرى من تظليل نغمة TW3 ، تستخدم لغروب الشمس الجميل (من المثير للاهتمام أنها تستخدم مع الحد الأدنى من إعدادات الرسومات!)
دعونا نتحقق من ذلك. أولاً ، كود المجمّع للتظليل:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret
في البداية ، قد يبدو الرمز مخيفًا ، ولكن في الواقع ، ليس كل شيء سيئًا للغاية. بعد تحليل موجز ، ستلاحظ أن هناك مكالمتين إلى وظيفة Uncharted2 مع مجموعات مختلفة من بيانات الإدخال (AF ، الحد الأدنى / الحد الأقصى للسطوع ...). لم يسبق لي أن رأيت مثل هذا القرار من قبل.
و HLSL:
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); }
هذا ، في الواقع ، لدينا مجموعتان من معلمات التحكم ، نحسب لونين مع تصحيح النغمة ، وفي النهاية نقوم بتقديرها. قرار ذكي!
الجزء 2: تكيف العين
الجزء الثاني سيكون أبسط بكثير.
في الجزء الأول ، أوضحت كيفية إجراء تصحيح الدرجة اللونية في TW3. لشرح الخلفية النظرية ، ذكرت بإيجاز تكيف العين. وهل تعرف ماذا؟ في هذا الجزء سأتحدث عن كيفية تحقيق هذا التكيف للعين.
لكن انتظر ، ما هو التكيف مع العين ولماذا نحتاجه؟
ويكيبيديا تعرف كل شيء عنها ، لكنني سأوضح: تخيل أنك في غرفة مظلمة (تذكر أن الحياة غريبة) أو في كهف ، والخروج حيث يكون الضوء. على سبيل المثال ، قد يكون مصدر الإضاءة الرئيسي هو الشمس.
في الظلام ، يتوسع تلاميذنا بحيث يدخل المزيد من الضوء إلى شبكية العين من خلالها. عندما يصبح الضوء ، ينخفض تلاميذنا وأحيانًا نغلق أعيننا لأنه "يؤلم".
لا يحدث هذا التغيير على الفور. يجب أن تتكيف العين مع التغيرات في السطوع. هذا هو السبب في أننا نقوم بإجراء تكيف العين في العرض في الوقت الحقيقي.
يعتبر
HDRToneMappingCS11 من DirectX SDK مثالاً جيدًا على الوقت الذي يكون فيه نقص التكيف ملحوظًا. التغييرات الحادة في السطوع المتوسط مزعجة وغير طبيعية إلى حد ما.
دعنا نبدأ! من أجل الاتساق ، سنقوم بتحليل نفس الإطار من Novigrad.
الآن سوف نتعمق في برنامج التقاط الإطار RenderDoc. عادة ما يتم إجراء تعديل للعين قبل تصحيح الدرجة اللونية مباشرة ، ولا يكون Witcher 3 استثناءً.
دعونا نلقي نظرة على حالة تظليل بكسل:
لدينا مصدران للإدخال - 2 مادة ، R32_FLOAT ، 1x1 (بكسل واحد). تحتوي المادة 0 على متوسط سطوع المشهد من الإطار السابق. الملمس 1 يحتوي على متوسط سطوع المشهد من الإطار الحالي (محسوبًا مباشرة قبل هذا الجهاز المظلل - قمت بوضع علامة عليه باللون الأزرق).
من المتوقع أن يكون هناك ناتج واحد - R32_FLOAT ، 1x1. دعونا نلقي نظرة على تظليل بكسل.
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret
رائع ، كم هو بسيط! فقط 7 أسطر من كود المجمع. ما الذي يحدث هنا؟ سأشرح كل سطر:
0) احصل على متوسط السطوع للإطار الحالي.
1) احصل على متوسط سطوع الإطار السابق.
2) إجراء فحص: هل السطوع الحالي أقل من أو يساوي سطوع الإطار السابق؟
إذا كانت الإجابة بنعم ، ينخفض السطوع ، إذا لم يكن كذلك ، فإن السطوع يزداد.
3) احسب الفرق:
الفرق = الحالية Lum - previousLum.4) هذا النقل الشرطي (movc) يعين عامل السرعة من المخزن المؤقت الثابت. يمكن تعيين قيمتين مختلفتين من السطر 2 ، اعتمادًا على نتيجة الفحص. هذه خطوة ذكية ، لأنه بهذه الطريقة يمكنك الحصول على سرعات تكيف مختلفة لكل من خفض وزيادة السطوع. ولكن في الإطار المدروس ، فإن كلا القيمتين هي نفسها وتتنوع من 0.11 إلى 0.3.
5) الحساب النهائي للسطوع
المتكيف : التكيف مع
الإنارة = سرعة العامل * الفرق + الإنارة السابقة.6) نهاية التظليل
يتم تنفيذ ذلك في HLSL بكل بساطة:
// The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; }
تعطينا هذه الأسطر نفس رمز المجمع. أقترح فقط استبدال نوع الإخراج بـ
float4 بـ
float . لا حاجة لإضاعة عرض النطاق الترددي. هذه هي الطريقة التي تنفذ Witcher 3 التكيف العين. بسيطة جدا ، أليس كذلك؟
ملاحظة. شكراً جزيلاً لـ Baldur Karlsson (Twitter:
baldurk ) لـ RenderDoc. البرنامج رائع.
الجزء 3: الزيغ اللوني
الزيغ اللوني تأثير موجود بشكل رئيسي في العدسات الرخيصة. يحدث ذلك لأن العدسات لها مؤشرات انكسار مختلفة لأطوال مختلفة من الضوء المرئي. نتيجة لذلك ، يظهر تشويه واضح. ومع ذلك ، لا يحبها الجميع. لحسن الحظ ، في Witcher 3 ، يكون هذا التأثير خفيًا جدًا ، وبالتالي ليس مزعجًا في طريقة اللعب (على الأقل). ولكن يمكنك إيقاف تشغيله إذا كنت ترغب في ذلك.
دعونا نلقي نظرة فاحصة على مثال لمشهد به انحراف لوني وبدونه:
وشملت الزيغ اللونيتم تعطيل الانحراف اللونيهل تلاحظ اختلافات بالقرب من الحواف؟ ولا أنا. لنجرب مشهدًا آخر:
يتم تضمين الزيغ اللوني. لاحظ التشوه الطفيف "الأحمر" في المنطقة المشار إليها.نعم ، أفضل بكثير! هنا يكون التباين بين المناطق المظلمة والخفيفة أقوى ، وفي الزاوية نرى تشوهًا طفيفًا. كما ترون ، فإن هذا التأثير ضعيف للغاية. ومع ذلك ، كنت أتساءل كيف يتم تنفيذه. دعنا ننتقل إلى الجزء الأكثر فضولاً: الرمز!
التنفيذأول شيء تفعله هو العثور على مكالمة السحب الصحيحة مع تظليل بكسل. في الواقع ، يعد الانحراف اللوني جزءًا من تظليل البكسل الكبير "بعد المعالجة" ، والذي يتكون من الانحراف اللوني والتظليل وتصحيح جاما. كل هذا داخل تظليل بكسل واحد. دعنا نلقي نظرة فاحصة على كود المجمّع لتظليل البكسل:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ...
وإلى قيم cbuffer:
لذا ، دعونا نحاول فهم ما يحدث هنا. في الواقع ، cb3_v17.xy هو مركز الانحراف اللوني ، لذلك تحسب الأسطر الأولى المتجه ثنائي الأبعاد من إحداثيات texel (cb3_v17.zw = متبادل حجم إطار العرض) إلى "مركز الانحراف اللوني" وطوله ، ثم تقوم بإجراء حسابات أخرى والتحقق والفروع . عند تطبيق الانحراف اللوني ، نحسب الإزاحة باستخدام قيم معينة من المخزن المؤقت الثابت ونشوه القناتين R و G. بشكل عام ، كلما اقتربنا من حواف الشاشة ، كلما كان التأثير أقوى. الخط 10 مثير للاهتمام لأنه يجعل البكسلات "تقترب" ، خاصة عندما نبالغ في الانحراف. سأشارك بكل سرور إدراكي للتأثير. كالعادة ، خذ أسماء متغيرة بحصة (صلبة) من الشك. ولاحظ أن التأثير يطبق
قبل تصحيح جاما.
void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } }
أضفت "fChromaticAberrationIntensity" لزيادة حجم الإزاحة ، وبالتالي قوة التأثير ، كما يوحي الاسم (TW3 = 1.0). الكثافة = 40:
هذا كل شيء! اتمنى ان تكون قد استمتعت بهذا الجزء
الجزء 4: التظليل
التظليل هو أحد تأثيرات ما بعد المعالجة الأكثر شيوعًا المستخدمة في الألعاب. وهو مشهور أيضًا في التصوير الفوتوغرافي. يمكن أن تخلق الزوايا المظللة قليلاً تأثيرًا جميلًا. هناك عدة أنواع من التظليل. على سبيل المثال ، يستخدم
Unreal Engine 4 الطبيعي. ولكن عد إلى The Witcher 3.
انقر هنا لرؤية مقارنة تفاعلية للإطارات مع التظليل أو بدونه. المقارنة مأخوذة من
دليل أداء NVIDIA لـ The Witcher 3 .
لقطة شاشة من "The Witcher 3" مع تشغيل التظليل.لاحظ أن الزاوية العلوية اليسرى (السماء) ليست مظللة مثل الأجزاء الأخرى من الصورة. في وقت لاحق سنعود إلى هذا.
تفاصيل التنفيذأولاً ، هناك اختلاف طفيف بين التظليل المستخدم في الإصدار الأصلي من The Witcher 3 (الذي تم إصداره في 19 مايو 2015) وفي The Witcher 3: Blood and Wine. في السابق ، يتم حساب "التدرج العكسي" داخل تظليل البكسل ، وفي الأخير ، يتم حسابه مسبقًا في نسيج 256 × 256 ثنائي الأبعاد:
الملمس 256 × 256 ، يستخدم كـ "التدرج العكسي" في مكمل "الدم والنبيذ".
سأستخدم شادر من "Blood and Wine" (لعبة رائعة ، بالمناسبة). كما هو الحال في معظم الألعاب الأخرى ، يتم حساب نقش Witcher 3 في تظليل البكسل للمعالجة اللاحقة النهائية. ألق نظرة على كود المجمع:
... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ...
مثير للاهتمام! يبدو أنه يتم استخدام كل من جاما (الخط 46) والمسافات الخطية (الخط 51) لحساب التظليل. في السطر 48 ، نقوم باختبار نسيج "التدرج العكسي". cb3 [9] .xyz لا يرتبط بالتظليل. في كل إطار تم فحصه ، يتم تعيين القيمة float3 (1.0 ، 1.0 ، 1.0) ، أي أنه ربما يكون المرشح النهائي المستخدم في تأثيرات التلاشي / التلاشي. هناك ثلاث معلمات رئيسية للتظليل في TW3:
- التعتيم (cb3 [6] .w) - يؤثر على قوة التظليل. 0 - لا نقش ، 1 - نقش أقصى. ووفقًا لملاحظاتي ، فإن Witcher 3 في القاعدة يبلغ تقريبًا 1.0 ، بينما يتقلب في الدم والنبيذ حوالي 0.15.
- اللون (cb3 [7] .xyz) - ميزة ممتازة للتظليل TW3 هي القدرة على تغيير لونه. لا يجب أن يكون أسود ، ولكن في الممارسة ... عادة ما يكون له القيم العائمة 3 (3.0 / 255.0 ، 4.0 / 255.0 ، 5.0 / 255.0) وهكذا - في الحالة العامة ، هذه مضاعفات 0.00392156 = 1.0 / 255.0
- الأوزان (cb3 [6] .xyz) هي معلمة مثيرة للاهتمام للغاية. لطالما رأيت نقوشًا "مسطحة" ، على سبيل المثال:
قناع التظليل النموذجيولكن باستخدام الأوزان (السطر 52) ، يمكنك الحصول على نتائج مثيرة للاهتمام:
قناع التظليل TW3 محسوب باستخدام الأوزانالأوزان قريبة من 1.0. انظر إلى بيانات الثوابت العازلة لإطار واحد من Blood and Wine (عالم سحري بقوس قزح): هذا هو السبب في أن التظليل لم يؤثر على وحدات البكسل الساطعة للسماء أعلاه.
كودفيما يلي تنفيذي لتظليل TW3 على HLSL.
GammaToLinear = الأسرى (اللون ، 2.2)
float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; }
اتمنى انك استمتعت به يمكنك أيضًا تجربة HLSLexplorer الخاص بي ، والذي ساعدني كثيرًا في فهم رمز مجمع HLSL.كما كان من قبل ، خذ أسماء المتغيرات بدرجة من الشك - تتم معالجة تظليل TW3 بواسطة D3DStripShader ، لذلك في الواقع لا أعرف شيئًا عنها تقريبًا ، لا يسعني إلا أن أخمن. بالإضافة إلى ذلك ، أنا لا أتحمل أي مسؤولية عن الأضرار التي لحقت بمعداتك من خلال هذا جهاز التظليل ؛)المكافأة: حساب التدرجفي The Witcher 3 ، الذي تم إصداره في عام 2015 ، تم حساب التدرج العكسي في تظليل البكسل ، بدلاً من أخذ عينات نسيج محسوب مسبقًا. ألق نظرة على كود المجمع: 35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000)
لحسن الحظ بالنسبة لنا ، إنها بسيطة للغاية. على HLSL ، ستبدو كما يلي: float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; }
أي أننا ببساطة نحسب المسافة من المركز إلى النسيج ، ونقوم ببعض السحر معها (الضرب ، التشبع ...) ، ثم ... حساب كثير الحدود! رائع.الجزء الخامس: تأثير التسمم
دعونا نرى كيف تنفذ لعبة "The Witcher 3: Wild Hunt" تأثير التسمم. إذا لم تكن قد قمت بتشغيله بعد ، فقم بإسقاط كل شيء ، وشراء وتشغيل ، ومشاهدة فيديو:مساء:الليل:أولاً ، نرى صورة مزدوجة ودوامة ، غالبًا ما تظهر عندما تشرب في الحياة الحقيقية. كلما زادت مسافة البكسل عن مركز الصورة ، زاد تأثير التدوير. لقد نشرت الفيديو الثاني عن قصد مع الليل ، لأنه يمكنك رؤية هذا الدوران بوضوح على النجوم (انظر 8 نقاط منفصلة؟)الجزء الثاني من تأثير التسمم ، والذي قد لا يكون ملحوظًا على الفور ، هو تغيير طفيف في التكبير. يمكن ملاحظته بالقرب من المركز.من الواضح أنه من الواضح أن هذا التأثير هو ما بعد المعالجة النموذجية (تظليل البكسل). ومع ذلك ، قد لا يكون موقعه في خط أنابيب التقديم واضحًا جدًا. اتضح أن تأثير التسمم يتم تطبيقه مباشرة بعد تصحيح الدرجة اللونية مباشرة قبل طمس الحركة (الصورة "في حالة سكر" هي مدخل لطمس الحركة).لنبدأ الألعاب برمز المجمع: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
يتم استخدام مخزنين ثابتين منفصلين هنا. دعونا نتحقق من قيمهم:نحن مهتمون ببعض منها:cb0_v0.x -> الوقت المنقضي (بالثواني)cb0_v1.xyzw - حجم إطار العرض ومقاس حجم إطار العرض (المعروف أيضًا باسم "حجم البكسل")cb3_v0.x - الدوران حول بكسل ، دائمًا ما تكون القيمة 1.0.cb3_v0.y - حجم تأثير التسمم. بعد تشغيله ، لا يعمل بكامل قوته ، ولكنه يزداد تدريجيًا من 0.0 إلى 1.0.cv3_v1.xy - إزاحة البكسل (المزيد عن هذا أدناه). هذا زوج خطيئة / كوس ، لذا يمكنك استخدام (الزمن) في تظليل إذا كنت ترغب في ذلك.cb3_v2.xy هو مركز التأثير ، وعادة ما يكون float2 (0.5 ، 0.5).هنا نريد أن نركز على فهم ما يحدث ، وليس فقط إعادة كتابة تظليل العمياء.سنبدأ بالسطر الأول: ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w
أدعو الخط 0 "نسبة التكبير" وسترى لماذا قريبا. مباشرة بعده (السطر 1) ، نحسب "إزاحة الدوران". هذا مجرد زوج بيانات إدخال sin / cos مضروبًا في 0.05.الخطوط 2-4: أولاً ، نحسب المتجه من مركز التأثير إلى إحداثيات الأشعة فوق البنفسجية للنسيج. ثم نحسب مربع المسافة (3) والمسافة البسيطة (4) (من المركز إلى texel)إحداثيات تكبير النسيج
دعونا نلقي نظرة على رمز المجمع التالي: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy
نظرًا لأنها معبأة بهذه الطريقة ، يمكننا تحليل زوج واحد فقط من العوامة.بالنسبة للمبتدئين ، r0.yz هي "إزاحة الدوران" ، r1.z هي المسافة من المركز إلى texel ، r1.xy هو المتجه من المركز إلى texel ، r0.x هو "عامل التكبير / التصغير".لفهم هذا ، دعنا نفترض الآن أن zoomFactor = 1.0 ، أي يمكنك كتابة ما يلي: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center;
لكن ZoomFactor = 1.0: r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter;
وبالمثل بالنسبة ل r3.xy: 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center
لكن ZoomFactor = 1.0: عظيم. هذا هو ، في الوقت الحالي لدينا بشكل أساسي إزاحة دوران TextureUV (texel) ± ، ولكن ماذا عن zoomFactor؟ انظر إلى السطر 0. في الواقع ، ZoomFactor = 1.0 - 0.1 * سكران. للحد الأقصى في حالة سكر ، يجب أن تكون قيمة ZoomFactor 0.9 ، ويتم الآن حساب إحداثيات النسيج مع التكبير على النحو التالي:r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter
baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter
ربما يكون هذا التفسير أكثر بديهية: فهو ببساطة استيفاء خطي بعامل ما بين إحداثيات الملمس الطبيعية والمركز. هذه صورة "تكبير". لفهم ذلك ، من الأفضل تجربة القيم. هنا رابط إلى Shadertoy ، حيث يمكنك رؤية التأثير في العمل.تعويض الملمس
الجزء بأكمله في كود المجمع: 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset
يخلق تدرجًا معينًا ، فلنطلق عليه "قناع كثافة الإزاحة". في الواقع ، إنه يعطي معنيين. الأول موجود في r0.w (سنستخدمه لاحقًا) والثاني أقوى 5 مرات في r0.x (السطر 15). يعمل هذا الأخير في الواقع كعامل لحجم texel ، لذلك يؤثر على قوة التحيز.أخذ العينات المتعلقة بالتناوب بعدذلك ، يتم إجراء سلسلة من أخذ عينات النسيج. في الواقع ، يتم استخدام سلسلتين من 8 عينات ، واحدة لكل "جانب". في HLSL ، يمكنك كتابة هذا على النحو التالي: static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB;
الحيلة هي أننا نضيف إلى baseTexcoordsA / B إزاحة إضافية ملقاة على دائرة الوحدة ، مضروبة في "كثافة تحول إحداثيات الملمس" المذكورة سابقًا. كلما ابتعدت البكسل عن المركز ، كلما كان نصف قطر الدائرة حول البكسل أكبر - نقوم بأخذ عينات منه 8 مرات ، والتي يمكن رؤيتها بوضوح على النجوم. قيم PointsAroundPixel (مضاعفات 45 درجة):أخذ عينات من دائرة واحدة ذات تكبير / تصغيرالجزء الثاني من تأثير السكر في The Witcher 3 هو التكبير مع التكبير والتصغير. دعونا نلقي نظرة على رمز المجمع الذي يقوم بهذه المهمة: 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw
نرى أن هناك ثلاث مكالمات نسيج منفصلة ، أي ثلاثة إحداثيات نسيج مختلفة. دعونا نحلل كيف يتم حساب إحداثيات الملمس منها. لكن أولاً ، نعرض المدخلات لهذا الجزء: float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60
بضع كلمات حول المدخلات. نحن نحسب الإزاحة في texels (على سبيل المثال ، حجم texel 8.0 *) ، والذي يتم إضافته بعد ذلك إلى إحداثيات الأشعة فوق البنفسجية الأساسية. يتراوح السعة ببساطة بين 0.98 و 1.02 لإعطاء إحساس بالتكبير ، كما يفعل ZoomFactor في الجزء الذي يؤدي الدوران.لنبدأ مع الزوج الأول - r1.xy (السطر 61) r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude);
هذا هو: float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude)
دعنا نتحقق من الزوج الثاني - r3.xy (السطر 62) r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV
هذا هو: float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates;
دعنا نتحقق من الزوج الثالث - r0.xy (الخطوط 63-64) r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV
هذا هو: float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates
تتم إضافة استعلامات النسيج الثلاثة معًا ، ويتم تخزين النتيجة في السجل r1. من الجدير بالذكر أن تظليل البكسل هذا يستخدم عينة عنونة محدودة.تجميع كل شيء معًا، في الوقت الحالي لدينا نتيجة التدوير في سجل r2 وثلاثة طلبات تكبير مطوية في سجل r1. دعونا نلقي نظرة على الأسطر الأخيرة من كود المجمع: 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
حول المدخلات الإضافية: يتم أخذ r0.w من السطر 7 ، وهذا هو قناع الشدة لدينا ، و cb3 [0] .y هو حجم تأثير التسمم.دعونا نرى كيف يعمل. توجهي الأول كان القوة الغاشمة: float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor;
ولكن ماذا بحق الجحيم ، لا أحد يكتب التظليل من هذا القبيل . أخذت قلم رصاص بالورق وكتبت هذه الصيغة: finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart
حيث t = تأثيرAmount * كثافة الشدةلذا نحصل على: finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t )
ونأتي إلى ما يلي: finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity);
نعم ، تبين أن هذا الجزء من المقالة مفصل للغاية ، ولكننا انتهينا أخيرًا! شخصيا ، لقد تعلمت شيئا في عملية الكتابة ، وآمل أن تفعل ذلك أيضا!إذا كنت مهتمًا ، فإن مصادر HLSL الكاملة متاحة هنا . لقد قمت باختبارها باستخدام HLSLexplorer الخاص بي ، وعلى الرغم من عدم وجود مراسلات مباشرة من شخص لآخر مع جهاز التظليل الأصلي ، إلا أن الاختلافات صغيرة جدًا (أقل من سطر واحد) يمكنني القول بثقة أنها تعمل. شكرا للقراءة!