مدونة مبرمج معماري

الداعي

مضيفكم وسام البهنسي يرحب بكم.

 

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

قائمة الصفحات

آخر التدوينات

أنت والمصفوفات! (الجزء الثاني)
التوقيت 27/شعبان/1430 09:01 ص بقلم الكاتب وسام البهنسي

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

 

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

 

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

 

لن أكون متعصباً مملاً وأطالبك باستخراج كتاب الرياضيات للسنة الثانية المتوسطة من غيابت الجب لتراجع فصل المصفوفات، بل سأذكر ما يهمنا معرفته عنها الآن وفوراً.

 

المصفوفة مجموعة من الأرقام المصفوفة بجانب بعضها البعض لتشكل مستطيلاً من الأرقام، وتعرف المصفوفة بأبعادها (طولها وعرضها). مثلاً:

 

14

13

12

11

24

23

22

21

34

33

32

31

 

المصفوفة أعلاه هي مصفوفة 4×3. عند كتابة أبعاد المصفوفة، نضع عدد الصفوف على اليسار وعدد الأعمدة على اليمين. في الهندسة الفراغية والرسوميات ثلاثية الأبعاد نستخدم مصفوفات 4×4 في أغلب الحسابات، وأحياناً نستخدم مصفوفات 4×3 وَ 3×3 عند التعامل مع النواظم والمتجهات بدل الإحداثيات المكانية.

 

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

 

والآن لنرى كيف يتم استخدام المصفوفات في إزاحة وتحجيم وتدوير الأجسام. سنبدأ بالإزاحة لأنها الأسهل فهماً. فلنفرض أن لدينا مجسماً مكوناً من عدد كبير من رؤوس المثلثات، ولنقل أن إحداثيات أحد هذه الرؤوس هي [10,0,0]. تخيل معي موقع هذه النقطة في الفراغ، ستلاحظ أنها تقع في مكان ما على مستوى الأرض (بفرض أن المحور Y يرتفع للأعلى). الآن نريد إزاحة المجسم يميناً بمقدار خمس وحدات، ولأعلى ستّ وحدات وللعمق سبع وحدات. مما يعني أنه بعد الإزاحة يجب أن يصبح الرأس إياه في الإحداثيات [15,6,7]. والسؤال هو… ما هي المصفوفة التي تنجز لي هذه المهمة؟ الجواب هو:

 

0

0

0

1

0

0

1

0

0

1

0

0

1

7

6

5

 

كل ما نحتاجه هو ضرب إحداثيات الرأس بهذه المصفوفة، وستنتقل الإحداثيات إلى المكان الصحيح. سنتحدث عن تفاصيل عملية الضرب لاحقاً، أما الآن فدعونا نستمتع سوية بالنظر إلى المصفوفة أعلاه. كما تلاحظون، كافة عناصر المصفوفة هي الصفر، باستثناء القطر الذي عناصره تساوي واحد، والسطر الأخير به الأرقام 5 وَ 6 وَ 7. هذا الكلام له دلالة هامة، فالأذكياء منكم لا بد وأن قد لاحظوا أن مقادير الإزاحة التي أردناها موجودة في السطر الأخير بالترتيب XYZ. جميل جداً، وبقليل من التفكير أيضاً، نستطيع أن نتخيل شكل المصفوفة لو جعلنا مقدار الإزاحة صفراً على كافة المحاور:

 

0

0

0

1

0

0

1

0

0

1

0

0

1

0

0

0

 

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

 

0

0

0

0.5

0

0

0.33

0

0

0.25

0

0

1

0

0

0

 

جميييل. الأذكياء منكم مرة أخرى سيلاحظون أن الخلية الأولى من العمود الأول تحوي مقدار التحجيم على المحور X، والخلية الثانية من العمود الثاني تحمل مقدار المحور Y والخلية الثالثة من العمود الثالث تحوي مقدار التحجيم على المحور Z. رائع. لنُزد العملية تعقيداً الآن. نريد أن نقوم بعملية تحجيم بنفس المقادير المذكورة أعلاه، ثم عملية إزاحة بالمقادير المذكورة في المصفوفة الأولى، وكل ذلك في عملية واحدة… فكيف يكون شكل المصفوفة التي تعبر عن هذه "الخلطبيطة"؟ الجواب أنها تكون هكذا:

 

0

0

0

0.5

0

0

0.33

0

0

0.25

0

0

1

7

6

5

 

ببساطة أخذنا خلايا الإزاحة من المصفوفة الأولى، وأخذنا خلايا التحجيم من المصفوفة الثانية ووضعناهما معاً في مصفوفة واحدة كما ترون، وهذه المصفوفة الآن قادرة على تطبيق عملية تحجيم فإزاحة بضربة واحدة! ليس هذا فقط، بل والأذكياء منكم (تباً لهم!) لاحظوا أيضاً أن كلاً من مقداري التحجيم والإزاحة على المحور X يقعان في العمود الأول، كما أن مقداري التحجيم والإزاحة على المحور Y يقعان في العمود الثاني، والمحور Z في العمود الثالث. هل هي مصادفة؟ كلا بالطبع. من الآن فصاعداً نستطيع أن نقول أن الأرقام في العمود الأول مخصصة للتأثير في المحور X والأرقام في العمود الثاني للمحور Y والأرقام في العمود الثالث للمحور Z.

 

لننظر أخيراً إلى مصفوفات الدوران. كما هو معروف، الدوران يتم حول محور معين. دعونا نلقي نظرة على مصفوفات الدوران حول المحاور الثلاثة الأساسية (XYZ). انظروا التالي:

 

0 0 0 1
0 0.71 0.71 0
0 0.71 0.71- 0
1 0 0 0

0

0.71-

0

0.71

0

0

1

0

0

0.71

0

0.71

1

0

0

0

0

0

0.71

0.71

0

0

0.71

0.71-

0

1

0

0

1

0

0

0

 

المصفوفة اليمنى هي لتدوير المجسم بمقدار 45 ْ حول المحور X. المصفوفة الوسطى لتدوير المجسم بنفس المقدار لكن حول المحور Y. وأخيراً المصفوفة اليسرى للمحور Z. كما تلاحظون، هناك نسق واضح في الخلايا. ففي مصفوفة التدوير حول X، نلاحظ أن العمود الخاص بالمحور X لم تتغير عناصره. نفس الملاحظة حول مصوفة التدوير حول Y وعناصر عمود المحور Y، وكذلك Z.

لنخرج الآن من هذه الملاحظة بقاعدة: محور الدوران في مصفوفة دوران هو العمود الذي لم تتغير قيم عناصره.

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

 

وأخيراً، فلننظر إلى مصفوفة تجمع كافة العمليات المذكورة سابقاً. مصفوفة واحدة تقوم بالتحجيم، فالتدوير حول X ثم Y ثم Z وأخيراً تقوم بإزاحة المجسم. سنستعمل نفس المقادير من المصفوفات السابقة. المصفوفة الناتجة هي:

 

0

0.35-

0.25

0.25

0

0.16

0.28

0.05-

0

0.12

0.04-

0.21

1

7

6

5

 

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

 

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

 

إلى اللقاء…

#include <stdio.h>
#include <d3dx9math.h>
 
#pragma comment(lib,"d3dx9")
 
void main(void)
{
    D3DXMATRIX mat,matS,matRX,matRY,matRZ,matT;
    D3DXMatrixScaling(&matS,0.5f,0.33f,0.25f); // مصفوفة التحجيم
    D3DXMatrixRotationX(&matRX,D3DXToRadian(45.0f)); // مصفوفة الدوران حول محور السين
    D3DXMatrixRotationY(&matRY,D3DXToRadian(45.0f)); // مصفوفة الدوران حول محور الصاد
    D3DXMatrixRotationZ(&matRZ,D3DXToRadian(45.0f)); // مصفوفة الدوران حول محور العين
    D3DXMatrixTranslation(&matT,5.0f,6.0f,7.0f); // مصفوفة الإزاحة
    mat = matS * matRX * matRY * matRZ * matT; // المصفوفة الشاملة
 
    // طباعة عناصر المصفوفة الشاملة على الشاشة
    printf("%0.2f , %0.2f , %0.2f , %0.2f\n",mat._11,mat._12,mat._13,mat._14);
    printf("%0.2f , %0.2f , %0.2f , %0.2f\n",mat._21,mat._22,mat._23,mat._24);
    printf("%0.2f , %0.2f , %0.2f , %0.2f\n",mat._31,mat._32,mat._33,mat._34);
    printf("%0.2f , %0.2f , %0.2f , %0.2f\n",mat._41,mat._42,mat._43,mat._44);
}




أنت والمصفوفات! (الجزء الأول)
التوقيت 01/شعبان/1430 07:56 ص بقلم الكاتب وسام البهنسي

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

 

حسناً، في هذه التدوينة سترون كيف سنشرح المصفوفات لمبرمج لا يعلم سوى عمليتي الجمع والضرب وأن الإحداثيات الفراغية تتألف من ثلاث محاور (س، ص، ع  أو XYZ).

 

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

 

المعلومات أعلاه كافية لأن تبدأ البرمجة باستخدام المصفوفات بارتياح لإنجاز المهام الشائعة في الرسوميات. أما الآن فلندخل بشرح أكثر إفادة. فلنسأل أنفسنا: ما فائدة المصفوفات وما استخداماتها في رسوميات الحاسوب؟ إن اقتنعنا بأنها مفيدة، تعلمناها واستخدمناها، وإلا هجرناها وابتعدنا عنها...

 

لماذا المصفوفات؟

 

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

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

 

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

struct vector3 { float x,y,z; }; // بنية تمثل رأس مثلث في الفراغ
 
// قائمة ثابتة برؤوس المثلث
vector3 triangle_vertices[] =
{
   { 0.5f, 1.0f, 0.0f },   // الرأس العلوي
   { 0.0f, 0.0f, 0.0f },   // الرأس الأيسر
   { 1.0f, 0.0f, 0.0f },   // الرأس الأيمن
};
 
float fScaleAmount = 0.0001f;  // مقدار التضخيم كل لقطة
float fMoveAmount = 0.0001f;  // مقدار الإزاحة كل لقطة
 
float fScaleAmountNow = 1.0f + fScaleAmount * fTimeElapsedInSeconds;  // مقدار التضخيم في هذه اللحظة
float fMoveAmountNow = fMoveAmount * fTimeElapsedInSeconds;  // مقدار الإزاحة في هذه اللحظة
 
vector3 new_vertices[3]; // قائمة الرؤوس بعد تطبيق كافة التحويلات
 
// تطبيق التضخيم
for (int i=0;i<3;i++)
{
   new_vertices[i].x = triangle_vertices[i].x * fScaleAmountNow;
   new_vertices[i].y = triangle_vertices[i].y * fScaleAmountNow;
   new_vertices[i].z = triangle_vertices[i].z * fScaleAmountNow;
}
 
// الإزاحة على المحور السيني
new_vertices[0].x += fMoveAmountNow;
new_vertices[1].x += fMoveAmountNow;
new_vertices[2].x += fMoveAmountNow;
 
// الرسم باستخدام دايركت ثري دي
pD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,1,new_vertices,sizeof(vector3));
 
// الرسم باستخدام أوبن جي إل
glVertexPointer(3, GL_FLOAT,0,new_vertices);
glDrawArrays(GL_TRIANGLES,0,3);
(ملاحظة: إن لم تستوعب أسطر الرسم في دايركت ثري دي أو أوبن جي إل فلا تحزن، فهي ليست محط اهتمامنا الآن. يكفي أن تعرف أنها تستقبل مجموعة إحداثيات لرؤوس مثلثات وترسمها على الشاشة)
 
اضطررنا إلى عمل نسخة جانبية من الإحداثيات الأصلية، وتعديل هذه النسخة ورسمها، وذلك كي نستطيع تطبيق العملية الرياضية المطلوبة بشكل صحيح. الكود السابق يتعامل مع مثلث واحد فقط، فما بالك بمجسم من ثلاثين ألف مثلث يمثل شخصية تتحرك بكل الاتجاهات وتقوم بحركات معقدة؟ في كل لقطة تعدل إحداثيات رؤوس ثلاثين ألف مثلث. تخيل!
 
في الحقيقة هذا ما كان يفعله مبرمجو الرسوميات منذ عشر سنوات، ولعلك تلاحظ أن المجسمات في تلك الأيام كانت في غاية البساطة. اليوم لدينا معالج رسوميات مختص (GPU) قادر على أداء هذا العمل دون إنهاك برنامجك بهذه التفاصيل.
 
والآن كود الرسم الكامل مع استخدام المصفوفات ومعالج الرسوميات لإنجاز التحويل:
struct vector3 { float x,y,z; }; // بنية تمثل رأس مثلث في الفراغ
 
// قائمة ثابتة برؤوس المثلث
vector3 triangle_vertices[] =
{
   { 0.5f, 1.0f, 0.0f },   // الرأس العلوي
   { 0.0f, 0.0f, 0.0f },   // الرأس الأيسر
   { 1.0f, 0.0f, 0.0f },   // الرأس الأيمن
};
 
float fScaleAmount = 0.0001f;  // مقدار التضخيم كل لقطة
float fMoveAmount = 0.0001f;  // مقدار الإزاحة كل لقطة
 
float fScaleAmountNow = 1.0f + fScaleAmount * fTimeElapsedInSeconds;  // مقدار التضخيم في هذه اللحظة
float fMoveAmountNow = fMoveAmount * fTimeElapsedInSeconds;  // مقدار الإزاحة في هذه اللحظة
 
// في دايركت ثري دي
D3DXMATRIX matScale,matMove;
D3DXMatrixTranslation(&matMove, fMoveAmountNow, 0.0f, 0.0f); // مصفوفة الإزاحة
D3DXMatrixScale(&matScale, fScaleAmountNow, fScaleAmountNow, fScaleAmountNow); // مصفوفة التضخيم
 
D3DXMATRIX matXForm = matScale * matMove; // المصفوفة الشاملة للتضخيم فالإزاحة
 
pD3DDevice->SetTransform(D3DTS_WORLD,&matXForm); // ضبط دايركت ثري دي ليستخدم المسفوفة الجديدة
pD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,1,triangle_vertices,sizeof(float)*3); // الرسم
 
 
// في أوبن جي إل
glLoadIdentity(); // ضبط مصفوفة الوحدة
glScalef(fScaleAmountNow,fScaleAmountNow,fScaleAmountNow); // إضافة تحجيم على المصفوفة
glTranslatef(fMoveAmountNow,0,0); // إضافة إزاحة إلى المصفوفة
 
glVertexPointer(3, GL_FLOAT,0,triangle_vertices);
glDrawArrays(GL_TRIANGLES,0,3); // الرسم
 
إذن رأينا كيف نستطيع تعميم الحسابات وتوفير الجهد وتبسيط المعالجة وتوحيدها باستخدام المصفوفات. فالآن كل ما عليك فعله لتغيير مواقع الرؤوس هو تحويل إحداثيات كل منها بالمصفوفة المناسبة وصلى الله وبارك.
 
يختص معالج الرسوميات بتنفيذ عملية التحويل بسرعة كبيرة على رؤوس المجسمات، لذلك كما ترى في الكود أعلاه، لم نقم بذلك بأنفسنا، وإنما تركنا هذه المهمة المملة لمعالج الرسوميات…

 

في الجزء الثاني من هذه التدوينة إن شاء الله سأشرح لك ممّ تتألف المصفوفة وما هي عملية التحويل وكيف يتأتى لمصفوفة واحدة أن تعبر عن كافة العمليات التي ذكرناها في الأعلى.

 

إلى اللقاء…