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); 

我们分析参数,即在其中寻找“ –model”键,并将其值放入字符串文件名中。 因此,使用此密钥,我们将具有三维模型的文件名传输到程序。 接下来,我们加载此模型并显示它

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

osg :: ArgumentParser类的read()方法有很多重载,使您不仅可以从命令行读取字符串值,还可以读取整数,浮点数,向量等。 例如,您可以读取float类型的某个参数

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

如果此参数未在命令行上显示,则其值将保留初始化size变量后的值。

2.通知和日志记录机制


OpenSceneGraph具有通知机制,可让您在渲染过程中以及由开发人员启动时显示调试消息。 在跟踪和调试程序时,这是一个很大的帮助。 OSG通知系统支持引擎核心级别及其插件的诊断信息(错误,警告,通知)的输出。 开发人员可以在程序操作期间使用osg :: notify()函数显示诊断消息。

通过操作符重载<<,此函数可以用作标准C ++库的标准输出流。 它以消息级别作为参数:始终,致命,警告,通知,信息,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()方法是类似的基类方法,我们将其重新定义为在操作期间通过msg参数输出到OSG发送的文件通知。

类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流。 但是,通过覆盖通知处理程序(如示例中所示),可以将此输出重定向到任何输出流,包括GUI元素。

请记住,设置较高级别的通知(例如FATAL)时,系统将忽略所有较低级别的通知。 例如,在类似情况下

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

自定义消息将不会显示。

3.拦截几何属性


osg :: Geometry类管理一组描述顶点的数据,并使用一组有序的图元显示多边形网格。 但是,此类不了解模型的拓扑元素,例如面,边以及它们之间的关系。 这种细微差别可防止诸如在制作模型动画时移动某些面孔之类的事情。 OSG当前不支持此功能。

但是,引擎实现了许多函子,使您可以重新读取任何对象的几何属性,并使用它们对多边形网格的拓扑进行建模。 在C ++中,函子是一种允许您将对象用作函数的构造。

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-先前函子的只读版本:指向向量数组的指针作为常量参数传递
  2. osg :: PrimitiveFunctor-模仿渲染OpenGL对象的过程。 以渲染对象为幌子,调用了开发人员重写的函子方法。 该仿函数具有两个重要的模板子类:osg :: TemplatePrimitiveFunctor <>和osg :: TriangleFunctor <>。 这些类接收原始顶点作为参数,并使用operator()运算符将它们传递给用户方法。
  3. osg :: PrimitiveIndexFunctor-执行与上一个函子相同的操作,但接受图元的顶点索引作为参数。

从osg :: Drawable派生的类(例如osg :: ShapeDrawable和osg :: Geometry)具有用于应用各种函子的accept()方法。

4.使用原始函子的示例


我们以收集关于三角形面和我们先前确定的某些几何形状的点的信息为例来说明所描述的功能。

函子示例
主文件

 #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结构,为此我们重新定义operator(),如下所示

 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。 要调用函子,请创建函子的实例,然后通过accept()方法将其传递给几何对象

 osg::TriangleFunctor<FaceCollector> functor; geom->accept(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-> accept(...)时,不会渲染三角形,而是模拟OpenGL调用,而不是模拟三角形的顶点数据,而是模拟它们的渲染



osg :: TemplatePrimitiveFunctor类不仅收集有关三角形的数据,而且还收集任何其他OpenGL原语的数据。 要实现此数据的处理,必须在template参数中覆盖以下运算符

 //   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基类提供的一个或多个虚拟重载的apply()方法。 大多数主要的OSG节点类型都具有这些方法。 当遍历场景图时,访问者将自动为每个访问的节点调用apply()方法。 开发人员将覆盖他需要的每种节点类型的apply()方法。

在执行apply()方法时,开发人员必须在适当的时候调用osg :: NodeVisitor基类的traverse()方法。 如果当前节点没有可以进行转换的子节点,则这将启动访问者到下一个节点的过渡,该节点是层次结构级别的子节点或邻居节点。 缺少对遍历()的调用意味着停止遍历场景图,其余场景图将被忽略。

apply()方法的重载具有统一的格式

 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.燃烧舱的结构分析


开发人员始终可以分析由从文件加载的模型生成的场景图的那部分。

函子示例
主文件

 #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; }; 

protected _level属性将指向我们的访问者类当前所在的场景图的级别。 在构造函数中,初始化级别计数器并设置节点遍历的模式-绕过所有子节点。

现在重新定义节点的apply()方法

 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()引发了对apply()方法的重复调用,对于从当前节点开始的子图,已经重复了traverse()。 我们得到递归的访问者执行,直到命中场景图的末端节点。

对于类型为osg :: Geode的终端节点,其apply()方法的重载将被覆盖

 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; } 

在main()函数中,我们处理命令行参数,通过该参数我们传递加载到场景中的模型列表并形成场景

 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()方法将调用类的实例附加到将对其进行处理的节点。渲染每一帧时,在场景图中的节点更新期间会自动调用operator()运算符。

下表列出了OSG中可供开发人员使用的回调。

回调函子虚方法附着对象的方法
节点更新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回调机制正确地完成所有操作。

带示例的回调
主文件

 #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); } 

调用工作的节点通过node参数传递给它。由于我们肯定知道这将是osg :: Switch类型的节点,因此我们将指向该节点的指针静态转换为指向该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()的第三个选项,将指向节点的指针和指向访问者实例的指针作为参数传递。与前两种情况一样,如果在该节点上未调用traverse(),则场景图的爬网将停止。

addUpdateCallback()方法还用于向节点添加回调。与setUpdateCallback()不同,它用于向现有回调中添加另一个回调。因此,同一节点可能有多个回调。

结论


我们检查了使用OpenSceneGraph图形引擎开发应用程序时使用的基本技术。但是,这与我想谈的所有要点相去甚远(尽管事实证明这篇文章很长),因此

待续...

Source: https://habr.com/ru/post/zh-CN437688/


All Articles