الهندسة العكسية لعرض The Witcher 3: تأثيرات السماء المختلفة

صورة

[أجزاء السابقة من التحليل: الأول و الثاني و الثالث .]

الجزء 1. سحابة سيروس


عندما تجري اللعبة في مساحات مفتوحة ، فإن أحد العوامل التي تحدد مصداقية العالم هي السماء. فكر في الأمر - معظم الوقت تأخذ السماء حرفيا حوالي 40-50 ٪ من الشاشة بأكملها. السماء هي أكثر بكثير من مجرد تدرج جميل. لديها النجوم والشمس والقمر ، وأخيرا الغيوم.

على الرغم من أن الاتجاهات الحالية يبدو أنها تتكون من تجسيد حجمي للسحب باستخدام Raymarching (انظر هذه المقالة ) ، فإن الغيوم في The Witcher 3 تعتمد بالكامل على الملمس. لقد قمت بفحصهم بالفعل من قبل ، ولكن اتضح أن كل شيء معهم أكثر تعقيدًا مما توقعت في الأصل. إذا اتبعت سلسلة مقالاتي ، فأنت تعلم أن هناك فرقًا بين Blood and Wine DLC وبقية اللعبة. وكما قد تتوقع ، هناك بعض التغييرات في العمل مع السحب في DLC.

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

تختلف بعض الطبقات من حيث القوام والتظليل المستخدم لتقديمها. من الواضح أن هذا يؤثر على مدى تعقيد وطول كود المجمّع لتظليل البكسل.

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


والأكثر إثارة للاهتمام أن بعض الطبقات يتم تقديمها مرتين بنفس المعلمات.

بعد النظر في الشفرة ، اخترت أقصر تظليل من أجل (1) على الأرجح تنفيذ هندستها العكسية الكاملة ، (2) معرفة جميع جوانبها.

لقد ألقيت نظرة فاحصة على سحابة السرخس من Witcher 3: Blood and Wine.

هنا إطار مثال:


قبل التقديم


بعد مرور التقديم الأول


بعد مرور التجسيد الثاني

في هذا الإطار بالذات ، تكون غيوم السيروت هي الطبقة الأولى في التجسيد. كما ترى ، يتم تقديمه مرتين ، مما يزيد من سطوعه.

تظليل هندسي ورأسي


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


جميع القمم في الفاصل الزمني [0-1] ، لذلك لتوسيط الشبكة على النقطة (0،0،0) ، يتم استخدام القياس والانحراف قبل التحويل إلى worldViewProj (نحن نعلم بالفعل هذا النمط من الأجزاء السابقة من السلسلة). في حالة السحب ، تمتد الشبكة بقوة على طول المستوى XY (يشير المحور Z للأعلى) لتغطية مساحة أكبر من هرم الرؤية. والنتيجة هي على النحو التالي:


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

بكسل تظليل


يبدو رمز تجميع تظليل البكسل كما يلي:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb12[238], immediateIndexed  
    dcl_constantbuffer cb4[13], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.w  
    dcl_input_ps linear v3.xyzw  
    dcl_input_ps linear v4.xyz  
    dcl_input_ps linear v5.xyz  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  
   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx  
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx  
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   8: add r1.xyz, r1.xyzx, r1.xyzx  
   9: dp3 r0.w, r1.xyzx, r1.xyzx  
  10: rsq r0.w, r0.w  
  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  
  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x  
  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x  
  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x  
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x  
  53: mul r0.w, r0.w, r1.x  
  54: mul_sat r0.w, r0.w, cb4[6].x  
  55: mul o0.xyz, r0.wwww, r0.xyzx  
  56: mov o0.w, r0.w  
  57: ret 

يتم إدخال قوامين سلسين. يحتوي أحدها على خريطة عادية (قنوات xyz ) وشكل سحابي (القناة a ). والثاني هو الضوضاء لتشويه الشكل.


خريطة عادية ، خاصية CD Projekt Red


شكل سحابة ، قرص مضغوط الملكية Projekt الأحمر


نسيج الضوضاء ، خاصية CD Projekt Red

المخزن المؤقت الرئيسي للثوابت مع معلمات السحابة هو cb4. لهذا الإطار ، له المعاني التالية:


بالإضافة إلى ذلك ، يتم استخدام قيم أخرى من الأصفاد الأخرى. لا تقلق ، سننظر فيها أيضًا.

عكس اتجاه ضوء الشمس


أول شيء يحدث في التظليل هو حساب الاتجاه الطبيعي لضوء الشمس المقلوب على طول المحور Z:

   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  

   float3 invertedSunlightDir = normalize(lightDir * float3(1, 1, -1) );

كما ذكرنا سابقًا ، يتم توجيه المحور Z إلى الأعلى ، و cb0 [9] هو اتجاه ضوء الشمس. وتهدف هذه النواقل في الشمس - من المهم! يمكنك التحقق من ذلك عن طريق كتابة تظليل حسابي بسيط يقوم بتشغيل NdotL بسيط وإدراجه في تمرير تظليل مؤجل.

سحابة أخذ العينات


والخطوة التالية هي حساب texcoords لأخذ عينات من نسيج السحابة ، وتفريغ الناقل العادي وتطبيعه.

   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx   
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx   
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0   
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)   
   8: add r1.xyz, r1.xyzx, r1.xyzx   
   9: dp3 r0.w, r1.xyzx, r1.xyzx   
  10: rsq r0.w, r0.w   
   
   
   // Calc sampling coords  
   float2 cloudTextureUV = Texcoords * textureScale + elapsedTime * speedFactors;  
   
   // Sample texture and get data from it  
   float4 cloudTextureValue = texture0.Sample( sampler0, cloudTextureUV ).rgba;  
   float3 normalMap = cloudTextureValue.xyz;  
   float cloudShape = cloudTextureValue.a;  
   
   // Unpack normal and normalize it  
   float3 unpackedNormal = (normalMap - 0.5) * 2.0;  
   unpackedNormal = normalize(unpackedNormal);

دعونا نتعامل معها تدريجياً.

للحصول على حركة السحب ، نحتاج إلى الوقت المنقضي بالثواني ( cb [0] .x ) مضروبًا في معامل السرعة ، مما يؤثر على مدى سرعة تحرك السحب عبر السماء ( cb4 [5] .xy ).

كما قلت سابقًا ، يتم تمديد الأشعة فوق البنفسجية على طول هندسة قبة السماء ، ونحتاج أيضًا إلى عوامل تحجيم النسيج التي تؤثر على حجم الغيوم ( cb4 [4] .xy ).

الصيغة النهائية هي:

samplingUV = Input.TextureUV * textureScale + time * speedMultiplier;

بعد أخذ عينات من جميع القنوات الأربع ، لدينا خريطة عادية (قنوات RGB) وشكل سحابي (القناة أ).

لتفريغ الخريطة العادية من الفاصل الزمني [0؛ 1] في الفاصل الزمني [-1 ؛ 1] نستخدم الصيغة التالية:

unpackedNormal = (packedNormal - 0.5) * 2.0;

يمكنك أيضًا استخدام هذا:

unpackedNormal = packedNormal * 2.0 - 1.0;

أخيرًا ، نقوم بتطبيع المتجه العادي غير المغلف.

تراكب عادي


عند وجود المتجهات العادية ، وناقلات الظل والمماس الثنائي من تظليل الرأس ، والمتجه الطبيعي من الخريطة العادية ، فإننا عادة ما نرسم المعايير الطبيعية.

  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
    
   // Perform bump mapping  
   float3 SkyTangent = Input.Tangent;  
   float3 SkyNormal = (float3( Input.Texcoords.zw, Input.param3.w ));  
   float3 SkyBitangent = Input.param3.xyz;  
        
   float3x3 TBN = float3x3(SkyTangent, SkyBitangent, SkyNormal);  
   float3 finalNormal = (float3)mul( unpackedNormal, (TBN) );

سطوع (1)


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

خذ بعين الاعتبار كود المجمع التالي:

  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  

فيما يلي تصور NdotL على الإطار المعني:


يتم استخدام هذا المنتج العددي (مع التشبع) للاستيفاء بين minIntensity و maxIntensity. وبفضل هذا ، ستكون أجزاء الغيوم المضاءة بأشعة الشمس أكثر إشراقًا.

   // Calculate cosine between normal and up-inv lightdir  
   float NdotL = saturate( dot(invertedSunlightDir, finalNormal) );  
   
   // Param 1, line 19, r0.x  
   float intensity1 = lerp( param1Min, param1Max, NdotL );

سطوع (2)


هناك عامل آخر يؤثر على سطوع الغيوم.

يجب أن تكون الغيوم الموجودة في ذلك الجزء من السماء حيث توجد الشمس أكثر إبرازًا. للقيام بذلك ، نحسب التدرج بناءً على المستوى XY.

يستخدم هذا التدرج لحساب الاستكمال الخطي بين قيم min / max ، على غرار ما يحدث في الجزء (1).

أي أنه من الناحية النظرية ، يمكننا أن نطلب تغميق السحب الموجودة على الجانب الآخر من الشمس ، ولكن هذا لا يحدث في هذا الإطار بالذات ، لأن param2Min و param2Max ( cb4 [0] .x و cb4 [1] .x ) يتم تعيينهما على 1.0f.

  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x   
   
   
   // Calculate normalized -lightDir.xy (20-22)  
   float2 lightDirXY = normalize( -lightDir.xy );  
   
   // Calculate world to camera  
   float3 vWorldToCamera = ( CameraPos - WorldPos );  
   float worldToCamera_distance = length(vWorldToCamera);  
        
   // normalize vector  
   vWorldToCamera = normalize( vWorldToCamera );  
        
   
   float LdotV = saturate( dot(lightDirXY, vWorldToCamera.xy) );  
   float highlightedSkySection = saturate( 2*LdotV );  
   float intensity2 = lerp( param2Min, param2Max, highlightedSkySection );  
   
   float finalIntensity = pow( intensity2 *intensity1, 2.2);

في النهاية ، نضرب السطوع ونرفع النتيجة إلى قوة 2.2.

لون الغيوم


يبدأ حساب لون الغيوم بالحصول على ثوابت من الثوابت العازلة تشير إلى لون الغيوم بجوار الشمس والغيوم على الجانب الآخر من السماء. فيما بينها ، يتم إجراء الاستكمال الخطي بناءً على highlightSkySection .

ثم يتم ضرب النتيجة في كثافة النهائي .

وفي النهاية ، يتم خلط النتيجة مع الضباب (لأسباب الأداء ، تم حسابها بواسطة جهاز تظليل الرأس).

  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
   
  float3 cloudsColor = lerp( cloudsColorBack, cloudsColorFront, highlightedSunSection );  
  cloudsColor *= finalIntensity;  
  cloudsColor = lerp( cloudsColor, FogColor, FogAmount );

جعل السحب الرقيقة أكثر وضوحا في الأفق


هذا ليس ملحوظًا جدًا على الإطار ، ولكن في الواقع هذه الطبقة أكثر وضوحًا بالقرب من الأفق أكثر من رأس Geralt. هيريس كيفية القيام بذلك.

هل يمكن أن تلاحظ أن عند حساب سطوع الثاني، حسبنا طول ناقلات worldToCamera :

  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w

دعونا نجد التكرارات التالية لهذا الطول في الكود:

  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  ...  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x

واو ، ما الأمر معنا؟

cb [7] .x و cb [8] .x لها قيم 2000.0 و 7000.0.

اتضح أن هذا هو نتيجة استخدام وظيفة linstep .

تتلقى ثلاث معلمات: الحد الأدنى / الأقصى - الفاصل الزمني والقيمة v .

يعمل هذا على النحو التالي: إذا كانت v في الفاصل الزمني [ min - max ] ، فستُرجع الدالة الاستكمال الداخلي الخطي في الفاصل الزمني [0.0 - 1.0]. من ناحية أخرى ، إذا كانت v خارج النطاق ، فإن linstep تُرجع 0.0 أو 1.0.

مثال بسيط:

linstep( 1000.0, 2000.0, 999.0) = 0.0
linstep( 1000.0, 2000.0, 1500.0) = 0.5
linstep( 1000.0, 2000.0, 2000.0) = 1.0

أي أنها تشبه إلى حد كبير الخطوة السلس من HLSL ، باستثناء أنه في هذه الحالة ، بدلاً من الاستيفاء الهرميتي ، يتم تنفيذ خطي.

Linstep ليست ميزة في HLSL ، ولكنها مفيدة جدًا. يجدر وجوده في مجموعة الأدوات الخاصة بك.

 // linstep:  
 //  
 // Returns a linear interpolation between 0 and 1 if t is in the range [min, max]   
 // if "v" is <= min, the output is 0  
 // if "v" i >= max, the output is 1  
   
 float linstep( float min, float max, float v )  
 {  
   return saturate( (v - min) / (max - min) );  
 } 

دعنا نعود إلى Witcher 3: بعد حساب هذا المؤشر ، والإبلاغ عن مدى مسافة جزء معين من السماء من Geralt ، نستخدمه لإضعاف سطوع السحب:

  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
   
   float distanceAttenuation = linstep( fadeDistanceStart, fadeDistanceEnd, worldToCamera_distance );  
    
   float fadedCloudShape = closeCloudsHidingFactor * cloudShape;  
   cloudShape = lerp( fadedCloudShape, cloudShape, distanceAttenuation );

cloudShape هي قناة .a من النسيج الأول ، و closeCloudsHidingFactor هي قيمة عازلة ثابتة تتحكم في رؤية الغيوم فوق رأس Geralt. في جميع الإطارات التي اختبرتها ، كانت تساوي 0.0 ، وهو ما يعادل غياب السحب. كما distanceAttenuation النهج 1.0 (المسافة من الكاميرا إلى قبة السماء الزيادات)، الغيوم تصبح أكثر وضوحا.

أخذ عينات نسيج الضوضاء


حساب إحداثيات أخذ العينات الملمس الضوضاء حسابات مماثلة لنسيج من الغيوم، إلا أنه يمكنك استخدام مجموعة مختلفة من textureScale و speedMultiplier .

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

  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
   
   // Calc sampling coords for noise  
   float2 noiseTextureUV = Texcoords * textureScaleNoise + elapsedTime * speedFactorsNoise;  
   
   // Sample texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;

ضع كل شيء معا


بعد استلام قيمة الضوضاء ، يجب أن ندمجها مع cloudShape.

واجهت بعض المشاكل في فهم هذه السطور ، حيث توجد معلمة 2.w (والتي تكون دائمًا 1.0) وضوضاء متعددة (لها قيمة 5.0 ، مأخوذة من المخزن المؤقت الثابت).

ومع ذلك ، فإن أهم شيء هنا هو القيمة النهائية لـ GeneralCloudsVisibility ، التي تؤثر على رؤية الغيوم.

ألق نظرة أيضًا على القيمة النهائية للضوضاء. يتم ضرب اللون الناتج للسحباللون بالضجيج النهائي ، والذي يتم إنتاجه أيضًا لقناة ألفا.

  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x
  53: mul r0.w, r0.w, r1.x
  54: mul_sat r0.w, r0.w, cb4[6].x
  55: mul o0.xyz, r0.wwww, r0.xyzx
  56: mov o0.w, r0.w
  57: ret   

   // Sample noise texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;  
   noiseTextureValue = noiseTextureValue * noiseMult - noiseMult;  
     
   float noiseValue = saturate( noiseMult * Input.param2.w + noiseTextureValue);  
   noiseValue *= cloudShape;  
     
   float finalNoise = saturate( noiseValue * generalCloudsVisibility);  
   
   return float4( cloudsColor*finalNoise, finalNoise ); 

مجموع


تبدو النتيجة النهائية قابلة للتصديق للغاية.

يمكنك المقارنة. الصورة الأولى هي تظليل بلدي ، والثانية هي تظليل اللعبة:


إذا كنت فضوليًا ، فإن جهاز تظليل متاح هنا .

الجزء 2. الضباب


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

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

على الرغم من حقيقة أن هذا العرض التقديمي ظهر في عام 2014 ، وتم إصدار The Witcher 3 في 2015/2016 ، فإن الضباب في الجزء الأخير من مغامرات Geralt يعتمد تمامًا على الشاشة ويتم تنفيذه كمعالجة نموذجية.

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

ومع ذلك ، تمكنت من العثور على أداة تظليل الضباب على الإنترنت والتي جذبت انتباهي على الفور بسبب تشابهها مع ضباب The Witcher 3 من حيث الأسماء المتغيرة والترتيب العام للتعليمات. لم يكن هذا التظليل هو نفسه تمامًا كما هو الحال في اللعبة ، لذلك كان علي إعادة صياغة الأمر قليلاً. أريد أن أقول هذا أن الجزء الرئيسي من كود HLSL الذي سترونه هنا ، باستثناء اثنين ، لم أقم بإنشائه / تحليله. تذكر هذا.

فيما يلي كود التجميع الخاص بتظليل بكسل الضباب - تجدر الإشارة إلى أنه هو نفسه للعبة بأكملها (الجزء الرئيسي من عام 2015 وكلا من DLC):

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[2], immediateIndexed  
    dcl_constantbuffer cb12[214], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_resource_texture2d (float,float,float,float) t2  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 7  
   0: ftou r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r1.x, r0.xyww, t0.xyzw  
   3: mad r1.y, r1.x, cb12[22].x, cb12[22].y  
   4: lt r1.y, r1.y, l(1.000000)  
   5: if_nz r1.y  
   6:  utof r1.yz, r0.xxyx  
   7:  mul r2.xyzw, r1.zzzz, cb12[211].xyzw  
   8:  mad r2.xyzw, cb12[210].xyzw, r1.yyyy, r2.xyzw  
   9:  mad r1.xyzw, cb12[212].xyzw, r1.xxxx, r2.xyzw  
  10:  add r1.xyzw, r1.xyzw, cb12[213].xyzw  
  11:  div r1.xyz, r1.xyzx, r1.wwww  
  12:  ld_indexable(texture2d)(float,float,float,float) r2.xyz, r0.xyww, t1.xyzw  
  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)  
  24:  div r0.yzw, r0.yyzw, r1.xxxx  
  25:  mad r1.y, r0.w, cb12[22].z, cb12[0].z  
  26:  add r1.x, r1.x, -cb12[22].z  
  27:  max r1.x, r1.x, l(0)  
  28:  min r1.x, r1.x, cb12[42].z  
  29:  mul r1.z, r0.w, r1.x  
  30:  mul r1.w, r1.x, cb12[43].x  
  31:  mul r1.zw, r1.zzzw, l(0.000000, 0.000000, 0.062500, 0.062500)  
  32:  dp3 r0.y, cb12[38].xyzx, r0.yzwy  
  33:  add r0.z, r0.y, cb12[42].x  
  34:  add r0.w, cb12[42].x, l(1.000000)  
  35:  div_sat r0.z, r0.z, r0.w  
  36:  add r0.w, -cb12[43].z, cb12[43].y  
  37:  mad r0.z, r0.z, r0.w, cb12[43].z  
  38:  mul r0.w, abs(r0.y), abs(r0.y)  
  39:  mad_sat r2.w, r1.x, l(0.002000), l(-0.300000)  
  40:  mul r0.w, r0.w, r2.w  
  41:  lt r0.y, l(0), r0.y  
  42:  movc r3.xyz, r0.yyyy, cb12[39].xyzx, cb12[41].xyzx  
  43:  add r3.xyz, r3.xyzx, -cb12[40].xyzx  
  44:  mad r3.xyz, r0.wwww, r3.xyzx, cb12[40].xyzx  
  45:  movc r4.xyz, r0.yyyy, cb12[45].xyzx, cb12[47].xyzx  
  46:  add r4.xyz, r4.xyzx, -cb12[46].xyzx  
  47:  mad r4.xyz, r0.wwww, r4.xyzx, cb12[46].xyzx  
  48:  ge r0.y, r1.x, cb12[48].y  
  49:  if_nz r0.y  
  50:   add r0.y, r1.y, cb12[42].y  
  51:   mul r0.w, r0.z, r0.y  
  52:   mul r1.y, r0.z, r1.z  
  53:   mad r5.xyzw, r1.yyyy, l(16.000000, 15.000000, 14.000000, 13.000000), r0.wwww  
  54:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  55:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  56:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  57:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  58:   mul r1.z, r5.y, r5.x  
  59:   mul r1.z, r5.z, r1.z  
  60:   mul r1.z, r5.w, r1.z  
  61:   mad r5.xyzw, r1.yyyy, l(12.000000, 11.000000, 10.000000, 9.000000), r0.wwww  
  62:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  63:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  64:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  65:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:   mul r1.z, r1.z, r5.x  
  67:   mul r1.z, r5.y, r1.z  
  68:   mul r1.z, r5.z, r1.z  
  69:   mul r1.z, r5.w, r1.z  
  70:   mad r5.xyzw, r1.yyyy, l(8.000000, 7.000000, 6.000000, 5.000000), r0.wwww  
  71:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  72:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  73:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  74:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  75:   mul r1.z, r1.z, r5.x  
  76:   mul r1.z, r5.y, r1.z  
  77:   mul r1.z, r5.z, r1.z  
  78:   mul r1.z, r5.w, r1.z  
  79:   mad r5.xy, r1.yyyy, l(4.000000, 3.000000, 0.000000, 0.000000), r0.wwww  
  80:   max r5.xy, r5.xyxx, l(0, 0, 0, 0)  
  81:   add r5.xy, r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  82:   div_sat r5.xy, r1.wwww, r5.xyxx  
  83:   add r5.xy, -r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  84:   mul r1.z, r1.z, r5.x  
  85:   mul r1.z, r5.y, r1.z  
  86:   mad r0.w, r1.y, l(2.000000), r0.w  
  87:   max r0.w, r0.w, l(0)  
  88:   add r0.w, r0.w, l(1.000000)  
  89:   div_sat r0.w, r1.w, r0.w  
  90:   add r0.w, -r0.w, l(1.000000)  
  91:   mul r0.w, r0.w, r1.z  
  92:   mad r0.y, r0.y, r0.z, r1.y  
  93:   max r0.y, r0.y, l(0)  
  94:   add r0.y, r0.y, l(1.000000)  
  95:   div_sat r0.y, r1.w, r0.y  
  96:   add r0.y, -r0.y, l(1.000000)  
  97:   mad r0.y, -r0.w, r0.y, l(1.000000)  
  98:   add r0.z, r1.x, -cb12[48].y  
  99:   mul_sat r0.z, r0.z, cb12[48].z  
  100:  else  
  101:   mov r0.yz, l(0.000000, 1.000000, 0.000000, 0.000000)  
  102:  endif  
  103:  log r0.y, r0.y  
  104:  mul r0.w, r0.y, cb12[42].w  
  105:  exp r0.w, r0.w  
  106:  mul r0.y, r0.y, cb12[48].x  
  107:  exp r0.y, r0.y  
  108:  mul r0.yw, r0.yyyw, r0.zzzz  
  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, cb12[188].w, l(-1.000000)  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.z, r0.w  
  115:  lt r0.z, l(0), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, cb12[190].w, l(-1.000000)  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.z, r0.w  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif  
  126:  mul r0.z, r0.x, r5.w  
  127:  mul r0.x, r0.x, r0.y  
  128:  dp3 r0.y, l(0.333000, 0.555000, 0.222000, 0.000000), r2.xyzx  
  129:  mad r1.xyz, r0.yyyy, r4.xyzx, -r2.xyzx  
  130:  mad r0.xyw, r0.xxxx, r1.xyxz, r2.xyxz  
  131:  add r1.xyz, -r0.xywx, r5.xyzx  
  132:  mad r0.xyz, r0.zzzz, r1.xyzx, r0.xywx  
  133: else  
  134:  mov r0.xyz, l(0, 0, 0, 0)  
  135: endif  
  136: mov o0.xyz, r0.xyzx  
  137: mov o0.w, l(1.000000)  
  138: ret 

بصراحة ، التظليل طويل جدًا. ربما تكون طويلة جدًا لإجراء عملية عكسية فعالة.

فيما يلي مثال لمشهد غروب الشمس مع الضباب:


دعنا نلقي نظرة على المدخلات:

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


المخزن المؤقت لعمق الوارد


انسداد المحيط الوارد


يبدو المخزن المؤقت للون HDR الوارد

... ونتيجة تطبيق تظليل الضباب في هذا المشهد كما يلي:


نسيج HDR بعد تطبيق الضباب.

يستخدم المخزن المؤقت للعمق لإعادة إنشاء الموضع في العالم. هذا هو النمط القياسي لتظليل Witcher 3.

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

يبدأ جهاز تظليل بتحديد ما إذا كانت البكسل في السماء. في حالة وجود البكسل في السماء (العمق == 1.0) ، فإن التظليل يعود باللون الأسود. إذا كانت البكسل في المشهد (العمق <1.0) ، فإننا نعيد إنشاء الموضع في العالم باستخدام المخزن المؤقت للعمق (السطور 7-11) ونستمر في حساب الضباب.

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

أول شيء تحتاج إلى معرفته عن الضباب في "The Witcher 3": يتكون من جزأين - "لون الضباب" و "لون الغلاف الجوي".

 struct FogResult  
 {  
    float4 paramsFog;     // RGB: color, A: influence  
    float4 paramsAerial;  // RGB: color, A: influence  
 };

لكل جزء ثلاثة ألوان: أمامي ومتوسط ​​وخلفي. أي أنه في المخزن المؤقت المستمر توجد بيانات مثل "FogColorFront" و "FogColorMiddle" و "AerialColorBack" وما إلى ذلك ... لنلق نظرة على البيانات الواردة:


   // *** Inputs *** //  
   float3 FogSunDir = cb12_v38.xyz;  
   float3 FogColorFront = cb12_v39.xyz;  
   float3 FogColorMiddle = cb12_v40.xyz;  
   float3 FogColorBack = cb12_v41.xyz;  
     
   float4 FogBaseParams = cb12_v42;  
   float4 FogDensityParamsScene = cb12_v43;  
   float4 FogDensityParamsSky = cb12_v44;  
     
   float3 AerialColorFront = cb12_v45.xyz;  
   float3 AerialColorMiddle = cb12_v46.xyz;  
   float3 AerialColorBack = cb12_v47.xyz;  
   float4 AerialParams = cb12_v48;

قبل حساب الألوان النهائية ، نحتاج إلى حساب المتجهات ومنتجات العددية. يمكن للتظليل الوصول إلى موضع البكسل في العالم ، وموضع الكاميرا (cb12 [0] .xyz) واتجاه الضباب / الإضاءة (cb12 [38] .xyz). هذا يسمح لنا بحساب الناتج العددي لناقل شكل الضباب واتجاهه.

   float3 frag_vec = fragPosWorldSpace.xyz - customCameraPos.xyz;  
   float frag_dist = length(frag_vec);  
     
   float3 frag_dir = frag_vec / frag_dist;  
   
   float dot_fragDirSunDir = dot(GlobalLightDirection.xyz, frag_dir);

لحساب تدرج المزج ، تحتاج إلى استخدام مربع منتج العدد المطلق ، ثم ضرب النتيجة مرة أخرى في بعض المعلمات التي تعتمد على المسافة:

   float3 curr_col_fog;  
   float3 curr_col_aerial;  
   {  
     float _dot = dot_fragDirSunDir;  
   
     float _dd = _dot;  
     {  
       const float _distOffset = -150;  
       const float _distRange = 500;  
       const float _mul = 1.0 / _distRange;  
       const float _bias = _distOffset * _mul;  
   
       _dd = abs(_dd);  
       _dd *= _dd;  
       _dd *= saturate( frag_dist * _mul + _bias );  
     }  
   
     curr_col_fog = lerp( FogColorMiddle.xyz, (_dot>0.0f ? FogColorFront.xyz : FogColorBack.xyz), _dd );  
     curr_col_aerial = lerp( AerialColorMiddle.xyz, (_dot>0.0f ? AerialColorFront.xyz : AerialColorBack.xyz), _dd );  
   }

يوضح هذا الكود البرمجي لنا من أين جاءت هذه 0.002 و -0.300. كما نرى ، فإن المنتج القياسي بين نواقل الرؤية والإضاءة مسؤول عن الاختيار بين الألوان "الأمامية" و "الخلفية". ذكي!

هنا تصور التدرج النهائي الناتج (_dd).


ومع ذلك ، فإن حساب تأثير الغلاف الجوي / الضباب أكثر تعقيدًا. كما ترون ، لدينا خيارات أكثر بكثير من ألوان RGB فقط. وهي تشمل ، على سبيل المثال ، كثافة المشهد. نستخدم Raymarching (16 خطوة ، وهذا هو السبب في أنه يمكن توسيع الدورة) لتحديد حجم الضباب وعامل المقياس: عند

وجود ناقل [camera ---> world] ، يمكننا تقسيم جميع مكوناته إلى 16 - ستكون هذه خطوة واحدة. كما نرى أدناه ، فإن مكون .z (الارتفاع) فقط ( curr_pos_z_step ) هو الذي يشارك في الحسابات .

اقرأ المزيد عن الضباب الذي يتم تطبيقه بواسطة raymarching ، على سبيل المثال ، هنا .

   float fog_amount = 1;  
   float fog_amount_scale = 0;  
   [branch]  
   if ( frag_dist >= AerialParams.y )  
   {  
     float curr_pos_z_base = (customCameraPos.z + FogBaseParams.y) * density_factor;  
     float curr_pos_z_step = frag_step.z * density_factor;  
   
     [unroll]  
     for ( int i=16; i>0; --i )  
     {  
       fog_amount *= 1 - saturate( density_sample_scale / (1 + max( 0.0, curr_pos_z_base + (i) * curr_pos_z_step ) ) );  
     }  
   
     fog_amount = 1 - fog_amount;  
     fog_amount_scale = saturate( (frag_dist - AerialParams.y) * AerialParams.z );  
   }  
   
   FogResult ret;  
   
   ret.paramsFog = float4 ( curr_col_fog, fog_amount_scale * pow( abs(fog_amount), final_exp_fog ) );  
   ret.paramsAerial = float4 ( curr_col_aerial, fog_amount_scale * pow( abs(fog_amount), final_exp_aerial ) );

من الواضح أن كمية الضباب تعتمد على الارتفاع (المكونات. z) ، وفي النهاية يتم رفع كمية الضباب إلى درجة الضباب / الغلاف الجوي. يتم أخذ

final_exp_fog و final_exp_aerial من المخزن المؤقت الثابت ؛ تسمح لك بالتحكم في كيفية تأثير ألوان الضباب والجو على العالم بارتفاع متزايد.

تجاوز الضباب


التظليل الذي وجدته ليس لديه جزء رمز التجميع التالي:

  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, l(-1.000000), cb12[188].w  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.w, r0.z  
  115:  lt r0.z, l(0.000000), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, l(-1.000000), cb12[190].w  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.w, r0.z  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif

إذا حكمنا بما تمكنت من فهمه ، فهذا يشبه إعادة تعريف اللون وتأثير الضباب: في

معظم الوقت ، يتم إجراء إعادة تعريف واحدة فقط (cb12_v192.x تساوي 0.0) ، ولكن في هذه الحالة بالذات تكون قيمتها 0.22 ، لذلك نقوم بالتخطي الثاني.


 #ifdef OVERRIDE_FOG  
     
   // Override  
   float fog_influence = ret.paramsFog.w; // r0.w  
   
   float override1ColorScale = cb12_v189.x;  
   float override1ColorBias = cb12_v189.y;  
   float3 override1Color = cb12_v188.rgb;  
     
   float override1InfluenceScale = cb12_v189.z;  
   float override1InfluenceBias = cb12_v189.w;  
   float override1Influence = cb12_v188.w;  
     
   float override1ColorAmount = saturate(fog_influence * override1ColorScale + override1ColorBias);  
   float override1InfluenceAmount = saturate(fog_influence * override1InfluenceScale + override1InfluenceBias);    
     

   float4 paramsFogOverride;  
   paramsFogOverride.rgb = lerp(curr_col_fog, override1Color, override1ColorAmount ); // ***r5.xyz   
     
   float param1 = lerp(1.0, override1Influence, override1InfluenceAmount); // r0.x  
   paramsFogOverride.w = saturate(param1 * fog_influence ); // ** r5.w  
   
     
   const float extraFogOverride = cb12_v192.x;  
     
   [branch]   
   if (extraFogOverride > 0.0)  
   {  
     float override2ColorScale = cb12_v191.x;  
     float override2ColorBias = cb12_v191.y;  
     float3 override2Color = cb12_v190.rgb;  
     
     float override2InfluenceScale = cb12_v191.z;  
     float override2InfluenceBias = cb12_v191.w;  
     float override2Influence = cb12_v190.w;  
       
     float override2ColorAmount = saturate(fog_influence * override2ColorScale + override2ColorBias);  
     float override2InfluenceAmount = saturate(fog_influence * override2InfluenceScale + override2InfluenceBias);  
      

     float4 paramsFogOverride2;  
     paramsFogOverride2.rgb = lerp(curr_col_fog, override2Color, override2ColorAmount); // r3.xyz   
           
     float ov_param1 = lerp(1.0, override2Influence, override2InfluenceAmount); // r0.z  
     paramsFogOverride2.w = saturate(ov_param1 * fog_influence); // r3.w  
   
     paramsFogOverride = lerp(paramsFogOverride, paramsFogOverride2, extraFogOverride);  
   
   }  
   ret.paramsFog = paramsFogOverride;  
     
 #endif

هذا هو السعر النهائي بدون إعادة تعريف الضباب (الصورة الأولى) ، مع إعادة تعريف واحدة (الصورة الثانية) وإعادة تعريف مزدوجة (الصورة الثالثة ، النتيجة النهائية):




تنظيم الانسداد المحيط


التظليل الذي وجدته أيضًا لم يستخدم الانسداد المحيط على الإطلاق. دعونا نلقي نظرة على نسيج AO مرة أخرى والرمز الذي يهمنا:


  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)

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


نبدأ بتحميل AO من النسيج ، ثم تنفيذ التعليمات القصوى. في هذا المشهد ، cb3_v1.x مرتفع جدًا (0.96888) ، مما يجعل AO ضعيفًا جدًا.

يحسب الجزء التالي من الرمز المسافة بين مواضع الكاميرا والبكسل في العالم.

أعتقد أن الشفرة تتحدث أحيانًا عن نفسها ، لذلك دعونا نلقي نظرة على HLSL ، الذي يقوم بمعظم هذا الإعداد:

 float AdjustAmbientOcclusion(in float inputAO, in float worldToCameraDistance)  
 {  
   // *** Inputs *** //  
   const float aoDistanceStart = cb3_v0.x;  
   const float aoDistanceEnd = cb3_v0.y;  
   const float aoStrengthStart = cb3_v0.z;  
   const float aoStrengthEnd = cb3_v0.w;  
      
   // * Adjust AO  
   float aoDistanceIntensity = linstep( aoDistanceStart, aoDistanceEnd, worldToCameraDistance );  
   float aoStrength = lerp(aoStrengthStart, aoStrengthEnd, aoDistanceIntensity);   
   float adjustedAO = lerp(1.0, inputAO, aoStrength);  
     
   return adjustedAO;   
 }

يتم استخدام المسافة المحسوبة بين الكاميرا والعالم لوظيفة لينستيب. نحن نعلم بالفعل هذه الوظيفة ، ظهرت في تظليل سحابة cirrus.

كما ترى ، في المخزن المؤقت الثابت لدينا قيم مسافة البدء / النهاية AO. يؤثر ناتج linstep على قوة AO (وكذلك من cbuffer) ، وتؤثر القوة على إخراج AO.

مثال موجز: البكسل بعيد ، على سبيل المثال ، المسافة 500.

ترجع linstep 1.0؛
aoStrength يساوي aoStrengthEnd ؛

ينتج عن هذا إرجاع AO ، وهو ما يقرب من 77٪ (القوة النهائية) من قيمة الإدخال.

تعرض AO الوارد لهذه الوظيفة مسبقًا إلى الحد الأقصى للتشغيل.

ضع كل شيء معا


بعد تلقي لون وتأثير لون الضباب ولون الغلاف الجوي ، يمكنك في النهاية الجمع بينهما.

نبدأ بتخفيف التأثير مع AO الناتج:

   ...
   FogResult fog = CalculateFog( worldPos, CameraPosition, fogStart, ao, false );  
      
   // Apply AO to influence  
   fog.paramsFog.w *= ao;  
   fog.paramsAerial.w *= ao; 
      
   // Mix fog with scene color  
   outColor = ApplyFog(fog, colorHDR);

كل السحر يحدث في وظيفة ApplyFog :

 float3 ApplyFog(FogResult fog, float3 color)  
 {  
   const float3 LuminanceFactors = float3(0.333f, 0.555f, 0.222f);  
   
   float3 aerialColor = dot(LuminanceFactors, color) * fog.paramsAerial.xyz;  
   color = lerp(color, aerialColor, fog.paramsAerial.w);  
   color = lerp(color, fog.paramsFog.xyz, fog.paramsFog.w);  
    
   return color.xyz;  
 }

أولاً ، نحسب لمعان البكسل:


ثم نضربها بلون الغلاف الجوي:


ثم نقوم بدمج لون HDR مع لون الغلاف الجوي:


الخطوة الأخيرة هي دمج النتيجة المتوسطة مع لون الضباب:


هذا كل شئ!

بعض لقطات شاشة التصحيح



التأثير الجوي


لون الغلاف الجوي


تأثير الضباب


لون الضباب


مشهد منتهي بدون ضباب


مشهد جاهز مع الضباب فقط


المشهد النهائي هو الضباب الرئيسي


مشهد جاهز مرة أخرى مع كل الضباب لسهولة المقارنة

مجموع


أعتقد أنه يمكنك فهم الكثير مما سبق ، إذا نظرت إلى التظليل ، فهو هنا .

أستطيع أن أقول بسرور أن هذا التظليل هو نفس الشيء الأصلي تمامًا - إنه يجعلني سعيدًا جدًا.

بشكل عام ، تعتمد النتيجة النهائية بشكل كبير على القيم التي يتم تمريرها إلى التظليل. هذا ليس حلاً "سحريًا" يعطي ألوانًا مثالية في الإخراج ؛ فهو يتطلب الكثير من التكرارات والفنانين لجعل النتيجة النهائية تبدو لائقة. أعتقد أنها يمكن أن تكون عملية طويلة ، ولكن بعد إكمالها ، ستكون النتيجة مقنعة للغاية ، تمامًا مثل مشهد غروب الشمس هذا.

تستخدم Witcher 3 Sky Shader أيضًا حسابات الضباب لإنشاء انتقال سلس للألوان بالقرب من الأفق. ومع ذلك ، يتم تمرير مجموعة مختلفة من معاملات الكثافة إلى تظليل السماء.

دعني أذكرك - لم يتم إنشاء / تحليل معظم هذا التظليل من قبلي. يجب إرسال جميع الإقرارات إلى CD PROJEKT RED. ادعمهم ، يقومون بعمل ممتاز.

الجزء 3. نجوم الرماية


في The Witcher 3 هناك تفاصيل صغيرة ولكنها غريبة - إطلاق النار على النجوم. ومن المثير للاهتمام أنها لا تبدو في الدم والنبيذ DLC.

في الفيديو يمكنك أن ترى كيف تبدو:


دعونا نرى كيف تمكنا من الحصول على هذا التأثير.

كما ترون ، فإن جسم نجم الرماية أكثر إشراقًا من الذيل. هذه خاصية مهمة سنستخدمها لاحقًا.

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

1. نظرة عامة


صف بإيجاز ما يحدث.

يتم رسم نجوم الرماية في ممر استباقي ، مباشرة بعد قبة السماء والسماء والقمر:



DrawIndexed (720) - قبة السماء ،
DrawIndexed (2160) - المجال للسماء / القمر ،
DrawIndexed (36) - غير ذي صلة ، يبدو وكأنه موازٍ لانسداد الشمس (؟)
DrawIndexed (12) - نجم الرماية
DrawIndexInstanced (1116 ، 1) - السحب الرقيقة

مثل السحب الرقيقة ، يتم رسم كل نجم إطلاق نار مرتين على التوالي.


قبل إجراء المكالمة الأولى


نتيجة مكالمة السحب الأول


نتيجة استدعاء السحب الثاني

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


2. الهندسة


فيما يتعلق بالهندسة ، فإن أول شيء يجب ذكره هو أن كل نجم رماية يتم تمثيله برباع رفيع مع أغطية: 4 رؤوس ، 6 مؤشرات. هذا هو أبسط رباعية ممكنة.


التقريب رباعي لنجم الرماية.


الأقرب هو الرباعي التقريبي لنجم الرماية. يمكنك أن ترى عرض الإطار السلكي لخط يشير إلى مثلثين.

انتظر دقيقة ، ولكن يوجد DrawIndexed (12) ! هل هذا يعني أننا نرسم نجمتين للرماية في نفس الوقت؟

نعم.


في هذا الإطار ، يقع أحد نجوم الرماية خارج هرم الرؤية تمامًا.

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

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[193], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xyzw  
    dcl_input v2.xy  
    dcl_input v3.xy  
    dcl_input v4.xy  
    dcl_input v5.xyz  
    dcl_input v6.x  
    dcl_input v7.x  
    dcl_output o0.xyzw  
    dcl_output o1.xyzw  
    dcl_output o2.xy  
    dcl_output o3.xyzw  
    dcl_output_siv o4.xyzw, position  
    dcl_temps 5  
   0: mov r0.xyz, v0.xyzx  
   1: mov r0.w, l(1.000000)  
   2: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   3: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   4: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   5: add r0.x, v2.x, v2.y  
   6: add r0.y, -v2.y, v2.x  
   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx  
  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx  
  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx  
  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000)  
  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  
  30: add r0.xyz, r0.xyzx, -cb12[0].xyzx  
  31: dp3 r0.w, r0.xyzx, r0.xyzx  
  32: sqrt r0.w, r0.w  
  33: div r0.xyz, r0.xyzx, r0.wwww  
  34: add r0.w, r0.w, -cb12[22].z  
  35: max r0.w, r0.w, l(0)  
  36: min r0.w, r0.w, cb12[42].z  
  37: dp3 r0.x, cb12[38].xyzx, r0.xyzx  
  38: mul r0.y, abs(r0.x), abs(r0.x)  
  39: mad_sat r1.x, r0.w, l(0.002000), l(-0.300000)  
  40: mul r0.y, r0.y, r1.x  
  41: lt r1.x, l(0), r0.x  
  42: movc r1.yzw, r1.xxxx, cb12[39].xxyz, cb12[41].xxyz  
  43: add r1.yzw, r1.yyzw, -cb12[40].xxyz  
  44: mad r1.yzw, r0.yyyy, r1.yyzw, cb12[40].xxyz  
  45: movc r2.xyz, r1.xxxx, cb12[45].xyzx, cb12[47].xyzx  
  46: add r2.xyz, r2.xyzx, -cb12[46].xyzx  
  47: mad o0.xyz, r0.yyyy, r2.xyzx, cb12[46].xyzx  
  48: ge r0.y, r0.w, cb12[48].y  
  49: if_nz r0.y  
  50:  mad r0.y, r0.z, cb12[22].z, cb12[0].z  
  51:  mul r0.z, r0.w, r0.z  
  52:  mul r0.z, r0.z, l(0.062500)  
  53:  mul r1.x, r0.w, cb12[43].x  
  54:  mul r1.x, r1.x, l(0.062500)  
  55:  add r0.x, r0.x, cb12[42].x  
  56:  add r2.x, cb12[42].x, l(1.000000)  
  57:  div_sat r0.x, r0.x, r2.x  
  58:  add r2.x, -cb12[43].z, cb12[43].y  
  59:  mad r0.x, r0.x, r2.x, cb12[43].z  
  60:  add r0.y, r0.y, cb12[42].y  
  61:  mul r2.x, r0.x, r0.y  
  62:  mul r0.z, r0.x, r0.z  
  63:  mad r3.xyzw, r0.zzzz, l(16.000000, 15.000000, 14.000000, 13.000000), r2.xxxx  
  64:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  65:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  67:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  68:  mul r2.y, r3.y, r3.x  
  69:  mul r2.y, r3.z, r2.y  
  70:  mul r2.y, r3.w, r2.y  
  71:  mad r3.xyzw, r0.zzzz, l(12.000000, 11.000000, 10.000000, 9.000000), r2.xxxx  
  72:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  73:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  74:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  75:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  76:  mul r2.y, r2.y, r3.x  
  77:  mul r2.y, r3.y, r2.y  
  78:  mul r2.y, r3.z, r2.y  
  79:  mul r2.y, r3.w, r2.y  
  80:  mad r3.xyzw, r0.zzzz, l(8.000000, 7.000000, 6.000000, 5.000000), r2.xxxx  
  81:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  82:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  83:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  84:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  85:  mul r2.y, r2.y, r3.x  
  86:  mul r2.y, r3.y, r2.y  
  87:  mul r2.y, r3.z, r2.y  
  88:  mul r2.y, r3.w, r2.y  
  89:  mad r2.zw, r0.zzzz, l(0.000000, 0.000000, 4.000000, 3.000000), r2.xxxx  
  90:  max r2.zw, r2.zzzw, l(0, 0, 0, 0)  
  91:  add r2.zw, r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  92:  div_sat r2.zw, r1.xxxx, r2.zzzw  
  93:  add r2.zw, -r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  94:  mul r2.y, r2.z, r2.y  
  95:  mul r2.y, r2.w, r2.y  
  96:  mad r2.x, r0.z, l(2.000000), r2.x  
  97:  max r2.x, r2.x, l(0)  
  98:  add r2.x, r2.x, l(1.000000)  
  99:  div_sat r2.x, r1.x, r2.x  
  100:  add r2.x, -r2.x, l(1.000000)  
  101:  mul r2.x, r2.x, r2.y  
  102:  mad r0.x, r0.y, r0.x, r0.z  
  103:  max r0.x, r0.x, l(0)  
  104:  add r0.x, r0.x, l(1.000000)  
  105:  div_sat r0.x, r1.x, r0.x  
  106:  add r0.x, -r0.x, l(1.000000)  
  107:  mad r0.x, -r2.x, r0.x, l(1.000000)  
  108:  add r0.y, r0.w, -cb12[48].y  
  109:  mul_sat r0.y, r0.y, cb12[48].z  
  110: else  
  111:  mov r0.xy, l(1.000000, 0.000000, 0.000000, 0.000000)  
  112: endif  
  113: log r0.x, r0.x  
  114: mul r0.z, r0.x, cb12[42].w  
  115: exp r0.z, r0.z  
  116: mul r0.z, r0.z, r0.y  
  117: mul r0.x, r0.x, cb12[48].x  
  118: exp r0.x, r0.x  
  119: mul o0.w, r0.x, r0.y  
  120: mad_sat r0.xy, r0.zzzz, cb12[189].xzxx, cb12[189].ywyy  
  121: add r2.xyz, -r1.yzwy, cb12[188].xyzx  
  122: mad r2.xyz, r0.xxxx, r2.xyzx, r1.yzwy  
  123: add r0.x, cb12[188].w, l(-1.000000)  
  124: mad r0.x, r0.y, r0.x, l(1.000000)  
  125: mul_sat r2.w, r0.x, r0.z  
  126: lt r0.x, l(0), cb12[192].x  
  127: if_nz r0.x  
  128:  mad_sat r0.xy, r0.zzzz, cb12[191].xzxx, cb12[191].ywyy  
  129:  add r3.xyz, -r1.yzwy, cb12[190].xyzx  
  130:  mad r1.xyz, r0.xxxx, r3.xyzx, r1.yzwy  
  131:  add r0.x, cb12[190].w, l(-1.000000)  
  132:  mad r0.x, r0.y, r0.x, l(1.000000)  
  133:  mul_sat r1.w, r0.x, r0.z  
  134:  add r0.xyzw, -r2.xyzw, r1.xyzw  
  135:  mad o1.xyzw, cb12[192].xxxx, r0.xyzw, r2.xyzw  
  136: else  
  137:  mov o1.xyzw, r2.xyzw  
  138: endif  
  139: mov o3.xyzw, v1.xyzw  
  140: mov o2.xy, v4.yxyy  
  141: ret

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

يتم تخزين معلمات الغلاف الجوي (rgb = color ، a = تأثير) في o0.xyzw ، ومعلمات الضباب في o1.xyzw.

o2.xy (السطر 140) عبارة عن تكسورد فقط.
o3.xyzw (السطر 139) غير ذي صلة.

الآن دعنا نقول بضع كلمات حول حساب مركز في العالم. تظليل Vertex يؤدي لوحة الإعلانات . أولاً وقبل كل شيء ، تأتي البيانات الواردة للوحات الإعلانات من ذاكرة التخزين المؤقت للرأس - دعنا نلقي نظرة عليها.

البيانات الأولى هي الوظيفة:


كما ذكرنا أعلاه ، لدينا هنا 2 رؤوس رباعية: 8 رؤوس ، 12 مؤشرًا.

ولكن لماذا هو الموقف نفسه لكل رباعية؟ بسيط للغاية - هذا هو موقف مركز الرباعي.

علاوة على ذلك ، يكون لكل قمة إزاحة من المركز إلى حافة الرباعي:


هذا يعني أن كل نجم إطلاق نار له حجم (400 ، 3) وحدة في الفضاء العالمي. (على المستوى XY ، في Witcher 3 ، يتم توجيه المحور Z لأعلى)

العنصر الأخير الذي يحتويه كل قمة هو ناقل اتجاه الوحدة في الفضاء العالمي الذي يتحكم في حركة نجم الرماية:


نظرًا لأن البيانات تأتي من وحدة المعالجة المركزية ، فمن الصعب فهم كيفية حسابها.

الآن دعنا ننتقل إلى رمز لوحة الإعلانات. الفكرة بسيطة للغاية - أولاً تحصل على ناقل وحدة من مركز الرباعي إلى الكاميرا:

   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx

ثم نحصل على متجه ظلام واحد يتحكم في حركة نجم الرماية.

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

  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx

إذا كان هناك متجهان ، فسيتم استخدام منتج متجه لتحديد متجه المماس الثنائي المتعامد مع كلا المتجهين القادمين.

  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx

الآن لدينا تطبيع المماس المماس (r3.xyz) و bitangent (r2.xyz).

دعنا نقدم Xsize و Ysize المطابقين للعنصر الوارد TEXCOORD1 ، على سبيل المثال (-200 ، 1.50).

يتم الحساب النهائي للموضع في الفضاء العالمي على النحو التالي:

  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000) 

بالنظر إلى أن r0.x و r0.y و r0.z تساوي 1.0 ، يتم تبسيط الحساب النهائي:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

الجزء الأخير هو ضرب بسيط لموضع في الفضاء العالمي بواسطة مصفوفة عرض الإسقاط للحصول على SV_Position:

  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  

3. بيكسل شادر


كما هو موضح في قسم النظرة العامة ، يتم استخدام حالة المزج التالية: حيث SrcColor و SrcAlpha هما المكونان .rgb و .a من تظليل البكسل ، على التوالي ، و DestColor هو لون .rgb حاليًا في rendertarget. المؤشر الرئيسي الذي يتحكم في الشفافية هو SrcAlpha . يحسبها العديد من أجهزة تظليل الألعاب الاستباقية على أنها عتامة وتطبقها في النهاية على النحو التالي: لم يكن ظل النجوم الساقط استثناءً. باتباع هذا النمط ، نعتبر ثلاث حالات تكون فيها العتامة 1.0 و 0.1 و 0.0.

FinalColor = SrcColor * One + DestColor * (1.0 - SrcAlpha) =
FinalColor = SrcColor + DestColor * (1.0 - SrcAlpha)






return float4( color * opacity, opacity )



a) opacity = 1.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = color = SrcColor



b) opacity = 0.1

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = 0.1 * color + 0.9 * DestColor



c) opacity = 0.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = DestColor



الفكرة الأساسية لهذا التظليل هي تصميم واستخدام عتامة وظيفة العتامة (x) ، التي تتحكم في عتامة البكسل على طول نجم التصوير. الشرط الرئيسي هو أن العتامة يجب أن تصل إلى القيم القصوى في نهاية النجم ("جسمه") وتتلاشى بسلاسة إلى 0.0 (إلى "ذيله").

عندما نبدأ في فهم رمز المجمّع لتظليل البكسل ، يصبح هذا واضحًا:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[2], immediateIndexed  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.y  
    dcl_input_ps linear v3.w  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mov_sat r0.x, v2.y  
   1: ge r0.y, r0.x, l(0.052579)  
   2: ge r0.z, l(0.965679), r0.x  
   3: and r0.y, r0.z, r0.y  
   4: if_nz r0.y  
   5:  ge r0.y, l(0.878136), r0.x  
   6:  add r0.z, r0.x, l(-0.052579)  
   7:  mul r1.w, r0.z, l(1.211303)  
   8:  mov_sat r0.z, r1.w  
   9:  mad r0.w, r0.z, l(-2.000000), l(3.000000)  
  10:  mul r0.z, r0.z, r0.z  
  11:  mul r0.z, r0.z, r0.w  
  12:  mul r2.x, r0.z, l(0.084642)  
  13:  mov r1.yz, l(0.000000, 0.000000, 0.084642, 0.000000)  
  14:  movc r2.yzw, r0.yyyy, r1.yyzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  15:  not r0.z, r0.y  
  16:  if_z r0.y  
  17:   ge r0.y, l(0.924339), r0.x  
  18:   add r0.w, r0.x, l(-0.878136)  
  19:   mul r1.w, r0.w, l(21.643608)  
  20:   mov_sat r0.w, r1.w  
  21:   mad r3.x, r0.w, l(-2.000000), l(3.000000)  
  22:   mul r0.w, r0.w, r0.w  
  23:   mul r0.w, r0.w, r3.x  
  24:   mad r1.x, r0.w, l(0.889658), l(0.084642)  
  25:   mov r1.yz, l(0.000000, 0.084642, 0.974300, 0.000000)  
  26:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  27:  else  
  28:   mov r2.y, l(0)  
  29:   mov r0.y, l(-1)  
  30:  endif  
  31:  not r0.w, r0.y  
  32:  and r0.z, r0.w, r0.z  
  33:  if_nz r0.z  
  34:   ge r0.y, r0.x, l(0.924339)  
  35:   add r0.x, r0.x, l(-0.924339)  
  36:   mul r1.w, r0.x, l(24.189651)  
  37:   mov_sat r0.x, r1.w  
  38:   mad r0.z, r0.x, l(-2.000000), l(3.000000)  
  39:   mul r0.x, r0.x, r0.x  
  40:   mul r0.x, r0.x, r0.z  
  41:   mad r1.x, r0.x, l(-0.974300), l(0.974300)  
  42:   mov r1.yz, l(0.000000, 0.974300, 0.000000, 0.000000)  
  43:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  44:  endif  
  45: else  
  46:  mov r2.yzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  47:  mov r0.y, l(0)  
  48: endif  
  49: mov_sat r2.w, r2.w  
  50: mad r0.x, r2.w, l(-2.000000), l(3.000000)  
  51: mul r0.z, r2.w, r2.w  
  52: mul r0.x, r0.z, r0.x  
  53: add r0.z, -r2.y, r2.z  
  54: mad r0.x, r0.x, r0.z, r2.y  
  55: movc r0.x, r0.y, r2.x, r0.x  
  56: mad r0.y, cb4[1].x, -cb0[9].w, l(1.000000)  
  57: mul_sat r0.y, r0.y, v3.w  
  58: mul r0.x, r0.y, r0.x  
  59: mul r0.yzw, cb2[2].xxyz, cb4[0].xxxx  
  60: mul r0.x, r0.x, cb2[2].w  
  61: dp3 r1.x, l(0.333000, 0.555000, 0.222000, 0.000000), r0.yzwy  
  62: mad r1.xyz, r1.xxxx, v0.xyzx, -r0.yzwy  
  63: mad r0.yzw, v0.wwww, r1.xxyz, r0.yyzw  
  64: add r1.xyz, -r0.yzwy, v1.xyzx  
  65: mad r0.yzw, v1.wwww, r1.xxyz, r0.yyzw  
  66: mul o0.xyz, r0.xxxx, r0.yzwy  
  67: mov o0.w, r0.x  
  68: ret

بشكل عام ، التظليل أكثر تعقيدًا قليلاً وكان من الصعب بالنسبة لي معرفة ما يجري فيه. على سبيل المثال ، من أين جاءت جميع القيم مثل 1.211303 و 21.643608 و 24.189651؟

إذا كنا نتحدث عن دالة التعتيم ، فإننا بحاجة إلى قيمة إدخال واحدة. هذا أمر بسيط للغاية - texcoord في النطاق من [0،1] (السطر 0) سيكون مفيدًا هنا ، حتى نتمكن من تطبيق الوظيفة على طول النيزك بالكامل.

تحتوي وظيفة التعتيم على ثلاثة أجزاء / فواصل محددة بواسطة أربع نقاط تحكم:

   // current status: no idea how these are generated  
   const float controlPoint0 = 0.052579;  
   const float controlPoint1 = 0.878136;  
   const float controlPoint2 = 0.924339;  
   const float controlPoint3 = 0.965679;

ليس لدي أي فكرة عن كيفية اختيارهم / حسابهم.

كما يمكننا أن نرى من كود المجمع ، الشرط الأول هو التحقق فقط ما إذا كانت قيمة الإدخال في النطاق [controlPoint0 - controlPoint3]. إذا لم يكن كذلك ، فإن التعتيم هو 0.0 فقط.

   // Input for the opacity function
   float y = saturate(Input.Texcoords.y);  // r0.x
     
   // Value of opacity function.  
   // 0 - no change  
   // 1 - full color  
   float opacity = 0.0;  
     
   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
      ...

يعد فك شفرة رمز المجمع أدناه ضروريًا إذا أردنا أن نفهم كيف تعمل وظيفة التعتيم:

   6: add r0.z, r0.x, l(-0.052579)   
   7: mul r1.w, r0.z, l(1.211303)   
   8: mov_sat r0.z, r1.w   
   9: mad r0.w, r0.z, l(-2.000000), l(3.000000)   
  10: mul r0.z, r0.z, r0.z   
  11: mul r0.z, r0.z, r0.w   
  12: mul r2.x, r0.z, l(0.084642)

يحتوي الخط 9 على المعاملين "-2.0" و "3.0" ، مما يلمح إلى استخدام وظيفة السلس . نعم ، هذا تخمين جيد.

وظيفة HLSL السلس مع النموذج الأولي: خطوة السلس (min ، max ، x) تحدد دائمًا x إلى [ min-max ]. من وجهة نظر المجمّع ، يطرح هذا الحد الأدنى من قيمة الإدخال (أي من r0.z في السطر 9) ، ولكن لا يوجد شيء مثل ذلك في التعليمات البرمجية. بالنسبة إلى الحد الأقصى ، فهذا يعني ضمناً مضاعفة قيمة الإدخال ، ولكن لا يوجد شيء مثل 'mul_sat' في التعليمات البرمجية. بدلاً من ذلك ، هناك "mov_sat". هذا يخبرنا أن دقيقة و الحد الأقصى لل وظائف smoothstep هي 0 و 1.

نحن نعرف الآن أن سيجب أن يكون في الفاصل الزمني [0 ، 1]. كما ذكر أعلاه ، هناك ثلاثة أجزاء في دالة التعتيم. يشير هذا بوضوح إلى أن الشفرة تبحث عن مكاننا في الفاصل الزمني [sectionStart-sectionEnd].

الجواب هو وظيفة Linstep!

 float linstep(float min, float max, float v)  
 {  
   return ( (v-min) / (max-min) );  
 }

على سبيل المثال ، لنأخذ الجزء الأول: [0.052579 - 0.878136]. يكون الطرح في السطر 6. إذا استبدلنا القسمة بالضرب -> 1.0 / (0.878136 - 0.052579) = 1.0 / 0.825557 = ~ 1.211303.

نتيجة السلاسة في النطاق [0 ، 1]. الضرب على الخط 12 هو وزن القطعة. كل قطعة لها وزنها الخاص ، مما يسمح لك بالتحكم في العتامة القصوى لهذه القطعة المعينة.

هذا يعني أنه بالنسبة للمقطع الأول [0.052579 - 0.878136] ، تكون التعتيم في النطاق [0 - 0.084642].

يمكن كتابة دالة HLSL التي تحسب العتامة لمقطع عشوائي على النحو التالي:

 float getOpacityFunctionValue(float x, float cpLeft, float cpRight, float weight)  
 {  
   float val = smoothstep( 0, 1, linstep(cpLeft, cpRight, x) );  
   return val * weight;  
 }

لذا ، فإن الهدف كله هو ببساطة استدعاء هذه الوظيفة للمقطع المقابل.

ألق نظرة على الأوزان:

   const float weight0 = 0.084642;  
   const float weight1 = 0.889658;  
   const float weight2 = 0.974300; // note: weight0+weight1 = weight2

وفقًا لكود المجمع ، يتم حساب دالة التعتيم (x) على النحو التالي:

   float opacity = 0.0;

   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
     // Range of v: [0, weight0]  
     float v = getOpacityFunctionValue(y, controlPoint0, controlPoint1, weight0);  
     opacity = v;  
     
     [branch]  
     if ( y >= controlPoint1 )  
     {  
       // Range of v: [0, weight1]  
       float v = getOpacityFunctionValue(y, controlPoint1, controlPoint2, weight1);  
       opacity = weight0 + v;  
   
       [branch]  
       if (y >= controlPoint2)  
       {  
         // Range of v: [0, weight2]  
         float v = getOpacityFunctionValue(y, controlPoint2, controlPoint3, weight2);
         opacity = weight2 - v;          
       }  
     }  
   }

فيما يلي رسم بياني لوظيفة التعتيم. يمكنك بسهولة رؤية زيادة حادة في التعتيم ، مما يشير إلى بداية جسم نجم الرماية:


دالة عتامة الرسم البياني.

القناة الحمراء - قيمة التعتيم - القناة
الخضراء - نقاط التحكم - القناة
الزرقاء - الأوزان.


بعد حساب التعتيم ، كل شيء آخر هو مجرد اللمسات الأخيرة. ثم هناك مضاعفات إضافية: عتامة النجوم ولون النجم الرامي وتأثير الضباب. كما هو معتاد في تظليل TW3 ، يمكنك أيضًا العثور على مضاعفات زائدة عن طريق 1.0 هنا:

   // cb4_v1.x = 1.0  
   float starsOpacity = 1.0 - cb0_v9.w * cb4_v1.x;    
   opacity *= starsOpacity;  

   // Calculate color of a shooting star  
   // cb4_v0.x = 10.0
   // cb2_v2.rgb = (1.0, 1.0, 1.0)
   float3 color = cb2_v2.rgb * cb4_v0.x;
     
   // cb2_v2.w = 1  
   opacity *= cb2_v2.w;
     
   FogResult fr = { Input.FogParams, Input.AerialParams };  
   color = ApplyFog(fr, color);
     
   return float4( color*opacity, opacity);  
 }

4. ملخص


تكمن الصعوبة الرئيسية في الجزء مع وظيفة التعتيم. بعد فك تشفيرها ، يصبح كل شيء آخر سهل الفهم.

قلت أعلاه أن تظليل البكسل معقد قليلاً قليلاً. في الواقع ، نحن نهتم فقط بقيمة قيمة التعتيم (x) ، التي يتم تخزينها في r2.x (بدءًا من السطر 49). ومع ذلك ، فإن وظيفة التعتيم في كود التجميع تنشئ ثلاثة متغيرات إضافية: minRange (r2.y) ، maxRange (r2.z) والقيمة (r2.w). كلها معلمات تستخدم لحساب التعتيم عند عدم استخدام التعتيم (x) :

lerp( minRange, maxRange, smoothstep(0, 1, value) );

في الواقع ، يتم الحصول على قيمة التعتيم النهائية في الفرع الشرطي في السطر 55 - إذا كانت قيمة الإدخال سفي النطاق [controlPoint0 - controlPoint3] ، هذا يعني أنه يتم استخدام دالة التعتيم ، لذلك يتم تحديد r2.x. خلاف ذلك ، عندما تكون x خارج الفاصل الزمني ، يتم حساب التعتيم من r0.x ، أي وفقًا للمعادلة أعلاه.

لقد قمت بتصحيح عدد قليل من وحدات البكسل خارج الفاصل الزمني [controlPoint0 - controlPoint3] ، وتبين دائمًا أن التعتيم النهائي هو صفر.

هذا كل شيء لهذا اليوم. و كالمعتاد، شكرا على القراءه.

All Articles