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

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

السلام عليكم،
 
الفكرة التالية تطبيق لمؤثر رسومي جذاب قرأته في مقالة علمية في موقع inframez حول طريقة بسيطة لرسم المجسمات وكأنها مصورة بأشعة اكس الطبية.



عنوان المقالة الأصلية: X-tasy (ecstasy) with Real-Time Shaders
المؤلف: همام البهنسي
الرابط للمقالة: http://www.inframez.com/papers/xsi_rtshaders2.htm

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

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

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

السلام عليكم،
 
فيما يلي بعض نتائج تطبيق المؤثر المقترح في المقالة، لكن ضمن مثال برمجي بمشهد مختلف (أحب تسميته مشهد الـ Matrix 😄 )...

لقطات بعد تطبيق المؤثر:








لقطة للمشهد الأصلي:

 


المثال البرمجي مبدؤه المثال MultiAnimation الذي يأتي مع الـ DirectX SDK. والذي يقوم برسم شخصية tiny وتحريكها بشكل لطيف أعجبني وأحسست أنه يناسب المؤثر الذي اخترته. التعديلات بشكل رئيسي فقط في ملف الـ effect المكتوب بلغة HLSL، وهي تطبيق للمفهوم المطروح في المقالة الأصلية، بالإضافة إلى بعض اللفتات التي أحببت أن أضيفها بنفسي ☺

في المشاركات التالية بإذن الله سأطرح كود المشروع وسأبدأ بشرح خطوات العمل المتبعة ضمن الـ Visual Studio للوصول لهذه المؤثر.

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

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

تجدون أعزائي كود المشروع الكامل مع المرفقات!
 
عند التشغيل، لو ظهرت لك رسالة خطأ بسبب عدم وجود مكتبة D3DX، فيرجى تنصيب آخر نسخة من DirectX9 على جهازك.
 
هذا الرابط يحوي النسخة المطلوبة من DirectX:
 
http://www.microsoft.com/DOWNLOADS/details.aspx?FamilyID=2da43d38-db71-4c1b-bc6a-9b6652cd92a3&displaylang=en
 
من الجدير بالذكر أيضاً أن المؤثر يحتاج إلى كرت شاشة قادر على تشغيل المظللات من الجيل الثاني (Shader Model 2.0)...

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

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

شرح تفاصيل العمل:
 
1- أولاً بدأت من نسخة من مثال MultiAnimation الموجود في الـ DirectX SDK. كل التغييرات تنحصر في الملفين MultiAnimation.cpp و MultiAnimation.fx.

2- أول التعديلات هي فصل كود رسم الأرضية، لأنني لا أريد أن تظهر الأرضية في المشهد النهائي. هذا يتم عن طريق فصل السطر:
 

V( g_pMeshFloor->DrawSubset( 0 ) );
 
والموجود في الإجراء OnFrameRender في ملف الـ CPP.
 
3- بالمثل قمت بفصل الأصوات لأنها مزعجة برأيي 😒  (تعديلات الكود غير مهمة لموضوعنا الآن).
 
4- أولاً استبدلت صورة الإكساء المستخدمة للشخصية (الصورة اليسرى) بصورة أخرى تحوي نفس التشويش المستخدم في المقالة الأصلية (الصورة اليمنى):
 



5- استبدلت لون الخلفية باللون الأسود بدلاً من اللون الأزرق في السطر الموجود في الإجراء OnFrameRender أيضاً:
 

pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
                   D3DCOLOR_ARGB( 0, 0x3F, 0xAF, 0xFF ), 1.0f, 0L );
 
ليصبح:
 
pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
                   D3DCOLOR_ARGB( 0xFF, 0, 0, 0 ), 1.0f, 0L );
 
 
6- الآن أصبح المشهد جاهزاً لتطبيق المؤثر. هذه لقطة من الوضع الحالي بعد التعديلات أعلاه:
 



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

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

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

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



إذن المسألة هي عبارة عن حساب هذه الزاوية وانتقاء اللون المناسب بحسبها:
   الزاوية الحادة = لون شفاف
   الزاوية القائمة فما أكثر = لون مصمت
 
الآن السؤال الذي يطرح نفسه، هو كيف ننفذ هذه الحسابات؟
يمكننا أن نقوم بأداء الحسابات بشكل مباشر داخل كود المظلل في الملف MultiAnimation.fx، ويجب أن أقوم بتنفيذ عمليات حساب زاوية وتحويل القيمة إلى لون وَ وَ وَ...
 
وهنا لاحظت شيئاً مضحكاً.. إن أساس المسألة هو قياس الزاوية بين الناظم وشعاع ما، واستنتاج قيمة لونية وفقاً للزاوية بينهما... أليس هذا يكافئ (بعبارة أخرى) نفس حسابات الإضاءة القياسية في D3D أو OpenGL؟
 
بنظرة أخرى، قررت عكس ألوان الصورة العلوية، وانظروا النتيجة:
 


أليست هذه الصورة لكرة مضاءة بشكل تقليدي؟
 
إذن تصبح المسألة ببساطة هي حساب الإضاءة على المجسم، ومن ثم عكس القيمة اللونية، وصلى الله وبارك 😄
 
الجزء الوحيد المتبقي من العمل إذن هو جعل اتجاه الضوء يتبع اتجاه الكاميرا دائماً، كي ينتج لدي نفس التأثير الموجود في المقالة الأصلية...
الآن وبتتبع الكود في مثال MultiAnimation، وجدت السطر التالي المستخدم لحساب اتجاه الضوء في ملف الـ CPP في نفس إجراء الـ OnFrameRender:
 
// Light direction is same as camera front (reversed)

vLightDir = -( *g_Camera.GetWorldAhead() );
 
لسخرية القدر، إنها تماماً نفس الحسابات المطلوبة!!! 😲 😲 😲
 
إذن كل ما نحتاجه هو بعض التعديلات في ملف المظلل وانتهى الأمر...
 
 
8- السطر المعني بالإضاءة في ملف MultiAnimation.fx هو:
 

// Shade (Ambient + etc.)
o.Diffuse = float4( MaterialAmbient.xyz + saturate( dot( Normal, lhtDir.xyz ) ) * MaterialDiffuse.xyz, 1.0 );
 
وهو كما نلاحظ، مرعب بما فيه الكفاية... لذلك فلنبسطه أولاً ونتخلص من الزوائد:
 

float Intensity = saturate( dot( Normal, lhtDir.xyz ) );
 
o.Diffuse = Intensity * MaterialDiffuse;
 
السطر الأول مختص بحساب شدة الإضاءة، والسطر الثاني يقوم بتلوين السطح وفقاً لهذه الشدة.
شدة الإضاءة هي قيمة تتراوح بين 0 و 1. حيث القيمة 0 تعني أن السطح قاتم، و 1 تعني أن السطح مضيء، و 0.5 مثلاً تعني أن السطح نصف مضيء...
 
الآن نقوم بعكس شدة الإضاءة، وذلك كالآتي:
 

float Intensity = saturate( dot( Normal, lhtDir.xyz ) );
 
Intensity = 1.0f - Intensity; // عكس شدة الإضاءة
 
o.Diffuse = Intensity * MaterialDiffuse;
 
 
والنتيجة هي كالآتي:




النتيجة مقاربة جداً للمقالة، وما زلنا لم نقم بكتابة أي كود يذكر!!  😄
 
نتابع البقية في المشاركة القادمة

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

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

9- النتيجة كما هي الآن ممتازة. لنضبط الأمور أكثر، أولاً نغير لون الشخصيات إلى الأخضر.  التعديل في بداية ملف المظلل:
 

float4 MaterialDiffuse : MATERIALDIFFUSE = { 0.0f, 0.8f, 0.0f, 1.0f };
 
 
10- في المقالة الأصلية، يتم التلاعب بنمط دمج الألوان، لتظهر الألوان باهرة حيث يكون هناك تقاطع بين المجسمات في المشهد. ركزوا في منطقة المفاصل في الصورة الأصلية:



 

نمط الدمج هذا شهير جداً، ويدعى additive، حيث أن الألوان تضاف إلى بعضها. يمكننا الوصول إليه عن طريق ضبط إعدادات الـ alpha blending لتقوم بإضافة الألوان مع بعضها بدلاً من دمجها بطريقة الاستبدال الاعتيادية... سنضيف بضعة أسطر في ملف المظلل:


technique Skinning20
{
  pass p0
  {
    VertexShader = ( vsArray20[ CurNumBones ] );
    PixelShader = compile ps_2_0 PixScene();
 
    // نمط دمج الإضافة
    AlphaBlendEnable = True;
    SrcBlend = One;
    DestBlend = One;
  }
}
 
إلا أن النتائج لم تختلف كثيراً، لأن الشخصيات لا ترسم فوق بعضها، فالـ z-buffer يمنعها من التقاطع كما نريد... لذلك علينا أيضاً فصل هذه العملية. الآن تصبح التعديلات في ملف المظلل هكذا:
 
// نمط دمج الإضافة
AlphaBlendEnable = True;
SrcBlend = One;
DestBlend = One;
ZEnable = False;
 
والنتيجة:
 

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


float2 UVOffset = { 0,0 };
 
وفي كود الـ vertex shader، نقوم بإضافة هذه القيمة إلى إحداثيات الـ texture coordinates للنقطة:


// copy the input texture coordinate through
o.Tex0 = i.Tex0.xy + UVOffset;
 
 
الآن في ملف الـ CPP نقوم بتغيير قيمة الـ UVOffset كل لقطة بمقدار الزمن:
 
FLOAT UVOffsetVal = (FLOAT)fTime * 0.1f;
D3DXVECTOR4 uvOffset(UVOffsetVal,UVOffsetVal,UVOffsetVal,UVOffsetVal);
g_pEffect->SetVector( "UVOffset", &uvOffset );
if( pMAEffect )
  pMAEffect->SetVector( "UVOffset", &uvOffset );
 
 
هذا هو التعديل الأخير في المشروع، والنتيجة النهائية تستطيعون رؤيتها المشروع المرفق، والذي يحتوي أيضاً الكود الكامل والنهائي للتأثير...
 
أرجو أن يحوز هذا التأثير على إعجابكم، وأن تستفيدوا من المعلومات التي فيه...
 
شكراً جزيلاً

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