OSG开源教程(转)
例:geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));
来指定要利用这些数据生成一个怎么样的形状。
该行代码中,使用DrawArrays类向Geometry类送入了新几何体的信息,即,该几何体是一个QUADS,它的顶点坐标从索引数组中读入,从第1个索引值开始,共读入4个索引值,组成一个四边形图形。
几何体的形状参数除了QUADS之外,还有数种方式,以用于不同的用户需求,列表如下:
POINTS |
绘制点 |
绘制用户指定的所有顶点。 |
LINES |
绘制直线 |
直线的起点、终点由数组中先后相邻的两个点决定;用户提供的点不止两个时,将尝试继续绘制新的直线。 |
LINE_STRIP |
绘制多段直线 |
多段直线的第一段由数组中的前两个点决定;其余段的起点位置为上一段的终点坐标,而终点位置由数组中随后的点决定。 |
LINE_LOOP |
绘制封闭直线 |
绘图方式与多段直线相同,但是最后将自动封闭该直线。 |
TRIANGLES |
绘制三角形 |
三角形的三个顶点由数组中相邻的三个点决定,并按照逆时针的顺序进行绘制;用户提供的点不止三个时,将尝试继续绘制新的三角形。 |
TRIANGLE_STRIP |
绘制多段三角形 |
第一段三角形的由数组中的前三个点决定;其余段三角形的绘制,起始边由上一段三角形的后两个点决定,第三点由数组中随后的一点决定。 |
TRIANGLE_FAN |
绘制三角扇面 |
第一段三角形的由数组中的前三个点决定;其余段三角形的绘制,起始边由整个数组的第一点和上一段三角形的最后一个点决定,第三点由数组中随后的一点决定。 |
QUADS |
绘制四边形 |
四边形的四个顶点由数组中相邻的四个点决定,并按照逆时针的顺序进行绘制;用户提供的点不止四个时,将尝试继续绘制新的四边形。 |
QUAD_STRIP |
绘制多段四边形 |
第一段四边形的起始边由数组中的前两个点决定,边的矢量方向由这两点的延伸方向决定;起始边的对边由其后的两个点决定,如果起始边和对边的矢量方向不同,那么四边形将会扭曲;其余段四边形的绘制,起始边由上一段决定,其对边由随后的两点及其延伸方向决定。 |
POLYGON |
绘制任意多边形 |
根据用户提供的顶点的数量,绘制多边形。 |
和opengl对比:
osg::PrimitiveSet::POINTS对应OpenGL中的GL_POINTS绘制单独的点
osg::PrimitiveSet::LINES对应OpenGL中的GL_LINES绘制每两点连接的线
osg::PrimitiveSet::LINE_STRIP对应OpenGL中的GL_LINE_STRIP绘制依次连接各点的线
osg::PrimitiveSet::LINE_LOOP对应OpenGL中的GL_LINE_LOOP绘制依次连接各点的线,首尾相连
osg::PrimitiveSet::POLYGON对应OpenGL中的GL_POLYGON绘制依次连接各点的多边形
osg::PrimitiveSet::QUADS对应OpenGL中的GL_QUADS绘制依次连接每四点的四边形
如:1、2、3、4、5、6、7、8点 绘制结果1、2、3、4组成四边形,5、6、7、8组成四边形
osg::PrimitiveSet::QUAD_STRIP对应OpenGL中的GL_QUAD_STRIP绘制四边形
如:1、2、3、4、5、6、7、8点 绘制结果1、2、3、4组成四边形,3、4、5、6组成四边形、5、
6、7、8组成四边形
osg::PrimitiveSet::TRIANGLES对应OpenGL中的GL_TRIANGLES绘制每三点连接的三角形
如:1、2、3、4、5、6点 绘制结果1、2、3组成三角形,4、5、6组成三角形
osg::PrimitiveSet::TRIANGLE_STRIP对应OpenGL中的GL_TRIANGLE_STRIP
如:1、2、3、4、5、6点 绘制结果1、2、3组成三角形,2、3、4组成三角形,3、4、5组成三角
形4、5、6组成三角形
osg::PrimitiveSet::TRIANGLE_FAN对应OpenGL中的GL_TRIANGLE_FAN
如:1、2、3、4、5、6点 绘制结果1、2、3组成三角形,1、3、4组成三角形,1、4、5组成三角
形,1、5、6组成三角形
目录
1.使用Open Scene Graph几何... 1
1.1背景... 1
1.2代码... 1
2.使用StateSet产生有纹理的几何体... 4
2.1本章目标... 4
2.2背景... 4
2.3加载纹理,生成状态集合并将他们附加到节点上... 6
3.使用Shape,改变state. 8
3.1本章目标... 8
3.2使用Shape类... 8
3.3设置状态... 9
4.更多的StateSet. 10
4.1StateSet如何工作... 10
4.2例子及代码... 10
5.从文件中加载模型并放入到场景中... 12
5.1本章目标... 12
5.2加载几何模型并加入到场景中... 12
6.osg Text、HUD、RenderBins. 15
6.1本章目标... 15
6.2摘要... 15
6.3代码... 15
7.搜索并控制开关和DOF(自由度)节点(Finding and Manipulating a Switch and DOF Node)... 20
7.1搜索场景图形中的一个有名节点... 20
7.2按照“访问器”模式搜索有名节点... 22
8.使用更新回调来更改模型... 26
8.1本章目标... 26
8.2回调概览... 26
8.3创建一个更新回调... 26
9.处理键盘输入... 29
9.1本章目标... 29
9.2 GUI(图形用户接口)事件处理器:... 29
9.3简单的键盘接口类... 30
9.4使用键盘接口类... 32
9.5处理键盘输入实现更新回调... 32
9.5.1本节目标... 32
9.5.2问题的提出... 32
9.5.3解决方案... 33
10.使用自定义矩阵来放置相机(Positioning a Camera with a User-Defined Matrix)... 36
10.1本章目标... 36
10.2设置矩阵的方向和位置... 36
10.3声明一个用于设置相机的矩阵... 37
10.4使用矩阵设置视口摄相机... 38
11. 实现跟随节点的相机... 38
11.1本章目标... 38
11.2概述... 39
11.3实现... 39
11.4环绕(始终指向)场景中节点的相机... 42
11.4.1本节目标... 42
11.4.2实现... 43
a.jpg (86.96 KB)
2007-11-23 01:05 PM
下面是操作状态配置并用节点将这些状态关联起来的代码。
// Set an osg::TexEnv instance's mode to BLEND,
// make this TexEnv current for texture unit 0 and assign
// a valid texture to texture unit 0
blendTexEnv->setMode(osg::TexEnv::BLEND);
stateRootBlend->setTextureAttribute(0,blendTexEnv,osg::StateAttribute::ON);
stateRootBlend->setTextureAttributeAndModes(0,ocotilloTexture,
osg::StateAttribute::ON);
// For state five, change the texture associated with texture unit 0
//all other attributes will remain unchanged as inherited from above.
// (texture mode will still be BLEND)
stateFiveDustTexture->setTextureAttributeAndModes(0,dustTexture,
osg::StateAttribute::ON);
// Set the mode of an osg::TexEnv instance to DECAL
//Use this mode for stateOneDecal.
decalTexEnv->setMode(osg::TexEnv::DECAL);
stateOneDecal->setTextureAttribute(0,decalTexEnv,osg::StateAttribute::ON);
// For stateTwo, turn FOG OFF and set to OVERRIDE.
//Descendants in this sub-tree will not be able to change FOG unless
//they set the FOG attribute value to PROTECTED
stateTwoFogON_OVRD->setAttribute(fog, osg::StateAttribute::ON);
stateTwoFogON_OVRD->setMode(GL_FOG,
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
// For stateThree, try to turn FOG OFF.
Since the attribute is not PROTECTED, and
// the parents set this attribute value to OVERRIDE, the parent's value will be used.
// (i.e. FOG will remain ON.)
stateThreeFogOFF->setMode(GL_FOG, osg::StateAttribute::OFF);
// For stateFour, set the mode to PROTECTED, thus overriding the parent setting
stateFourFogOFF_PROT->setMode(GL_FOG,
osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
// apply the StateSets above to appropriates nodes in the scene graph.
root->setStateSet(stateRootBlend);
mtOne->setStateSet(stateOneDecal);
mtTwo->setStateSet(stateTwoFogON_OVRD);
mtThree->setStateSet(stateThreeFogOFF);
mtSix->setStateSet(stateFiveDustTexture);
mtFour->setStateSet(stateFourFogOFF_PROT);
此文件到那个文件夹下。
几何模型使用scene graph的节点表示。因此,为了加载并操作一个几何模型文件,我们需要声明一个句柄(或指针)指向osg::Node类型实例。(在一些要求的#include后)。
#include
#include
...
osg::Node* cessnaNode = NULL;
osg::Node* tankNode = NULL;
...
cessnaNode = osgDB::readNodeFile("cessna.osg");
tankNode = osgDB::readNodeFile("Models/T72-tank/t72-tank_des.flt");
这就是加载数据库需要做的事。下一步我们把它作为scene graph的一部分加入。将模型加载到transform节点的子节点上,这样我们就可以重新定位它了。
// Declare a node which will serve as the root node
// for the scene graph. Since we will be adding nodes
// as 'children' of this node we need to make it a 'group'
// instance.
// The 'node' class represents the most generic version of nodes.
// This includes nodes that do not have children (leaf nodes.)
// The 'group' class is a specialized version of the node class.
// It adds functions associated with adding and manipulating
// children.
osg::Group* root = new osg::Group();
root->addChild(cessnaNode);
// Declare transform, initialize with defaults.
osg::PositionAttitudeTransform* tankXform =
new osg::PositionAttitudeTransform();
// Use the 'addChild' method of the osg::Group class to
// add the transform as a child of the root node and the
// tank node as a child of the transform.
root->addChild(tankXform);
tankXform->addChild(tankNode);
// Declare and initialize a Vec3 instance to change the
// position of the tank model in the scene
osg::Vec3 tankPosit(5,0,0);
tankXform->setPosition( tankPosit );
现在,我们的scene graph由一个根节点和两个子节点组成。一个是cessna的几何模型,另一个是一个右子树,由一个仅有一个tank的几何模型的transform节点组成。为了观察场景,需要建立一个viewer和一个仿真循环。就像这样做的:
#include
// Declare a 'viewer'
osgProducer::Viewer viewer;
// For now, we can initialize with 'standard settings'
// Standard settings include a standard keyboard mouse
// interface as well as default drive, fly and trackball
// motion models for updating the scene.
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
// Next we will need to assign the scene graph we created
// above to this viewer:
viewer.setSceneData( root );
// create the windows and start the required threads.
viewer.realize();
// Enter the simulation loop. viewer.done() returns false
// until the user presses the 'esc' key.
// (This can be changed by adding your own keyboard/mouse
// event handler or by changing the settings of the default
// keyboard/mouse event handler)
while( !viewer.done() )
{
// wait for all cull and draw threads to complete.
viewer.sync();
// Initiate scene graph traversal to update nodes.
// Animation nodes will require update. Additionally,
// any node for which an 'update' callback has been
// set up will also be updated. More information on
// settting up callbacks to follow.
viewer.update();
// initiate the cull and draw traversals of the scene.
viewer.frame();
}
你应当能编译并执行上面的代码(保证调用的顺序是正确的,已经添加了main等等)。执行代码的时候,按h键会弹出一个菜单选项(似乎没有这个功能——译者)。按‘esc’退出。
这里*。‘osgExample Text’工程示范了许多方法。这个教程提供了文本类的几个有限的函数。绘制一个HUD牵扯到下面两个概念:
1、生成一个子树,它的根节点有合适的投影及观察矩阵...
2、将HUD子树中的几何体指定到合适的RenderBin上,这样HUD几何体就会在场景的其他部分之后按正确地状态设置绘制。
渲染HUD的子树涉及到一个投影矩阵和一个观察矩阵。对于投影矩阵,我们会使用相当于屏幕维数水平和垂直扩展的正投影。根据这种模式,坐标相当于象素坐标。为了简单起见,观察矩阵使用单位矩阵。
为了渲染HUD,我们把它里面的几何体附加到一个指定的RenderBin上。RenderBin允许用户在几何体绘制过程中指定顺序。这对于HUD几何体需要最后绘制来说很有用。
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/T72Tank.osg
现在我们已经获得了需要控制的开关节点的名称(sw1),亦可获取其指针对象。获取节点指针的方法有两种:一是编写代码遍历整个场景图形;二是使用后面将会介绍的访问器(visitor)。在以前的教程中,我们已经知道如何加载flight文件,将其添加到场景并进入仿真循环的方法。
#include
#include
#include
#include
#include
#include
int main()
{
osg::Node* tankNode = NULL;
osg::Group* root = NULL;
osgViewer::Viewer viewer;
osg::Vec3 tankPosit;
osg::PositionAttitudeTransform* tankXform;
tankNode = osgDB::readNodeFile("../NPS_Data/Models/t72-tank/t72-tank_des.flt");
root = new osg::Group();
tankXform = new osg::PositionAttitudeTransform();
root->addChild(tankXform);
tankXform->addChild(tankNode);
tankPosit.set(5,0,0);
tankXform->setPosition( tankPosit );
viewer.setCameraManipulator(new osgGA::TrackballManipulator());
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.frame();
}
}
现在我们需要修改上述代码,以添加查找节点的函数。下面的递归函数有两个参数值:用于搜索的字符串,以及用于指定搜索开始位置的节点。函数的返回值是指定节点子树中,第一个与输入字符串名称相符的节点实例。如果没有找到这样的节点,函数将返回NULL。特别要注意的是,使用访问器将提供更为灵活的节点访问方式。而下面的代码只用于展示如何手动编写场景图形的遍历代码。
osg::Node* findNamedNode(const std::string& searchName,
osg::Node* currNode)
{
osg::Group* currGroup;
osg::Node* foundNode;
// 检查输入的节点是否是合法的,
// 如果输入节点为NULL,则直接返回NULL。
if ( !currNode)
{ return NULL; }
// 如果输入节点合法,那么先检查该节点是否就是我们想要的结果。
// 如果确为所求,那么直接返回输入节点。
if (currNode->getName() == searchName)
{ return currNode; }
// 如果输入节点并非所求,那么检查它的子节点(不包括叶节点)情况。
// 如果子节点存在,则使用递归调用来检查每个子节点。
// 如果某一次递归的返回值非空,说明已经找到所求的节点,返回其指针。
// 如果所有的节点都已经遍历过,那么说明不存在所求节点,返回NULL。
currGroup = currNode->asGroup();
if ( currGroup )
{
for (unsigned int i = 0 ; i < currGroup->getNumChildren(); i ++)
{
foundNode = findNamedNode(searchName, currGroup->getChild(i));
if (foundNode)
return foundNode; // 找到所求节点。
}
return NULL; // 遍历结束,不存在所求节点。
}
else
{
return NULL; // 该节点不是组节点,返回NULL。
}
}
现在我们可以在代码中添加这个函数,用于查找场景中指定名称的节点并获取其指针。注意这是一种深度优先的算法,它返回第一个符合的节点指针。
我们将在设置场景之后,进入仿真循环之前调用该函数。函数返回的开关节点指针可以用于更新开关的状态。下面的代码用于模型载入后,执行查找节点的工作。
osg::Switch* tankStateSwitch = NULL;
osg::Node* foundNode = NULL;
foundNode = findNamedNode("sw1",root);
tankStateSwitch = (osg::Switch*) foundNode;
if ( !tankStateSwitch)
{
std::cout << "tank state switch node not found, quitting." << std::endl;
return -1;
}
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/findNodeVisitor.zip
#include
#include
#include
#include
#include
class findNodeVisitor : public osg::NodeVisitor {
public:
findNodeVisitor();
findNodeVisitor(const std::string &searchName) ;
virtual void apply(osg::Node &searchNode);
virtual void apply(osg::Transform &searchNode);
void setNameToFind(const std::string &searchName);
osg::Node* getFirst();
typedef std::vector
nodeListType& getNodeList() { return foundNodeList; }
private:
std::string searchForName;
nodeListType foundNodeList;
};
现在,我们创建的类可以做到:启动一次节点访问遍历,访问指定场景子树的每个子节点,将节点的名称与用户指定的字符串作比较,并建立一个列表用于保存名字与搜索字符串相同的节点。那么如何启动这个过程呢?我们可以使用osg::Node的“accept”方法来实现节点访问器的启动。选择某个执行accept方法的节点,我们就可以控制遍历开始的位置。(遍历的方向是通过选择遍历模式来决定的,而节点类型的区分则是通过重载相应的apply方法来实现)“accpet”方法将响应某一类的遍历请求,并执行用户指定节点的所有子类节点的apply方法。在这里我们将重载一般节点的apply方法,并选择TRAVERSE_ALL_CHILDREN的遍历模式,因此,触发accept方法的场景子树中所有的节点,均会执行这一apply方法。
在这个例子中,我们将读入三种不同状态的坦克。第一个模型没有任何变化,第二个模型将使用多重开关(multSwitch)来关联损坏状态,而第三个模型中,坦克的炮塔将旋转不同的角度,同时枪管也会升高。
下面的代码实现了从文件中读入三个坦克模型并将其添加到场景的过程。其中两个坦克将作为变换节点(PositionAttitudeTransform)的子节点载入,以便将其位置设置到坐标原点之外。
// 定义场景树的根节点,以及三个独立的坦克模型节点
osg::Group* root = new osg::Group();
osg::Group* tankOneGroup = NULL;
osg::Group* tankTwoGroup = NULL;
osg::Group* tankThreeGroup = NULL;
// 从文件中读入坦克模型
tankOneGroup = dynamic_cast
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankTwoGroup = dynamic_cast
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankThreeGroup = dynamic_cast
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
// 将第一个坦克作为根节点的子节点载入
root->addChild(tankOneGroup);
// 为第二个坦克定义一个位置变换
osg::PositionAttitudeTransform* tankTwoPAT =
new osg::PositionAttitudeTransform();
// 将第二个坦克向右移动5个单位,向前移动5个单位
tankTwoPAT->setPosition( osg::Vec3(5,5,0) );
// 将第二个坦克作为变换节点的子节点载入场景
root->addChild(tankTwoPAT);
tankTwoPAT->addChild(tankTwoGroup);
// 为第三个坦克定义一个位置变换
osg::PositionAttitudeTransform* tankThreePAT =
new osg::PositionAttitudeTransform();
// 将第二个坦克向右移动10个单位
tankThreePAT->setPosition( osg::Vec3(10,0,0) );
// 将坦克模型向左旋转22.5度(为此,炮塔的旋转应当与坦克的头部关联)
tankThreePAT->setAttitude( osg::Quat(3.14159/8.0, osg::Vec3(0,0,1) ));
// 将第三个坦克作为变换节点的子节点载入场景
root->addChild(tankThreePAT);
tankThreePAT->addChild(tankThreeGroup);
我们准备将第二个模型设置为损坏的状态,因此我们使用findNodeVisitor类获取控制状态的多重开关(multiSwitch)的句柄。这个节点访问器需要从包含了第二个坦克的组节点开始执行。下面的代码演示了声明和初始化一个findNodeVisitor实例并执行场景遍历的方法。遍历完成之后,我们即可得到节点列表中符合搜索字符串的第一个节点的句柄。这也就是我们准备使用multiSwitch来进行控制的节点句柄。
// 声明一个findNodeVisitor类的实例,设置搜索字符串为“sw1”
findNodeVisitor findNode("sw1");
// 开始执行访问器实例的遍历过程,起点是tankTwoGroup,搜索它所有的子节点
// 并创建一个列表,用于保存所有符合搜索条件的节点
tankTwoGroup->accept(findNode);
// 声明一个开关类型,并将其关联给搜索结果列表中的第一个节点。
osgSim::MultiSwitch* tankSwitch = NULL;
tankSwitch = dynamic_cast
更新开关节点
当我们获取了一个合法的开关节点句柄后,下一步就是从一个模型状态变换到另一个状态。我们可以使用setSingleChildOn方法来实现这个操作。setSingleChildOn()方法包括两个参数:第一个无符号整型量相当于多重开关组(switchSet)的索引号;第二个无符号整型量相当于开关的位置。在这个例子中,我们只有一个多重开关,其值可以设置为未损坏状态或者损坏状态,如下所示:
// 首先确认节点是否合法,然后设置其中的第一个(也是唯一的)多重开关
if (tankSwitch)
{
//tankSwitch->setSingleChildOn(0,false); // 未损坏的模型
tankSwitch->setSingleChildOn(0,true); // 损坏的模型
}
更新DOF节点
坦克模型还包括了两个DOF(自由度)节点“turret”和“gun”。这两个节点的句柄也可以使用上文所述的findNodeVisitor来获取。(此时,访问器的场景遍历应当从包含第三个模型的组节点处开始执行)一旦我们获取了某个DOF节点的合法句柄之后,即可使用setCurrentHPR方法来更新与这些节点相关的变换矩阵。setCurrentHPR方法只有一个参数:这个osg::Vec3量相当于三个欧拉角heading,pitch和roll的弧度值。(如果要使用角度来描述这个值,可以使用osg::DegreesToRadians方法)
// 声明一个findNodeVisitor实例,设置搜索字符串为“turret”
findNodeVisitor findTurretNode("turret");
// 遍历将从包含第三个坦克模型的组节点处开始执行
tankThreeGroup->accept(findTurretNode);
// 确认我们找到了正确类型的节点
osgSim::DOFTransform * turretDOF =
dynamic_cast
// 如果节点句柄合法,则设置炮塔的航向角为向右22.5度。
if (turretDOF)
{
turretDOF->setCurrentHPR( osg::Vec3(-3.14159/4.0,0.0,0.0) );
}
同理,机枪的自由度也可以如下设置:
// 声明一个findNodeVisitor实例,设置搜索字符串为“gun”
findNodeVisitor findGunNode("gun");
// 遍历将从包含第三个坦克模型的组节点处开始执行
tankThreeGroup->accept(findGunNode);
// 确认我们找到了正确类型的节点
osgSim::DOFTransform * gunDOF =
dynamic_cast
// 如果节点句柄合法,则设置机枪的俯仰角为向上22.5度。
if (gunDOF)
{
gunDOF->setCurrentHPR( osg::Vec3(0.0,3.14159/8.0,0.0) );
}
8.使用更新回调来更改模型
8.1本章目标
使用回调类实现对场景图形节点的更新。前一个教程介绍了在进入主仿真循环之前,更新DOF和开关节点的方法。本节将讲解如何使用回调来实现在每帧的更新遍历(update traversal)中进行节点的更新。
8.2回调概览
用户可以使用回调来实现与场景图形的交互。回调可以被理解成是一种用户自定义的函数,根据遍历方式的不同(更新update,拣选cull,绘制draw),回调函数将自动地执行。回调可以与个别的节点或者选定类型(及子类型)的节点相关联。在场景图形的各次遍历中,如果遇到的某个节点已经与用户定义的回调类和函数相关联,则这个节点的回调将被执行。如果希望了解有关遍历和回调的更多信息,请参阅David Eberly所著的《3D Game Engine Design》第四章,以及SGI的《Performer Programmer's Guide》第四章。相关的示例请参见osgCallback例子。
8.3创建一个更新回调
更新回调将在场景图形每一次运行更新遍历时被执行。与更新回调相关的代码可以在每一帧被执行,且实现过程是在拣选回调之前,因此回调相关的代码可以插入到主仿真循环的viewer.update()和viewer.frame()函数之间。而OSG的回调也提供了维护更为方便的接口来实现上述的功能。善于使用回调的程序代码也可以在多线程的工作中更加高效地运行。
从前一个教程展开来说,如果我们需要自动更新与坦克模型的炮塔航向角和机枪倾角相关联的DOF(自由度)节点,我们可以采取多种方式来完成这一任务。譬如,针对我们将要操作的各个节点编写相应的回调函数:包括一个与机枪节点相关联的回调,一个与炮塔节点相关联的回调,等等。这种方法的缺陷是,与不同模型相关联的函数无法被集中化,因此增加了代码阅读、维护和更新的复杂性。另一种(极端的)方法是,只编写一个更新回调函数,来完成整个场景的节点操作。本质上来说,这种方法和上一种具有同样的问题,因为所有的代码都会集中到仿真循环当中。当仿真的复杂程度不断增加时,这个唯一的更新回调函数也会变得愈发难以阅读、维护和修改。关于编写场景中节点/子树回调函数的方法,并没有一定之规。在本例中我们将创建单一的坦克节点回调,这个回调函数将负责更新炮塔和机枪的自由度节点。
为了实现这一回调,我们需要在节点类原有的基础上添加新的数据。我们需要获得与炮塔和机枪相关联的DOF节点的句柄,以更新炮塔旋转和机枪俯仰的角度值。角度值的变化要建立在上一次变化的基础上。因为回调是作为场景遍历的一部分进行初始化的,我们所需的参数通常只有两个:一个是与回调相关联的节点指针,一个是用于执行遍历的节点访问器指针。为了获得更多的参数数据(炮塔和机枪DOF的句柄,旋转和俯仰角度值),我们可以使用节点类的userData数据成员。userData是一个指向用户定义类的指针,其中包含了关联某个特定节点时所需的一切数据集。而对于用户自定义类,只有一个条件是必需的,即,它必须继承自osg::Referenced类。Referenced类提供了智能指针的功能,用于协助用户管理内存分配。智能指针记录了分配给一个类的实例的引用计数值。这个类的实例只有在引用计数值到达0的时候才会被删除。有关osg::Referenced的更详细叙述,请参阅本章后面的部分。基于上述的需求,我们向坦克节点添加如下的代码:
class tankDataType : public osg::Referenced
{
public:
// 公有成员……
protected:
osgSim::DOFTransform* tankTurretNode;
osgSim::DOFTransform* tankGunNode;
double rotation;
double elevation;
};
为了正确实现tankData类,我们需要获取DOF节点的句柄。这一工作可以在类的构造函数中使用前一教程所述的findNodeVisitor类完成。findNodeVisitor将从一个起始节点开始遍历。本例中我们将从表示坦克的子树的根节点开始执行遍历,因此我们需要向tankDataType的构造函数传递坦克节点的指针。因此,tankDataType类的构造函数代码应当编写为:(向特定节点分配用户数据的步骤将随后给出)
tankDataType::tankDataType(osg::Node* n)
{
rotation = 0;
elevation = 0;
findNodeVisitor findTurret("turret");
n->accept(findTurret);
tankTurretNode =
dynamic_cast
findNodeVisitor findGun("gun");
n->accept(findGun);
tankGunNode =
dynamic_cast< osgSim::DOFTransform*> (findGun.getFirst());
}
我们也可以在tankDataType类中定义更新炮塔旋转和机枪俯仰的方法。现在我们只需要简单地让炮塔和机枪角度每帧改变一个固定值即可。对于机枪的俯仰角,我们需要判断它是否超过了实际情况的限制值。如果达到限制值,则重置仰角为0。炮塔的旋转可以在一个圆周内自由进行。
void tankDataType::updateTurretRotation()
{
rotation += 0.01;
tankTurretNode->setCurrentHPR( osg::Vec3(rotation,0,0) );
}
void tankDataType::updateGunElevation()
{
elevation += 0.01;
tankGunNode->setCurrentHPR( osg::Vec3(0,elevation,0) );
if (elevation > .5)
elevation = 0.0;
}
将上述代码添加到类的内容后,我们新定义的类如下所示:
class tankDataType : public osg::Referenced
{
public:
tankDataType(osg::Node*n);
void updateTurretRotation();
void updateGunElevation();
protected:
osgSim::DOFTransform* tankTurretNode;
osgSim::DOFTransform* tankGunNode;
double rotation; //(弧度值)
double elevation; //(弧度值)
};
下一个步骤是创建回调,并将其关联到坦克节点上。为了创建这个回调,我们需要重载“()”操作符,它包括两个参数:节点的指针和节点访问器的指针。在这个函数中我们将执行DOF节点的更新。因此,我们需要执行tankData实例的更新方法,其中tankData实例使用坦克节点的userData成员与坦克节点相关联。坦克节点的指针可以通过使用getUserData方法来获取。由于这个方法的返回值是一个osg::Referenced基类的指针,因此需要将其安全地转换为tankDataType类的指针。为了保证用户数据的引用计数值是正确的,我们使用模板类型osg::ref_ptr
class tankNodeCallback : public osg::NodeCallback
{
public:
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::ref_ptr
dynamic_cast
if(tankData)
{
tankData->updateTurretRotation();
tankData->updateGunElevation();
}
traverse(node, nv);
}
};
下一步的工作是“安装”回调:将其关联给我们要修改的坦克节点,以实现每帧的更新函数执行。因此,我们首先要保证坦克节点的用户数据(tankDataType类的实例)是正确的。然后,我们使用osg::Node类的setUpdateCallback方法将回调与正确的节点相关联。代码如下所示:
// 初始化变量和模型,建立场景……
tankDataType* tankData = new tankDataType(tankNode);
tankNode->setUserData( tankData );
tankNode->setUpdateCallback(new tankNodeCallback);
创建了回调之后,我们进入仿真循环。仿真循环的代码不用加以改变。当我们调用视口类实例的frame()方法时,我们即进入一个更新遍历。当更新遍历及至坦克节点时,将触发tankNodeCallback类的操作符“()”函数。
// 视口的初始化,等等……
while( !viewer.done() )
{
viewer.frame();
}
return 0;
}
9.处理键盘输入
9.1本章目标
使程序具备将键盘事件与特定函数相关联的能力。在前面的教程中我们已经可以使用更新回调来控制炮塔的旋转。本章中我们将添加一个键盘接口类,实现通过用户的键盘输入来更新炮塔的转角。
9.2 GUI(图形用户接口)事件处理器:
GUI事件适配器GUIEventAdapter和GUI动作适配器GUIActionAdapter。
GUIEventHandler类向开发者提供了窗体系统的GUI事件接口。这一事件处理器使用GUIEventAdapter实例来接收更新。事件处理器还可以使用GUIActionAdapter实例向GUI系统发送请求,以实现一些特定的操作。
GUIEventAdapter实例包括了各种事件类型(PUSH,RELEASE,DOUBLECLICK,DRAG,MOVE,KEYDOWN,KEYUP,FRAME,RESIZE,SCROLLUP,SCROLLDOWN,SCROLLLEFT)。依据GUIEventAdapter事件类型的不同,其实例可能还有更多的相关属性。例如X,Y坐标与鼠标事件相关。KEYUP和KEYDOWN事件则与一个按键值(例如“a”,“F1”)相关联。
GUIEventHandler使用GUIActionAdapter来请求GUI系统执行动作。这些动作包括重绘请求requestRedraw(),多次更新请求requestContinuousUpdate(),光标位置重置请求requestWarpPointer(x,y)。
GUIEventHandler类主要通过handle方法来实现与GUI的交互。handle方法有两个参数:一个GUIEventAdapter实例用于接收GUI的更新,以及一个GUIActionAdapter用于向GUI发送请求。handle方法用于检查GUIEventAdapter的动作类型和值,执行指定的操作,并使用GUIActionAdapter向GUI系统发送请求。如果事件已经被正确处理,则handle方法返回的布尔值为true,否则为false。
一个GUI系统可能与多个GUIEventAdapter相关联(GUIEventAdapter的顺序保存在视口类的eventHandlerList中),因此这个方法的返回值可以用于控制单个键盘事件的多次执行。如果一个GUIEventHandler返回false,下一个GUIEventHandler将继续响应同一个键盘事件。
后面的例子将演示GUIEventHandler与GUI系统交互的方法:TrackballManipulator类(继承自GUIEventHandler)以GUIEventAdapter实例的形式接收鼠标事件的更新。鼠标事件的解析由TrackballManipulator类完成,并可以实现“抛出”的操作(所谓抛出,指的是用户按下键拖动模型并突然松开,以实现模型的持续旋转或移动)。解析事件时,TrackBallManipulator将发送请求到GUI系统(使用GUIActionAdapter),启动定时器并使自己被重复调用,以计算新的模型方向或者位置数据。
9.3简单的键盘接口类
以下主要介绍如何创建一个用于将键盘输入关联到特定函数的键盘接口类。当用户将按键注册到接口类并设定相应的C++响应函数之后,即可建立相应的表格条目。该表格用于保存键值(“a”,“F1”等等),按键状态(按下,松开)以及C++响应函数。本质上讲,用户可以由此实现形同“按下f键,即执行functionOne”的交互操作。由于新的类将继承自GUIEventHandler类,因此每当GUI系统捕获到一个GUI事件时,这些类的handle方法都会被触发。而handle方法触发后,GUI事件的键值和按键状态(例如,松开a键)将与表格中的条目作比较,如果发现相符的条目,则执行与此键值和状态相关联的函数
用户通过addFunction方法可以注册按键条目。这个函数有两种形式。第一种把键值和响应函数作为输入值。这个函数主要用于用户仅处理KEY_DOWN事件的情形。例如,用户可以将“a”键的按下事件与一个反锯齿效果的操作函数相关联。但是用户不能用这个函数来处理按键松开的动作。
另一个情形下,用户可能需要区分由单个按键的“按下”和“松开”事件产生的不同动作。例如控制第一人称视角的射击者动作。按下w键使模型加速向前。松开w键之后,运动模型逐渐停止。一种可行的设计方法是,为按下按键和松开按键分别设计不同的响应函数。两者中的一个用来实现按下按键的动作。
#ifndef KEYBOARD_HANDLER_H
#define KEYBOARD_HANDLER_H
#include
class keyboardEventHandler : public osgGA::GUIEventHandler
{
public:
typedef void (*functionType) ();
enum keyStatusType
{
KEY_UP, KEY_DOWN
};
// 用于保存当前按键状态和执行函数的结构体。
// 记下当前按键状态的信息以避免重复的调用。
// (如果已经按下按键,则不必重复调用相应的方法)
struct functionStatusType
{
functionStatusType() {keyState = KEY_UP; keyFunction = NULL;}
functionType keyFunction;
keyStatusType keyState;
};
// 这个函数用于关联键值和响应函数。如果键值在之前没有注册过,它和
// 它的响应函数都会被添加到“按下按键”事件的映射中,并返回true。
// 否则,不进行操作并返回false。
bool addFunction(int whatKey, functionType newFunction);
// 重载函数,允许用户指定函数是否与KEY_UP或者KEY_DOWN事件关联。
bool addFunction(int whatKey, keyStatusType keyPressStatus,
functionType newFunction);
// 此方法将比较当前按下按键的状态以及注册键/状态的列表。
// 如果条目吻合且事件较新(即,按键还未按下),则执行响应函数。
virtual bool handle(const osgGA::GUIEventAdapter& ea,
osgGA::GUIActionAdapter&);
// 重载函数,用于实现GUI事件处理访问器的功能。
virtual void accept(osgGA::GUIEventHandlerVisitor& v)
{ v.visit(*this); };
protected:
// 定义用于保存已注册键值,响应函数和按键状态的数据类型。
typedef std::map
// 保存已注册的“按下按键”方法及其键值。
keyFunctionMap keyFuncMap;
// 保存已注册的“松开按键”方法及其键值。
keyFunctionMap keyUPFuncMap;
};
#endif
9.4使用键盘接口类
下面的代码用于演示如何使用上面定义的类:
// 建立场景和视口。
// ……
// 声明响应函数:
// startAction(),stopAction(),toggleSomething()
// ……
// 声明并初始化键盘事件处理器的实例。
keyboardEventHandler* keh = new keyboardEventHandler();
// 将事件处理器添加到视口的事件处理器列表。
// 如果使用push_front且列表第一项的handle方法返回true,则其它处理器
// 将不会再响应GUI同一个GUI事件。我们也可以使用push_back,将事件的
// 第一处理权交给其它的事件处理器;或者也可以设置handle方法的返回值
// 为false。OSG 2.x版还允许使用addEventHandler方法来加以替代。
//viewer.getEventHandlers().push_front(keh);
viewer.addEventHandler(keh);
// 注册键值,响应函数。
// 按下a键时,触发toggelSomething函数。
// (松开a键则没有效果)
keh->addFunction('a',toggleSomething);
// 按下j键时,触发startAction函数。(例如,加快模型运动速度)
// 注意,也可以不添加第二个参数。
keh->addFunction('j',keyboardEventHandler::KEY_DOWN, startAction);
// 松开j键时,触发stopAction函数。
keh->addFunction('j',keyboardEventHandler::KEY_UP, stopAction);
// 进入仿真循环
// ……
9.5处理键盘输入实现更新回调
9.5.1本节目标
上一个教程我们讲解了键盘事件处理器类,它用于注册响应函数。本章提供了用于键盘输入的更方便的方案。我们将重载一个GUIEventHandler类,而不必再创建和注册函数。在这个类中我们将添加新的代码,以便执行特定的键盘和鼠标事件响应动作。我们还将提出一种键盘事件处理器与更新回调通讯的方法。
9.5.2问题的提出
教程第8部分演示了如何将回调与DOF节点相关联,以实现场景中DOF节点位置的持续更新。那么,如果我们希望使用键盘输入来控制场景图形中的节点,应该如何处理呢?例如,如果我们有一个基于位置变换节点的坦克模型,并希望在按下w键的时候控制坦克向前运动,我们需要进行如下一些操作:
1. 读取键盘事件;
2. 保存键盘事件的结果;
3. 在更新回调中响应键盘事件。
9.5.3解决方案
第一步:基类osgGA::GUIEventHandler用于定义用户自己的GUI键盘和鼠标事件动作。我们可以从基类派生自己的类并重载其handle方法,以创建自定义的动作。同时还编写accept方法来实现GUIEventHandlerVisitor(OSG 2.0版本中此类已经废弃)的功能。其基本的框架结构如下所示:
class myKeyboardEventHandler : public osgGA::GUIEventHandler
{
public:
virtual bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter&);
virtual void accept(osgGA::GUIEventHandlerVisitor& v) { v.visit(*this); };
};
bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter&ea,osgGA::GUIActionAdapter& aa)
{
switch(ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
switch(ea.getKey())
{
case 'w':
std::cout << " w key pressed" << std::endl;
return false;
break;
default:
return false;
}
}
default:
return false;
}
}
上述类的核心部分就是我们从基类中重载的handle方法。这个方法有两个参数:一个GUIEventAdapter类的实例,用于接收GUI事件;另一个是GUIActionAdapter类的实例,用于生成并向GUI系统发送请求,例如重绘请求和持续更新请求。
我们需要根据第一个参数编写代码以包含更多的事件,例如KEYUP,DOUBLECLICK,DRAG等。如果要处理按下按键的事件,则应针对KEYDOWN这个分支条件来扩展相应的代码。
事件处理函数的返回值与事件处理器列表中当前处理器触发的键盘和鼠标事件相关。如果返回值为true,则系统认为事件已经处理,不再传递给下一个事件处理器。如果返回值为false,则传递给下一个事件处理器,继续执行对事件的响应。
为了“安装”我们的事件处理器,我们需要创建它的实例并添加到osgViewer::Viewer的事件处理器列表。代码如下:
myKeyboardEventHandler* myFirstEventHandler = new myKeyboardEventHandler();
viewer.getEventHandlerList().push_front(myFirstEventHandler);
第二步:到目前为止,我们的键盘处理器还并不完善。它的功能仅仅是在每次按下w键时向控制窗口输出。如果我们希望按下键时可以控制场景图形中的元素,则需要在键盘处理器和更新回调之间建立一个通讯结构。为此,我们将创建一个用于保存键盘状态的类。这个事件处理器类用于记录最近的键盘和鼠标事件状态。而更新回调类也需要建立与键盘处理器类的接口,以实现场景图形的正确更新。现在我们开始创建基本的框架结构。用户可以在此基础上进行自由的扩展。下面的代码是一个类的定义,用于允许键盘事件处理器和更新回调之间通讯。
class tankInputDeviceStateType
{
public:
tankInputDeviceStateType::tankInputDeviceStateType() :
moveFwdRequest(false) {}
bool moveFwdRequest;
};
下一步的工作是确认键盘事件处理器和更新回调都有正确的数据接口。这些数据将封装到tankInputdeviceStateType的实例中。因为我们仅使用一个事件处理器来控制坦克,因此可以在事件处理器中提供指向tankInputDeviceStateType实例的指针。我们将向事件处理器添加一个数据成员(指向tankInputDeviceStateType的实例)。同时我们还会将指针设置为构造函数的输入参量。以上所述的改动,即指向tankInputDeviceStateType实例的指针,以及新的构造函数如下所示:
class myKeyboardEventHandler : public osgGA::GUIEventHandler {
public:
myKeyboardEventHandler(tankInputDeviceStateType* tids)
{
tankInputDeviceState = tids;
}
// ……
protected:
tankInputDeviceStateType* tankInputDeviceState;
};
我们还需要修改handle方法,以实现除了输出到控制台之外更多的功能。我们通过修改标志参量的值,来发送坦克向前运动的请求。
bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& aa)
{
switch(ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
switch(ea.getKey())
{
case 'w':
tankInputDeviceState->moveFwdRequest = true;
return false;
break;
default:
return false;
}
}
default:
return false;
}
}
第三步:用于更新位置的回调类也需要编写键盘状态数据的接口。我们为更新回调添加与上述相同的参数。这其中包括一个指向同一tankInputDeviceStateType实例的指针。类的构造函数则负责将这个指针传递给成员变量。获得指针之后,我们就可以在回调内部使用其数值了。目前的回调只具备使坦克向前运动的代码,前提是用户执行了相应的键盘事件。回调类的内容如下所示:
class updateTankPosCallback : public osg::NodeCallback {
public:
updateTankPosCallback::updateTankPosCallback(tankInputDeviceStateType* tankIDevState)
: rotation(0.0) , tankPos(-15.,0.,0.)
{
tankInputDeviceState = tankIDevState;
}
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::PositionAttitudeTransform* pat =
dynamic_cast
if(pat)
{
if (tankInputDeviceState->moveFwdRequest)
{
tankPos.set(tankPos.x()+.01,0,0);
pat->setPosition(tankPos);
}
}
traverse(node, nv);
}
protected:
osg::Vec3d tankPos;
tankInputDeviceStateType* tankInputDeviceState;
};
现在,键盘和更新回调之间的通讯框架已经基本完成。下一步是创建一个tankInputDeviceStateType的实例。这个实例将作为事件处理器构造函数的参数传入。同时它也是模型位置更新回调类的构造函数参数。当事件处理器添加到视口的事件处理器列表中之后,我们就可以进入仿真循环并执行相应的功能了。
// 定义用于记录键盘事件的类的实例。
tankInputDeviceStateType* tIDevState = new tankInputDeviceStateType;
// 设置坦克的更新回调。
// 其构造函数将传递上面的实例指针作为实参。
tankPAT->setUpdateCallback(new updateTankPosCallback(tIDevState));
// 键盘处理器类的构造函数同样传递上面的实例指针作为实参。
myKeyboardEventHandler* tankEventHandler = new myKeyboardEventHandler(tIDevState);
// 将事件处理器压入处理器列表。
viewer.getEventHandlerList().push_front(tankEventHandler);
// 设置视口并进入仿真循环。
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.frame();
}
不过用户还有更多可以扩展的地方:例如使坦克在按键松开的时候停止运动,转向,加速等等。
10.使用自定义矩阵来放置相机(Positioning a Camera with a User-Defined Matrix)
10.1本章目标
手动放置相机,以实现场景的观览。
10.2设置矩阵的方向和位置
我们可以使用osg::Matrix类来设置矩阵的数据。本章中我们将使用双精度类型的矩阵类osg::Matrixd。要设置矩阵的位置和方向,我们可以使用矩阵类的makeTranslate()和makeRotate()方法。为了方便起见,这两个方法均提供了多种可重载的类型。本例中我们使用的makeRotate()方法要求三对角度/向量值作为输入参数。旋转量由围绕指定向量轴所旋转的角度(表示为弧度值)决定。这里我们简单地选用X,Y,Z直角坐标系作为旋转参照的向量轴。将平移矩阵右乘旋转矩阵后,即可创建一个单一的表示旋转和平移的矩阵。代码如下:
如下是设置场景的代码。此场景包括一个小型的地形和坦克模型。坦克位于(10,10,8)的位置。
int main()
{
osg::Node* groundNode = NULL;
osg::Node* tankNode = NULL;
osg::Group* root = new osg::Group();
osgProducer::Viewer viewer;
osg::PositionAttitudeTransform* tankXform;
groundNode = osgDB::readNodeFile("\\Models\\JoeDirt\\JoeDirt.flt");
tankNode = osgDB::readNodeFile("\\Models\\T72-Tank\\T72-tank_des.flt");
// 创建绿色的天空布景。
osg::ClearNode* backdrop = new osg::ClearNode;
backdrop->setClearColor(osg::Vec4(0.0f,0.8f,0.0f,1.0f));
root->addChild(backdrop);
root->addChild(groundNode);
tankXform = new osg::PositionAttitudeTransform();
root->addChild(tankXform);
tankXform->addChild(tankNode);
tankXform->setPosition( osg::Vec3(10,10, );
tankXform->setAttitude(
osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) );
osgGA::TrackballManipulator *Tman = new osgGA::TrackballManipulator();
viewer.setCameraManipulator(Tman);
viewer.setSceneData( root );
viewer.realize();
10.3声明一个用于设置相机的矩阵
矩阵的位置设置为坦克模型后方60个单元,上方7个单元。同时设置矩阵的方向。
osg::Matrixd myCameraMatrix;
osg::Matrixd cameraRotation;
osg::Matrixd cameraTrans;
cameraRotation.makeRotate(
osg::DegreesToRadians(-20.0), osg::Vec3(0,1,0), // 滚转角(Y轴)
osg::DegreesToRadians(-15.0), osg::Vec3(1,0,0) , // 俯仰角(X轴)
osg::DegreesToRadians( 10.0), osg::Vec3(0,0,1) ); // 航向角(Z轴)
// 相机位于坦克之后60个单元,之上7个单元。
cameraTrans.makeTranslate( 10,-50,15 );
myCameraMatrix = cameraRotation * cameraTrans;
10.4使用矩阵设置视口摄相机
场景的视口类实例使用当前MatrixManipulator控制器类(TrackballManipulator,DriveManipulator等)矩阵的逆矩阵来设置主摄像机的位置。为了在视口中使用我们自定义的摄像机位置和方向矩阵,我们需要首先计算自定义矩阵的逆矩阵。
除了求取逆矩阵之外,我们还需要提供世界坐标系的方向。通常osgGA::MatrixManipulator矩阵(osgProducer::Viewer中使用)使用的坐标系为Z轴向上。但是Producer和osg::Matrix(也就是上文所创建的)使用Y轴向上的坐标系系统。因此,在获得逆矩阵之后,我们需要将其从Y轴向上旋转到Z轴向上的形式。这一要求可以通过沿X轴旋转-90度来实现。其实现代码如下所示:
while( !viewer.done() )
{
if (manuallyPlaceCamera)
{
osg::Matrixd i = myCameraMatrix.inverse(myCameraMatrix);
Tman->setByInverseMatrix(
osg::Matrix(i.ptr() )
* osg::Matrix::rotate( -3.1415926/2.0, 1, 0, 0 ) );
}
viewer.frame();
}
注意:按下V键可以手动切换摄像机。
11. 实现跟随节点的相机
提示:自OSG 0.9.7发布之后,新的osgGA::MatrixManipulator类(TrackerManipulator)允许用户将摄相机“依附”到场景图形中的节点。这一新增的操纵器类可以高效地替代下面所述的方法。
本章教程将继续使用回调和节点路径(NodePath)来检索节点的世界坐标。
11.1本章目标
在一个典型的仿真过程中,用户可能需要从场景中的各种车辆和人物里选择一个进行跟随。本章将介绍一种将摄像机“依附”到场景图形节点的方法。此时视口的摄像机将跟随节点的世界坐标进行放置。
11.2概述
视口类包括了一系列的矩阵控制器(osgGA::MatrixManipulator)。因而提供了“驱动控制(Drive)”,“轨迹球(Trackball)”,“飞行(Fly)”等交互方法。矩阵控制器类用于更新摄像机位置矩阵。它通常用于回应GUI事件(鼠标点击,拖动,按键,等等)。本文所述的功能需要依赖于相机位置矩阵,并参照场景图形节点的世界坐标。这样的话,相机就可以跟随场景图形中的节点进行运动了。
为了获得场景图形中节点的世界坐标,我们需要使用节点访问器的节点路径功能来具现一个新的类。这个类将提供一种方法将自己的实例关联到场景图形,并因此提供访问任意节点世界坐标的方法。此坐标矩阵(场景中任意节点的世界坐标)将作为相机位置的矩阵,由osgGA::MatrixManipulator实例使用。
11.3实现
首先我们创建一个类,计算场景图形中的多个变换矩阵的累加结果。很显然,所有的节点访问器都会访问当前的节点路径。节点路径本质上是根节点到当前节点的所有节点列表。有了节点路径的实例之后,我们就可以使用场景图形的方法computeWorldToLocal( osg::NodePath)来获取表达节点世界坐标的矩阵了。
这个类的核心是使用更新回调来获取某个给定节点之前所有节点的矩阵和。整个类的定义如下:
struct updateAccumulatedMatrix : public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
matrix = osg::computeWorldToLocal(nv->getNodePath() );
traverse(node,nv);
}
osg::Matrix matrix;
};
下一步,我们需要在场景图形的更新遍历中启动回调类。因此,我们将创建一个类,其中包括一个osg::Node实例作为数据成员。此节点数据成员的更新回调是上述updateAccumulatedMatrix类的实例,同时此节点也将设置为场景的一部分。为了读取用于描绘节点世界坐标的矩阵(该矩阵与节点实例相关联),我们需要为矩阵提供一个“get”方法。我们还需要提供添加节点到场景图形的方法。我们需要注意的是,用户应如何将节点关联到场景中。此节点应当有且只有一个父节点。因此,为了保证这个类的实例只有一个相关联的节点,我们还需要记录这个类的父节点。类的定义如下面的代码所示:
struct transformAccumulator
{
public:
transformAccumulator();
bool attachToGroup(osg::Group* g);
osg::Matrix getMatrix();
protected:
osg::ref_ptr<osg::Group> parent;
osg::Node* node;
updateAccumulatedMatrix* mpcb;
};
类的实现代码如下所示:
transformAccumulator::transformAccumulator()
{
parent = NULL;
node = new osg::Node;
mpcb = new updateAccumulatedMatrix();
node->setUpdateCallback(mpcb);
}
osg::Matrix transformAccumulator::getMatrix()
{
return mpcb->matrix;
}
bool transformAccumulator::attachToGroup(osg::Group* g)
// 注意不要在回调中调用这个函数。
{
bool success = false;
if (parent != NULL)
{
int n = parent->getNumChildren();
for (int i = 0; i < n; i++)
{
if (node == parent->getChild(i) )
{
parent->removeChild(i,1);
success = true;
}
}
if (! success)
{
return success;
}
}
g->addChild(node);
return true;
}
现在,我们已经提供了类和方法来获取场景中节点的世界坐标矩阵,我们所需的只是学习如何使用这个矩阵来变换相机的位置。osgGA::MatrixManipulator类即可提供一种更新相机位置矩阵的方法。我们可以从MatrixManipulator继承一个新的类,以实现利用场景中某个节点的世界坐标矩阵来改变相机的位置。为了实现这一目的,这个类需要提供一个数据成员,作为上述的accumulateTransform实例的句柄。新建类同时还需要保存相机位置矩阵的相应数据。
MatrixManipulator类的核心是“handle”方法。这个方法用于检查选中的GUI事件并作出响应。对我们的类而言,唯一需要响应的GUI事件就是“FRAME”事件。在每一个“帧事件”中,我们都需要设置相机位置矩阵与transformAccumulator矩阵的数值相等。我们可以在类的成员中创建一个简单的updateMatrix方法来实现这一操作。由于我们使用了虚基类,因此某些方法必须在这里进行定义(矩阵的设置及读取,以及反转)。综上所述,类的实现代码如下所示:
class followNodeMatrixManipulator : public osgGA::MatrixManipulator
{
public:
followNodeMatrixManipulator( transformAccumulator* ta);
bool handle (const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa);
void updateTheMatrix();
virtual void setByMatrix(const osg::Matrixd& mat) {theMatrix = mat;}
virtual void setByInverseMatrix(const osg::Matrixd&mat) {}
virtual osg::Matrixd getInverseMatrix() const;
virtual osg::Matrixd getMatrix() const;
protected:
~followNodeMatrixManipulator() {}
transformAccumulator* worldCoordinatesOfNode;
osg::Matrixd theMatrix;
};
The class implementation is as follows:
followNodeMatrixManipulator::followNodeMatrixManipulator( transformAccumulator* ta)
{
worldCoordinatesOfNode = ta; theMatrix = osg::Matrixd::identity();
}
void followNodeMatrixManipulator::updateTheMatrix()
{
theMatrix = worldCoordinatesOfNode->getMatrix();
}
osg::Matrixd followNodeMatrixManipulator::getMatrix() const
{
return theMatrix;
}
osg::Matrixd followNodeMatrixManipulator::getInverseMatrix() const
{
// 将矩阵从Y轴向上旋转到Z轴向上
osg::Matrixd m;
m = theMatrix * osg::Matrixd::rotate(-M_PI/2.0, osg::Vec3(1,0,0) );
return m;
}
void followNodeMatrixManipulator::setByMatrix(const osg::Matrixd& mat)
{
theMatrix = mat;
}
void followNodeMatrixManipulator::setByInverseMatrix(const osg::Matrixd& mat)
{
theMatrix = mat.inverse();
}
bool followNodeMatrixManipulator::handle
(const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa)
{
switch(ea.getEventType())
{
case (osgGA::GUIEventAdapter::FRAME):
{
updateTheMatrix();
return false;
}
}
return false;
}
上述的所有类都定义完毕之后,我们即可直接对其进行使用。我们需要声明一个transformAccumulator类的实例。该实例应当与场景图形中的某个节点相关联。然后,我们需要声明nodeFollowerMatrixManipulator类的实例。此操纵器类的构造函数将获取transformAccumulator实例的指针。最后,将新的矩阵操纵器添加到视口操控器列表中。上述步骤的实现如下:
// 设置场景和视口(包括tankTransform节点的添加)……
transformAccumulator* tankWorldCoords = new transformAccumulator();
tankWorldCoords->attachToGroup(tankTransform);
followNodeMatrixManipulator* followTank =
new followNodeMatrixManipulator(tankWorldCoords);
osgGA::KeySwitchMatrixManipulator *ksmm = new osgGA::KeySwitchMatrixManipulator();
if (!ksmm)
return -1;
// 添加跟随坦克的矩阵控制器的。按下“m”键即可实现视口切换到该控制器。
ksmm->addMatrixManipulator('m',"tankFollower",followTank);
viewer.setCameraManipulator(ksmm);
// 进入仿真循环……
11.4环绕(始终指向)场景中节点的相机
11.4.1本节目标
创建回调,以实现用于沿轨道环绕,同时指向场景中某个节点的世界坐标矩阵的更新。使用此矩阵的逆矩阵来放置相机。
11.4.2实现
本章的回调类基于上一篇的osgFollowMe教程。本章中,我们将添加一个新的矩阵数据成员,以保存视口相机所需的世界坐标。每次更新遍历启动时,我们将调用环绕节点的当前轨道世界坐标矩阵。为了实现环绕节点的效果,我们将添加一个“angle”数据成员,其值每帧都会增加。矩阵的相对坐标基于一个固定数值的位置变换,而旋转量基于每帧更新的角度数据成员。为了实现相机的放置,我们还将添加一个方法,它将返回当前的轨道位置世界坐标。类的声明如下所示:
class orbit : public osg::NodeCallback
{
public:
orbit(): heading(M_PI/2.0) {}
osg::Matrix getWCMatrix(){return worldCoordMatrix;}
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::MatrixTransform *tx = dynamic_cast
if( tx != NULL )
{
heading += M_PI/180.0;
osg::Matrixd orbitRotation;
orbitRotation.makeRotate(
osg::DegreesToRadians(-10.0), osg::Vec3(0,1,0), // 滚转角(Y轴)
osg::DegreesToRadians(-20.0), osg::Vec3(1,0,0) , // 俯仰角(X轴)
heading, osg::Vec3(0, 0, 1) ); // 航向角(Z轴)
osg::Matrixd orbitTranslation;
orbitTranslation.makeTranslate( 0,-40, 4 );
tx->setMatrix ( orbitTranslation * orbitRotation);
worldCoordMatrix = osg::computeLocalToWorld( nv->getNodePath() );
}
traverse(node, nv);
}
private:
osg::Matrix worldCoordMatrix;
float heading;
};
使用回调时,我们需要向场景添加一个矩阵变换,并将更新回调设置为“orbit”类的实例。我们使用前述osgManualCamera教程中的代码来实现用矩阵世界坐标来放置相机。我们还将使用前述键盘接口类的代码来添加一个函数来更新全局量,该全局量用于允许用户自行选择缺省和“环绕”的视口。
int main()
{
osg::Node* groundNode = NULL;
osg::Node* tankNode = NULL;
osg::Group* root = NULL;
osgViewer::Viewer viewer;
osg::PositionAttitudeTransform* tankXform = NULL;
groundNode = osgDB::readNodeFile("\\Models\\JoeDirt\\JoeDirt.flt");
tankNode = osgDB::readNodeFile("\\Models\\T72-Tank\\T72-tank_des.flt");
root = new osg::Group();
// 创建天空。
osg::ClearNode* backdrop = new osg::ClearNode;
backdrop->setClearColor(osg::Vec4(0.0f,0.8f,0.0f,1.0f));
root->addChild(backdrop);
tankXform = new osg::PositionAttitudeTransform();
root->addChild(groundNode);
root->addChild(tankXform);
tankXform->addChild(tankNode);
tankXform->setPosition( osg::Vec3(10,10,8) );
tankXform->setAttitude(
osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) );
osgGA::TrackballManipulator *Tman = new osgGA::TrackballManipulator();
viewer.setCameraManipulator(Tman);
viewer.setSceneData( root );
viewer.realize();
// 创建矩阵变换节点,以实现环绕坦克节点。
osg::MatrixTransform* orbitTankXForm = new osg::MatrixTransform();
// 创建环绕轨道回调的实例。
orbit* tankOrbitCallback = new orbit();
// 为矩阵变换节点添加更新回调的实例。
orbitTankXForm->setUpdateCallback( tankOrbitCallback );
// 将位置轨道关联给坦克的位置,即,将其设置为坦克变换节点的子节点。
tankXform->addChild(orbitTankXForm);
keyboardEventHandler* keh = new keyboardEventHandler();
keh->addFunction('v',toggleTankOrbiterView);
viewer.addEventHandler(keh);
while( !viewer.done() )
{
if (useTankOrbiterView)
{
Tman->setByInverseMatrix(tankOrbitCallback->getWCMatrix()
*osg::Matrix::rotate( -3.1415926/2.0, 1, 0, 0 ));
}
viewer.frame();
}
return 0;
}
提示:按下V键来切换不同的视口。