OpenSceneGraph: تقنيات البرمجة الأساسية

الصورة

مقدمة


لن تركز هذه المقالة على الرسومات بقدر تركيزها على كيفية تنظيم التطبيق الذي تستخدمه ، مع مراعاة خصوصيات مشغل OpenSceneGraph والبرامج التي يوفرها.

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

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

1. تحليل خيارات سطر الأوامر


في C / C ++ ، يتم تمرير معلمات سطر الأوامر عبر الوسائط إلى الدالة main (). في الأمثلة السابقة ، حددنا هذه المعلمات بعناية بأنها غير مستخدمة ، والآن سنستخدمها لإخبار برنامجنا ببعض البيانات عند بدء تشغيله.

لدى OSG أدوات تحليل سطر الأوامر مضمنة.

قم بإنشاء المثال التالي

مثال لسطر الأوامر
الرئيسية

#ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif // MAIN_H 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); std::string filename; args.read("--model", filename); osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


قم بتعيين معلمات بدء تشغيل البرنامج في QtCreator



تشغيل البرنامج للتنفيذ ، نحصل على النتيجة (طراز شاحنة مأخوذ من نفس OpenSceneGraph-Data )



الآن دعونا نلقي نظرة على مثال سطرا سطرا

 osg::ArgumentParser args(&argc, argv); 

إنشاء مثيل لفئة محلل سطر الأوامر osg :: ArgumentParser. عند الإنشاء ، يتم تمرير مُنشئ الفصل الوسيطات المقبولة بواسطة الدالة main () من نظام التشغيل.

 std::string filename; args.read("--model", filename); 

نحن نحلل الحجج ، أي أننا نبحث عن مفتاح "النموذج" بينها ، ونضع قيمته في اسم ملف السلسلة. وبالتالي ، باستخدام هذا المفتاح ، نقوم بنقل اسم الملف بنموذج ثلاثي الأبعاد إلى البرنامج. بعد ذلك نقوم بتحميل هذا النموذج وعرضه

 osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); 

يحتوي أسلوب read () لفئة osg :: ArgumentParser على الكثير من الأحمال الزائدة ، مما يسمح لك بقراءة قيم السلسلة ليس فقط من سطر الأوامر ، ولكن أيضًا الأعداد الصحيحة وأرقام الفاصلة العائمة والمتجهات وما إلى ذلك. على سبيل المثال ، يمكنك قراءة معلمة معينة من النوع float

 float size = 0.0f; args.read("--size", size); 

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

2. آلية الإخطار وتسجيل


يشتمل OpenSceneGraph على آلية إعلام تتيح لك عرض رسائل تصحيح الأخطاء أثناء عملية العرض ، وكذلك التي بدأها المطور. هذا هو مساعدة كبيرة عند تتبع وتصحيح البرنامج. يدعم نظام إعلام OSG إخراج معلومات التشخيص (الأخطاء والتحذيرات والإشعارات) على مستوى المحرك الأساسي والمكونات الإضافية الخاصة به. يمكن للمطور عرض رسالة تشخيصية أثناء تشغيل البرنامج باستخدام وظيفة osg :: notify ().

تعمل هذه الوظيفة كدفق إخراج قياسي لمكتبة C ++ القياسية من خلال التحميل الزائد للمشغل <<. كحجة ، يستغرق مستوى الرسالة: دائمًا ، FATAL ، WARN ، إشعار ، INFO ، DEBUG_INFO و DEBUG_FP. على سبيل المثال

 osg::notify(osg::WARN) << "Some warning message" << std::endl; 

يعرض تحذير مع النص المعرفة من قبل المستخدم.

يمكن أن تحتوي إشعارات OSG على معلومات مهمة حول حالة البرنامج ، وملحقات النظام الفرعي للرسومات بالكمبيوتر ، والمشاكل المحتملة في المحرك.

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

باستخدام مثال بسيط ، ضع في اعتبارك كيف يمكنك إعادة توجيه إخراج الإشعارات ، على سبيل المثال ، إلى ملف سجل نصي. اكتب الكود التالي

يخطر المثال
الرئيسية

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <fstream> #endif // MAIN_H 

main.cpp

 #include "main.h" class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; int main(int argc, char *argv[]) { osg::setNotifyLevel(osg::INFO); osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


لإعادة توجيه الإخراج ، نكتب فئة LogFileHandler ، وهي خليفة osg :: NotifyHandler. يتحكم مُنشئ ومدمّر هذه الفئة في فتح وإغلاق دفق الإخراج _log المرتبط بالملف النصي. طريقة notify () هي طريقة مشابهة لفئة أساسية أعيد تعريفها لإخراجها إلى إشعارات الملفات المرسلة بواسطة OSG أثناء العملية من خلال معلمة msg.

فئة LogFileHandler

 class LogFileHandler : public osg::NotifyHandler { public: LogFileHandler(const std::string &file) { _log.open(file.c_str()); } virtual ~LogFileHandler() { _log.close(); } virtual void notify(osg::NotifySeverity severity, const char *msg) { _log << msg; } protected: std::ofstream _log; }; 

بعد ذلك ، في البرنامج الرئيسي ، قم بإجراء الإعدادات اللازمة

 osg::setNotifyLevel(osg::INFO); 

قم بتعيين مستوى إعلامات INFO ، أي الإخراج إلى سجل جميع المعلومات حول تشغيل المحرك ، بما في ذلك الإخطارات الحالية للتشغيل العادي.

 osg::setNotifyHandler(new LogFileHandler("../logs/log.txt")); 

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

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root) { OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl; return -1; } 

في الوقت نفسه ، نتعامل مع حالة نقص البيانات في سطر الأوامر ، ونعرض رسالة في السجل في الوضع اليدوي باستخدام ماكرو OSG_FATAL. قم بتشغيل البرنامج باستخدام الوسائط التالية



الحصول على الإخراج إلى ملف سجل مثل هذا

مثال لسجل OSG
 Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll OSGReaderWriter wrappers loaded OK CullSettings::readEnvironmentalVariables() void StateSet::setGlobalDefaults() void StateSet::setGlobalDefaults() ShaderPipeline disabled. StateSet::setGlobalDefaults() Setting up GL2 compatible shaders CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce8f0 CullSettings::readEnvironmentalVariables() ShaderComposer::ShaderComposer() 0xa5ce330 View::setSceneData() Reusing existing scene0xa514220 CameraManipulator::computeHomePosition(0, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 CameraManipulator::computeHomePosition(0xa52f138, 0) boundingSphere.center() = (-6.40034 1.96225 0.000795364) boundingSphere.radius() = 16.6002 Viewer::realize() - No valid contexts found, setting up view across all screens. Applying osgViewer::ViewConfig : AcrossAllScreens . . . . ShaderComposer::~ShaderComposer() 0xa5ce330 ShaderComposer::~ShaderComposer() 0xa5ce8f0 ShaderComposer::~ShaderComposer() 0xa5d6228 close(0x1)0xa5d3e50 close(0)0xa5d3e50 ContextData::unregisterGraphicsContext 0xa5d3e50 DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. ShaderComposer::~ShaderComposer() 0xa5de4e0 close(0x1)0xa5ddba0 close(0)0xa5ddba0 ContextData::unregisterGraphicsContext 0xa5ddba0 Done destructing osg::View DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. DatabasePager::RequestQueue::~RequestQueue() Destructing queue. Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll 


لا يهم أنه في الوقت الحالي قد تبدو هذه المعلومات غير مجدية بالنسبة لك - في المستقبل ، يمكن أن يساعد هذا الاستنتاج في تصحيح الأخطاء في البرنامج.

بشكل افتراضي ، يرسل OSG رسائل إلى رسائل الإخراج والخطأ القياسية std :: cout إلى الدفق std :: cerr. ومع ذلك ، من خلال تجاوز معالج الإعلام ، كما هو موضح في المثال ، يمكن إعادة توجيه هذا الإخراج إلى أي دفق إخراج ، بما في ذلك عناصر واجهة المستخدم الرسومية.

ضع في اعتبارك أنه عند تعيين مستوى عالٍ من الإعلامات (على سبيل المثال FATAL) ، يتجاهل النظام جميع إعلامات المستوى الأدنى. على سبيل المثال ، في حالة مماثلة

 osg::setNotifyLevel(osg::FATAL); . . . osg::notify(osg::WARN) << "Some message." << std::endl; 

رسالة مخصصة ببساطة لن يتم عرضها.

3. اعتراض السمات الهندسية


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

ومع ذلك ، فإن المحرك ينفذ عددًا من العوامل التي تسمح لك بإعادة قراءة السمات الهندسية لأي كائن واستخدامها لنمذجة طوبولوجيا شبكة المضلعات. في C ++ ، يعد functor بنية تتيح لك استخدام كائن كدالة.

توفر الفئة osg :: Drawable للمطور أربعة أنواع من العوامل:

  1. osg :: Drawable :: AttributeFunctor - يقرأ سمات القمم كصفيف من المؤشرات. لديها عدد من الطرق الافتراضية لتطبيق سمات قمة الرأس لأنواع البيانات المختلفة. لاستخدام هذا المشغل ، يجب أن تصف الفصل وتجاوز طريقة أو أكثر من طرقها ، حيث يتم تنفيذ الإجراءات المطلوبة من قبل المطور.


 virtual void apply( osg::Drawable::AttributeType type, unsigned int size, osg::Vec3* ptr ) { //  3-     ptr. //      } 

  1. osg :: Drawable :: ConstAttributeFunctor - إصدار للقراءة فقط من functor السابق: يتم تمرير مؤشر إلى مجموعة من المتجهات كمعلمة ثابتة
  2. osg :: PrimitiveFunctor - يقلد عملية تقديم كائنات OpenGL. تحت ستار تقديم كائن ، يتم استدعاء أساليب functor التي تجاوزها المطور. يحتوي هذا العامل على فئتين فرعيتين مهمتين: osg :: TemplatePrimitiveFunctor <> و osg :: TriangleFunctor <>. تتلقى هذه الفئات رؤوسًا بدائية كمعلمات وتمريرها إلى طرق المستخدم باستخدام عامل التشغيل ().
  3. osg :: PrimitiveIndexFunctor - ينفذ نفس الإجراءات التي يقوم بها المشغل السابق ، لكنه يقبل مؤشرات قمة الرأس للبدائية كمعلمة.

الفصول المشتقة من osg :: Drawable ، مثل osg :: ShapeDrawable و osg :: Geometry ، لها طريقة قبول () لتطبيق الدالات المختلفة.

4. مثال على استخدام functor بدائية


نوضح الوظيفة الموصوفة باستخدام مثال جمع المعلومات حول الوجوه الثلاثية ونقاط بعض الأشكال الهندسية التي حددناها سابقًا.

مثال Functor
الرئيسية

 #ifndef MAIN_H #define MAIN_H #include <osg/Geode> #include <osg/Geometry> #include <osg/TriangleFunctor> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" std::string vec2str(const osg::Vec3 &v) { std::string tmp = std::to_string(vx()); tmp += " "; tmp += std::to_string(vy()); tmp += " "; tmp += std::to_string(vz()); return tmp; } struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(0.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(2.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(3.0f, 0.0f, 1.5f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(4.0f, 0.0f, 1.0f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) ); osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); geom->setNormalBinding(osg::Geometry::BIND_OVERALL); geom->addPrimitiveSet(new osg::DrawArrays(GL_QUAD_STRIP, 0, 10)); osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable(geom.get()); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); return viewer.run(); } 


حذف عملية إنشاء هندسة نظرنا فيها عدة مرات ، دعونا نلاحظ ما يلي. نحدد بنية FaceCollector التي نعيد تعريف عامل التشغيل () على النحو التالي

 struct FaceCollector { void operator()(const osg::Vec3 &v1, const osg::Vec3 &v2, const osg::Vec3 &v3) { std::cout << "Face vertices: " << vec2str(v1) << "; " << vec2str(v2) << "; " << vec2str(v3) << std::endl; } }; 

سيعرض المشغل ، عند الاتصال به ، إحداثيات القمم الثلاثة المرسلة إليه بواسطة المحرك. وظيفة vec2str مطلوبة لترجمة مكونات ناقل osg :: Vec3 إلى std :: string. للاتصال بالمشغل ، أنشئ مثيلًا له وقم بتمريره إلى كائن الهندسة عبر طريقة قبول ()

 osg::TriangleFunctor<FaceCollector> functor; geom->accept(functor); 

هذه الدعوة ، كما ذكر أعلاه ، تقلد تقديم الهندسة ، لتحل محل الرسم نفسه عن طريق استدعاء طريقة functor متجاوزة. في هذه الحالة ، سيتم استدعاؤها أثناء "الرسم" لكل من المثلثات التي تشكل هندسة المثال.

على الشاشة نحصل على مثل هذه الهندسة



ومثل هذا العادم إلى وحدة التحكم

 Face vertices: 0.000000 0.000000 0.000000; 0.000000 0.000000 1.000000; 1.000000 0.000000 0.000000 Face vertices: 0.000000 0.000000 1.000000; 1.000000 0.000000 1.500000; 1.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 0.000000; 1.000000 0.000000 1.500000; 2.000000 0.000000 0.000000 Face vertices: 1.000000 0.000000 1.500000; 2.000000 0.000000 1.000000; 2.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 0.000000; 2.000000 0.000000 1.000000; 3.000000 0.000000 0.000000 Face vertices: 2.000000 0.000000 1.000000; 3.000000 0.000000 1.500000; 3.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 0.000000; 3.000000 0.000000 1.500000; 4.000000 0.000000 0.000000 Face vertices: 3.000000 0.000000 1.500000; 4.000000 0.000000 1.000000; 4.000000 0.000000 0.000000 

في الواقع ، عند استدعاء geom-> قبول (...) ، لا يتم تقديم المثلثات ، يتم محاكاة مكالمات OpenGL ، وبدلاً منها بيانات حول رؤوس المثلث ، يتم محاكاة العرض



تجمع فئة osg :: TemplatePrimitiveFunctor البيانات ليس فقط حول المثلثات ، ولكن أيضًا عن أي بدائل أخرى لـ OpenGL. لتنفيذ معالجة هذه البيانات ، يجب عليك تجاوز العوامل التالية في وسيطة القالب

 //   void operator()( const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); //   void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool ); 


5. نمط الزائر


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

لتطبيق هذه الآلية في OSG ، يتم تعريف فئة osg :: NodeVisitor. تتحرك الفئة الموروثة من osg :: NodeVisitor حول الرسم البياني للمشهد ، وتزور كل عقدة وتطبق العمليات التي يحددها المطور عليها. هذه هي الفئة الرئيسية المستخدمة للتدخل في عملية تحديث العقد وتقطيع العقد غير المرئية ، بالإضافة إلى تطبيق بعض العمليات الأخرى المتعلقة بتعديل هندسة عقد المشهد ، مثل osgUtil :: SmoothingVisitor و osgUtil :: Simplifier و osgUtil :: TriStripVisitor.

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

في تطبيق الأسلوب application () ، يجب على المطور ، في الوقت المناسب ، استدعاء الأسلوب traverse () للفئة الأساسية osg :: NodeVisitor. يؤدي هذا إلى انتقال الزائر إلى العقدة التالية ، إما طفل أو جار في مستوى التسلسل الهرمي ، إذا كانت العقدة الحالية لا تحتوي على أي عُقدة تابعة يمكن الانتقال إليها. يعني غياب استدعاء traverse () إيقاف اجتياز الرسم البياني للمشهد ويتم تجاهل ما تبقى من الرسم البياني للمشهد.

تحتوي الأحمال الزائدة من الأسلوب application () على تنسيقات موحدة

 virtual void apply( osg::Node& ); virtual void apply( osg::Geode& ); virtual void apply( osg::Group& ); virtual void apply( osg::Transform& ); 

لتخطي الرسم البياني الفرعي للعقدة الحالية لكائن الزائر ، يجب عليك تعيين وضع الزحف ، على سبيل المثال

 ExampleVisitor visitor; visitor->setTraversalMode( osg::NodeVisitor::TRAVERSE_ALL_CHILDREN ); node->accept( visitor ); 

يتم ضبط وضع الالتفافية بواسطة عدة عدّاد

  1. TRAVERSE_ALL_CHILDREN - التنقل عبر جميع العقد الفرعية.
  2. TRAVERSE_PARENTS - تمر مرة أخرى من العقدة الحالية ، وليس الوصول إلى العقدة الجذرية
  3. TRAVERSE_ACTIVE_CHILDREN - تجاوز العقد النشطة بشكل حصري ، أي تلك التي يتم تنشيط رؤيتها من خلال عقدة osg :: Switch.


6. تحليل هيكل سيسنا حرق


يمكن للمطور دائمًا تحليل ذلك الجزء من الرسم البياني للمشهد الذي تم إنشاؤه بواسطة النموذج الذي تم تحميله من الملف.

مثال Functor
الرئيسية

 #ifndef MAIN_H #define MAIN_H #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } InfoVisitor infoVisitor; root->accept(infoVisitor); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


نقوم بإنشاء فئة InfoVisitor ، ورثها من osg :: NodeVisitor

 class InfoVisitor : public osg::NodeVisitor { public: InfoVisitor() : _level(0) { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); } std::string spaces() { return std::string(_level * 2, ' '); } virtual void apply(osg::Node &node); virtual void apply(osg::Geode &geode); protected: unsigned int _level; }; 

ستشير خاصية _level المحمية إلى مستوى الرسم البياني للمشهد الذي توجد به فئة زوارنا حاليًا. في المنشئ ، قم بتهيئة عداد المستوى وتعيين وضع اجتياز العقدة - لتجاوز كل العقد الفرعية.

الآن إعادة تعريف أساليب application () للعقد

 void InfoVisitor::apply(osg::Node &node) { std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; _level++; traverse(node); _level--; } 

هنا سنقوم بإخراج نوع العقدة الحالية. تعرض طريقة libraryName () للعقدة اسم مكتبة OSG حيث يتم تنفيذ هذه العقدة ، وتعرض طريقة className اسم فئة العقدة. يتم تطبيق هذه الطرق من خلال استخدام وحدات الماكرو في رمز مكتبات OSG.

 std::cout << spaces() << node.libraryName() << "::" << node.className() << std::endl; 

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

بالنسبة لعقدة نهاية type osg :: Geode ، تم تجاوز التحميل الزائد لطريقة application ()

 void InfoVisitor::apply(osg::Geode &geode) { std::cout << spaces() << geode.libraryName() << "::" << geode.className() << std::endl; _level++; for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } traverse(geode); _level--; } 

مع رمز العمل بالمثل ، باستثناء أننا نعرض البيانات على جميع الكائنات الهندسية المرتبطة بالعقدة الهندسية الحالية

 for (unsigned int i = 0; i < geode.getNumDrawables(); ++i) { osg::Drawable *drawable = geode.getDrawable(i); std::cout << spaces() << drawable->libraryName() << "::" << drawable->className() << std::endl; } 

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

 osg::ArgumentParser args(&argc, argv); osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args); if (!root.valid()) { OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl; return -1; } 

في الوقت نفسه ، نقوم بمعالجة الأخطاء المتعلقة بعدم وجود أسماء الملفات النموذجية في سطر الأوامر. الآن نقوم بإنشاء فئة زائر وتمريرها إلى الرسم البياني للمشهد لتنفيذه

 InfoVisitor infoVisitor; root->accept(infoVisitor); 

فيما يلي الخطوات لإطلاق العارض ، وهو ما قمنا به بالفعل عدة مرات. بعد بدء البرنامج مع المعلمات

 $ visitor ../data/cessnafire.osg 

سوف نرى الإخراج التالي إلى وحدة التحكم

 osg::Group osg::MatrixTransform osg::Geode osg::Geometry osg::Geometry osg::MatrixTransform osgParticle::ModularEmitter osgParticle::ModularEmitter osgParticle::ParticleSystemUpdater osg::Geode osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem osgParticle::ParticleSystem 

في الواقع ، حصلنا على شجرة كاملة للمشهد المحمّل. إسمح لي ، أين توجد الكثير من العقد؟ كل شيء بسيط للغاية - نماذج التنسيق * .osg نفسها عبارة عن حاويات تخزن ليس فقط هندسة النموذج ، ولكن أيضًا معلومات أخرى حول هيكلها في شكل رسم تخطيطي لمشهد OSG. هندسة النموذج ، والتحولات ، وتأثيرات الجسيمات التي تدرك الدخان واللهب كلها عقد من رسم بياني مشهد OSG.يمكن تنزيل أي مشهد من * .osg أو إلغاء تحميله من العارض إلى تنسيق * .osg.

هذا مثال بسيط لتطبيق ميكانيكا الزوار. في الواقع ، داخل الزوار ، يمكنك إجراء الكثير من العمليات لتعديل العقد أثناء تنفيذ البرنامج.

7. التحكم في سلوك العقد في الرسم البياني للمشهد من خلال تجاوز طريقة traverse ()


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

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

نعلم بالفعل أن العقدة osg :: Switch يمكنها التحكم في عرض العقد التابعة لها ، بما في ذلك عرض بعض العقد وإيقاف عرض الآخرين. لكنه لا يعرف كيفية القيام بذلك تلقائيًا ، لذلك سننشئ عقدة جديدة استنادًا إلى العقدة القديمة ، والتي سيتم التبديل بين العقد الفرعية في نقاط مختلفة في الوقت المناسب ، وفقًا لقيمة العداد الداخلي.

مثال Animswitch
الرئيسية

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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<AnimatingSwitch> root = new AnimatingSwitch; root->addChild(model1.get(), true); root->addChild(model2.get(), false); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


دعنا نلقي نظرة على هذا المثال. نحن بصدد إنشاء فئة AnimatingSwitch جديدة ترث من osg :: Switch.

 class AnimatingSwitch : public osg::Switch { public: AnimatingSwitch() : osg::Switch(), _count(0) {} AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} META_Node(osg, AnimatingSwitch); virtual void traverse(osg::NodeVisitor &nv); protected: unsigned int _count; }; void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

هذه الفئة تحتوي على المنشئ الافتراضي.

 AnimatingSwitch() : osg::Switch(), _count(0) {} 

ومنشئ للنسخ ، تم إنشاؤه وفقا لمتطلبات OSG

 AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) : osg::Switch(copy, copyop), _count(copy._count) {} 

يجب أن يحتوي مُنشئ النسخ كمعلمات: مرجع ثابت لمثيل الفئة المراد نسخها والمعلمة osg :: CopyOp التي تحدد إعدادات النسخ للفئة. رسائل غريبة جدا متابعة

 META_Node(osg, AnimatingSwitch); 

هذا هو ماكرو الذي يشكل البنية اللازمة لأحفاد فئة مشتقة من osg :: Node. حتى نعلق أهمية كبيرة على هذا الماكرو ، من المهم أن يكون موجودًا عند الوراثة من osg :: Switch عند تعريف جميع الفئات المنحدرة. يحتوي الفصل على الحقل المحمي _count - العداد بالاعتماد عليه الذي نحوله. نحن نطبق التبديل عند تجاوز طريقة traverse ()

 void AnimatingSwitch::traverse(osg::NodeVisitor &nv) { if (!((++_count) % 60) ) { setValue(0, !getValue(0)); setValue(1, !getValue(1)); } osg::Switch::traverse(nv); } 

سيحدث تبديل حالة عرض العقد في كل مرة تكون قيمة العداد (زيادة كل استدعاء طريقة) مضاعفات 60. نجمع المثال ونعمل عليه



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

يمكن تعريف ممر القطع عن طريق تحويل osg :: NodeVisitor إلى كائن osg :: CullVisitor

 osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv); if (cv) { ///  - ,     } 


8. آلية رد الاتصال


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

هناك عدة أنواع من عمليات الاسترجاعات في المحرك. يتم تطبيق عمليات الاسترجاعات من خلال فئات خاصة ، من بينها osg :: NodeCallback تم تصميمه للتعامل مع عملية تحديث عقد المشهد ، و osg :: Drawable :: UpdateCallback ، osg :: Drawable :: EventCallback و osg :: Drawable: CullCallback - أداء الوظائف نفسها ، ولكن لكائنات الهندسة.

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

يسرد الجدول التالي عمليات الاسترجاعات المتاحة للمطور في OSG.

الاسم الأولرد الاتصال functorطريقة افتراضيةطريقة لربط كائن
تحديث العقدةosg :: NodeCallbackالمشغل ()osg :: العقدة :: setUpdateCallback ()
حدث العقدةosg :: NodeCallbackالمشغل ()osg :: العقدة :: setEventCallback ()
لقطة العقدةosg :: NodeCallbackالمشغل ()osg :: العقدة :: setCullCallback ()
تحديث الهندسةosg::Drawable::UpdateCallbackupdate()osg::Drawable::setUpdateCallback()
osg::Drawable::EventCallbackevent()osg::Drawable::setEventCallback()
osg::Drawable::CullCallbackcull()osg::Drawable::setCullCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setUpdateCallback()
osg::StateAttributeCallbackoperator()osg::StateAttribute::setEventCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setUpdateCallback()
osg::Uniform::Callbackoperator()osg::Uniform::setEvevtCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PreDrawCallback()
osg::Camera::DrawCallbackoperator()osg::Camera::PostDrawCallback()


9. osg::Switch


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

Callbackswith المثال
الرئيسية

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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, true); root->addChild(model2, false); root->setUpdateCallback( new SwitchingCallback ); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


يجب عليك إنشاء فئة وراثة من osg :: NodeCallback ، والتي تتحكم في عقدة osg :: Switch

 class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()(osg::Node *node, osg::NodeVisitor *nv); protected: unsigned int _count; }; 

سيتحكم عداد _count في تبديل عقدة osg :: Switch من تعيين عقدة فرعية إلى أخرى ، اعتمادًا على قيمتها. في المنشئ ، نقوم بتهيئة العداد ونعيد تعريف طريقة التشغيل الظاهري ()

 void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osg::Switch *switchNode = static_cast<osg::Switch *>(node); if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } traverse(node, nv); } 

يتم تمرير العقدة التي عملت عليها المكالمة من خلال معلمة العقدة. نظرًا لأننا نعرف بالتأكيد أن هذا سيكون عقدة من النوع osg :: Switch ، فإننا نقوم بإجراء قالب ثابت من المؤشر للعقدة إلى المؤشر إلى عقدة التبديل

 osg::Switch *switchNode = static_cast<osg::Switch *>(node); 

سنقوم بتبديل العقد الفرعية المعروضة بالقيمة الصالحة لهذا المؤشر ، وعندما تكون قيمة العداد مضاعفة 60

 if ( !((++_count) % 60) && switchNode ) { switchNode->setValue(0, !switchNode->getValue(0)); switchNode->setValue(1, !switchNode->getValue(0)); } 

لا تنس استدعاء الأسلوب traverse () لمتابعة اجتياز العودية للرسم البياني للمشهد

 traverse(node, nv); 

بقية رمز البرنامج تافهة ، باستثناء السطر

 root->setUpdateCallback( new SwitchingCallback ); 

حيث نقوم بتعيين رد الاتصال أنشأناه على عقدة الجذر من نوع osg :: Switch. البرنامج يعمل على غرار المثال السابق



حتى الآن ، استخدمنا طريقة traverse الغامضة () لغرضين: تجاوز هذه الطريقة في الفصول اللاحقة واستدعاء هذه الطريقة في فئة osg :: NodeVisitor لمواصلة اجتياز الرسم البياني للمشهد.

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

تعمل أساليب addUpdateCallback () أيضًا على إضافة رد اتصال إلى العقدة. بخلاف setUpdateCallback () ، يتم استخدامه لإضافة رد اتصال آخر إلى تلك الموجودة. وبالتالي ، قد يكون هناك عمليات رد اتصال متعددة للعقدة نفسها.

الخاتمة


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

أن تستمر ...

Source: https://habr.com/ru/post/ar437688/


All Articles