本章主要介绍 OSG 场景的工作机制以及如何动态变更场景数据,主要内容包括内存管理、访问器、回调的设计及数据变量。
1. 内存管理
面对大规模的场景时,经常需要增加或删除一些数据,这些操作往往会因为不小心造成内存泄露或野指针,从而导致系统崩溃。一个熟练的开发人员也不能保证能够实时、正确地释放已经不用的内存空间。作为初学者,这更是一个非常难的问题。在OSG中,提供了一种新的机制——智能指针。智能指针(Smart pointer)是一种类的模板,它针对某一特定类型的对象(即 Referenced 类及其派生类)构建,提供了自己的管理模式,以避免因为用户使用 new运算符创建对象实例之后,没有及时用 delete运算符释放对象而造成内存泄露。
智能指针的使用为用户提供了一种自动内存释放的机制,即场景图形中的每一个节点均关联一个内存计数器,当计数器的计数减到0时,该对象将被自动释放。用户如果希望释放整个场景图形的节点,则只需要删除根节点,根节点以下的所有分支节点均会被自动删除,不用担心内存泄露的问题。
要使用 OSG 的智能指针,必须满足以下两个条件:
- 用户的类必须派生自Referenced类,这样才能使用与其自身关联的内存计数器。
- 当用户使用智能指针模板 osg::ref_ptr<class T>定义类的实例时,内存计数器即被启用并加1;同理,当ref_ptr 模板超出其生命范围时,类实例的内存计数器将被减1,如果减到0则对象自动被释放。
1.1 Referenced 类
Referenced类(命名空间:osg)实现了对内存区段的引用计数器功能。所有的0SG节点和场景图形数据,包括状态信息、顶点数组、法线以及纹理坐标,均派生自Referenced类。因此,所有的OSG场景图形均可以进行内存引用计数。
Referenced 类包括了如下3个主要组成部分:
- 保护成员整型变量_refCount。用作引用计数,在构造时被初始化为0。
- 公有函数ref()和unref()。用于实现_refCount值的增加和减少。当_refCount为0时,unref()将自动释放该对象所占用的内存。
- 作为保护成员存在的虚析构函数。堆栈的创建和显示的析构均会因为析构函数受保护而被禁止,而虚函数的特性将允许用户执行子类的析构函数。总体上来说,用户的代码基本上不需要直接调用ref()和unref()函数,只要使用ref_ptr<>进行处理即可。
1.2 ref_ptr<>模板类
ref_ptr>(命名空间:osg)用于实现一个指向Referenced对象的智能指针,并对其引用计数器进行管理。当最后一个引用 Referenced 对象的refptr>失去作用时,对象将确保被释放。ref ptr>简化了场景图形内存释放的工作,并保证当错误地调用堆栈展开时,对象也可以被正确释放。
ref_ptr<>模板类包括以下3个主要组成部分:
- 一个私有指针_ptr。用于保存管理内存区域的地址,可以用get()方法返回_ptr的值。
- 为了使ref_ptr<>可以像正常的C++指针一样工作,重载或定义了一些方法,如 operator->()和operator==()。
- valid()方法。用于判断 ref_ptr<>是否为空,不为NULL时返回tnue。当程序将一个地址指定给ref_ptr<>变量时,ref_ptr<>的重载函数operator==()将会假定此地址指向一个 Referenced 派生对象,并自动调用 Referenced::ref(),将引用计数值自动加1。
ref_ptr<>变量引用计数值减少的情形有两种,分别是ref_ptr<>被释放(在类的析构函数里执行减1)或重新进行了赋值(在operator==()里执行减1)。在这两种情况中,ref_ptr<>都会通过调用Referenced::unref()来执行减少引用计数值的操作。
1.3 智能指针
通过上面的介绍,可以看出OSG中的智能指针的机制在管理内存方面是非常智能的。在 OSG中,大多数场景图形类都继承自osg::Reference,因此,在OSG中,智能指针几乎无处不在。
虽然智能指针能够有效地管理内存,能够在很大程度上避免内存泄露。在实际使用中,还有如下几个需要注意的地方:
- 使用智能指针模板的类必须继承自osg::Reference 类,否则将无法使用,如osg::ref_ptr<osg::Matrix>的用法是错误的,编译会提示错误。
- 在创建智能指针之后,不能手动调用 delete 来删除该智能指针,否则编译会提示错误信息。因为 osg::Reference 的析构函数是保护类型的,所以这种行为是不允许发生的。
- 在 osg::Reference 类成员函数中,有两个共有的成员函数ref()和unref(),它们主要用来控制内存计数器。在应用程序中,不要随意使用ref()和unref()函数来改变内存计数器的值,这样可能会导致对象无故被删除而引起程序崩溃。
- 在OSG中,同样是允许new运算指针的,如“osg::Node*node=new osg::Node();”这样的做法在OSG是允许的,但是需要注意的是,不能混用智能指针。否则,在读者的应用程序中会出现意想不到的情况。在实际开发中需要统一规定使用某一种方法,这样可以避免很多不必要的错误调试。
2. 访问器机制
2.1 访问器设计模式
访问器设计模式是对在多个抽象的对象群的一种特殊处理,它可以作为一个作用于某对象结构中的各元素的操作。它使读者可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问器的设计主要适用于以下情况:
- 一个对象结构包含很多类对象,它们有不同的接口,而读者想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而读者想避免让这些操作“污染”这些对象的类。Visitor使读者可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,可以用 Visitor 模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
2.2 osg::NodeVisitor 类
osg::NodeVisitor 类是对设计模式 Visitor 中的设计思想的具体实现。osg::NodeVisitor 类继承自osg::Reference 类,继承关系图如图7-1所示。
图7-1 osg:::NodeVisitor 的继承关系图
从继承关系图可以看出,OSG 众多场景管理类都继承自osg::NodeVisitor 类。可以说 OSG 遍历整个场景函数并调用被访问子节点的函数,如osgUtil::TriStripVisitor等。
osg::NodeVisitor是一个虚基类,在程序中无法实例化。在osg::NodeVisitor中主要有apply()和accept()两大函数。apply()决定了遍历的方式,如可以获得各个节点的属性,也可以修改节点的属性,这完全取决于apply()函数的实现,用户可以创建新的继承于osg::NodeVisitor的类,重写apply()函数。调用accept()函数可以关联需要访问的节点,并启动访问器进行遍历。在用户应用程序中,可以编写继承自osg::NodeVisitor 的新类,再通过重载 apply()函数来实现自己的功能。
根据上面访问器的特性,访问器的设计允许应用程序将某个特定节点的指定函数应用到场景中的所有节点。
遍历的类型包括:
enum VisitorType
{
NODE_VISITOR=0,//节点访问器
UPDATE_VISITOR,//更新访问器
EVENT_VISITOR,//时间访问器
COLLECT_OCCLUDER_VISITOR,//遮挡节点访问器
CULL_VISITOR //拣选访问器
};
遍历的模式包括:
enum TraversalMModc
{
TRAVERSE_NONE.//仅传递到当前节点
TRAVERSE_PARENTS,//传递给当前节点及父节点
TRAVERSE_ALL_CHILDREN,//传递给场册中所有的节点及其子节点
TRAVERSE_ACTTVE_CHILDREN//传递给场景中所有的活动节点及其子节点
};
在应用程序中定义自己的访问器主要包括以下步骤:
(1)继承自osg::NodeVisitor 类写一个新类,重载其中的 apply()方法,添加自己的代码实现相应的功能。
(2)在应用程序中调用 accept()方法关联相应的节点,启动访问器遍历,下面的内容将着重说明如何定义自己的访问器。
2.3 顶点访问器示例
顶点访问器(VertexVisitor)示例的代码如程序清单7-1所示,
// 顶点访问器,继承自osg::NodeVisitor类class VertexVisitor : public osg::NodeVisitor{public://保存顶点数据osg::ref_ptr<osg::Vec3Array> extracted_verts;//构造函数,初始化一下并设置为遍历所有子节点VertexVisitor() :osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN){extracted_verts = new osg::Vec3Array();}//重载apply方法void apply(osg::Geode& geode){//得到每一个drawablefor (unsigned int i = 0; i < geode.getNumDrawables(); ++i){//得到几何体osg::Geometry* geom = dynamic_cast<osg::Geometry*>(geode.getDrawable(i));if (!geom){std::cout << "几何体错误!" << std::endl;continue;}//得到顶点数组osg::Vec3Array* verts = dynamic_cast<osg::Vec3Array*>(geom->getVertexArray());if (!verts){std::cout << "顶点数组错误!" << std::endl;continue;}//添加到extracted_vertsextracted_verts->insert(extracted_verts->end(), verts->begin(), verts->end());}}};void vertexVisitor_7_1(const string &strDatafolder){// 创建Viewer对象,场景浏览器osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;traits->x = 40;traits->y = 40;traits->width = 600;traits->height = 480;traits->windowDecoration = true;traits->doubleBuffer = true;traits->sharedContext = 0;osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());osg::ref_ptr<osg::Camera> camera = new osg::Camera;camera->setGraphicsContext(gc.get());camera->setViewport(new osg::Viewport(0, 0, traits->width, traits->height));GLenum buffer = traits->doubleBuffer ? GL_BACK : GL_FRONT;camera->setDrawBuffer(buffer);camera->setReadBuffer(buffer);viewer->addSlave(camera.get());osg::ref_ptr<osg::Group> root = new osg::Group();// 读取滑翔机模型string strNodePath = strDatafolder + "glider.osg";osg::ref_ptr<osg::Node> node = osgDB::readNodeFile(strNodePath);root->addChild(node.get());// 创建顶点访问器对象VertexVisitor vtea;node->accept(vtea); // 开始执行访问器遍历// 申请一个输出流string strNodeOutPath = strDatafolder + "glider.vertexs";std::ofstream fout(strNodeOutPath);// 得到顶点数组的大小int size_t = vtea.extracted_verts->size();// 初始化一个迭代器std::vector <osg::Vec3 > ::iterator iter = vtea.extracted_verts->begin();// 写入文件for (int i = 0; i < size_t; i++){fout << iter->x() << " " << iter->y() << " " << iter->z() << std::endl;iter++;}// 优化场景数据osgUtil::Optimizer optimizer;optimizer.optimize(root.get());viewer->setSceneData(root.get());viewer->realize();viewer->run();}
运行程序,截图如图 7-2 所示。
图7-2顶点访问器示例截图
2.4 纹理访问器示例
纹理访问器(TextureVisitor)示例的代码如程序清单7-2所示。
// 7-2 节点纹理访问器class TextureVisitor : public osg::NodeVisitor{public:// 构造函数,遍历所有子节点TextureVisitor() :osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN){//}// 重载apply方法virtual void apply(osg::Node& node){if (node.getStateSet()){apply(node.getStateSet());}//实现继续遍历节点traverse(node);}// 重载apply方法virtual void apply(osg::Geode& geode){if (geode.getStateSet()){apply(geode.getStateSet());}unsigned int cnt = geode.getNumDrawables();for (unsigned int i = 0; i < cnt; i++){apply(geode.getDrawable(i)->getStateSet());}traverse(geode);}// 得到贴图列表void apply(osg::StateSet* state){//得到纹理属性列表osg::StateSet::TextureAttributeList& texAttribList = state->getTextureAttributeList();for (unsigned int i = 0; i < texAttribList.size(); i++){//得到纹理osg::Texture2D* tex2D = NULL;if (tex2D = dynamic_cast<osg::Texture2D*>(state->getTextureAttribute(i, osg::StateAttribute::TEXTURE))){//得到贴图if (tex2D->getImage()){//写入映射表_imageList.insert(std::make_pair(tex2D->getImage()->getFileName(), tex2D->getImage()));}}}}// 得到贴图std::map<std::string, osg::Image*>& getImages(void){return _imageList;}protected:// 贴图映射表,用来保存贴图名和贴图std::map<std::string, osg::Image*> _imageList;};// 7-2 节点纹理访问器void textureVisitor_7_2(const string &strDatafolder){// 创建Viewer对象,场景浏览器osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;traits->x = 50;traits->y = 50;traits->width = 1000;traits->height = 800;traits->windowDecoration = true;traits->doubleBuffer = true;traits->sharedContext = 0;osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());osg::ref_ptr<osg::Camera> camera = new osg::Camera;camera->setGraphicsContext(gc.get());camera->setViewport(new osg::Viewport(0, 0, traits->width, traits->height));GLenum buffer = traits->doubleBuffer ? GL_BACK : GL_FRONT;camera->setDrawBuffer(buffer);camera->setReadBuffer(buffer);viewer->addSlave(camera.get());osg::ref_ptr<osg::Group> root = new osg::Group();// 创建一个节点并读取牛的模型string strNodePath = strDatafolder + "cow.osg";osg::ref_ptr<osg::Node> node = osgDB::readNodeFile(strNodePath);// 创建纹理访问器TextureVisitor textureTV;node->accept(textureTV); // 启动访问器,执行遍历std::map<std::string, osg::Image*> imageList = textureTV.getImages(); // 初始化一个贴图映射表std::map<std::string, osg::Image*>::iterator iter = imageList.begin(); // 初始化一个映射表迭代器unsigned int cnt = 0;char strTemp[256];for (; iter != imageList.end(); ++iter){sprintf(strTemp, "TextureImage%d.jpg", ++cnt);osgDB::writeImageFile(*(iter->second), strTemp);// 写入文件}root->addChild(node.get());// 优化场景数据osgUtil::Optimizer optimizer;optimizer.optimize(root.get());viewer->setSceneData(root.get());viewer->realize();viewer->run();}
运行程序,截图如图 7-3 所示
图7-3 纹理访问器示例截图
2.5 节点访问器示例
节点访问器(FindNodeVisitor)示例的代码如程序清单7-3所示。
#ifndef FIND_NODE_VISITOR_H#define FIND_NODE_VISITOR_H#include <osg/NodeVisitor>#include <osg/Node>#include <osgSim/DOFTransform>#include <iostream>#include <vector>#include <string>// 节点查找访问器,继承自osg::NodeVisitorclass FindNodeVisitor : public osg::NodeVisitor{public:// 构造函数,参数为需要查找的节点名FindNodeVisitor(const std::string &searchName) ;// 重载apply方法virtual void apply(osg::Node &searchNode);virtual void apply(osg::Geode &geode);virtual void apply(osg::Transform &searchNode);// 设置要查找的节点名void setNameToFind(const std::string &searchName);// 得到查找节点向量的第一个节点osg::Node* getFirst(); // 定义一个节点向量typedef std::vector<osg::Node*> nodeListType;// 得到查找节点向量nodeListType& getNodeList(){return foundNodeList;}private:std::string searchForName; // 节点名 nodeListType foundNodeList; // 用来保存查找的节点};#endif#include "FindNodeVisitor.h"//构造函数,初始化并设置遍历所有的子节点FindNodeVisitor::FindNodeVisitor(const std::string &searchName) :osg::NodeVisitor(TRAVERSE_ALL_CHILDREN),searchForName(searchName){//}//重载apply方法 void FindNodeVisitor::apply(osg::Node &searchNode){//判断节点名称是否与查找的节点名称一样if (searchNode.getName() == searchForName){//添加到节点系列foundNodeList.push_back(&searchNode);}//继续遍历traverse(searchNode);}//重载apply方法void FindNodeVisitor::apply(osg::Transform &searchNode){apply ((osg::Node&)searchNode);traverse(searchNode);}//重载apply方法void FindNodeVisitor::apply(osg::Geode &geode){apply ( (osg::Node&)geode);traverse((osg::Node&)geode);}//设置要查找的节点名称void FindNodeVisitor::setNameToFind(const std::string &searchName){searchForName = searchName;foundNodeList.clear();}//得到查找节点向量中第一个节点osg::Node* FindNodeVisitor::getFirst(){return *(foundNodeList.begin());}#include <osgViewer/Viewer>#include <osg/Node>#include <osg/Geode>#include <osg/Group>#include <osgDB/ReadFile>#include <osgDB/WriteFile>#include <osgUtil/Optimizer>#include "FindNodeVisitor.h"int main(){// 创建Viewer对象,场景浏览器osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();osg::ref_ptr<osg::Group> root = new osg::Group();// 创建一个节点,读取牛的模型osg::ref_ptr<osg::Node> node = osgDB::readNodeFile("cow.osg");// 创建节点查找访问器FindNodeVisitor cow("cow.osg");node->accept(cow); //启动访问器,开始执行遍历if(!cow.getFirst()){std::cout<<"无法找到节点,查找失败"<<std::endl ;}else{std::cout<<"查找节点成功,成功找到节点"<<std::endl ;}root->addChild(node.get());// 优化场景数据osgUtil::Optimizer optimizer ;optimizer.optimize(root.get()) ;viewer->setSceneData(root.get());viewer->realize();viewer->run();return 0 ;}