الشبكة العربية لمطوري الألعاب

خبير  أحمد عبد الغني مشاركة 1

السلام عليكم
 
سأحاول هنا تحليل طريقة رسم تأثير الغيوم الذي نجده في هذا المثال:
 
http://xna-uk.net/blogs/randomchaos/archive/2008/10/02/volumetric-clouds-source.aspx
 
ولمن قام بتحميل البرنامج وتشغيله. ستجدون أنه يقدم نتائج جميلة وجذابة. ☺
 
سأقدم ما فهمته حتى الآن، كما أنني أريد أن أطرح أسئلة عن أمور لم أفهمها لنتناقش بها.
 
الطريقة هي طريقة رسم غيوم تقليدية باستخدام البيلبوردز billboards وهي ليست حجمية في واقعها كما يسميها نيمو كراد (صانع البرنامج).
 
الغيمة مكونة من عدد من البيلبوردز كل منها يحمل صورة مختلفة غالباً. الصور محفوظة في ملف واحد يوضع هذه الصور بجانب بعضها البعض. ويقوم الكود بانتقاء الصورة المطلوبة لكل بيلبورد (بصراحة طريقة حسابه لها لم تعجبني، فهي تحوي أرقام سحرية لن تنفع لو غيرت الصورة إلى صورة أخرى)
 
الشيء الذي يميز هذه الطريقة عن غيرها هي إضافة معاملات تنعيم وتشتيت عند حواف الغيمة وعند اقترابها من عين الناظر. مما يعطي إيحاء ناعما برأيي لا نجده عادة في تطبيقات رسم الجزيئات حتى في الألعاب التجارية.
 
كما أنه يقوم بترتيب البيلبوردز من الخلف إلى الأمام بالنسبة لعين الناظر وذلك كي يضمن نتائج دمج لوني صحيحة عند استخدام دمج ألفا. وهو يقوم بذلك في كل لقطة تتحرك بها عين الناظر.
 
طريقة تلوين الغيمة بسيطة جدا وهي تعتمد على لونين. لون للغيمة ولون للسماء. يتم دمج اللونين سوية وضربه باللون القادم من صورة الغيمة في الإكساء.
 
فيما عدا ذلك فإن الكود.. مممم. أقل من عادي. هناك إشارات استفهام لدي حول بعض الأمور:


public virtual void LoadContent()
{       
        rnd = new Random((int)DateTime.Now.Ticks);
        Thread.Sleep(15); // لماذا هذا السطر؟ 😒 
        BuildCloud();

}
 
السؤال الآخر أن لديه طريقتين لرسم الجزيئات. واحدة باستخدام البيلبوردز المكونة من أربع نقاط وتكون دائما تواجه عين الناظر. والطريقة الأخرى هي الاعتماد على ميزة الأشباح النقطية point sprite المدعومة في XNA و D3D9. لا أدري لم يعتمد على إحدى الطريقتين بدلا من الدمج بينهما. ما رأيكم؟
 
انتقادي الأخير هو أنه لا يستخدم مخازن الرؤوس كما يجب لتسريع الرسم (استخدام DrawPrimitiveUP).
 
كمحصلة نهائية فإن هذا المشروع ناجح في رسم الغيوم السماوية التي لا تتقاطع مع أي أجسام أخرى.
 
فلنتباحث هذه الأفكار سوية 😄

اللهم انصر أهلنا في فلسطين وآجرنا أن نكون عوناً لهم

خبير مدير وسام البهنسي مشاركة 2

وفي 16 ابريل 2009 09:06 ص، أعرب أحمد عبد الغني عن رأيه بالموقف كالآتي:

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

فعلاً، خاصة عملية القسمة على 100 هذه. هو يذكر أن المشكلة أنه عندما يحاول إرسال عدد أكبر من 1.0 من مظلل الرؤوس (vertex shader) إلى مظلل البكسلات (pixel shader)، فإن العدد يتم قصه، وهذا طبعاً متوقع للسبب الآتي. لاحظ معي البنية التي يقوم بتخريجها من مظلل الرؤوس:


struct VertexOut{
    half4 Position       : POSITION0;          half2 TextureCoords: TEXCOORD0;
    half4  Color        : COLOR0;
    half image : COLOR1; // هذا هو المتغير الذي يحمل رقم الصورة المستخدمة لإكساء البيلبورد
    half lightLerp : TEXCOORD2;
};
 
المتغير image يحمل الدلالة COLOR1، مما يعني أنه يجب التعامل معه كقيمة لونية. القيم اللونية ضمن هذه الدلالة لا تخرج عن المجال [0,1]، وبالتالي سيتم قص الرقم لو تعدى هذا المجال. الحل هو تغيير COLOR1 إلى شيء مثل TEXCOORD1 حيث المجال كامل ويتسع لأعداد خارج  [0,1].
 
 
 
بالنسبة لهذا السطر:


Thread.Sleep(15); // لماذا هذا السطر؟ 😒 
 
فهو فعلاً غير مبرر، ولم أجد أي شرح عنه في مدونة نيمو 😖
 


في 16 ابريل 2009 09:06 ص، قال أحمد عبد الغني بهدوء وتؤدة:

السؤال الآخر أن لديه طريقتين لرسم الجزيئات. واحدة باستخدام البيلبوردز المكونة من أربع نقاط وتكون دائما تواجه عين الناظر. والطريقة الأخرى هي الاعتماد على ميزة الأشباح النقطية point sprite المدعومة في XNA و D3D9. لا أدري لم يعتمد على إحدى الطريقتين بدلا من الدمج بينهما. ما رأيكم؟

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


في 16 ابريل 2009 09:06 ص، عقد أحمد عبد الغني حاجبيه بتفكير وقال:

انتقادي الأخير هو أنه لا يستخدم مخازن الرؤوس كما يجب لتسريع الرسم (استخدام DrawPrimitiveUP).

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

وسام البهنسي
مبرمج في إنفيديا وإنفريمز

مفصول عمر سمير  مشاركة 3

وفي 16 نيسان 2009 04:06 م، قال أحمد عبد الغني متحمساً:

Thread.Sleep(15); // لماذا هذا السطر؟

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

OSF متخصص محترف

خبير  أحمد عبد الغني مشاركة 4

شكراً على التوضيح أخي وسام، خصوصاً شرح مشكلة الدلالة في المظلل! 😄
 
أتمنى أن تشرح الموضوع أكثر لأنني أحب أن أعرف التفاصيل في هذا المجال 😒
 
جزيل الشكر

اللهم انصر أهلنا في فلسطين وآجرنا أن نكون عوناً لهم

خبير  سلوان الهلالي مشاركة 5

وفي 18 نيسان 2009 06:42 م، قال عمر سمير متحمساً:

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



وفي 18 نيسان 2009 07:28 ص، ظهر شبح ابتسامة على وجه عمر سمير وهو يقول:

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


لا أعلم لماذا تحرج نفسك هكذا، الأمر غير مضحك، بل محزن. 🙁
ما علاقة السرعة بكوكب نبتون بإجراء Sleep؟ كيف توصلت لهذا الإستنتاج العجيب، فقط لو اعرف.
سوف أقولها لك بشكل مباشر، كل ما ذكرته عن Sleep خاطئ تماماً، يجب عليك أن تراجع معلوماتك، وأن تعود من الصفر لتدرس البرمجة والرسوميات بطريقة صحيحة لكي تجنب نفسك الإحراج المرة القادمة، وتلك نصيحتي لك، صدقني عندما أقول إنك في موقف لا تحسد عليه. 
(ملاحظة/ لقد وضعت هذا الرد في الموضوع الآخر لكي لا ألّوث النقاش هنا، ولكن تم نقله من قبل الإدارة.)

محترف مشرف عبد اللطيف حاجي علي مشاركة 6

أما في 19 نيسان 2009 10:06 ص، فقد تنهد سلوان الهلالي بارتياح وهو يرد:

وماذا عن هذا الرد الذي كتبته بعد ساعة
لم يكتبه بعد ساعة. بل قبل 11 ساعة. 7:28 ص و 6:24 م

في 19 نيسان 2009 10:06 ص، غمغم سلوان الهلالي باستغراب قائلاً:

سوف أقولها لك بشكل مباشر، كل ما ذكرته عن Sleep خاطئ تماماً
أخي سلوان. مع أني معك في خطأ ما توصل له عمر. إلا أن هذا لا يعني أن نبدأ بمهاجمته دون تصحيح خطأه. جميعنا يخطئ و، أكرر، نحن هنا لنتعلم


بتاريخ 19 نيسان 2009 10:06 ص، قطب سلوان الهلالي حاجبيه بشدة وهو يقول:

(ملاحظة/ كتبت هذا الرد هنا لكي لا ألّوث الموضوع الآخر.)
لا توجد مشاركة "تلوث" موضوعاً. جميع المشاركات الهادفة تغني الموضوع ولا تلوثه. الفكرة هي في جعل مشاركاتنا هادفة.


الفكرة التي توصل لها عمر ليس خطأ مئة بالمئة إلا أن هذا ليس مكانها بالضبط.
الفكرة هي أن هناك نوعين من البرامج (أو الـ Threads) وهي برامج تستخدم المعالج بكثرة CPU Bound وبرامج تستخدم أجهزة الدخل والخرج بكثرة IO
Bound
يقوم نظام التشغيل بتوزيع شرائح وقت Time-Slice لكل برنامج حيث يعطيه حق التنفيذ في هذه الشريحة. عندما يقوم البرنامج بالوصول إلى جهاز دخل أو خرج فإنه غالباً لا يكون لديه ما يفعله بانتظار انتهاء عملية الوصول. لذلك فإن نظام التشغيل يقوم بإيقافه وتنفيذ برنامج آخر

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

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

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

أو على الأقل هذا ما أعرفه. إن كنت أخطأت في نقطة ما يرجى التصحيح

عبد اللطيف حاجي علي
مبرمج
In|Framez

خبير  سلوان الهلالي مشاركة 7

بتاريخ 19 نيسان 2009 10:14 ص، قطب عبد اللطيف حاجي علي حاجبيه بشدة وهو يقول:

أخي سلوان. مع أني معك في خطأ ما توصل له عمر. إلا أن هذا لا يعني أن نبدأ بمهاجمته دون تصحيح خطأه. جميعنا يخطئ و، أكرر، نحن هنا لنتعلم
 

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

محترف مشرف عبد اللطيف حاجي علي مشاركة 8

أشكر لك غيرتك على الموقع أخي سلوان وأبشرك بأن المسألة قد تم حلها بشكل جذري

والآن لنعد إلى مناقشة مثال الغيوم. فهو موضوع ممتع حقاً. وأتمنى من كل عضو أن يشارك بما يعرفه (أو لا يعرفه ☺ )

عبد اللطيف حاجي علي
مبرمج
In|Framez

خبير مدير وسام البهنسي مشاركة 9

وفي 18 ابريل 2009 07:57 ص، أعرب أحمد عبد الغني عن رأيه بالموقف كالآتي:

أتمنى أن تشرح الموضوع أكثر لأنني أحب أن أعرف التفاصيل في هذا المجال 😒

حسناً. فلنبدأ بلمحة سريعة عن ماهية المظلل في معالِـجات الرسم الفوري المعاصرة.
 
من أهم خطوات خط المعالجة في معالج الرسم الفوري هما خطوتي معالجة الرؤوس ومعالجة البكسلات.  معالجة الرؤوس هي عملية تتكرر لكل رأس في المجسم وتبدأ باستقبال معلومات الرأس من مخزن الرؤوس (vertex buffer) وتمريرها على مظلل الرؤوس، والذي يقوم بدوره بتحويل هذه المعلومات إلى إحداثيات جاهزة للإظهار على الشاشة (موقع على الشاشة ومعلومات إضافية).
 
بعد نداء مظلل الرؤوس على كافة الرؤوس لمثلث ما، يتم تمرير هذا المثلث إلى وحدة الرسترة (rasterizer)، والتي تقوم بتحويل المثلث إلى بكسلات على الشاشة. لمعرفة القيمة اللونية لكل بكسل من المثلث، يتم نداء مظلل البكسلات لكل من بكسلات المثلث. مظلل البكسلات يأخذ كمدخلات جميع المعلومات الإضافية التي قام بتخريجها مظلل الرؤوس، والتي يتم استيفاؤها عبر مساحة المثلث لتحصل على قيم وسيطة بين الرؤوس الثلاثة.
 
الآن نستطيع الكلام عن الدلالات (semantics). يستقبل مظلل الرؤوس مدخلاته على هيئة بنية struct تشابه بشكل كبير البنى في لغة C. مثلاً:



struct GridVertex
{
    float3 Pos : POSITION;
    float3 Nrm : NORMAL;
    float4 Color : COLOR0;
};
 
الفرق الوحيد هنا هو الكلمة الإضافية التي نجدها بعد النقطتين العموديتين :
هذه هي الدلالة (semantic)، وهي شيء خاص فقط بالبنى المستخدمة في المظللات كمدخلات أو مخرجات. هذه الدلالة تخبر المظلل بماهية المعلومات التي سيتم حفظها في ذلك المتغير. وهو يستخدم هذه الدلالة في الأمور التالية:
 
 
1 - جلب القيمة المقابلة لهذا المتغير من مخزن الرؤوس. لأن ترتيب سرد المتغيرات في بنية الرأس في لغة C (والمستخدم في تعبئة مخزن الرؤوس) ليس مجبراً على أن يطابق نفس الترتيب في بنية الرأس في مظلل الرؤوس. كمثال، البنية التالية يمكن استخدامها مع مظلل رؤوس يستخدم البنية التي ذكرناها أعلاه:


struct GRIDVERTEX
{
   D3DXVECTOR3 Normal;
   D3DXVECTOR3 Position;
};
 
لاحظ أن متغير الناظم يسبق متغير الموقع، ومتغير اللون غائب كلياً! هذا سليم، وليس خطأ! الآن سيقوم المظلل بتعبئة المتغير Color بقيمة لونية بيضاء بشكل افتراضي وذلك نظراً لغياب القيمة من بنية الرأس في المخزن.
 
 
2 - معرفة القيمة الافتراضية التي يجب استخدامها في حال غياب المتغير من مخزن الرؤوس. فالمتغيرات ذات الدلالة اللونية يتم تعبئتها بالقيم (1،1،1،1)، والمتغيرات ذات الدلالة "موقع" (position) يتم تعبئتها بـ (0،0،0،1) وهكذا.
 
 
3 - تحديد مجال القيم المسموح حفظها في المتغير (وهذا هو سبب المشكلة التي تحدثنا عنها في مثال الجزيئات). القيم اللونية تحفظ في مجال [0،1]، وقيم إحداثيات الإكساء مثلاً تحفظ في مجال كامل [+لانهاية، -لانهاية].
 
4 - تحديد طريقة استيفاء قيمة المتغير عبر المثلث عند الرسترة.
 
5 - ربط المتغير كخرج من مظلل الرؤوس مع نفس المتغير كدخل في مظلل البكسلات.
 
 
أخيراً، لعل القارئ يتساءل، كيف يستطيع المظلل في النقطة 1 أن يربط بين البنية في C وبين البنية التي لديه؟
هنا يأتي دور ما يدعى المُعـلـِن (Vertex Declarator)، وهو كما يظهر من الاسم، يقوم بالإعلان عن بنية الرأس كما تظهر في C لكن يضيف الدلالة على كل متغير في البنية، وهكذا تكتمل الحلقة.
 
أرجو أن تساعدكم هذه المعلومات في فهم خط المعالجة أكثر. ☺

وسام البهنسي
مبرمج في إنفيديا وإنفريمز