文章目录
一、场景图形的工作机制(以及如何动态变更场景数据)
1.内存管理
智能指针的使用为用户提供了一种自动内存释放机制,即场景图形中的每一个节点均关联一个内存计数器,当计数器的计数减到0时,该对象将被自动释放。
用户如果希望释放整个场景图形的节点,则只需要删除根节点,根节点以下的所有分支节点均会被自动删除。
Referenced类
实现了对内存区段的引用计数器功能,主要组成部分:
- 保护成员整型变量_refCount,用作引用计数,在构造时被初始化为0.
- 公有函数ref()和unref()。用于实现_refCount值的增加和减少。当_refCount为0时,unref将自动释放该对象所占用的内存。
- 作为保护的虚析构函数。堆栈的创建和显示的析构均会因为析构函数受保护而被禁止,而虚析构函数的特性将允许用户执行子类的析构函数。
所以一般派生自Referenced的类的虚析构函数一般都是保护的(只能在堆上创建,即osg::Node *node = new osg::Node;)
用户代码一般基本上不需要直接使用ref()和unref()函数,只要使用ref_ptr<>进行处理即可。
ref_ptr<>智能指针模板类
主要组成部分:
- 一个私有指针_ptr,可以使用get()获得原始指针(被使用会增加计数)。一般使用的时候,最好调用这个方法使用,因为有些函数可能没有重载指针。
- 重载了一些方法,可以像正常C++指针一样工作。
- valid()判断ref_ptr是否为空,不为NULL时返回true。
- 当程序将一个地址指定给ref_ptr<>变量时,它的operator=()将会假定此地址指向一个Referenced派生对象,并调用Referenced::ref(),将Referenced::refCount引用计数加一。
- 当ref_ptr对象被释放或者重新赋值,都会通过调用Referenced::unref()减少引用计数值。
- release():使用栈保存“堆”中的指针,使函数结束时传递的数值能被成功地传递。
一般是用在例如,在局部创建一个ref_ptr<> a,并返回该a,但是返回类型不是ref_ptr<>,而是原始类型,这样子局部的a会导致因为引用计数先-1为0,从而内存先被释放了,导致返回了一个野指针!
(该例子可以参考这里)
智能指针
使用注意事项:
- 使用智能指针模板必须继承自Referenced类,否则编译错误。
- 在创建智能指针之后,不能手动调用delete来删除智能指针,否则编译错误。
- 不要随意用Referenced类中的ref()和unref()函数来改变内存计数器,可能会导致对象无辜被删除而程序崩溃。
- 允许new运算指针,例如osg::Node *node = new osg::Node(而且一般都因为析构函数为保护的,也只能在堆上创建),但是不能混用智能指针,除非是作为参数传递(只是不能它不能被引用出来)。
2.访问器机制
访问器设计模式(方便遍历节点)
可以向不同的节点元素(一颗树多种节点元素)施加用户自定义操作,将这些操作整合到一个对象中。
每一个数据元素节点都可以通过accept调用访问器,而访问器通过apply获取传入的节点对象,并对其执行apply自定义操作。
一般用作于定义对象结构的类很少改变,但是经常需要在此结构上定义新的操作。
osg:: NodeVisitor: osg::Referenced,是个虚基类
OSG众多场景管理类都继承自osg::NodeVisitor,或者是说OSG遍历整个场景函数并调用被访问子节点的函数都继承自osg::NodeVisitor。
NodeVisitor只是访问器角色的抽象接口,当我们试图使用访问器访问节点,并执行自定义操作时,需要继承并编写自己的访问器类,重写各个节点类型(需要用到的)对应的apply函数。
- apply():决定了遍历的方式,例如可以获得各个节点的属性,也可以修改节点的属性,取决于它的实现。
Node也需要新增成员函数:
- virtual void osg::Node::accept ( NodeVisitor & nv )
:可以关联需要访问的节点,将一个具体的访问器川地给节点,并启动访问器进行遍历;
在其中节点反过来执行访问器的apply函数,并将自身传入访问器:
大概表达:
void Node::accept(NodeVistor &nv)
{
nv.apply(*this);
}
节点的遍历函数,在OSG中通过节点自身的方法来实现:
osg::Node :
void ascend(NodeVistor& nv);//虚函数,向上一级节点推进访问器
void traverse(NodeVistor& nv);//虚函数,向下一级节点推进访问器
NodeVisitor的traverse函数实现了集中访问器对节点的遍历方式:
将访问器向下/向上推进
所以在apply中,在对节点执行完自定义用户操作之后,再执行节点/NodeVistor的traverse/ascend,从而允许应用程序将某个特定节点的指定函数应用到场景中的所有节点。
由此可见,OSG对于场景书的访问采用的是深度优先遍历,先直达末端叶节点,再逐步返回到上一级未访问的节点。
NodeVisitor (VisitorType type, TraversalMode tm=TRAVERSE_NONE)
enum TraversalMode
{
TRAVERSE_NONE, // 仅传递到当前节点
TRAVERSE_PARENTS, // 传递给当前节点及父节点
TRAVERSE_ALL_CHILDREN, // 传递给场景中所有节点及其子节点
TRAVERSE_ACTIVE_CHILDREN // 传递给场景中所有活动节点及其子节点
}
enum VisitorType
{
NODE_VISITOR = 0, // 节点访问器
UPDATE_VISITOR, // 更新访问器
EVENT_VISITOR, // 时间访问器
COLLECT_OCCLUDER_VISITOR,// 遮挡节点访问器
CULL_VISITOR, // 拣选访问器
INTERSECTION_VISITOR
}
- 自定义访问器步骤:
1.继承osg::NodeVisitor类写一个新类,重载其中apply方法,添加自己的代码实现相应功能。
2.应用程序中调用accept方法关联相应的节点,启动访问器遍历。其中记得调用节点遍历函数。
顶点访问器
示例1:
class VistorNodePath:public osg::NodeVisitor
{
public:
VistorNodePath():osg::NodeVisitor(TRAVERSE_ALL_CHILDREN){}
void apply(osg::Node &node)
{
std::cout<< "Apply node:" <<node.getName()<<std::endl;
if("root"==node.getName())
{
// Do something
}
traverse(node);// 所有子节点也会进行同样操作。
}
void apply(osg::Group &group)
{
std::cout<< "Apply group:" <<group.getName()<<std::endl;
if("group"==node.getName())
{
// Do something
}
traverse(group);// 所有子节点也会进行同样操作。
}
};
int main()
{
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;
osg::ref_ptr<osg::Group> group= new osg::Group;
osg::ref_ptr<osg::Node> node= osgDB::readNodeFile("glider.osg");
group->setName("root");
node->setName("glider");
group->addChild(node);
viewer->setSceneData(group);
// 自定义访问器
VistorNodePath vn;
group->accept(vn);
return viewer->run();
}
从叶节点访问根节点:
// 使用TraversalMode::TRAVERSE_PARENTS
// 并用叶节点来调用accept即可从该节点往根节点遍历
示例2:
class BoundVisitor:public osg::NodeVisitor
{
public:
void apply(osg::Geode& geode)
{
// 得到每一个drawable
osg::Geode::DrawableList dl = geode.getDrawableList();
for(osg::Geode::DrawableList::iterator it = dl.begin();it!=dl.end();it++)
{
osg::ref_ptr<osg::Geometry> gm = dynamic_cast<osg::Geometry>((*it).get());
osg::Vec3Array vx = dynamic_cast<osg::Vec3Array*>(gm->getVertexArray());
for(osg::Vec3Array::iterator iter = vx.begin();iter!= vx.end();iter++)
{
//if("root" == m_group.getName())
//{
// // 向几何体中每个节点添加一个小方块
// group->addChild(createBox(it));
//}需要将循环注释掉
std::cout <<iter->x()<<","<<iter->y()<<","<<iter->z()<<std::endl;
}
}
}
void setGroup(osg::Group *gp)
{
m_group = gp;
}
//private:
//osg::ref_ptr<osg::Group> m_group;
};
纹理访问器(补
3.回调机制osg::NodeCallback:osg::Callback
OSG中节点主要使用回调Callback来完成用户临时定义的、需要每帧执行的工作,是一种方便扩展节点的功能。但是对于功能要求比较复杂的用户节点,重构Node::traverse函数并编写自定义的实现代码,应当是一种结构更为清晰的方式。
回调
回调是一种用户编写的功能模块,可以作为一个参数传递给其他功能模块,从而实现对某些底层系统事件的响应和处理。C++中一般使用函数指针,将回调函数的地址作为传入参数。
根据回调功能被调用的时机划分为更新回调(Update CallBack)和人机交互事件回调(Event CallBack)。前者在每一帧中系统遍历到当前节点时调用,后者则由交互事件触发,如操作键盘、鼠标、关闭窗口、改变窗口大小等动作。回调类基类是osg::NodeCallBack(),主要函数如下:
// 默认构造函数
NodeCallBack();
// 虚函数,回调函数主要操作在此函数中,子类应当重写
// 当回调动作发生时,将会执行里面的内容,并将节点和访问器作为参数传入
void operator()(Node* node, NodeVisitor* nv)
{
。。。
// 继续遍历
traverse(node,nv);
}
// 为当前更新回调添加(删除)一个后继/临近的回调对象
// 临近回调的内容将在节点回调的执行过程中被依次调用
void addNestedCallback(NodeCallback* nc);
void removeNestedCallback(NodeCallback* nc);
//直接设置/获取一个最近/临近的回调
void setNestedCallback(NodeCallback* nc);
NodeCallback* getNestedCallback();
//调用临近中的下一个更新回调
void traverse(Node* node,NodeVisitor* nv);
实现回调有以下步骤:
- 编写继承自osg::NodeCallback的新类(当然也可以用库里已经派生了的)。
- 重写operator(),添加相关的代码,实现场景的动态更新。
- 初始化一个回调实例,将其作为节点扩展/更新回调设置到某个节点,即关联到相应的对象,可以使用场景有:
- osg::Node(例如它派生类osg::MatrixTransform)可以在OSG执行更新和拣选遍历时进行回调。
- osg::Drawable可以在拣选和绘制遍历时进行回调。
- osg::Camera可以在更新遍历时进行回调。
- osgDB::Registry::instance可以在读写的时候进行回调。
回调的设置和获取
以osg::Node为例子:
// 设置/获取节点的更新回调
void setUpdateCallback(NodeCallback* );
NodeCallback* getUpdateCallback();
// 设置/获取节点的事件回调
void setEventCallback(NodeCallback*);
NodeCallback* getEventCallback();
// 设置一些特殊的回调
void setCullCallback (Callback *nc);
void setComputeBoundingSphereCallback (ComputeBoundingSphereCallback *callback);
总结大部分关联的方法:
setUpdateCallback
setCullCallback
setDrawCallback
setEventCallback
setReadFileCallback
setWriteFileCallback
。。。。
节点回调示例:使用回调实现旋转动画
class RotateCallback :public osg::NodeCallback
{
public:
RotateCallback() :_rotateZ(0.0) {};
virtual void operator()(osg::Node* node,osg::NodeVisitor*nv)
{
osg::PositionAttitudeTransform* pat = dynamic_cast<osg::PositionAttitudeTransform*>(node);
if (pat)
{
osg::Quat quat(osg::DegreesToRadians(_rotateZ),osg::Z_AXIS);
pat->setAttitude(quat);
_rotateZ += 1.0f;
}
traverse(node,nv);
}
protected:
double _rotateZ;
};
int main()
{
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();
osg::Node *model = osgDB::readNodeFile("cow.osg");
osg::ref_ptr<osg::PositionAttitudeTransform> pat = new osg::PositionAttitudeTransform;
pat->addChild(model);
pat->setUpdateCallback(new RotateCallback);
viewer->setSceneData(pat.get());
return viewer->run();
}
事件回调
class MyEventCallback : public osg::NodeCallback
{
public:
virtual void operator()(osg::Node* node,osg::NodeVisitor* nv)
{
// 判断访问器类型
if (nv->getVisitorType() == osg::NodeVisitor::EVENT_VISITOR)
{
// 创建一个事件访问器并初始化
osg::ref_ptr<osgGA::EventVisitor> ev = dynamic_cast<osgGA::EventVisitor*>(nv);
if (ev)
{
// 得到执行动作
osgGA::GUIActionAdapter* aa = ev->getActionAdapter();
// 得到事件队列
osgGA::EventQueue::Events& events = ev->getEvents();
for (osgGA::EventQueue::Events::iterator it = events.begin();
it != events.end(); it++)
{
osgGA::GUIEventAdapter* ea = dynamic_cast<osgGA::GUIEventAdapter*>(it->get());
//处理事件
handle(*ea,*aa);
}
}
}
}
virtual bool handle(const osgGA::GUIEventAdapter &ea,osgGA::GUIActionAdapter &aa)
{
// 得到场景数据
osg::ref_ptr<osgViewer::Viewer> viewer = dynamic_cast<osgViewer::Viewer*>(&aa);
osg::ref_ptr<osg::MatrixTransform> mx = dynamic_cast<osg::MatrixTransform*>(viewer->getSceneData());
switch (ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
if (ea.getKey() == 'w')
{
osg::Matrix mt;
mt.makeTranslate(0.0,1.0,0.0);
mx->preMult(mt);// 连续移动
}
break;
}
default:
break;
}
return false;
}
};
int main()
{
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;
// 需要用MatrixTransform,而不是group因为要在回调中获得
osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform;
mt->addChild(osgDB::readNodeFile("glider.osg"));
// 事件回调
osg::ref_ptr<osg::Camera> camera = viewer->getCamera();
camera->setEventCallback(new MyEventCallback());//第一种事件处理方法
viewer->addEventHandler(new osgViewer::HelpHandler);//第二种事件处理方法
viewer->setSceneData(mt.get());
return viewer->run();
}
更新回调
文件读取回调(补
4.数据变量
OSG允许场景数据的动态变更,这是所有场景图形系统都具备的。
在前面的场景渲染流程中已经提到,在拣选遍历中,关联场景中所有的几何数据和状态信息(上下文)需要在绘制遍历(viewer->run)中处理(例如一帧走一次渲染管线)。
然后osgViewer库支持多线程模式,每一个线程均独立地运行拣选及绘制遍历,出于性能优化的考虑,OSG并没有为线程的安全性增设内存锁,而是要求程序只可在拣选cull及绘制遍历的时域之外修改场景图形。
不过拣选和绘制难免会发生冲突,所以可以在frame之外修改场景:
while(!viewer->done(())
{
// 添加自己的代码修改场景
viewer->frame();
}
访问器和回调也是改变场景数据的好办法。
下面再介绍一种能避免冲突的新机制:
osgViewer支持的多线程模型允许用户程序主循环不必等到绘制遍历结束就可以继续运行,也就是说Viewer::frame()在绘制遍历仍未结束就可以返回,即上一帧的绘制遍历可以与下一帧的更新遍历产生交叠,这样子几乎很难避免与绘制遍历线程的冲突。
所以如果在开发动态更改场景图形,然后在修改场景图形时奔溃错误,这类问题一般都是因为用户在拣选和绘制遍历的过程中修改了场景数据而造成!
osg::Object::setDataVariance()的参数:
enum DataVariance { DYNAMIC, STATIC, UNSPECIFIED }
例如:
在设置一个Object对象的数据变量时,调用setDataVariance将数据变量改为STATIC静态的或DYNAMIC动态的(默认UNSPECIFIED 未指定)。
OSG确保绘制遍历在所有DYNAMIC节点和数据处理完成之后才会返回,由于绘制遍历在函数返回后仍然可以继续渲染场景图形,OSG将确保此时只有STATIC数据可以继续进行图形渲染。