原文地址:http://www.flmnware.com/
osgexample http://dis.dankook.ac.kr/lectures/med08/?page=1
目的:通过本教程,学会将自定义节点添加入场景图,并控制节点在每一帧的运动。
1、添加自定义节点
OpenSceneGraph提供的节点类(Node及其子类)实现了通用的场景图结构,但是不可能提供每一个用户需要的类,比如我要做一个飞行模拟程序,OpenSceneGraph不会给我提供一个Aircraft类。所以要实现自己的应用程序逻辑,就要自定义自己的类,这个类以OpenSceneGraph提供的类为基类,并能添加到场景图中,实现自己的特定功能。
一个飞机的运动,最简单情况是我们要考虑飞机的位置和姿态,而要想在OpenSceneGraph里控制一个模型的位置、旋转或缩放,需要在这个节点上面添加一个Transform的子类,这要用到矩阵运算,矩阵运算非常复杂,对于控制位置姿态这个经常用到的功能,OpenSceneGraph提供了类PositionAttitudeTransform,通过它可以方便控制位置姿态而不用理会复杂的矩阵运算。
飞机需要导入一个模型,在场景图中,我们就把这个模型加为这个类实例的儿子,这样,这个类可以这样定义,飞机类CCessna:
class CCessna :
public osg::PositionAttitudeTransform
{
public:
CCessna(void);
~CCessna(void);
private:
osg::ref_ptr<osg::Node> _Model;
};
class CCessna :
public osg::PositionAttitudeTransform
{
public:
CCessna(void);
~CCessna(void);
private:
osg::ref_ptr<osg::Node> _Model;
};
class CCessna :
public osg::PositionAttitudeTransform
{
public:
CCessna(void);
~CCessna(void);
private:
osg::ref_ptr<osg::Node> _Model;
};
class CCessna :
public osg::PositionAttitudeTransform
{
public:
CCessna(void);
~CCessna(void);
private:
osg::ref_ptr<osg::Node> _Model;
};
成员变量_Model是保存模型的指针。
这样,在构造函数里,将_Model加为儿子:
CCessna::CCessna(void)
{
_Model = osgDB::readNodeFile("cessna.osg");
this->addChild(_Model.get());
}
CCessna::CCessna(void)
{
_Model = osgDB::readNodeFile("cessna.osg");
this->addChild(_Model.get());
}
CCessna::CCessna(void)
{
_Model = osgDB::readNodeFile("cessna.osg");
this->addChild(_Model.get());
}
CCessna::CCessna(void)
{
_Model = osgDB::readNodeFile("cessna.osg");
this->addChild(_Model.get());
}
继续使用第一个教程的源码,将这一句:
osg::Node* node = osgDB::readNodeFile("cessna.osg");
viewer.setSceneData(node);
osg::Node* node = osgDB::readNodeFile("cessna.osg");
viewer.setSceneData(node);
osg::Node* node = osgDB::readNodeFile("cessna.osg");
viewer.setSceneData(node);
osg::Node* node = osgDB::readNodeFile("cessna.osg");
viewer.setSceneData(node);
改为:
CCessna* cessna = new Ccessna();
viewer.setSceneData(cessna);
CCessna* cessna = new Ccessna();
viewer.setSceneData(cessna);
CCessna* cessna = new Ccessna();
viewer.setSceneData(cessna);
CCessna* cessna = new Ccessna();
viewer.setSceneData(cessna);
就能看到效果了。
2、给节点添加更新回调并控制节点运动
为了控制飞机的运动,我们应该在每一帧根据速度等条件计算飞机的位置,并更新飞机节点,但是我们的更新代码应该加在哪里呢?
在程序的主循环里,可以看到这句:
viewer.update();
viewer.update();
viewer.update();
viewer.update();
在这个方法里,OpenSceneGraph会遍历场景图,调用每个节点的更新回调,我们只要把自己代码放在更新回调里,就能保证每帧代码得到运行,也就能实现我们要求的功能了。
在OpenSceneGraph中,每个回调都是类NodeCallback的子类,我们只需要继承它,就能实现功能了。为了保证飞机更新的时候能得到需要的足够信息,又不让飞机的内部实现暴露于外,我们让更新回调只进行一个转手的操作,而实际的更新函数放在CCessna类里。
更新回调代码如下:
class CCessnaUpdateCallback :
public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
CCessna* cessna = dynamic_cast<CCessna*>(node);
if(cessna != NULL)
{
cessna->update();
}
}
};
class CCessnaUpdateCallback :
public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
CCessna* cessna = dynamic_cast<CCessna*>(node);
if(cessna != NULL)
{
cessna->update();
}
}
};
class CCessnaUpdateCallback :
public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
CCessna* cessna = dynamic_cast<CCessna*>(node);
if(cessna != NULL)
{
cessna->update();
}
}
};
class CCessnaUpdateCallback :
public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
CCessna* cessna = dynamic_cast<CCessna*>(node);
if(cessna != NULL)
{
cessna->update();
}
}
};
这个类覆盖了基类对操作符()的重载,所以说它实际上是一个函数对象,因为这个重载是通用的,在函数里我们要先动态转换成 CCessna 类的指针,然后调用她的更新成员函数。
为了让这个回调起作用,不要忘了在 CCessna 类的构造函数里设置更新回调:
this->setUpdateCallback(new CCessnaUpdateCallback());
this->setUpdateCallback(new CCessnaUpdateCallback());
this->setUpdateCallback(new CCessnaUpdateCallback());
this->setUpdateCallback(new CCessnaUpdateCallback());
下面设计让飞机绕圈飞行,同时让飞机横滚,代码都是写计算操作,需要注意的是PositionAttitudeTransform类提供的设置姿态的方法不是使用通常习惯的HPR为参数,而是一个四元数,关于四元数数学请参考相关书籍,但是HPR向四元数的转换还是很直接的,这就是我们的setRotation方法:
void
CCessna::setRotation(osg::Vec3& rot)
{
osg::Quat q(rot.x(), osg::Vec3(1.0f, 0.0f, 0.0f),
rot.y(), osg::Vec3(0.0f, 1.0f, 0.0f),
rot.z(), osg::Vec3(0.0f, 0.0f, 1.0f));
this->setAttitude(q);
}
void
CCessna::setRotation(osg::Vec3& rot)
{
osg::Quat q(rot.x(), osg::Vec3(1.0f, 0.0f, 0.0f),
rot.y(), osg::Vec3(0.0f, 1.0f, 0.0f),
rot.z(), osg::Vec3(0.0f, 0.0f, 1.0f));
this->setAttitude(q);
}
void
CCessna::setRotation(osg::Vec3& rot)
{
osg::Quat q(rot.x(), osg::Vec3(1.0f, 0.0f, 0.0f),
rot.y(), osg::Vec3(0.0f, 1.0f, 0.0f),
rot.z(), osg::Vec3(0.0f, 0.0f, 1.0f));
this->setAttitude(q);
}
void
CCessna::setRotation(osg::Vec3& rot)
{
osg::Quat q(rot.x(), osg::Vec3(1.0f, 0.0f, 0.0f),
rot.y(), osg::Vec3(0.0f, 1.0f, 0.0f),
rot.z(), osg::Vec3(0.0f, 0.0f, 1.0f));
this->setAttitude(q);
}
在update方法里,通过设置位置和旋转来控制飞机运动,具体请查看代码。
3、完善程序
由于Viewer类在程序开始会根据模型的包围盒调整视点初始位置,这样能保证程序开始能看到模型,但是因为我们的模型是运动的,运行一点之后飞机会飞出视线,所以我们给飞机提供一个场地,这个场地就是一个正方体,代码如下:
osg::Group* root = new osg::Group();
osg::Geode* geode = new osg::Geode();
geode->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0.0f, 0.0f, -20.0f), 200.0f, 200.0f, 2.0f)));
root->addChild(geode);
CCessna* cessna = new CCessna();
root->addChild(cessna);
viewer.setSceneData(root);
osg::Group* root = new osg::Group();
osg::Geode* geode = new osg::Geode();
geode->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0.0f, 0.0f, -20.0f), 200.0f, 200.0f, 2.0f)));
root->addChild(geode);
CCessna* cessna = new CCessna();
root->addChild(cessna);
viewer.setSceneData(root);
osg::Group* root = new osg::Group();
osg::Geode* geode = new osg::Geode();
geode->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0.0f, 0.0f, -20.0f), 200.0f, 200.0f, 2.0f)));
root->addChild(geode);
CCessna* cessna = new CCessna();
root->addChild(cessna);
viewer.setSceneData(root);
osg::Group* root = new osg::Group();
osg::Geode* geode = new osg::Geode();
geode->addDrawable(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0.0f, 0.0f, -20.0f), 200.0f, 200.0f, 2.0f)));
root->addChild(geode);
CCessna* cessna = new CCessna();
root->addChild(cessna);
viewer.setSceneData(root);
这里用到了Geode 类和Drawable类的子类ShapeDrawable, Geode 是场景图的叶子节点,所有的几何体都包含在 Geode 里,而一个 Geode 里可以有多个Drawable的子类,我们看到的图像真正是Drawable画的,这个后面在讨论。
总结:要想实现自己的功能,加入自己的代码,就要定义自己的节点类,由于OpenSceneGraph使用了组合设计模式,我们自己定义的类在OpenSceneGraph看来和它自己的类是一样的,OpenSceneGraph的这种场景图结构,基本上是业界的一种标准,很多程序都实现了类似的结构。