كيف يتم تنفيذ تقديم Witcher 3: البرق ، ميل witcher ، وغيرها من الآثار

صورة

الجزء 1. السوستة


في هذا الجزء ، سننظر في عملية تقديم البرق في Witcher 3: Wild Hunt.

يتم تنفيذ تجسيد البرق في وقت متأخر قليلاً عن تأثير ستارة المطر ، ولكن لا يزال يحدث في ممر التجسيد المباشر. يمكن ملاحظة البرق في هذا الفيديو:


تختفي بسرعة كبيرة ، لذلك من الأفضل مشاهدة الفيديو عند 0.25.

يمكنك أن ترى أن هذه ليست صورًا ثابتة ؛ بمرور الوقت ، يتغير سطوعها قليلاً.

من ناحية تقديم الفروق الدقيقة ، هناك الكثير من أوجه التشابه مع رسم ستارة مطر في المسافة ، على سبيل المثال ، حالات المزج نفسها (المزج الإضافي) والعمق (تمكين الفحص ، لا يتم تسجيل العمق).


المشهد دون البرق


مشهد البرق

من حيث هندسة البرق ، فإن The Witcher 3 عبارة عن شبكة تشبه الأشجار. يمثل هذا المثال من البرق الشبكة التالية:


لديها إحداثيات الأشعة فوق البنفسجية وناقلات طبيعية. كل هذا مفيد في مرحلة تظليل قمة الرأس.

فيرتكس شادر


دعونا نلقي نظرة على رمز تظليل قمة الرأس المجمعة:

vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret 

هناك العديد من أوجه التشابه مع ستارة المطر تظليل قمة الرأس ، لذلك لن أكرر. أريد أن أبين لك الفرق المهم في السطور 11-18:

  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 

أولاً ، cb1 [8] .xyz هو موضع الكاميرا ، و r2.xyz هو الموضع في الفضاء العالمي ، أي أن السطر 11 يحسب المتجه من الكاميرا إلى الموضع في العالم. ثم خطوط 12-15 حساب الطول (worldPos - cameraPos) * 0.000001.

v2.xyz هي المتجه الطبيعي للهندسة الواردة. يمتد السطر 16 من الفاصل الزمني [0-1] إلى الفاصل الزمني [-1 ؛ 1].

ثم يتم احتساب المركز النهائي في العالم:

finalWorldPos = worldPos + length (worldPos - cameraPos) * 0.000001 * normalVector
سيكون مقتطف شفرة HLSL لهذه العملية مثل هذا:

  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output; 

تؤدي هذه العملية إلى "انفجار" صغير للشبكة (في اتجاه المتجه العادي). لقد جربت استبدال 0.000001 بعدة قيم أخرى. وهنا النتائج:


0.000002


0.000005


0.00001


0.000025

بكسل تظليل


حسنًا ، اكتشفنا تظليل قمة الرأس ، لقد حان الوقت الآن للوصول إلى رمز المجمّع لتظليل البكسل!

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret 

خبر جيد: الكود ليس طويلاً.

الأخبار السيئة:

  3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 

... ما هو كل شيء؟

بصراحة ، ليست هذه هي المرة الأولى التي رأيت فيها قطعة من كود المجمّع في تظليل Witcher 3. لكن عندما قابلته للمرة الأولى ، فكرت: "ما هذا بحق الجحيم؟"

يمكن العثور على شيء مشابه في بعض تظليل TW3 الأخرى. لن أصف مغامراتي مع هذه القطعة ، وأقول فقط إن الإجابة تكمن في الضوضاء الصحيحة :

  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } 

كما ترون ، في تظليل بكسل يطلق عليه مرتين. باستخدام الأدلة من هذا الموقع ، يمكننا أن نفهم كيف يتم تنفيذ الضوضاء السلسة بشكل صحيح. سوف أعود إلى هذا في غضون دقيقة.

انظر إلى السطر 0 - نحن هنا مفعولون بالاعتماد على الصيغة التالية:

حركة = elapsedTime * animationSpeed ​​+ TextureUV.x
هذه القيم ، بعد التقريب إلى الجانب السفلي ( الأرضية ) (إرشادات round_ni ) في المستقبل ، تصبح نقاط إدخال للضوضاء الصحيحة. عادةً ما نقوم بحساب قيمة الضوضاء لاثنين من الأعداد الصحيحة ، ثم نقوم بحساب القيمة النهائية المقربة بينهما (انظر موقع libnoise على الويب للاطلاع على التفاصيل).

حسنًا ، هذه ضوضاء عدد صحيح ، ولكن بعد كل شيء ، فإن جميع القيم المذكورة سابقًا (تقريبًا أيضًا لأسفل) تطفو!

لاحظ أنه لا توجد تعليمات ftoi هنا . أفترض أن المبرمجين من CD Projekt Red قد استخدموا وظيفة HLSL asint الداخلية هنا ، والتي تقوم بتحويل قيم الفاصلة العائمة "reinterpret_cast" وتعاملهم كنمط صحيح.

يتم حساب وزن الاستيفاء للقيمتين في الأسطر 10-11.

الاستيفاء الوزن = 1.0 - فارك (الرسوم المتحركة) ؛
هذا النهج يسمح لنا بالتحريف بين القيم مع مرور الوقت.

لإنشاء ضوضاء سلسة ، يتم تمرير هذا المحرف إلى وظيفة SCurve :

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } 


وظيفة Smoothstep [libnoise.sourceforge.net]

تُعرف هذه الميزة باسم "ناعم". ولكن كما ترون من كود المجمع ، هذه ليست وظيفة نعومة داخلية من HLSL . تطبق الوظيفة الداخلية القيود بحيث تكون القيم صحيحة. ولكن نظرًا لأننا نعرف أن وزن الاستيفاء سيكون دائمًا في النطاق [0-1] ، فيمكن تخطي عمليات الفحص هذه بأمان.

عند حساب القيمة النهائية ، يتم استخدام العديد من عمليات الضرب. تعرف على كيفية تغيير مخرجات ألفا النهائية اعتمادًا على قيمة الضوضاء. هذا مناسب لأنه سيؤثر على عتامة البرق المقدم ، تمامًا كما في الحياة الحقيقية.

استعداد تظليل بكسل:

  cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); } 

لتلخيص


في هذا الجزء ، وصفت طريقة لتقديم البرق في The Witcher 3.

أنا مسرور جدًا لأن رمز المجمّع الذي خرج من تظليلي يتوافق تمامًا مع النص الأصلي!


الجزء 2. سخيفة سكاي الخدع


سيكون هذا الجزء مختلفًا قليلاً عن السابق. في ذلك ، أريد أن أريك بعض جوانب تظليل السماء Witcher 3.

لماذا "الحيل سخيفة" وليس تظليل كامل؟ حسنا ، هناك عدة أسباب. أولاً ، تظليل السماء Witcher 3 هو وحش معقد إلى حد ما. يحتوي تظليل البكسل من إصدار 2015 على 267 سطرًا من رمز المجمّع ، بينما يحتوي التظليل من Blood and Wine DLC على 385 سطرًا.

علاوة على ذلك ، يحصلون على الكثير من المدخلات ، والتي لا تؤدي إلى عكس هندسة كود HLSL الكامل (والقابل للقراءة!).

لذلك ، قررت إظهار جزء فقط من الحيل من هذه تظليل. إذا وجدت شيئًا جديدًا ، فسوف أكمل المشاركة.

الاختلافات بين إصدار 2015 و DLC (2016) ملحوظة للغاية. على وجه الخصوص ، تشمل الاختلافات في حساب النجوم وميضها ، طريقة مختلفة لجعل الشمس ... حتى تظليل Blood and Wine يحسب درب التبانة في الليل.

سأبدأ بالأساسيات ثم أتحدث عن الحيل الغبية.

الأساسيات


مثل معظم الألعاب الحديثة ، يستخدم Witcher 3 skydome لتصميم السماء. انظر إلى نصف الكرة المستخدم لهذا في Witcher 3 (2015). ملاحظة: في هذه الحالة ، يكون المربع المحيط لهذه الشبكة في النطاق من [0،0،0] إلى [1،1،1] (Z هو المحور الذي يشير لأعلى) وقام بتوزيع الأشعة فوق البنفسجية بسلاسة. في وقت لاحق نستخدمها.


الفكرة وراء skydome تشبه فكرة skybox (الفرق الوحيد هو الشبكة المستخدمة). في مرحلة تظليل قمة الرأس ، نقوم بتحويل الكود المرتبط بالمراقب (عادةً ما يكون متفقًا مع موضع الكاميرا) ، مما يخلق الوهم بأن السماء بعيدة جدًا بالفعل - لن نصل إليها أبدًا.

إذا قرأت الأجزاء السابقة من هذه السلسلة من المقالات ، فأنت تعلم أن "The Witcher 3" يستخدم عمق معكوس ، أي أن المستوى البعيد هو 0.0f ، والأقرب هو 1.0f. لاستكمال إخراج skydome على المستوى البعيد ، في معلمات نافذة التصفح ، قمنا بتعيين MinDepth على نفس قيمة MaxDepth :


لمعرفة كيفية استخدام حقلي MinDepth و MaxDepth أثناء تحويل نافذة الاستعراض ، انقر هنا (docs.microsoft.com).

فيرتكس شادر


لنبدأ مع تظليل قمة الرأس. في Witcher 3 (2015) ، يكون رمز تظليل المجمّع كما يلي:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret 

في هذه الحالة ، يقوم تظليل قمة الرأس بنقل texcoords فقط وموضع في الفضاء العالمي إلى الإخراج. في Blood and Wine ، يعرض أيضًا ناقلًا طبيعيًا طبيعيًا. سوف أفكر في إصدار 2015 لأنه أبسط.

انظر إلى المخزن المؤقت الثابت المعين كـ cb2 :


هنا لدينا مصفوفة من العالم (تحجيم موحد بنسبة 100 ونقل نسبة إلى وضع الكاميرا). لا شيء معقد. cb2_v4 و cb2_v5 هما عوامل الحجم / الانحراف المستخدمة لتحويل مواضع الرأس من الفاصل الزمني [0-1] إلى الفاصل الزمني [-1 ؛ 1]. ولكن هنا ، هذه المعاملات "ضغط" المحور Z (أعلى).


في الأجزاء السابقة من السلسلة ، كان لدينا تظليل قمة الرأس مماثلة. تهدف الخوارزمية العامة إلى نقل texcoords إلى أبعد من ذلك ، ثم يتم احتساب Position مع مراعاة معاملات المقياس / الانحراف ، ثم يتم حساب PositionW في الفضاء العالمي ، ثم يتم احتساب الموضع النهائي لمساحة القطع عن طريق ضرب matWorld و matViewProj -> يتم استخدام المنتج الخاص بهما بضرب Position للحصول على SV_Position النهائي .

لذلك ، يجب أن يكون HLSL لتظليل قمة الرأس مثل هذا:

  struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; } 

مقارنة ظلالي (يسار) والأصل (يمين):


خاصية ممتازة لـ RenderDoc هي أنها تسمح لنا بضخ تظليلنا بدلاً من التظليل الأصلي ، وسوف تؤثر هذه التغييرات على خط الأنابيب حتى نهاية الإطار. كما ترون من كود HLSL ، لقد قدمت العديد من الخيارات لتكبير وتحويل الهندسة النهائية. يمكنك تجربة معهم والحصول على نتائج مضحكة للغاية:


فيرتكس شادر التحسين


هل لاحظت مشكلة تظليل قمة الرأس الأصلي؟ الضرب قمة الرأس من مصفوفة مصفوفة زائدة تماما! لقد وجدت هذا في عدد قليل من تظليل قمة الرأس (على سبيل المثال ، في التظليل ستارة من المطر في المسافة ). يمكننا تحسينه عن طريق ضرب PositionW على الفور بواسطة matViewProj !

لذلك ، يمكننا استبدال هذا الرمز بـ HLSL:

  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); 

على النحو التالي:

  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW ); 

الإصدار الأمثل يعطينا رمز التجميع التالي:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret 

كما ترون ، قمنا بتقليل عدد التعليمات من 26 إلى 12 - وهو تغيير مهم إلى حد ما. لا أعرف مدى انتشار هذه المشكلة في اللعبة ، ولكن من أجل الله ، CD Projekt Red ، ربما أطلق رقعة؟ :)

وأنا لا أمزح. يمكنك إدراج التظليل المحسن الخاص بي بدلاً من RenderDoc الأصلي ، وسوف ترى أن هذا التحسين لا يؤثر بصريًا على أي شيء. بصراحة ، لا أفهم لماذا قررت شركة CD Projekt Red إجراء عملية الضرب القصوى لمصفوفة بمصفوفة ...

الشمس


في The Witcher 3 (2015) ، يتكون حساب الانتثار الجوي والشمس من نداءين منفصلين للرسم:


ويتشر 3 (2015) - حتى


ويتشر 3 (2015) - مع السماء


ويتشر 3 (2015) - مع السماء + الشمس

يشبه تجسيد الشمس في إصدار عام 2015 إلى حد بعيد تجسيد القمر من حيث الشكل الهندسي وحالات الخلط / العمق.

من ناحية أخرى ، في "الدم والنبيذ" تظهر السماء مع الشمس في مسار واحد:


ويتشر 3: الدم والنبيذ (2016) - إلى الجنة


ويتشر 3: الدم والنبيذ (2016) - مع الجنة والشمس

بغض النظر عن طريقة تجسيدك للشمس ، فأنت في مرحلة ما ما زلت بحاجة إلى الاتجاه (الطبيعي) لأشعة الشمس. الطريقة الأكثر منطقية للحصول على هذا المتجه هي استخدام الإحداثيات الكروية . في الواقع ، نحن بحاجة إلى اثنين فقط من القيم التي تشير إلى زاويتين (بالراديان!): Phi و theta . بعد استلامهم ، يمكننا افتراض أن r = 1 ، وبالتالي تقليله. ثم بالنسبة للإحداثيات الديكارتية مع الإشارة إلى المحور Y ، فيمكنك كتابة التعليمات البرمجية التالية في HLSL:

  float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir); 

عادة ، يتم حساب اتجاه ضوء الشمس في التطبيق ، ثم يتم تمريره إلى المخزن المؤقت الثابت للاستخدام في المستقبل.

بعد تلقي اتجاه أشعة الشمس ، يمكننا الخوض في رمز المجمّع لتظليل بكسل "الدم والنبيذ" ...

  ... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ... 

أولاً ، cb12 [0] .xyz هو موضع الكاميرا ، وفي r0.xyz نقوم بتخزين موضع قمة الرأس (هذا هو الناتج من شادر قمة الرأس). لذلك ، يحسب السطر 100 المتجه worldToCamera . لكن إلقاء نظرة على السطور 105-107. يمكننا كتابتها بشكل طبيعي (-WorldToCamera) ، أي نحسب متجه cameraToWorld الطبيعي.

  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx 

ثم نقوم بحساب منتج العددية الخاص بكاميرات مكافحة ناقلات الاتجاه و sunDirection ! تذكر أنه يجب تطبيعها. نحن أيضا تشبع هذا التعبير الكامل لقصره على الفاصل الزمني [0-1].

! ممتاز يتم تخزين هذا المنتج القياسي في r1.x. لنرى أين ينطبق بعد ذلك ...

  152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x 

الثالوث "log، mul، exp" هو الأس. كما ترون ، فإننا نرفع جيب التمام لدينا (المنتج القياسي للناقلات الطبيعية) إلى حد ما. قد تسأل لماذا. بهذه الطريقة ، يمكننا إنشاء تدرج يحاكي الشمس. (ويؤثر السطر 155 على عتامة هذا التدرج اللوني ، على سبيل المثال ، نعيد تعيينه لإخفاء الشمس تمامًا). فيما يلي بعض الأمثلة:


الأس = 54


الأس = 2400

بعد هذا التدرج ، نستخدمه للتحريف بين skyColor و sunColor ! لتجنب القطع الأثرية ، تحتاج إلى تشبع القيمة على السطر 120.

تجدر الإشارة إلى أنه يمكن استخدام هذه الخدعة لمحاكاة تيجان القمر (عند قيم الأس المنخفضة). للقيام بذلك ، نحتاج إلى متجه moonDirection ، والذي يمكن حسابه بسهولة باستخدام الإحداثيات الكروية.

قد يبدو رمز HLSL الجاهز مثل المقتطف التالي:

  float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient ); 

حركة النجوم


إذا نجحت في قضاء وقت طويل في سماء Witcher 3 الصافية الليلية ، يمكنك أن ترى أن النجوم ليست ثابتة - إنها تتحرك قليلاً عبر السماء! لقد لاحظت هذا بالصدفة تقريبًا وأردت معرفة كيفية تنفيذه.

لنبدأ بحقيقة أن النجوم في Witcher 3 يتم تقديمها كخريطة مكعبة بحجم 1024x1024x6. إذا فكرت في الأمر ، يمكنك أن تفهم أن هذا هو الحل المريح للغاية الذي يتيح لك بسهولة التقاط الاتجاهات للحصول على عينات من الخريطة المكعبة.

دعونا نلقي نظرة على رمز المجمع التالي:

  159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0 

لحساب متجه أخذ العينات النهائي (السطر 173) ، نبدأ بحساب متجه worldToCamera الطبيعي (الخطوط 159-162).

بعد ذلك نحسب منتجين متجهين (163-164 ، 165-166) باستخدام moonDirection ، وبعد ذلك نحسب ثلاثة منتجات قياسية للحصول على متجه أخذ العينات النهائي. كود HLSL:

  float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb; 

ملاحظة لنفسي: هذا رمز مصمم بشكل جيد للغاية ، وينبغي عليّ التحقيق فيه بمزيد من التفاصيل.

ملاحظة للقراء: إذا كنت تعرف المزيد عن هذه العملية ، فأخبرني!

النجوم المتلألئة


الخدعة الأخرى المثيرة للاهتمام التي أود استكشافها بمزيد من التفصيل هي وميض النجوم.على سبيل المثال ، إذا تجولت في نوفيغراد في طقس صافٍ ، فستلاحظ أن النجوم تتلألأ.

كنت فضولية كيف تم تنفيذ هذا. اتضح أن الفرق بين إصدار 2015 و "Blood and Wine" كبير جدًا. للبساطة ، سأنظر في إصدار 2015.

لذلك ، نبدأ مباشرة بعد أخذ عينات من النجوماللون من القسم السابق:

  174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx 

. .

starsColor 173 - offset . offset (r2.xyz, 235), , - (237-242) (243).

, ? , . offset . skydome — .

offset , , UV skydome (v0.xy) , (cb[0].x).

ishr/xor/and, .

, , , . , ( iadd ) ( reversebits ; bfrev ).

, . .

4 «» . , 4 :

  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B ); 

4 ( , itof ):

1 — r5.x,

2 — r4.w,

3 — r1.w,

4 — r5.y

itof ( 216) :

  217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 

S- UV, . لذلك:

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY ); 

, :

  229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb; 

offset :


بعد حساب starsColorDisturbed ، اكتمل الجزء الأصعب. الصيحة!

والخطوة التالية هي إجراء تصحيح جاما لكل من starsColor و starsColorDisturbed ، وبعد ذلك يتم ضربهما:

  starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed; 

النجوم - اللمسات الأخيرة


لدينا starsFinal في r1.xyz. في نهاية معالجة النجوم ، يحدث ما يلي:

  256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000) 

هذا أسهل بكثير مقارنة بالنجوم المتلألئة والمتحركة.

لذلك ، نبدأ برفع starsFinal إلى قوة 2.5 - وهذا يسمح لنا بالتحكم في كثافة النجوم. ذكي جدا. ثم نجعل اللون الأقصى للنجوم يساوي float3 (1 ، 1 ، 1).

cb0 [9] .w يستخدم للتحكم في الرؤية الكلية للنجوم. لذلك ، يمكننا أن نتوقع أن تكون هذه القيمة خلال النهار هي 1.0 (والتي تعطي الضرب بمقدار صفر) ، وفي الليل - 0.0.

في النهاية ، نزيد من وضوح النجوم بمقدار 10. وهذا كل شيء!

الجزء 3. الذوق ويتشر (الأجسام وخريطة السطوع)


لم تكن جميع التأثيرات والتقنيات الموصوفة سابقًا مرتبطة فعليًا بـ Witcher 3. توجد أشياء مثل تصحيح النغمة أو التظليل أو حساب متوسط ​​السطوع في كل لعبة حديثة تقريبًا. حتى تأثير التسمم واسع الانتشار.

هذا هو السبب في أنني قررت إلقاء نظرة فاحصة على آليات تقديم "غريزة witcher". جيرالت ساحر ، وبالتالي فإن مشاعره أكثر حدة من شعور الشخص العادي. وبالتالي ، يمكنه أن يرى ويسمع أكثر من الآخرين ، مما يساعده بشكل كبير في تحقيقاته. تسمح ميكانيكا الذوق الساحرة للاعب بتصور هذه الآثار.

فيما يلي عرض للتأثير:


واحد آخر ، مع إضاءة أفضل:


, : , ( ) , ( ). , , ( ). , « ( ).

, .

, — , — .


كما قلت ، هناك نوعان من الكائنات ، ونحن بحاجة إلى التمييز بينهما. في Witcher 3 ، يتم تطبيق ذلك باستخدام مخزن مؤقت للاستنسل. عند إنشاء شبكات GBuffer التي يجب تمييزها كـ "تتبعات" (أحمر) ، يتم تقديمها باستخدام الاستنسل = 8. يتم تقديم الشبكات ذات اللون الأصفر ككائنات "مثيرة للاهتمام" باستخدام stencil = 4.

على سبيل المثال ، تظهر القوامان التاليان إطارًا نموذجيًا مع غريزة witcher مرئية ومخزن الاستنسل المقابل:



استنسل العازلة موجز


غالبًا ما يتم استخدام المخزن المؤقت للاستنسل في الألعاب لتمييز الشبكات. يتم تعيين نفس الهوية لفئات معينة من الشبكات.

تتمثل الفكرة في استخدام وظيفة Always مع المشغل Replace في حالة نجاح اختبار الاستنسل ومع المشغل Keep في جميع الحالات الأخرى.

إليك كيفية تنفيذه باستخدام D3D11:

  D3D11_DEPTH_STENCIL_DESC depthstencilState; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue ); 

يتم تمرير قيمة stensil المراد كتابتها إلى المخزن المؤقت كـ StencilRef في استدعاء API:

  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... ); 

تقديم السطوع


R11G11B10_FLOAT, R G.

? , , , .

:



, .

: „“, — :


— :


— :


, , ? -!

-, , „8“ ( ) „4“.

- :


… :


? - . - :

  if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel 

حيث:
StencilRef هي القيمة التي تم تمريرها بواسطة استدعاء API ،

StencilReadMask هو القناع المستخدم لقراءة قيمة stensil (لاحظ أنه موجود على كلا الجانبين الأيسر والأيمن) ،

OP هو عامل المقارنة ، تم تعيينه عبر API ،

StencilValue هي قيمة المخزن المؤقت للاستنسل في بكسل الحالي قيد المعالجة.

من المهم أن نفهم أننا نستخدم AND AND الثنائية لحساب المعاملات.

بعد التعرف على الأساسيات ، دعونا نرى كيف يتم استخدام هذه المعلمات في مكالمات السحب هذه:


حالة الاستنسل للآثار


دولة الاستنسل لكائنات مثيرة للاهتمام

ها! كما يمكننا أن نرى ، والفرق الوحيد هو ReadMask. دعونا التحقق من ذلك! استبدل هذه القيم في معادلة اختبار الاستنسل:

  Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE 

ذكي. كما ترون ، في هذه الحالة ، لا نقارن قيمة stensil ، ولكن تحقق مما إذا كان قد تم تعيين جزء معين من المخزن المؤقت للاستنسل. كل بكسل من المخزن المؤقت الاستنسل لديه تنسيق uint8 ، لذلك الفاصل الزمني للقيم هو [0-255].

ملاحظة: ترتبط جميع مكالمات DrawIndexed (36) بتقديم آثار الأقدام كتتبعات ، وبالتالي في هذا الإطار المحدد ، تحتوي خريطة السطوع على الشكل النهائي التالي:


ولكن قبل اختبار الاستنسل هناك تظليل بكسل. كلا 28738 و 28748 استخدام نفس تظليل بكسل:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret 

render target, 24-27 .

, — ( ), 1. ( 2-6).

(cb3[7].xyz — , !), ( 7-9).

:

— cb3[0].rgb — . float3(0, 1, 0) () float3(1, 0, 0) ( ),
- cb3 [6]. عامل قياس المسافة. يؤثر مباشرة على دائرة نصف قطرها وسطوع الناتج النهائي.

في وقت لاحق ، لدينا صيغ صعبة للغاية لحساب السطوع اعتمادًا على المسافة بين جيرالت والكائن. أستطيع أن أفترض أن جميع المعاملات يتم اختيارها تجريبياً.

الإخراج النهائي هو اللون * شدة .

سيبدو رمز HLSL كالتالي:

  struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; } 

مقارنة صغيرة من الرمز الأصلي (يسار) ورمز تظليل المجمّع (يمين).


كانت هذه هي المرحلة الأولى من تأثير الذوق الساحرة . في الواقع ، هو أبسط.

الجزء 4. الذوق ويتشر (خريطة المخطط)


مرة أخرى ، ألق نظرة على المشهد الذي نستكشفه:


في الجزء الأول من تحليل تأثير غريزة الساحرة ، أوضحت كيف يتم إنشاء "خريطة السطوع".

لدينا نسيج بملء الشاشة بتنسيق R11G11B10_FLOAT ، والذي قد يبدو كالتالي:


تعني القناة الخضراء "آثار أقدام" ، الأشياء الحمراء المثيرة للاهتمام التي يمكن لـ Geralt التفاعل معها.

بعد تلقي هذا الملمس ، يمكننا الانتقال إلى المرحلة التالية - لقد أطلق عليها "خريطة الكنتور".


هذا نسيج غريب بعض الشيء بتنسيق 512x512 R16G16_FLOAT. من المهم أن يتم تنفيذه بأسلوب "ping pong". خريطة الكفاف من الإطار السابق هي بيانات الإدخال (إلى جانب خريطة السطوع) لإنشاء خريطة كفاف جديدة في الإطار الحالي.

يمكن تنفيذ المخازن المؤقتة لـ Ping-pong بعدة طرق ، لكنني شخصياً أحب ما يلي (الكود الزائف):

  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; } 

, [m_outlineIndex] , [!m_outlineIndex] , .

:

  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_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret 

, , , :

  0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 

floor( TextureUV * 2.0 ), :


:

  float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; } 

, 1.0 float2(0.0, 0.0).

. , texcoords float2(1, 0), float2(0, 1), — float2(1.0, 1.0).

لذلك:

  float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 ); 

mask , , . , mask.r mask.w :


mask.r


mask.w

حصلنا على قناع ، دعنا ننتقل. خط 15 عينات خريطة الإنارة. لاحظ أن نسيج النصوع في التنسيق R11G11B10_FLOAT ، على الرغم من أننا نأخذ عينات من جميع مكونات rgba. في هذه الحالة ، من المفترض أن .a هي 1.0f.

يمكن حساب Texcoords المستخدمة في هذه العملية كـ frac (TextureUV * 2.0) . لذلك ، قد تبدو نتيجة هذه العملية ، على سبيل المثال ، كما يلي:


رؤية التشابه؟

الخطوة التالية ذكية جدًا - يتم تنفيذ المنتج القياسي المكون من أربعة مكونات (dp4):

  16: dp4 r1.x, r1.xyzw, r2.xyzw 

( ), — ( ), — ( .w 1.0). . :


masterFilter , . , . — .

: (: 1.0/256.0!) :

  float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference); 

الآن إذا ضاعفنا عامل التصفية بواسطة maxAbsDifference ...


بسيطة جدا وفعالة.

بعد تلقي الخطوط ، نقوم بتجربة خريطة الكفاف من الإطار السابق.

بعد ذلك ، من أجل الحصول على تأثير "شبحي" ، نأخذ جزءًا من المعلمات المحسوبة على المسار الحالي والقيم من خريطة الكنتور.

قل مرحباً لصديقنا القديم - ضوضاء عدد صحيح. إنه موجود هنا. معلمات الرسوم المتحركة (cb3 [0] .zw) مأخوذة من المخزن المؤقت الثابت وتتغير بمرور الوقت.

  float2 outlines = masterFilter * maxAbsDifference; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y 

: , [-1;1] ( -). TW3 , .

, ( 1.0/512.0), .x:

  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0; 

, , , :

  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0; 

والخطوة التالية هي تشويه القيمة من خريطة الكنتور "القديمة" باستخدام الضوضاء - وهذا هو الخط الرئيسي الذي يعطي نسيج الإخراج يشعر ممتلئ الجسم.

ثم هناك حسابات أخرى ، وبعدها ، في النهاية ، يتم حساب "التوهين".

  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0); 

إليك مقطع فيديو قصير يوضح خريطة تفصيلية قيد التنفيذ:


إذا كنت مهتمًا بتظليل البكسل الكامل ، فهو متوفر هنا . شادر متوافق مع RenderDoc.

من المثير للاهتمام (ولأكون صريحًا ومزعجًا قليلاً) أنه على الرغم من هوية رمز المجمع مع التظليل الأصلي من Witcher 3 ، فإن المظهر النهائي لخريطة الكنتور في RenderDoc يتغير!

ملاحظة: في المقطع الأخير (انظر الجزء التالي) ، سترى أنه لا يتم استخدام سوى قناة r. لخريطة الكفاف. لماذا إذن نحتاج إلى قناة .g؟ أعتقد أن هذا نوع من المخزن المؤقت بينج بونج في نسيج واحد - لاحظ أن .r يحتوي على قناة .g + بعض القيمة الجديدة.

الجزء 5: الذوق ويتشر (فيش والنتيجة النهائية)


, : , , , . , .

. ! — . : , .

:



بعد:


:


, , , « », ( ) , .

:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret 

82 — , !

:

  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x ); 

القيمة الأساسية المسؤولة عن حجم التأثير هي fisheyeAmount . أعتقد أنه يرتفع تدريجياً من 0.0 إلى 1.0 عندما يبدأ جيرالت في استخدام غريزته. لا تتغير بقية القيم كثيرًا ، لكنني أظن أن بعضها سيكون مختلفًا إذا قام المستخدم بتعطيل تأثير فيش في الخيارات (لم أفحص هذا).

أول ما يحدث هنا هو أن التظليل يحسب القناع المسؤول عن الزوايا الرمادية:

  0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 

في HLSL ، يمكننا كتابة هذا على النحو التالي:

  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); } 

أولاً ، الفاصل الزمني [-1؛ 1] الأشعة فوق البنفسجية وقيمها المطلقة. ثم هناك "ضغط" صعبة. القناع النهائي هو كما يلي:


سأعود إلى هذا القناع لاحقًا.

الآن سوف أتخطى عن قصد بضعة أسطر من التعليمات البرمجية وأدرس بعناية الكود المسؤول عن تأثير التكبير / التصغير.

  22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 

أولاً ، يتم حساب إحداثيات نسيج "المضاعفة" ويتم تنفيذ الطرح 2 (1 ، 1):

  float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0); 

يمكن تصور مثل texcoord على النحو التالي:


ثم يتم حساب نقطة المنتج العددية (uv4 ، uv4) ، والتي تعطينا القناع:


الذي يستخدم لضرب texcoords أعلاه:


هام: في الزاوية اليسرى العليا (بكسل أسود) القيم سالبة. يتم عرضها باللون الأسود (0.0) بسبب الدقة المحدودة لتنسيق R11G11B10_FLOAT. لا يحتوي على علامة بت ، لذلك لا يمكن تخزين القيم السلبية فيها.

ثم يتم حساب معامل التوهين (كما قلت أعلاه ، يختلف fisheyeAmount من 0.0 إلى 1.0).

  float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation; 

ثم يتم تنفيذ التقييد (كحد أقصى / دقيقة) وضرب واحد.

وبالتالي ، يتم حساب الإزاحة. لحساب الأشعة فوق البنفسجية النهائية ، والتي سيتم استخدامها

لأخذ عينات من نسيج اللون ، فإننا ببساطة نقوم بإجراء الطرح: float2 colorUV = mainUv - offset؛

عن طريق أخذ عينات لونية colorUV المدخلة ، نحصل على صورة مشوهة بالقرب من الزوايا:


حدود


والخطوة التالية هي لأخذ عينات من خريطة الكنتور للعثور على الأكفة. الأمر بسيط للغاية ، أولاً ، نجد texcoords لأخذ عينات من معالم الأشياء المثيرة للاهتمام ، ثم القيام بنفس الشيء في المسارات:

  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y 


كائنات مثيرة للاهتمام من خريطة كفاف


آثار من خريطة الكنتور

تجدر الإشارة إلى أننا نأخذ عينات من قناة .x فقط من خريطة الكنتور ونأخذ في الاعتبار المربعات العليا فقط.

حركة


, . 8 , .

, 8.0.

[0-1] 2 , 1 :


, , . 15-21. , (, - ). (15-21) (41-42):

  15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 

كما ترون ، لا نعتبر سوى الإطارات من [0.00 - 0.03] بجانب كل سطح ، ونلخص قيمها ، واضربها 20 وشبعتها. إليك الشكل الذي تبدو عليه بعد الخطوط 15-21:


وإليك كيف بعد السطر 41:


في السطر 42 ، نضرب هذا في 0.03 ، هذه القيمة هي نصف قطر الدائرة للشاشة بأكملها. كما ترون ، أقرب إلى حواف الشاشة ، يصبح نصف القطر أصغر.

الآن يمكننا أن ننظر إلى رمز المجمّع المسؤول عن الحركة:

  40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 

دعنا نبقى هنا لمدة دقيقة. في السطر 40 ، نحصل على معامل الوقت - فقط بعد انقضاء الوقت * 0.1 . في السطر 43 ، لدينا مخزن مؤقت لملمس الألوان الذي تم الحصول عليه داخل الحلقة.

r0.x (الأسطر 41-42) ، كما نعرف الآن ، نصف قطر الدائرة. r4.x (السطر 44) هو الخطوط العريضة للكائنات المثيرة للاهتمام ، r4.y (السطر 45) هو الخطوط العريضة للمسارات (مقسومة سابقًا على 8!) ، و r4.z (السطر 46) هو عداد الحلقة.

كما تتوقع ، تحتوي الحلقة على 8 تكرارات. نبدأ بحساب الزاوية في راديان i * PI_4 ، والتي تعطينا 2 * PI - دائرة كاملة. زاوية مشوهة مع مرور الوقت.

باستخدام sincos ، نحدد نقطة أخذ العينات (دائرة الوحدة) ونغير نصف القطر باستخدام الضرب (السطر 54).

بعد ذلك ، ندور حول البيكسل في دائرة ونختبر ملامحنا وألواننا. بعد الدورة ، نحصل على متوسط ​​القيم (بسبب القسمة على 8) للخطوط والألوان.

  float timeParam = time * 0.1; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; } 

سيتم تنفيذ أخذ عينات الألوان بنفس الطريقة ، لكننا سنضيف إزاحة مضروبة في دائرة "مفردة" إلى colorUU الأساسية .

سطوع


بعد الدورة ، نقوم بتجربة خريطة السطوع وتغيير قيم السطوع النهائي (لأن خريطة السطوع لا تعرف شيئًا عن المخططات):

  67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 

كود HLSL:

  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces ); 

الزوايا الرمادية والتوحيد النهائي لكل شيء


يتم حساب اللون الرمادي الأقرب إلى الزوايا باستخدام المنتج القياسي (خط التجميع 69):

  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx; 


ثم اثنين من الاستيفاء متابعة. يجمع الأول بين الرمادي مع "اللون في الدائرة" باستخدام القناع الأول الذي وصفته ، وبالتالي تصبح الزوايا رمادية. بالإضافة إلى ذلك ، هناك معامل 0.6 ، مما يقلل من تشبع الصورة النهائية:


الثاني يجمع بين اللون الأول مع ما سبق باستخدام fisheyeAmount . هذا يعني أن الشاشة تصبح أكثر قتامة تدريجيا (بسبب الضرب بنسبة 0.6) والرمادي في الزوايا! الرائعة.

HLSL:

  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount ); 

الآن يمكننا الانتقال إلى إضافة معالم الكائنات.

يتم أخذ الألوان (الأحمر والأصفر) من المخزن المؤقت الثابت.

  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting; 


تفو! نحن تقريبا في خط النهاية!

لدينا اللون النهائي ، هناك لون غريزة الساحرة ... يبقى الجمع بينهما بطريقة أو بأخرى!

ولهذا ، إضافة بسيطة ليست مناسبة. أولاً نحسب المنتج القياسي:

  78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); 

الذي يبدو مثل هذا:


وتستخدم هذه القيم في النهاية للتحريف بين اللون وميل الساحرة (المشبعة):

  76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 ); 


وهذا كل شيء.

تظليل كامل متاح هنا .

مقارنة بين بلدي (يسار) وتظليل الأصلي (يمين):


أتمنى أن تستمتعوا بهذا المقال! هناك العديد من الأفكار الرائعة في آليات "غريزة witcher" ، والنتيجة النهائية معقولة للغاية.

[في الماضي جزءا فحص: الأول و الثاني .]

Source: https://habr.com/ru/post/ar450332/


All Articles