
مقدمة
عندما يتم رسم نقطة أو خط أو مضلع معقد في عالم ثلاثي الأبعاد ، سيتم عرض النتيجة النهائية في النهاية على شاشة مسطحة ثنائية الأبعاد. وفقًا لذلك ، تمر الكائنات ثلاثية الأبعاد بمسار تحويل معين ، وتتحول إلى مجموعة من وحدات البكسل معروضة في نافذة ثنائية الأبعاد.
لقد حان تطوير أدوات البرمجيات التي تنفذ الرسومات ثلاثية الأبعاد ، بغض النظر عن أي واحد تختاره ، إلى نفس المفهوم تقريبًا للأوصاف الرياضية والخوارزمية للتحولات المذكورة أعلاه. من الناحية الإيديولوجية ، تستخدم واجهات برمجة التطبيقات الرسومية "النظيفة" مثل OpenGL ، ومحركات الألعاب الرائعة مثل Unity و Unreal ، آليات مماثلة لوصف التحول في مشهد ثلاثي الأبعاد. OpenSceneGraph ليست استثناء.
في هذه المقالة ، سنراجع آليات تجميع وتحويل الكائنات ثلاثية الأبعاد في OSG.
1. مصفوفة النموذج ، مصفوفة العرض ومصفوفة الإسقاط
تشارك ثلاث مصفوفات أساسية تشارك في تحويل الإحداثيات في التحول بين أنظمة الإحداثيات المختلفة. في كثير من الأحيان ، في مصطلحات OpenGL ، تسمى
مصفوفة النموذج ، مصفوفة العرض ، ومصفوفة الإسقاط .
يتم استخدام مصفوفة النموذج لوصف موقع الكائن في العالم ثلاثي الأبعاد. يحول القمم من
نظام الإحداثيات المحلي للكائن إلى
نظام الإحداثيات العالمي . بالمناسبة ، جميع أنظمة الإحداثيات في OSG هي
اليد اليمنى .
الخطوة التالية هي تحويل إحداثيات العالم إلى مساحة عرض ، يتم تنفيذها باستخدام مصفوفة العرض. لنفترض أن لدينا كاميرا تقع في أصل نظام الإحداثيات العالمي. يتم استخدام المصفوفة العكسية لمصفوفة تحويل الكاميرا كمصفوفة عرض. في نظام الإحداثيات الأيمن ، يقوم برنامج OpenGL دائمًا افتراضيًا بتحديد الكاميرا الموجودة عند النقطة (0 ، 0 ، 0) من نظام الإحداثيات العالمي والموجهة على طول الاتجاه السلبي للمحور Z.
ألاحظ أنه في OpenGL لم يتم فصل مصفوفة النموذج ومصفوفة العرض. ومع ذلك ، يتم تحديد مصفوفة عرض النموذج هناك ، مما يحول الإحداثيات المحلية للكائن إلى إحداثيات مساحة العرض. هذه المصفوفة ، في الواقع ، هي نتاج مصفوفة النموذج ومصفوفة النموذج. وبالتالي ، يمكن كتابة تحويل قمة الرأس V من الإحداثيات المحلية إلى مساحة من النموذج بشكل مشروط كمنتج
Ve = V * modelViewMatrix
المهمة المهمة التالية هي تحديد كيفية عرض كائنات ثلاثية الأبعاد على مستوى الشاشة وحساب ما يسمى
هرم القطع - مساحة من المساحة تحتوي على كائنات ليتم عرضها على الشاشة. يتم استخدام مصفوفة الإسقاط لتحديد هرم القطع المحدد في الفضاء العالمي بست طائرات: اليسار واليمين والسفلي والعلوي والقريب والبعيدة. يوفر OpenGL وظيفة gluPerapective () ، والتي تسمح لك بتحديد هرم قطع وطريقة لإسقاط عالم ثلاثي الأبعاد على مستوى.
يُطلق على نظام الإحداثيات الذي تم الحصول عليه بعد التحولات المذكورة أعلاه
نظام الإحداثيات المعياري للجهاز ، وله نطاق إحداثيات من -1 إلى 1 على كل محور وهو أعسر. وكخطوة أخيرة ، يتم عرض البيانات المستلمة في منفذ العرض (إطار العرض) للنافذة ، المحدد بواسطة مستطيل منطقة العميل في النافذة. بعد ذلك ، يظهر العالم ثلاثي الأبعاد على شاشة ثنائية الأبعاد. يمكن التعبير عن القيمة النهائية لإحداثيات الشاشة للرؤوس من خلال التحويل التالي
Vs = V * modelViewMatrix * projectionMatrix * windowMatrix
أو
Vs = V * MVPW
حيث MVPW هو مصفوفة التحويل المكافئة مساوية لمنتج ثلاث مصفوفات: مصفوفات عرض النموذج ، مصفوفات الإسقاط ، ومصفوفات النافذة.
Vs في هذه الحالة هو ناقل ثلاثي الأبعاد يحدد موضع بكسل ثنائي الأبعاد بقيمة عمق. بعكس عملية تحويل الإحداثيات ، نحصل على خط في الفضاء ثلاثي الأبعاد. لذلك ، يمكن اعتبار نقطة ثنائية الأبعاد نقطتين - واحدة على مقربة (Zs = 0) ، والأخرى على مستوى القطع البعيد (Zs = 1). إحداثيات هذه النقاط في الفضاء ثلاثي الأبعاد
V0 = (Xs, Ys, 0) * invMVPW V1 = (Xs, Ys, 1) * invMVPW
حيث invMVPW هو معكوس MVPW.
في جميع الأمثلة التي تمت مناقشتها حتى الآن ، أنشأنا كائنًا واحدًا ثلاثي الأبعاد في المشاهد. في هذه الأمثلة ، تزامنت الإحداثيات المحلية للكائن دائمًا مع الإحداثيات العالمية. حان الوقت الآن للحديث عن الأدوات التي تسمح لك بوضع العديد من الأشياء في المشهد وتغيير موضعها في الفضاء.
2. عقد المجموعة
فئة osg :: Group هي ما يسمى
العقدة الجماعية للرسم البياني للمشهد في OSG. يمكن أن يحتوي على أي عدد من العقد الفرعية ، بما في ذلك عقد أوراق الهندسة أو عقد المجموعة الأخرى. هذه هي العقد الأكثر استخدامًا مع وظائف واسعة النطاق.
فئة osg :: Group مشتقة من فئة osg :: Node ، وبالتالي ترث من فئة osg :: Referenced. osg :: Group يحتوي على قائمة بالعقد الفرعية ، حيث يتم التحكم في كل عقدة فرعية بواسطة مؤشر ذكي. وهذا يضمن عدم وجود تسرب للذاكرة عند تسلسل فرع من شجرة المشهد. توفر هذه الفئة للمطور عددًا من الأساليب العامة.
- addChild () - لإلحاق العقدة في نهاية قائمة العقد الفرعية. من ناحية أخرى ، هناك طريقة insertChild () ، التي تضع العقدة الفرعية في موضع معين في القائمة ، والتي يتم تحديدها بواسطة فهرس صحيح أو مؤشر للعقدة التي تم تمريرها كمعلمة.
- removeChild () و removeChildren () - إزالة عقدة واحدة أو مجموعة من العقد.
- getChild () - الحصول على مؤشر للعقدة من خلال فهرسها في القائمة
- getNumChildren () - الحصول على عدد العقد التابعة لهذه المجموعة.
إدارة العقدة الأم
كما نعلم بالفعل ، تدير فئة osg :: Group مجموعات من كائناتها الفرعية ، من بينها قد تكون هناك حالات osg :: Geode تتحكم في هندسة كائنات المشهد. كلا الفصلين لديه واجهة لإدارة العقد الأصلية.
تسمح OSG لعقد المشهد بأن تحتوي على عدة عقد رئيسية (سنتحدث عن هذا يومًا ما لاحقًا). في غضون ذلك ، سنلقي نظرة على الأساليب المحددة في osg :: Node المستخدمة في معالجة العقد الأصلية:
- getParent () - يقوم بإرجاع مؤشر من نوع osg :: Group يحتوي على قائمة العقد الأصلية.
- getNumParants () - إرجاع عدد العقد الأصلية.
- getParentalNodePath () - إرجاع جميع المسارات الممكنة إلى العقدة الجذرية للمشهد من العقدة الحالية. تقوم بإرجاع قائمة المتغيرات من النوع osg :: NodePath.
osg :: NodePath هو ناقل متجه: مؤشرات إلى عقد المشهد.

على سبيل المثال ، بالنسبة للمشهد الموضح في الشكل ، الرمز التالي
osg::NodePath &nodePath = child3->getParentalNodePaths()[0]; for (unsigned int i = 0; i < nodePath.size(); ++i) { osg::Node *node = nodePath[i];
سيعود العقد الجذر ، Child1 ، Child2.
يجب ألا تستخدم آليات إدارة الذاكرة للإشارة إلى العقد الأصلية. عند حذف العقدة الرئيسية ، يتم حذف جميع العقد الفرعية تلقائيًا ، مما قد يؤدي إلى تعطل التطبيق.
3. إضافة نماذج متعددة إلى شجرة المشهد
نوضح آلية استخدام المجموعات مع المثال التالي.
مثال مجموعة كاملةmain.h #ifndef MAIN_H #define MAIN_H #include <osg/Group> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc, (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cow.osg"); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
بشكل أساسي ، يختلف المثال عن جميع الأمثلة السابقة في أننا نقوم بتحميل نموذجين ثلاثي الأبعاد ، ولإضافتهما إلى المشهد ، نقوم بإنشاء عقدة جذر مجموعة ونضيف نماذج نموذجية لها كعقد فرعية
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(model1.get()); root->addChild(model2.get());

نتيجة لذلك ، نحصل على مشهد يتكون من نموذجين - طائرة وبقرة مرآة مضحكة. بالمناسبة ، لن تكون البقرة المرآة مرآة إلا إذا قمت بنسخ نسيجها من OpenSceneGraph-Data / Images / reflect.rgb إلى دليل البيانات / الصور لمشروعنا.
يمكن لفئة osg :: Group قبول أي نوع من العقد كعناوين فرعية ، بما في ذلك العقد من نوعها. على العكس من ذلك ، لا تحتوي فئة osg :: Geode على أي عقد فرعية على الإطلاق - إنها عقدة طرفية تحتوي على هندسة كائن المشهد. هذه الحقيقة مناسبة عند السؤال عما إذا كانت العقدة هي عقدة من النوع osg :: Group أو نوع آخر من المشتقات من osg :: Node. دعونا نلقي نظرة على مثال صغير.
osg::ref_ptr<osg::Group> model = dynamic_cast<osg::Group *>(osgDB::readNodeFile("../data/cessna.osg"));
القيمة التي يتم إرجاعها بواسطة دالة osgDB :: readNodeFile () تكون دائمًا من النوع osg :: Node * ، ولكن يمكن تحويلها إلى سليل osg :: Group *. إذا كانت عقدة نموذج سيسنا عقدة مجموعة ، فحينئذٍ سينجح التحويل ؛ وإلا ، فسيُرجع التحويل قيمة فارغة.
يمكنك أيضًا تنفيذ نفس الحيلة التي تعمل على معظم المترجمين
من الأفضل استخدام طرق تحويل خاصة في مواضع الرموز ذات الأداء الحرج
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg"); osg::Group* convModel1 = model->asGroup();
4. عقد التحول
Osg :: لا يمكن عقد العقد أي تحويلات باستثناء القدرة على الذهاب إلى العقد التابعة لها. يوفر OSG فئة osg :: Transform للحركة الهندسية المكانية. هذه الفئة هي خلف لفئة osg :: Group ، لكنها مجردة أيضًا - عمليًا ، يتم استخدام ورثتها بدلاً من ذلك ، والتي تنفذ التحولات المكانية المختلفة للهندسة. عند اجتياز الرسم البياني للمشهد ، تضيف العقدة osg :: Transform تحولها إلى مصفوفة تحويل OpenGL الحالية. هذا يعادل ضرب مصفوفات تحويل OpenGL بواسطة الأمر glMultMatrix ()

يمكن ترجمة مثال الرسم البياني للمشهد هذا إلى كود OpenGL التالي
glPushMatrix(); glMultMatrix( matrixOfTransform1 ); renderGeode1(); glPushMatrix(); glMultMatrix( matrixOfTransform2 ); renderGeode2(); glPopMatrix(); glPopMatrix();
يمكننا القول أن موضع Geode1 تم تعيينه في نظام الإحداثيات Transform1 ، وموضع Geode2 تم تعيينه في نظام الإحداثيات Transform2 ، الإزاحة بالنسبة إلى Transform1. في الوقت نفسه ، يمكن تمكين تحديد الموقع في الإحداثيات المطلقة في OSG ، مما سيؤدي إلى سلوك الكائن ، وهو ما يعادل نتيجة الأمر glGlobalMatrix () OpenGL
transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF );
يمكنك العودة إلى وضع تحديد المواقع النسبي
transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF );
5. مفهوم مصفوفة تحويل الإحداثيات
نوع osg :: Matrix هو نوع OSG أساسي لا تتحكم فيه المؤشرات الذكية. يوفر واجهة للعمليات على مصفوفات 4 × 4 التي تصف تحويل الإحداثيات ، مثل تحريك ، تدوير ، تحجيم وحساب التوقعات. يمكن تحديد المصفوفة بشكل صريح.
توفر فئة osg :: Matrix الطرق العامة التالية:
- postMult () والعامل * () - الضرب الصحيح للمصفوفة الحالية بواسطة المصفوفة أو المتجه التي تم تمريرها كمعلمة. تقوم طريقة preMult () بعملية الضرب اليسرى.
- makeTranslate () و makeRotate () و makeScale () - إعادة تعيين المصفوفة الحالية وإنشاء مصفوفة 4 × 4 تصف الحركة والتدوير والتحجيم. يمكن استخدام نسخهم الثابتة المترجمة () و rotate () و scale () لإنشاء كائن مصفوفة بمعلمات محددة.
- invert () - تحسب معكوس المصفوفة الحالية. تأخذ نسخته الثابتة من معكوس () مصفوفة كمعلمة وتعيد مصفوفة جديدة معكوسة إلى المعطى.
تفهم OSG المصفوفات على أنها مصفوفات من السلاسل ، وناقلات على أنها سلاسل ، لذلك ، لتطبيق تحويل المصفوفة إلى ناقل ، قم بذلك
osg::Matrix mat = …; osg::Vec3 vec = …; osg::Vec3 resultVec = vec * mat;
من السهل فهم ترتيب عمليات المصفوفة من خلال النظر في كيفية ضرب المصفوفات للحصول على تحويل مكافئ
osg::Matrix mat1 = osg::Matrix::scale(sx, sy, sz); osg::Matrix mat2 = osg::Matrix::translate(x, y, z); osg::Matrix resultMat = mat1 * mat2;
يجب على المطور قراءة عملية التحويل من اليسار إلى اليمين. أي ، في جزء الشفرة الموصوف ، يتجه المتجه أولاً ، ثم حركته.
osg :: Matrixf يحتوي على عناصر من النوع العائم.
6. باستخدام فئة osg :: MatrixTransform
نطبق المعرفة النظرية المكتسبة في الممارسة العملية عن طريق تحميل طائرتين من الطائرات في نقاط مختلفة من المشهد.
النص الكامل لمثال التحويلmain.h #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform; transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0)); transform1->addChild(model.get()); osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
المثال في الواقع تافه للغاية. تحميل طراز الطائرة من الملف
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");
إنشاء عقدة تحويل
osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
قمنا بتعيين النموذج لتحريك النموذج على طول المحور X 25 وحدة إلى اليسار كمصفوفة تحويل
transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0));
وضعنا نموذجنا لعقدة التحويل كعقدة فرعية
transform1->addChild(model.get());
نفعل نفس الشيء مع التحول الثاني ، ولكن كمصفوفة نضع الحركة إلى اليمين بمقدار 25 وحدة
osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform; transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0)); transform2->addChild(model.get());
نقوم بإنشاء عقدة جذر وكعقد تحويل لها نقوم بتعيين عقد التحويل convert1 و convert2
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(transform1.get()); root->addChild(transform2.get());
قم بإنشاء عارض وقم بتمرير العقدة الجذرية إليه كبيانات المشهد
osgViewer::Viewer viewer; viewer.setSceneData(root.get());
تشغيل البرنامج يعطي مثل هذه الصورة

هيكل الرسم البياني للمشهد في هذا المثال على النحو التالي.

لا ينبغي الخلط بيننا من حقيقة أن عقد التحول (الطفل 1.1 والطفل 1.2) تشير إلى نفس الكائن الفرعي لنموذج الطائرة (الطفل 2). هذه آلية OSG عادية ، عندما يمكن أن تحتوي عقدة فرعية واحدة في الرسم البياني للمشهد على عدة عقد رئيسية. وبالتالي ، لا يتعين علينا تخزين مثيلين من النموذج في ذاكرتنا من أجل الحصول على طائرتين متطابقتين في المشهد. تسمح لك هذه الآلية بتخصيص الذاكرة بكفاءة عالية في التطبيق. لن يتم حذف النموذج من الذاكرة حتى يشار إليه باسم الطفل ، على الأقل عقدة واحدة.
تعادل فئة osg :: MatrixTransform في عمليتها أوامر OpenGL glMultMatrix () و glLoadMatrix () ، وتنفذ جميع أنواع التحولات المكانية ، ولكن يصعب استخدامها بسبب الحاجة إلى حساب مصفوفة التحويل.
تعمل فئة osg :: PositionAttitudeTransform مثل دالات OpenGL glTranslate () و glScale () و glRotate (). يوفر طرقًا عامة لتحويل العقد الفرعية:
- setPosition () - انقل العقدة إلى نقطة معينة في المساحة التي حددتها المعلمة osg :: Vec3
- setScale () - قم بقياس الكائن على طول محاور الإحداثيات. يتم تعيين عوامل القياس على طول المحاور المقابلة بواسطة معلمة من نوع osg :: Vec3
- setAttitude () - ضبط الاتجاه المكاني للكائن. كمعلمة ، يستغرق الأمر ربع تحويل تحويل الدوران osg :: Quat ، والذي يحتوي مُنشئه على العديد من الأحمال الزائدة التي تسمح لك بتحديد quaternion بشكل مباشر (بشكل مكون) وعلى سبيل المثال ، من خلال زوايا أويلر osg :: Quat (xAngle، osg :: X_AXIS، yAngle، osg :: Y_AXIS، zAngle، osg :: Z_AXIS) (الزوايا معطاة بوحدات الراديان!)
7. تبديل العقد
ضع في اعتبارك فئة أخرى - osg :: Switch ، والتي تتيح لك عرض أو تخطي عرض عقدة المشهد ، وفقًا لبعض الشروط المنطقية. ينحدر من فئة osg :: Group ويربط بعض القيمة المنطقية لكل من أطفالها. لديها العديد من الطرق العامة المفيدة:
- تأخذ addChild () الزائدة ، كمعلمة ثانية ، مفتاحًا منطقيًا يشير إلى ما إذا كان سيتم عرض هذه العقدة أم لا.
- setValue () - اضبط مفتاح الرؤية / الخفي. يأخذ مؤشر العقدة الفرعية التي تهمنا والقيمة الرئيسية المطلوبة. وفقًا لذلك ، يتيح لك getValue () الحصول على القيمة الرئيسية الحالية من خلال فهرس العقدة التي تهمنا.
- setNewChildDefaultValue () - لتعيين القيمة الافتراضية لمفتاح الرؤية لجميع الكائنات الجديدة المضافة كأبناء.
خذ بعين الاعتبار تطبيق هذه الفئة بالقدوة.
تبديل المثال الكاملmain.h #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
المثال تافه - نقوم بتحميل نموذجين: سيسنا تقليدية و سيسنا مع تأثير محرك محترق
osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg");
ومع ذلك ، فإننا ننشئ osg :: Switch كعقدة الجذر ، والتي تتيح لنا ، عند إضافة نماذج إليها كعقد تابعة ، لتعيين مفتاح الرؤية لكل منها
osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), false); root->addChild(model2.get(), true);
أي ، لن يتم تقديم نموذج 1 ، وسيتم عرض نموذج 2 ، والذي سنلاحظه عن طريق تشغيل البرنامج

من خلال تبادل قيم المفاتيح سنرى الصورة المعاكسة
root->addChild(model1.get(), true); root->addChild(model2.get(), false);

تصويب كلا المفتاحين ، سنرى نموذجين في نفس الوقت
root->addChild(model1.get(), true); root->addChild(model2.get(), true);

يمكنك تمكين الرؤية وعدم رؤية العقدة ، وهي تابع لـ osg :: Switch ، مباشرة أثناء التنقل باستخدام طريقة setValue ()
switchNode->setValue(0, false); switchNode->setValue(0, true); switchNode->setValue(1, true); switchNode->setValue(1, false);
الخلاصة
في هذا البرنامج التعليمي ، نظرنا في جميع فئات العقدة المتوسطة الرئيسية المستخدمة في OpenSceeneGraph. وهكذا ، وضعنا لبنة أساسية أخرى في أساس المعرفة حول جهاز محرك الرسومات المثير للاهتمام بلا شك. الأمثلة التي تمت مناقشتها في المقالة ، كما كان من قبل ،
متوفرة في مستودعي على Github .
يتبع ...