قبل عامين ، كنت أجرب بالفعل
مواد الظل في Phaser 2D. في فيلم Ludum Dare الأخير ، قررنا فجأة إحداث رعب ، يا له من رعب بدون ظلال وأضواء! أنا متصدع بلدي المفاصل ...
... وليس شيء لعنة في الوقت المناسب ل LD. في اللعبة ، بالطبع ، هناك القليل من الضوء والظل ، ولكن هذا هو مظهر بائس لما كان ينبغي أن يكون بالفعل.
بعد عودتي إلى المنزل بعد إرسال اللعبة إلى المسابقة ، قررت "إغلاق الجشطالت" وإنهاء هذه الظلال المؤسفة. ما حدث - يمكنك أن
تشعر في اللعبة واللعب في العرض التوضيحي والنظر إلى الصورة وقراءة المقالة.
كما هو الحال دائمًا في مثل هذه الحالات ، لا معنى لمحاولة كتابة حل عام ، فأنت بحاجة إلى التركيز على موقف معين. يمكن تمثيل عالم اللعبة على شكل مقاطع - على الأقل تلك الكيانات التي تلقي بظلالها. الجدران مستطيلة ، الناس مستطيلات ، تدور فقط ، المفسد الجهنمية عبارة عن دائرة ، لكن في نموذج القطع ، يمكن تبسيطه بطول قطر يكون دائمًا عموديًا على شعاع الضوء.
هناك العديد من مصادر الضوء (20-30) ، وكلها دائرية (مصابيح موضعية) وتقع بشكل مشروط أقل من الكائنات المضيئة (بحيث يمكن أن تكون الظل غير محدودة).
رأيت في رأسي الطرق التالية لحل المشكلة:
- لكل مصدر إضاءة ، نقوم ببناء نسيج بحجم الشاشة (جيدًا ، أو أصغر 2-4 مرات). على هذا الملمس ، نرسم ببساطة شبه منحرف BCC'D ، حيث A هو مصدر الضوء ، BC هو قطعة الخط ، B'C 'هو إسقاط الخط على حافة النسيج. بعد ذلك ، يتم إرسال هذه القوام إلى التظليل ، حيث يتم خلطها في صورة واحدة.
قام مؤلف برنامج منهاج سيليست بشيء من هذا القبيل ، وهو مكتوب جيدًا في مقالته عن المتوسط: medium.com/@NoelFB/remaking-celestes-lighting-3478d6f10bf
المشكلات: 20 إلى 30 حجمًا على الشاشة تحتاج إلى إعادة رسم كل إطار تقريبًا وتحميلها في وحدة معالجة الرسومات. أتذكر أن هذه كانت عملية سريعة للغاية.
- الطريقة الموضحة في منشور على habr - habr.com/post/272233 . لكل مصدر ضوء نقوم ببناء "خريطة عمق" ، أي مثل هذا النسيج ، حيث x = زاوية "الحزمة" من مصدر الضوء ، y = رقم مصدر الضوء ، واللون == المسافة من المصدر إلى أقرب عقبة. إذا اتخذنا خطوة من 0.7 درجة (360/512) ، و 32 مصدرًا للضوء ، فسنحصل على نسيج 512 × 32 واحد ، لم يتم تحديثه لفترة طويلة.
(مثال الملمس لخطوة 45 درجة)
- الطريقة السرية التي سأصفها في النهاية
في النهاية ، استقرت على الطريقة الثانية. ومع ذلك ، فإن الوصف الموضح في المقالة لم يناسبني حتى النهاية. هناك ، تم بناء الملمس أيضًا في التظليل باستخدام rakecast - انتقل التظليل في الدورة من مصدر الضوء في اتجاه الحزمة وتبحث عن عقبة. في تجاربي السابقة ، صنعت أيضًا rakecast في التظليل ، وكانت باهظة الثمن ، وإن كانت عالمية.
"اعتقدت أن هناك شرائح فقط في النموذج" ، كما اعتقدت ، و 10-20 جزءًا يقع في دائرة نصف قطر أي مصدر للضوء. لا يمكنني بسرعة حساب خريطة المسافة بناء على هذا؟ "
لذلك قررت أن أفعل ذلك.
بادئ ذي بدء ، عرضت ببساطة على الشاشة الجدران ، "الشخصية الرئيسية" الشرطية ومصادر الضوء. حول مصادر الضوء ، دائرة من الضوء الصافي النقي مقطوعة في الظلام. للحصول على هذا:
( تجريبي )بدأت على الفور القيام به مع تظليل حتى لا الاسترخاء. كان من الضروري المرور فيه لكل مصدر ضوئي إحداثياته ونصف قطر عمله (لا يتجاوزه الضوء) ، ويتم ذلك ببساطة من خلال مجموعة موحدة. وبعد ذلك في التظليل (الذي هو مجزأ ، والذي يتم تنفيذه لكل بكسل على الشاشة) ، بقي لفهم ما إذا كان البيكسل الحالي في دائرة مضاءة أم لا.
class SimpleLightShader extends Phaser.Filter { constructor(game) { super(game); let lightsArray = new Array(MAX_LIGHTS*4); lightsArray.fill(0, 0, lightsArray.length); this.uniforms.lightsCount = {type: '1i', value: 0}; this.uniforms.lights = {type: '4fv', value: lightsArray}; this.fragmentSrc = ` precision highp float; uniform int lightsCount; uniform vec4 lights[${MAX_LIGHTS}]; void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; lightness += step(length(light.xy - gl_FragCoord.xy), light.z); } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,0.5), vec4(0,0,0,0), lightness); } `; } updateLights(lightSources) { this.uniforms.lightsCount.value = lightSources.length; let i = 0; let array = this.uniforms.lights.value; for (let light of lightSources) { array[i++] = light.x; array[i++] = game.world.height - light.y; array[i++] = light.radius; i++; } } }
الآن نحن بحاجة إلى فهم لكل مصدر للضوء الأجزاء التي ستلقي بظلالها. بدلاً من ذلك ، أي أجزاء من الشرائح - في الشكل أدناه ، لسنا مهتمين بالأجزاء "الحمراء" للجزء ، لأن النور لا يزال لا يصل إليهم.
ملاحظة: تعريف التقاطع هو نوع من التحسين الأولي. هناك حاجة لتقليل وقت إجراء مزيد من المعالجة ، والقضاء على قطع كبيرة من القطاعات خارج دائرة نصف قطرها من مصدر الضوء. هذا منطقي عندما يكون لدينا العديد من القطاعات التي يكون طولها أكبر بكثير من نصف قطر "التوهج". إذا لم يكن الأمر كذلك ، ولدينا العديد من الأجزاء القصيرة ، فقد يكون من الصواب عدم إضاعة الوقت في تحديد التقاطع ومعالجة الأجزاء بأكملها ، لأن توفير الوقت لا يزال لا يعمل.للقيام بذلك ، استخدمت الصيغة المعروفة للعثور على تقاطع خط مستقيم ودائرة ، والتي يتذكرها الجميع عن ظهر قلب من دورة مدرسية في الهندسة ... في عالم شخص ما الوهمي. أنا فقط لم أتذكرها ، لذلك اضطررت إلى
google .
نحن تشفير ، انظروا الى ما حدث.
( تجريبي )يبدو أن يكون هو القاعدة. الآن نحن نعرف القطاعات التي يمكن أن تلقي بظلالها ويمكنها تنفيذ rakecast.
هنا لدينا أيضا خيارات:
- نذهب فقط في دائرة في دائرة ، ورمي الأشعة والبحث عن التقاطعات. المسافة إلى أقرب تقاطع هي القيمة التي نحتاجها
- يمكنك فقط الانتقال إلى تلك الزوايا التي تقع في شرائح. بعد كل شيء ، نحن نعرف بالفعل النقاط ، فإنه ليس من الصعب حساب الزوايا.
- علاوة على ذلك ، إذا كنا نسير على طول مقطع ، فلن نحتاج إلى إلقاء الأشعة وحساب التقاطعات - يمكننا التحرك على طول الجزء مع الخطوة المطلوبة. إليك كيف تعمل:
هنا
- الجزء (الجدار) ،
هو مركز مصدر الضوء ،
- عمودي على القطعة.
هيا
- الزاوية إلى وضعها الطبيعي ، والتي تحتاج إلى معرفة المسافة من المصدر إلى القطعة ،
- نقطة في الجزء
حيث يسقط شعاع. مثلث
- مستطيل
- الساق ، وطولها معروف وثابت لهذا الجزء ،
- الطول المطلوب.
. إذا كنت تعرف الخطوة مقدمًا (ونعرفها) ، فيمكنك حساب جدول جيب التمام العكسي مسبقًا والبحث عن المسافات بسرعة كبيرة.
سأقدم مثالاً على الكود لمثل هذا الجدول. يتم استبدال كل العمل مع الزوايا تقريبًا بالعمل مع الفهارس ، أي أعداد صحيحة من 0 إلى N ، حيث N = عدد الخطوات في الدائرة (أي خطوة الزاوية =
)
class HypTable { constructor(steps = 512, stepAngle = 2*Math.PI/steps) { this.perAngleStep = [1]; for (let i = 1; i < steps/4; i++) {
بالطبع ، تقدم هذه الطريقة خطأ للحالات التي لا تكون فيها الزاوية المبدئية ACD مضاعفة لخطوة. ولكن بالنسبة لخطوات 512 ، لا أرى بصريًا أي فرق.
فما نعرفه بالفعل كيف نفعل:
- ابحث عن مقاطع داخل نطاق مصدر الضوء يمكنها أن تلقي بظلالها
- بالنسبة للخطوة t ، قم بإنشاء جدول dist (زاوية) ، يمر عبر كل مقطع وحساب المسافات.
إليك ما يبدو عليه هذا الجدول إذا قمت برسمه في الأشعة.
( تجريبي )وإليك كيفية البحث عن 10 مصادر ضوئية ، إذا كتبت في نسيج.
هنا ، يتوافق كل بكسل أفقي مع زاوية ، واللون إلى المسافة بالبكسل.
هو مكتوب في شبيبة مثل هذا باستخدام
imageData fillBitmap(data, index) { let total = index + this.steps*4; let d1, d2; let i = 0;
الآن نقوم بتمرير الملمس إلى تظليلنا ، الذي يحتوي بالفعل على إحداثيات ونصف قطر مصادر الضوء. ومعالجتها مثل هذا:
النتيجة:
( تجريبي )الآن يمكنك إحضار القليل من الجمال. دع الضوء يتلاشى مع المسافة ، وسوف تكون الظلال ضبابية.
لطمس ، أنا أنظر إلى الزوايا المجاورة ، + - الخطوة ، مثل هذا:
thisLightness = (1. - getShadow(i, angle, distance)) * 0.4 + (1. - getShadow(i, angle-SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle+SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle-SMOOTH_STEP*2., distance)) * 0.1 + (1. - getShadow(i, angle+SMOOTH_STEP*2., distance)) * 0.1;
إذا قمت بتجميع كل شيء معًا وقمت بقياس FPS ، فسيظهر كالتالي:
- على بطاقات الفيديو المضمنة - كل شيء سيء (<30-40) ، حتى للحصول على أمثلة بسيطة
- كل شيء آخر على ما يرام ، طالما أن مصادر الضوء ليست قوية جدا. أي أن عدد مصادر الضوء لكل بكسل مهم ، وليس العدد الإجمالي.
هذه النتيجة مناسبة تماما لي. لا يزال بإمكانك اللعب بلون الإضاءة ، لكنني لم أفعل ذلك. بعد التواء قليلاً وإضافة بعض الخرائط العادية ، حملت إصدارًا محدثًا من NOPE. لقد بدت هكذا الآن:
ثم بدأ في إعداد مقال. نظرت إلى مثل هذه GIF والفكر.
"هكذا يكون شكل شبه ثلاثي الأبعاد ، كما هو الحال في ولفنشتاين" ، صرخت (نعم ، لدي خيال جيد). في الواقع - إذا افترضنا أن جميع الجدران هي نفس الارتفاع ، فإن خرائط المسافة ستكون كافية لنا لبناء المشهد. لماذا لا تجرب ذلك؟
يجب أن يبدو المشهد بشيء من هذا القبيل.
لذلك مهمتنا:
- على نقطة على الشاشة ، احصل على إحداثيات العالم للحالة عندما لا توجد جدران.
سننظر في هذا:
- أولاً ، نضبط إحداثيات نقطة ما على الشاشة بحيث تكون هناك نقطة (0،0) في وسط الشاشة ، وفي الزوايا (-1 ، -1) و (1،1) ، على التوالي
- تصبح الإحداثي س الزاوية من اتجاه العرض ، ما عليك سوى ضربها بواسطة A / 2 ، حيث A هي زاوية المشاهدة
- تحدد إحداثي y المسافة من المراقب إلى النقطة ، في الحالة العامة d ~ 1 / y. للحصول على نقطة على الحافة السفلية من الشاشة ، المسافة = 1 ، لنقطة في وسط الشاشة ، المسافة = اللانهاية.
- وبالتالي ، إذا لم تأخذ في الحسبان الجدران ، فسيكون لكل نقطة مرئية في العالم نقطتان على الشاشة - واحدة فوق الوسط (في "السقف") والأخرى أدناه (في "الأرضية")
- الآن يمكننا أن ننظر إلى جدول المسافات. إذا كان هناك جدار أقرب من وجهة نظرنا ، فأنت بحاجة إلى رسم الجدار. إذا لم يكن كذلك ، فهذا يعني الكلمة أو السقف
نحصل على النحو المطلوب:
( تجريبي )أضف إضاءة - بنفس الطريقة ، قم بالتكرار على مصادر الإضاءة وتحقق من إحداثيات العالم. و - اللمسات الأخيرة - إضافة القوام. للقيام بذلك ، في نسيج ذي مسافات ، تحتاج أيضًا إلى كتابة الإزاحة u لملمس الحائط في هذه المرحلة. هذا هو المكان الذي جاءت فيه القناة ب في متناول يدي.
( تجريبي )الكمال.
مجرد مزاح.
ناقص ، بالطبع. لكن الجحيم ، ما زلت أقرأ عن كيفية جعل ولفنشتاين الخاص بي من خلال برنامج rakecast منذ حوالي 15 عامًا ، وأردت فعل كل شيء ، وهنا هذه فرصة!
بدلا من الاستنتاج
في بداية المقال ، ذكرت طريقة سرية أخرى. ومن هنا:
فقط تأخذ المحرك الذي يعرف بالفعل كيف.
في الواقع ، إذا كنت بحاجة إلى إنشاء لعبة ، فستكون هذه هي الطريقة الأصح والأسرع. لماذا تحتاج إلى سياج الدراجات الخاصة بك وحل المشاكل طويلة الأمد؟
لكن لماذا.
في الصف العاشر ، انتقلت إلى مدرسة أخرى واجهت مشاكل في الرياضيات. لا أتذكر المثال الدقيق ، لكنه كان معادلة مع الدرجات ، والتي في جميع النواحي تحتاج إلى التبسيط ، لكنها لم تنجح. يائسًا ، استشرت أختي وقالت: "لذا أضف x
2 على كلا الجانبين ، وكل شيء سيتحلل". وكان هذا هو الحل: أضف ما لم يكن هناك.
عندما ، بعد ذلك بكثير ، ساعدت صديقي في بناء منزلي ، كنت بحاجة لوضع حجر على العتبة - لملء مكانة. وهنا أقف وأفرز قطع القضبان. واحد يبدو مناسبا ، ولكن ليس تماما. البعض الآخر أصغر بكثير. أفكر في كيفية جمع كلمة السعادة هنا ، ويقول أحد الأصدقاء: "لذلك شربوا الأخاديد في مكان دائري حيث تتداخل". والآن الشريط الكبير يقف بالفعل صامدا.
يتم توحيد هذه القصص من خلال هذا التأثير ، والذي سوف أسميه "تأثير المخزون". عندما تحاول اتخاذ قرار من الأجزاء الموجودة ، دون رؤية المواد التي يمكن معالجتها وصقلها في هذه الأجزاء. الأرقام هي الخشب أو المال أو الرمز.
في كثير من الأحيان لاحظت نفس التأثير مع الزملاء في البرمجة. لا يشعرون بالثقة في المواد ، فهم يستسلمون أحيانًا عندما يكون من الضروري القيام بذلك ، على سبيل المثال ، عناصر التحكم غير القياسية. أو إضافة اختبارات وحدة إلى حيث لم تكن. أو يحاولون توفير كل شيء وكل شيء عند تصميم الفصل ، ثم نحصل على حوار مثل:
- هذا ليس ضروري الآن
- ماذا لو أصبح من الضروري؟
- ثم سنضيف. اترك نقاط التوسع ، هذا كل شيء. الكود ليس من الجرانيت ، إنه بلاستيني.
ولكي نتعلم كيف نرى ونشعر بالمواد التي نعمل بها ، نحتاج أيضًا إلى الدراجات.
هذه ليست مجرد تمرين للعقل أو التدريب. هذه طريقة للوصول إلى مستوى مختلف نوعيًا من العمل باستخدام الكود.
شكرا لكم جميعا على القراءة.
الروابط ، إذا نسيت النقر فوق مكان ما: