في سبتمبر من هذا العام ، من المقرر إطلاق لعبة Titan World للجوال من Unstoppable ، مكتب مينسك لشركة Glu mobile. تم إلغاء المشروع قبل الإصدار العالمي مباشرة. لكن الإنجازات بقيت ، وأكثرها إثارة للاهتمام ، بإذن من رؤساء الاستوديو دينيس زدونوف وأليكس بالي ، أود أن أشاركها مع الجمهور.في مارس 2018 ، عقدت أنا وقائد الفريق اجتماعًا ناقشنا فيه ما يجب القيام به بعد ذلك: تم اكتمال كود التقديم ، ولم تكن هناك ميزات جديدة وتأثيرات خاصة في الخطط. بدا وكأنه اختيار منطقي لإعادة كتابة نظام الجسيمات من نقطة الصفر - وفقًا لجميع الاختبارات ، أعطى أكبر انخفاض في الإنتاجية ، بالإضافة إلى أنه دفع المصممين إلى التجاهل مع واجهته (text config-file) وقدرات ضئيلة للغاية.
وتجدر الإشارة إلى أنه في معظم الوقت عمل الفريق على اللعبة في وضع "إصدار الغد" ، لذلك كتبت جميع الأنظمة الفرعية ، أولاً ، محاولًا عدم كسر ما يعمل بالفعل ، وثانيًا ، مع دورة تطوير قصيرة. على وجه الخصوص ، معظم التأثيرات التي لم يكن النظام العادي قادرًا عليها تم إجراؤها في تظليل الأجزاء دون التأثير على الكود الرئيسي.
القيد على عدد الجسيمات (تم تشكيل مصفوفات التحويل لكل جسيم على وحدة المعالجة المركزية ، وكان الاستنتاج من خلال مثبت ios القابل للامتداد gl) ، على سبيل المثال ، كان من الضروري كتابة تظليل "يحاكي" مجموعة كبيرة من الجسيمات على أساس التمثيل التحليلي لشكل الأشياء ومضاعفة مع الفضاء يزيل البيانات المزيفة في المخزن المؤقت للعمق.
تم حساب الإحداثيات z للجزء لجسيم مسطح ، كما لو كنا نرسم كرة ، وتم تعديل نصف قطر هذا المجال بواسطة جيب ضوضاء بيرلين مع مراعاة الوقت:
r=.5+.5*sin(perlin(specialUV)+time)
يمكن العثور على وصف كامل لإعادة بناء عمق المجال في
igoñigo Quílez ، لكنني استخدمت رمزًا مبسطًا وأسرع. بالطبع ، كان تقريبًا تقريبًا ، ولكن على الأشكال الهندسية المعقدة (الدخان والانفجارات) أعطى صورة لائقة جدًا.
لقطة شاشة اللعب. تم عمل "التنورة" الدخانية في جزء صغير ، وتركت عدة أجزاء أخرى على الجسم الرئيسي للانفجار. بالطبع ، بدا الأمر أكثر إثارة "من الأرض" ، عندما كان الدخان يلف المباني والوحدات بلطف ، ومع ذلك ، لم تدخل الاقتراحات لتغيير موضع الكاميرا أثناء الانفجار حيز الإنتاج.بيان المشكلة
ما الذي تريده للخروج؟ لقد ذهبنا ، بدلاً من ذلك ، من القيود التي تعذبنا بها على النظام السابق للجسيمات. تفاقم الوضع بسبب حقيقة أن ميزانية الإطار قد استنفدت تقريبًا ، وعلى الأجهزة الضعيفة (مثل هواء ipad) ، تم تحميل كل من ناقلات البكسل والرأس بالكامل. لذلك ، كنت أرغب في الحصول على النظام الأكثر إنتاجية نتيجة لذلك ، حتى لو قمت بتحديد الوظائف قليلاً.
قام المصممون بتجميع قائمة من الميزات ووضعوا رسمًا تخطيطيًا لواجهة المستخدم استنادًا إلى خبرتهم وممارستهم بالوحدة وغير الواقعية والآثار اللاحقة.
التكنولوجيا المتاحة
بسبب الإرث والقيود التي يفرضها المكتب الرئيسي ، اقتصرنا على opengl es 2. لذلك ، لم تكن التقنيات مثل ردود الفعل التحويلية المستخدمة في أنظمة الجسيمات الحديثة متاحة.
ماذا بقي؟ هل تستخدم إحضار نسيج الرأس وتخزين المواقف / التسارع في القوام؟ خيار عملي ، ولكن الذاكرة انتهت تقريبًا ، وأداء مثل هذا الحل ليس هو الأمثل ، والنتيجة ليست مختلفة في الجمال المعماري.
بحلول هذا الوقت ، كنت قد قرأت العديد من المقالات حول تنفيذ أنظمة الجسيمات على وحدة معالجة الجرافيكس. احتوت الغالبية العظمى على عنوان مشرق ("ملايين الجسيمات على وحدة معالجة الجرافيكس المتنقلة ، مع تفضيل وشاعرات") ، ومع ذلك ، جاء التنفيذ إلى أمثلة بسيطة ، وإن كانت مسلية تبحث عن بواعث / جاذبين ، وبشكل عام كان عديم الفائدة تقريبًا للاستخدام الحقيقي في اللعبة.
جلبت
هذه المقالة أقصى فائدة: قام المؤلف بحل المشكلة الحقيقية ، ولم يفعل "الجسيمات الكروية في الفراغ". وفرت الأرقام المعيارية من هذه المقالة ونتائج التنميط الكثير من الوقت في مرحلة التصميم.
ابحث عن المناهج
لقد بدأت بتصنيف المشاكل التي تم حلها بواسطة نظام الجسيمات والبحث عن حالات معينة. اتضح ما يلي تقريبًا (قطعة من الأحواض الحقيقية للمفهوم من المراسلات مع قائد الفريق):
"- صفائف الجسيمات / الشبكات ذات الحركة الدورية. لا يوجد وضع معالجة ، كل ذلك من خلال معادلة الحركة. التطبيقات - الدخان من الأنابيب ، والبخار فوق الماء ، والثلج / المطر ، والضباب الحجمي ، والأشجار المتمايلة ، والاستخدام الجزئي على الآثار غير الدورية للانفجارات المعروفة ممكن.
- شرائط. تشكيل vb حسب الحدث ، ومعالجته فقط على GPU (لقطات بالأشعة ، ورحلات على طول مسار ثابت (؟) مع تتبع). ربما المتغير مع نقل إحداثيات البداية إلى الزي الرسمي وبناء الشريط بواسطة vertexID سوف ينطلق. مع tz تقديم الصليب مع فريسنل كما هو الحال في directlights + uvscroll.
- توليد الجسيمات ومعالجتها بسرعة. الخيار الأكثر تنوعًا والأصعب / الأبطأ ، انظر معالجة الحركة التقنية ".
باختصار: هناك تأثيرات جسيمات مختلفة ، ويمكن تنفيذ بعضها بسهولة من غيرها.
قررنا تقسيم المهمة إلى عدة تكرارات - من البسيط إلى المعقد. تم عمل النماذج الأولية على محركي / محرري تحت windows / directx11 نظرًا لحقيقة أن سرعة هذا التطور كانت أعلى بعدة أوامر من حيث الحجم. تم تجميع المشروع في بضع ثوانٍ ، وتم تحرير تظليل بالكامل على الطاير وتجميعه في الخلفية ، وعرض النتيجة في الوقت الحقيقي ودون الحاجة إلى أي إيماءات إضافية مثل الضغط على الأزرار. أعتقد أن أي شخص قام ببناء مشاريع كبيرة باستخدام مجموعة من macbook / xcode سيفهم أسباب هذا القرار.
سيتم أخذ جميع أمثلة التعليمات البرمجية من النموذج الأولي للنوافذ.
بيئة تطوير النوافذ.التنفيذ
الخطوة الأولى هي الإخراج الثابت لصفيف القسم. لا يوجد شيء معقد: ابدأ عازلة الذروة ، واملأها بالكواد (اكتب الأشعة فوق البنفسجية الصحيحة لكل رباعية) ، وخيط معرف الرأس في الأشعة فوق البنفسجية "الإضافية". بعد ذلك ، في التظليل ، بواسطة معرف قمة الرأس بناءً على إعدادات الباعث ، نشكل مواضع الجسيمات ، وعن طريق الأشعة فوق البنفسجية ، نستعيد إحداثيات الشاشة.
إذا كان vertex_id متاحًا أصلاً ، يمكنك الاستغناء تمامًا عن وجود مخزن مؤقت ودون الأشعة فوق البنفسجية لاستعادة إحداثيات الشاشة (ونتيجة لذلك تم إجراؤها في إصدار Windows).
شادر:
struct VS_INPUT { … uint v_id:SV_VertexID; … } //float index = input.uv2.x/6.0;// vertex_id index = floor(input.v_id/6.0);// vertex_id float2 map[6]={0,0,1,0,1,1,0,0,1,1,0,1}; float2 quaduv=map[frac(input.v_id/6.0)*6];
بعد ذلك ، يمكنك تنفيذ سيناريوهات بسيطة بكمية صغيرة جدًا من التعليمات البرمجية ، على سبيل المثال ، الحركة الدورية مع انحرافات صغيرة مناسبة لتأثير الثلج. ومع ذلك ، كان هدفنا هو إعطاء السيطرة على سلوك الجسيمات إلى جانب الفنانين ، ونادرًا ما يعرفون ، كما تعلمون ، كيفية التظليل. الخيار مع الإعدادات المسبقة للسلوك ومعلمات التحرير من خلال أشرطة التمرير لم يستأنف أيضًا - تبديل التظليل أو التفرع في الداخل ، مضاعفة خيارات الإعداد المسبق ، نقص التحكم الكامل.
كانت المهمة التالية هي تنفيذ التلاشي في / التلاشي لهذا النظام. يجب ألا تظهر الجسيمات من العدم وتختفي في العدم. في التنفيذ الكلاسيكي لنظام الجسيمات ، نقوم بمعالجة المخزن برمجيًا باستخدام وحدة المعالجة المركزية ، وإنشاء جزيئات جديدة وإزالة الجسيمات القديمة. في الواقع ، للحصول على أداء جيد ، تحتاج إلى كتابة مدير ذاكرة ذكي. ولكن ماذا يحدث إذا لم ترسم الجزيئات "الميتة"؟
افترض (بالنسبة للمبتدئين) أن الفاصل الزمني لانبعاث الجسيمات وعمر الجسيم هو ثابت داخل باعث واحد.

ثم يمكننا تقديم المخزن المؤقت الخاص بنا (الذي يحتوي فقط على معرف القمة) بشكل دائري وتحديد حجمه الأقصى على النحو التالي:
pCount = round (prtPerSec * LifeTime / 60.0); pCountT = floor (prtPerSec * EmissionEndTime / 60.0); pCount=min (pCount, pCountT);
وفي التظليل ، احسب الوقت على أساس المؤشر والوقت (الوقت المنقضي منذ بداية التأثير)
pTime=time-index/prtPerSec;
إذا كان الباعث في طور دوري (تنبعث جميع الجسيمات وتموت الآن وتولد بشكل متزامن) ، فنحن نصنع frac من وقت الجسيم وبالتالي نحصل على حلقة.
لسنا بحاجة لرسم الجسيمات مع pTime أقل من الصفر - لم يولدوا بعد. وينطبق الشيء نفسه على الجسيمات التي يتجاوز فيها مجموع العمر الزمني والوقت الحالي وقت انتهاء الانبعاثات. في كلتا الحالتين ، لن نرسم أي شيء عن طريق إبطال حجم الجسيمات و / أو إسقاطه خلف الشاشة. سيعطي هذا النهج عبءًا صغيرًا في مراحل التلاشي / التلاشي ، مع الحفاظ على الأداء الأقصى في مرحلة الاستدامة.
يمكن تحسين الخوارزمية قليلاً عن طريق إرسال ذلك الجزء فقط من المنطقة العازلة التي تحتوي على جزيئات حية للعرض. نظرًا لحقيقة حدوث الانبعاث بالتتابع ، سيتم تقسيم الجسيمات الحية مرة واحدة على الأكثر ، أي مطلوب اثنين من drawcalls.
الآن ، مع معرفة الوقت الحالي لكل جسيم ، يمكنك تعيين السرعة والتسارع (وبشكل عام ، أي معلمات أخرى) لكتابة معادلة الحركة ، مما يؤدي إلى إحداثيات في الفضاء العالمي.
باستخدام المستعادة من vertex_id uv ، سنحصل بالفعل على أربع نقاط (بتعبير أدق ، سنقوم بتحريك كل من النقاط الرباعية في الاتجاه الذي نحتاج إليه) ، حيث سيكمل جهاز تظليل الرأس ، بعد الانتهاء من الإسقاط ، عمله.
p.xy+=(quaduv-.5);
مع المكافأة المجانية ، حصلنا على فرصة ليس فقط لإيقاف الباعث مؤقتًا ، ولكن أيضًا لإرجاع الوقت ذهابًا وإيابًا بدقة للإطار. تبين أن هذه الميزة مفيدة جدًا في تصميم التأثيرات المعقدة.
نقوم بزيادة الوظائف
التكرار التالي في التطوير كان حل مشكلة باعث متحرك. لم يكن نظامنا الخاص يعرف أي شيء عن موقعه ، وعندما تحرك الباعث ، تحرك التأثير كله بشكل متزامن خلفه. بالنسبة للدخان من ماسورة العادم والتأثيرات المماثلة ، بدا أكثر من غريب.
كانت الفكرة هي تسجيل موضع الباعث في المنطقة العازلة عند ولادة جسيم جديد. نظرًا لأن عدد هذه الجسيمات صغير ، فيجب أن يكون الحمل في أدنى حد.
اقترح أحد الزملاء أنه عند تطوير واجهة المستخدم الخاصة به ، استخدم الخريطة / unmap فقط جزءًا من المخزن المؤقت لذروة الرأس وكان سعيدًا جدًا بأداء هذا الحل. لقد أجريت الاختبارات ، وتبين أن هذا النهج يعمل بشكل جيد حقًا على كل من منصات سطح المكتب والجوّال.
نشأت الصعوبة مع تزامن الوقت على وحدة المعالجة المركزية ووحدة معالجة الرسومات. كان من الضروري التأكد من أن تحديث المخزن المؤقت تم بالضبط عندما كان الجسيم الحلقي "الجديد" في وضع البداية. أي ، فيما يتعلق بالمخزن المؤقت الحلقي ، من الضروري مزامنة حدود منطقة التحديث مع وقت تشغيل الباعث.
لقد قمت بنقل كود hlsl إلى C ++ ، للاختبار كتبت الباعث يتحرك حول Lissajous ، وعمل كل هذا فجأة. ومع ذلك ، من وقت لآخر ، "يبصق" النظام على جسيم واحد أو أكثر ، ويطلقها في اتجاه تعسفي ، ولا يزيلها في الوقت المناسب ، أو يخلق جسيمات جديدة في أماكن عشوائية.
تم حل المشكلة عن طريق تدقيق دقة حساب الوقت في المحرك والتحقق في الوقت نفسه من دلتا الوقت عند تسجيل موضع المرسل الجديد - بحيث يتم تحديث قسم المخزن المؤقت بالكامل الذي لم يتأثر بالتكرار السابق. كان من الضروري أيضًا أن يعمل النظام في ظروف desync القسري - لا ينبغي أن يؤدي التخفيض المفاجئ للثانية في الثانية إلى كسر التأثير ، خاصة أنه بالنسبة للأجهزة المختلفة ، سجلت لعبتنا fps مختلفة وفقًا للأداء - 60/30/20.
نما رمز الطريقة كثيرًا جدًا (من الصعب معالجة المخزن المؤقت الحلقي بأناقة) ، ومع ذلك ، بعد أخذ جميع الشروط في الاعتبار ، عمل النظام بشكل صحيح ومستقر.
في هذا الوقت ، كان الشريك قد صنع بالفعل "سمكة" المحرر ، وهو كافٍ لاختبار النظام ، وكتب قوالب / api لدمج النظام في محركنا.
قمت بنقل جميع التعليمات البرمجية إلى ios / opengl ، مدمجة وأخيراً أجرت اختبارات تأثيرات حقيقية على جهاز حقيقي. أصبح من الواضح أن النظام لا يعمل فقط ، ولكنه مناسب أيضًا للإنتاج. بقي لإنهاء محرر واجهة المستخدم وصقل الرمز إلى الدولة "ليس من المخيف إعطاؤه للإفراج عنه غدًا".
لقد استعدنا بالفعل لكتابة مدير ذاكرة من أجل عدم تخصيص / تدمير المخزن المؤقت (الذي خزن في نهاية المطاف vertex_id ، والأشعة فوق البنفسجية ، والموضع ومتجه الجسيمات الأولي) لكل تأثير جديد مع باعث ديناميكي ، حيث ظهرت فكرة أخرى في رأسي.
حقيقة وجود المنطقة العازلة في الرأس في هذا النظام يلازمني. لقد نظر بوضوح في أركتها ، "إرث العصور المظلمة للناقل الثابت". عند إجراء تأثيرات اختبار على نموذج أولي للنوافذ ، اعتقدت أن حركة المرسل دائمًا ما تكون سلسة وأبطأ دائمًا من حركة الجسيم. علاوة على ذلك ، مع وجود عدد كبير من الجسيمات ، يؤدي تحديث الموقع إلى حقيقة أن مئات الجسيمات تسجل نفس البيانات. اتضح أن الحل بسيط: نقدم مصفوفة ثابتة يسقط فيها "تاريخ" وضع المرسل ، الذي تمت تسويته على مدار عمر الجسيم. وعلى GPU نقوم باستيفاء البيانات. بعد ذلك ، اختفت الحاجة إلى المخازن المؤقتة الديناميكية في إصدار ios / gles2 (بقيت الحالة العامة فقط لتطبيق vertex_id) ، وفي إصدارات windows / dx11 اختفت المخازن المؤقتة تمامًا بسبب vertex_id الأصلي وقدرة api d3d على قبول null بدلاً من الارتباط بالمخزن المؤقت vertex.
وبالتالي ، فإن النسخة الرابحة من النظام ، بالمعايير الحديثة ، لا تستهلك الذاكرة على الإطلاق ، بغض النظر عن عدد الجسيمات التي نريد عرضها. فقط مخزن مؤقت صغير مع معلمات ، مخزن مؤقت من المواقف / القواعد (60 زوجًا من المتجهات تبين أنها كافية ، مع هامش ، في أي حالة) ، وإذا لزم الأمر ، نسيج. تظهر قياسات الأداء سرعة قريبة من الاختبارات الاصطناعية.
بالإضافة إلى ذلك ، بدأ "الذيل" في التأثيرات مثل الشرارات تبدو أكثر طبيعية ، حيث أن الاستيفاء جعل من الممكن إزالة التمييز عن طريق الإطارات وبالتالي قام المرسل بتغيير موضعه بسلاسة ، كما لو تم إجراء مكالمات الرسم بتردد مئات هرتز.
الميزات
بالإضافة إلى الوظائف الأساسية لرحلة الجسيم (السرعة ، التسارع ، الجاذبية ، مقاومة الوسط) ، كنا بحاجة إلى كمية معينة من "الدهون" الوظيفية.
ونتيجة لذلك ، ضبابية الحركة (الجسيمات الممتدة على طول ناقل الحركة) ، وتوجيه الجسيمات عبر ناقل الحركة (يسمح هذا ، على سبيل المثال ، بعمل مجال من الجسيمات) ، وتغيير حجم الجسيمات وفقًا للوقت الحالي من حياتها ، وتم تنفيذ العشرات من الأشياء الصغيرة الأخرى.
نشأ التعقيد مع الحقول الموجهة: نظرًا لأن النظام لا يخزن حالته (الموضع ، التسارع ، إلخ) لكل جسيم ، ولكنه يحسبها في كل مرة من خلال معادلة الحركة ، كان من المستحيل عددًا من التأثيرات (مثل حركة الرغوة عند تحريك القهوة). ومع ذلك ، فإن التعديل البسيط للسرعة والتسارع بفعل ضجيج بيرلين أعطى نتائج تبدو حديثة تمامًا. تبين أن حساب الضوضاء في الوقت الفعلي للعديد من الجسيمات باهظ الثمن (حتى مع حد من خمسة أوكتافات) ، لذلك تم إنشاء نسيج يتم من خلاله اختبار تظليل الرأس. لتعزيز تأثير حقل المتجه المزيف ، تمت إضافة تحول صغير لإحداثيات العينة اعتمادًا على الوقت الحالي للمرسل.
يعمل اختبار دخان السجائر من خلال توزيع السرعة الأولية والتسارع على ضجيج بيرلين.ناقل بكسل
في البداية ، خططنا فقط لتغيير لون / شفافية الجسيم اعتمادًا على وقته. أضفت العديد من الخوارزميات إلى تظليل بكسل.
دوران لون الملمس - مبسط ، خطيئة (لون + وقت). يسمح إلى حد ما بتقليد تأثير التبادل من AfterEffects.
إضاءة زائفة - تعديل لون الجسيم بواسطة التدرج في إحداثيات العالم ، بغض النظر عن زاوية دوران الجسيم.
تطور الحدود - عندما يتحرك الجسيم في الفضاء ، يتم تعديل حدوده (قناة ألفا) من خلال مزيج من الأضواء وضوضاء بيرلين ، والتي تعطي ديناميكيات التدفق ، تشبه إلى حد كبير السحب والدخان وتأثيرات السوائل الأخرى.
كود شادر الزائف:
b=perlin(uv)
في إصدار معقد قليلاً ، يمكن لهذا التظليل أن يرسم حدودًا بنعومة اعتباطية وبتمييز كفافي ، مما أضاف تأثيرات "متفجرة" إلى الواقعية.
التجارب الأولى مع تطور الحدود.ما هي الخطوة التالية؟
على الرغم من أن المحرر جاهز بالفعل للعمل واندماجه في المحرك ، لم يكن لدى المصممين الوقت لإحداث تأثير واحد عليه - تم إغلاق المشروع. ومع ذلك ، لا توجد عوائق لاستخدام هذه الممارسات في مكان آخر - على سبيل المثال ، للقيام بعمل في المراجعة التجريبية.
من وجهة نظر تكنولوجية ، هناك أيضًا مساحة للتحرك - الآن ، على سبيل المثال ، هناك العديد من تأثيرات تدمير كائنات الإطار السلكي قيد التشغيل:

لا تزال مسألة فرز الجسيمات لمزج ألفا مفتوحة حتى الآن: نظرًا لأن كل شيء يعتبر تحليليًا في التظليل ، فلا توجد في الواقع بيانات إدخال للفرز. ولكن هناك مجال كبير للتجريب!
خلال تطوير Titan World ، تم تطبيق الكثير من الحيل في الجزء الرسومي من اللعبة ، ولكن المزيد عن ذلك في المرة القادمة.
ملاحظة: يمكنك الحفر في محرك ألفا المصدر
هنا . الأمثلة في مجلد الإصدار / العينات ، مفاتيح التحكم الرئيسية هي المسافة ، alt | control + mouse. تقع Shaders مباشرة في ملفات fxp ، رمزها متاح من خلال نافذة المحرر.