Wolfenstein 3D: تتبع الأشعة باستخدام WebGL1

صورة

بعد ظهور بطاقات رسومات Nvidia RTX في الصيف الماضي ، استعاد تتبع الشعاع شعبيته السابقة. خلال الأشهر القليلة الماضية ، تمت تعبئة خلاصتي على Twitter بدفق لا نهائي من مقارنات الرسومات مع تمكين RTX وتعطيله.

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

تعاني من متلازمة رفض تطورات الآخرين ، وكنتيجة لذلك ، قمتُ بإنشاء محرك عرض هجين خاص بي يستند إلى WebGL1. يمكنك اللعب باستخدام مستوى العرض التجريبي من Wolfenstein 3D مع المجالات (التي استخدمتها بسبب تتبع الأشعة) هنا .

النموذج


لقد بدأت هذا المشروع من خلال إنشاء نموذج أولي ، في محاولة لإعادة إنشاء الإضاءة العالمية باستخدام تتبع الأشعة من Metro Exodus .


أول نموذج أولي لإظهار الإضاءة العالمية المنتشرة (Diffuse GI)

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

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

ولفنشتاين 3D


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

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

المشكلة في بيئة أكثر تعقيدًا هي أنني ما زلت بحاجة إلى أن أتمكن من تتبع الأشعة في المشهد في الوقت الفعلي. عادة ، سيتم استخدام بنية التسلسل الهرمي لمستوى الصوت المحيط (BVH) لتسريع عملية تتبع الشعاع ، لكن قراري لإنشاء هذا المشروع على WebGL1 لم يسمح بذلك: من المستحيل تحميل بيانات 16 بت إلى نسيج في WebGL1 ولا يمكن استخدام العمليات الثنائية في تظليل. هذا يعقد الحساب الأولي وتطبيق BVH في تظليل WebGL1.

هذا هو السبب في أنني قررت استخدام المستوى التجريبي Wolfenstein 3D لهذا الغرض. في عام 2013 ، قمت بإنشاء تظليل WebGL واحد مجزأ في Shadertoy لا يؤدي فقط إلى تقديم مستويات تشبه Wolfenstein ، ولكن أيضًا من الناحية الإجرائية يخلق جميع القوام الضروري. من تجربتي في العمل على هذا التظليل ، كنت أعلم أن تصميم مستوى شبكة Wolfenstein يمكن استخدامه أيضًا كهيكل تسريع سريع وسهل ، وأن تتبع الأشعة على هذا الهيكل سيكون سريعًا جدًا.

يتم عرض لقطة شاشة العرض التوضيحي أدناه ، وفي وضع ملء الشاشة ، يمكنك تشغيلها هنا: https://reindernijhoff.net/wolfrt .


وصف قصير


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


شبح


منتشر gi


تأملات

تقديم استباقية


يمكن ترميز بطاقات Wolfenstein بالكامل في شبكة ثنائية الأبعاد 64 × 64. تستند الخريطة المستخدمة في العرض التوضيحي إلى المستوى الأول من الحلقة 1 من Wolfenstein 3D.

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

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

الظلال والإضاءة


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

varying vec3 vWorldPos; varying vec3 vNormal; void main(void) { vec3 ro = vWorldPos; vec3 normal = normalize(vNormal); vec3 light = vec3(0); for (int i=0; i<LIGHTS_ENCODED_IN_MAP; i++) { light += sampleLight(i, ro, normal); } 

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

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

تتبع راي


على الرغم من أن شفرة تتبع شعاع النموذج الأولي لـ GI المنتشر تم دمجها مع تظليل وقائي ، فقد قررت في العرض التوضيحي فصلها.


لقد قمت بفصلهم عن طريق تقديم عرض ثانٍ لجميع الأشكال الهندسية إلى هدف تجسيد منفصل (Diffuse GI Render Target) باستخدام تظليل آخر ينبعث منه أشعة عشوائية فقط لجمع GI منتشر (راجع قسم "نشر GI" أدناه). تتم إضافة الإضاءة المجمعة في هدف التجسيد هذا إلى الإضاءة المباشرة المحسوبة في مقطع التقديم الأمامي.

بفصل التمريرات الاستباقية و GI المنتشرة ، يمكننا أن نصدر أقل من حزمة GI منتشرة لكل بكسل شاشة. يمكن القيام بذلك عن طريق تقليل مقياس المخزن المؤقت (عن طريق تحريك شريط التمرير في الخيارات الموجودة في الزاوية اليمنى العليا من الشاشة).

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

تنبعث منها شعاع


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

  • في القناة الحمراء للون الملمس ، يتم تشفير جميع الكائنات الموجودة في الخلية المقابلة x و y من شبكة الخريطة. إذا كانت قيمة القناة الحمراء تساوي صفرًا ، فلا توجد كائنات في الخلية ، وإلا ، فسيتم شغلها بواسطة جدار (قيم من 1 إلى 64) أو باب أو مصدر ضوء أو كرة تحتاج إلى التحقق من التقاطع.
  • إذا كان المجال يشغل خلية شبكة مستوى ، فسيتم استخدام قناة خضراء وزرقاء وألفا لتشفير نصف القطر والإحداثيات النسبية x و y في الكرة داخل خلية الشبكة.

تنبعث الأشعة في مشهد من خلال اجتياز نسيج باستخدام الكود التالي:

 bool worldHit(n vec3 ro,in vec3 rd,in float t_min, in float t_max, inout vec3 recPos, inout vec3 recNormal, inout vec3 recColor) { vec3 pos = floor(ro); vec3 ri = 1.0/rd; vec3 rs = sign(rd); vec3 dis = (pos-ro + 0.5 + rs*0.5) * ri; for( int i=0; i<MAXSTEPS; i++ ) { vec3 mm = step(dis.xyz, dis.zyx); dis += mm * rs * ri; pos += mm * rs; vec4 mapType = texture2D(_MapTexture, pos.xz * (1. / 64.)); if (isWall(mapType)) { ... return true; } } return false; } 

يمكن العثور على رمز مشابه للبحث عن المفقودين في شادر Wolfenstein على Shadertoy.

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

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



منتشر gi


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

عند وجود اتجاه الحزمة rd ونقطة البدء ro ، يمكن حساب الإضاءة المنعكسة باستخدام الدورة التالية:

 vec3 getBounceCol(in vec3 ro, in vec3 rd, in vec3 col) { vec3 emitted = vec3(0); vec3 recPos, recNormal, recColor; for (int i=0; i<MAX_RECURSION; i++) { if (worldHit(ro, rd, 0.001, 20., recPos, recNormal, recColor)) { // if (isLightHit) { // direct light sampling code // return vec3(0); // } col *= recColor; for (int i=0; i<2; i++) { emitted += col * sampleLight(i, recPos, recNormal); } } else { return emitted; } rd = cosWeightedRandomHemisphereDirection(recNormal); ro = recPos; } return emitted; } 

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

انعكاس


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

 #ifdef REFLECTION col = mix(col, getReflectionCol(ro, reflect(normalize(vWorldPos - _CamPos), normal), albedo), .15); #endif 

تتم إضافة انعكاسات في تمرير التقديم الأمامي ، لذلك ، سوف تنبعث شعاع انعكاس واحد دائمًا شعاع انعكاس واحد.


الزماني لمكافحة التعرج


نظرًا لأن كلا من الظلال الناعمة في تمرير العرض الاستباقي وتقريب GI المنتشر يستخدمان عينة واحدة لكل بكسل تقريبًا ، تكون النتيجة النهائية صاخبة للغاية. لتقليل مقدار الضوضاء ، تم استخدام التعتيم الزماني (TAA) استنادًا إلى TAA الخاص بـ Playdead: Anti-Aliasing Reprojection Reprojection in INSIDE .

إعادة الإسقاط


الفكرة وراء TAA بسيطة للغاية: تقوم TAA بحساب بكسل واحد لكل إطار ، ثم تقوم بتقييم قيمها بالبكسل المترابط من الإطار السابق.

لمعرفة مكان وجود البيكسل الحالي في الإطار السابق ، تتم إعادة عرض موضع الجزء باستخدام مصفوفة عرض نموذج الإسقاط للإطار السابق.

إسقاط العينات والحد من الأحياء


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

 vec3 history = texture2D(_History, uvOld ).rgb; for (float x = -1.; x <= 1.; x+=1.) { for (float y = -1.; y <= 1.; y+=1.) { vec3 n = texture2D(_New, vUV + vec2(x,y) / _Resolution).rgb; mx = max(n, mx); mn = min(n, mn); } } vec3 history_clamped = clamp(history, mn, mx); 

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

اهتزازات الكاميرا


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

 this._projectionMatrix[2 * 4 + 0] += (this.getHaltonSequence(frame % 51, 2) - .5) / renderWidth; this._projectionMatrix[2 * 4 + 1] += (this.getHaltonSequence(frame % 41, 3) - .5) / renderHeight; 

الضوضاء


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

أخشى أن الضوضاء البيضاء المستخدمة في هذا العرض ليست جيدة جدًا.

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

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

 vec2 hash2() { vec3 p3 = fract(vec3(g_seed += 0.1) * HASHSCALE3); p3 += dot(p3, p3.yzx + 19.19); return fract((p3.xx+p3.yz)*p3.zy); } 


الحد من الضوضاء


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

لم أكن أرغب في قضاء الكثير من الوقت في هذا الجزء من العرض التوضيحي ، لذا حاولت تطبيق مرشح واحد فقط للحد من الضوضاء: Median3x3 من Morgan McGuire و Kyle Witson . لسوء الحظ ، لا يعمل هذا الفلتر جيدًا مع رسومات "فن البيكسل" لأنسجة الحائط: فهو يزيل جميع التفاصيل في المسافة ويدور حول زوايا بكسل الجدران القريبة.

في تجربة أخرى ، قمت بتطبيق نفس المرشح على Diffuse GI Render Target. على الرغم من أنه قام بتقليل الضوضاء بشكل طفيف ، وفي نفس الوقت تقريبًا دون تغيير تفاصيل قوام الجدار ، فقد قررت أن هذا التحسن لا يستحق ميلي ثانية إضافية تم إنفاقها.

عرض


يمكنك أن تلعب التجريبي هنا .

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


All Articles