例:geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));php
来指定要利用这些数据生成一个怎么样的形状。html
该行代码中,使用DrawArrays类向Geometry类送入了新几何体的信息,即,该几何体是一个QUADS,它的顶点坐标从索引数组中读入,从第1个索引值开始,共读入4个索引值,组成一个四边形图形。node
几何体的形状参数除了QUADS以外,还有数种方式,以用于不一样的用户需求,列表以下:ios
POINTSc++ |
绘制点算法 |
绘制用户指定的全部顶点。数据库 |
LINESwindows |
绘制直线api |
直线的起点、终点由数组中前后相邻的两个点决定;用户提供的点不止两个时,将尝试继续绘制新的直线。数组 |
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绘制依次链接每四点的四边形
如:一、二、三、四、五、六、七、8点 绘制结果一、二、三、4组成四边形,五、六、七、8组成四边形
osg::PrimitiveSet::QUAD_STRIP对应OpenGL中的GL_QUAD_STRIP绘制四边形
如:一、二、三、四、五、六、七、8点 绘制结果一、二、三、4组成四边形,三、四、五、6组成四边形、五、
六、七、8组成四边形
osg::PrimitiveSet::TRIANGLES对应OpenGL中的GL_TRIANGLES绘制每三点链接的三角形
如:一、二、三、四、五、6点 绘制结果一、二、3组成三角形,四、五、6组成三角形
osg::PrimitiveSet::TRIANGLE_STRIP对应OpenGL中的GL_TRIANGLE_STRIP
如:一、二、三、四、五、6点 绘制结果一、二、3组成三角形,二、三、4组成三角形,三、四、5组成三角
形四、五、6组成三角形
osg::PrimitiveSet::TRIANGLE_FAN对应OpenGL中的GL_TRIANGLE_FAN
如:一、二、三、四、五、6点 绘制结果一、二、3组成三角形,一、三、4组成三角形,一、四、5组成三角
形,一、五、6组成三角形
目录
7.搜索并控制开关和DOF(自由度)节点(Finding and Manipulating a Switch and DOF Node)... 20
10.使用自定义矩阵来放置相机(Positioning a Camera with a User-Defined Matrix)... 36
本节涵盖了生成基本几何形状的一些方法。生成几何物体的方法有这么几种:在最底层对OpenGL基本几何进行松散的包装,中级是使用Open Scene Graph的基本形状,以及更高级一些的从文件读取。这篇教程涵盖的是最低层的。这种方法弹性最大但最费力。一般在Scene Graph级别,几何形状是从文件加载的。文件加载器完成了跟踪顶点的大部分工做。
对一下几个类的简单解释:
Geode类:
geode类继承自node类。在一个Scene Graph中,node(固然包含geode)能够做为叶子节点。Geode实例能够有多个相关的drawable。
Drawable类层次:
基类drawable是一个有六个具体子类的抽象类。
geometry类能够直接有vertex和vertex数据,或者任意个primitiveSet实例。
vertex和vertex属性数据(颜色、法线、纹理坐标)存放在数组中。既然多个顶点能够共享相同的颜色、法线或纹理坐标,那么数组索引就能够用来将顶点数组映射到颜色、法线、或纹理坐标数组。
PrimitiveSet类:
这个类松散的包装了OpenGL的基本图形-POINTS,LINES,LINE_STRIP,LINE_LOOP,...,POLYGON.
如下这节代码安装了一个viewer来观察咱们建立的场景,一个‘group’实例做为scene graph的根节点,一个几何节点(geode)来收集drawable,和一个geometry实例来关联顶点和顶点数据。(这个例子中渲染的形状是一个四面体)
...
int main()
{
...
osgProducer::Viewer viewer;
osg::Group* root = new osg::Group();
osg::Geode* pyramidGeode = new osg::Geode();
osg::Geometry* pyramidGeometry = new osg::Geometry();
下一步,须要将锥体geometry和锥体geode关联起来,并将pyramid geode加到scene graph的根节点上。
pyramidGeode->addDrawable(pyramidGeometry);
root->addChild(pyramidGeode);
声明一个顶点数组。每一个顶点由一个三元组表示——vec3类的实例。这些三元组用osg::Vec3Array类的实例存贮。既然osg::Vec3Array继承自STL的vector类,那么咱们就可使用push_back方法来添加数组成员。push_back将元素加到向量的尾端,所以第一个元素的索引是0,第二个是1,依此类推。
使用‘z’轴向上的右手坐标系系统,下面的0...4数组元素表明着产生一个简单锥体所需的5个点。
osg::Vec3Array* pyramidVertices = new osg::Vec3Array;
pyramidVertices->push_back( osg::Vec3( 0, 0, 0) ); // front left
pyramidVertices->push_back( osg::Vec3(10, 0, 0) ); // front right
pyramidVertices->push_back( osg::Vec3(10,10, 0) ); // back right
pyramidVertices->push_back( osg::Vec3( 0,10, 0) ); // back left
pyramidVertices->push_back( osg::Vec3( 5, 5,10) ); // peak
将这个顶点集合和与咱们加到场景中的geode相关的geometry关联起来。
pyramidGeometry->setVertexArray( pyramidVertices );
下一步,产生一个基本集合并将其加入到pyramid geometry中。使用pyramid的前四个点经过DrawElementsUint类的实例来定义基座。这个类也继承自STL的vector,因此push_back方法会顺序添加元素。为了保证合适的背面剔除,顶点的顺序应当是逆时针方向的。构造器的参数是基本的枚举类型(和opengl的基本枚举类型一致),和起始的顶点数组索引。
osg::DrawElementsUInt* pyramidBase =
new osg::DrawElementsUInt(osg::PrimitiveSet::QUADS, 0);
pyramidBase->push_back(3);
pyramidBase->push_back(2);
pyramidBase->push_back(1);
pyramidBase->push_back(0);
pyramidGeometry->addPrimitiveSet(pyramidBase);
对每一个面重复相同的动做。顶点仍要按逆时针方向指定。
osg::DrawElementsUInt* pyramidFaceOne =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceOne->push_back(0);
pyramidFaceOne->push_back(1);
pyramidFaceOne->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceOne);
osg::DrawElementsUInt* pyramidFaceTwo =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceTwo->push_back(1);
pyramidFaceTwo->push_back(2);
pyramidFaceTwo->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceTwo);
osg::DrawElementsUInt* pyramidFaceThree =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceThree->push_back(2);
pyramidFaceThree->push_back(3);
pyramidFaceThree->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceThree);
osg::DrawElementsUInt* pyramidFaceFour =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceFour->push_back(3);
pyramidFaceFour->push_back(0);
pyramidFaceFour->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceFour)
声明并加载一个vec4为元素的数组来存储颜色。
osg::Vec4Array* colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) ); //index 0 red
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f) ); //index 1 green
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f) ); //index 2 blue
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); //index 3 white
声明的这个变量能够将顶点数组元素和颜色数组元素匹配起来。这个容器的元素数应当和顶点数一致。这个容器是顶点数组和颜色数组的链接。这个索引数组中的条目就对应着顶点数组中的元素。他们的值就是颜色数组中的索引。顶点数组元素与normal和纹理坐标数组的匹配也是遵循这种模式。
注意,这种状况下,咱们将5个顶点指定4种颜色。顶点数组的0和4元素都被指定为颜色数组的0元素。
osg::TemplateIndexArray
<unsigned int, osg::Array::UIntArrayType,4,4> *colorIndexArray;
colorIndexArray =
new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,4>;
colorIndexArray->push_back(0); // vertex 0 assigned color array element 0
colorIndexArray->push_back(1); // vertex 1 assigned color array element 1
colorIndexArray->push_back(2); // vertex 2 assigned color array element 2
colorIndexArray->push_back(3); // vertex 3 assigned color array element 3
colorIndexArray->push_back(0); // vertex 4 assigned color array element 0
下一步,将颜色数组和geometry关联起来,将上面产生的颜色索引指定给geometry,设定绑定模式为_PER_VERTEX。
pyramidGeometry->setColorArray(colors);
pyramidGeometry->setColorIndices(colorIndexArray);
pyramidGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
osg::Vec2Array* texcoords = new osg::Vec2Array(5);
(*texcoords)[0].set(0.00f,0.0f);
(*texcoords)[1].set(0.25f,0.0f);
(*texcoords)[2].set(0.50f,0.0f);
(*texcoords)[3].set(0.75f,0.0f);
(*texcoords)[4].set(0.50f,1.0f);
pyramidGeometry->setTexCoordArray(0,texcoords);
注:一下部分可能错误。
// Declare and initialize a transform node.
osg::PositionAttitudeTransform* pyramidTwoXForm =
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
// pyramid node as a child of the transform.
root->addChild(pyramidTwoXForm);
pyramidTwoXForm->addChild(pyramidGeode);
// Declare and initialize a Vec3 instance to change the
// position of the tank model in the scene
osg::Vec3 pyramidTwoPosition(15,0,0);
pyramidTwoXForm->setPosition( pyramidTwoPosition );
既然咱们生成了一个geometry节点并将它加到了场景中,咱们就能够重用这个geometry。例如,若是咱们想让另外一个pyramid在第一个的右侧15个单位处,咱们就能够在咱们的scene graph中将这个geode加到transform节点的子节点上。
最后一步,创建并进入一个仿真循环。
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
为教程1中介绍的由OpenGL基本绘制单位定义的几何体添加纹理。
前一节教程介绍了包含由OpenGL基本单位产生的基本形状的视景。本节讲解如何为这些形状添加纹理。为了使代码更方便使用,咱们将pyramid的代码放到一个函数中,产生geode并返回它的指针。下面的代码来自教程1。
osg::Geode* createPyramid()
{
osg::Geode* pyramidGeode = new osg::Geode();
osg::Geometry* pyramidGeometry = new osg::Geometry();
pyramidGeode->addDrawable(pyramidGeometry);
// Specify the vertices:
osg::Vec3Array* pyramidVertices = new osg::Vec3Array;
pyramidVertices->push_back( osg::Vec3(0, 0, 0) ); // front left
pyramidVertices->push_back( osg::Vec3(2, 0, 0) ); // front right
pyramidVertices->push_back( osg::Vec3(2, 2, 0) ); // back right
pyramidVertices->push_back( osg::Vec3( 0,2, 0) ); // back left
pyramidVertices->push_back( osg::Vec3( 1, 1,2) ); // peak
// Associate this set of vertices with the geometry associated with the
// geode we added to the scene.
pyramidGeometry->setVertexArray( pyramidVertices );
// Create a QUAD primitive for the base by specifying the
// vertices from our vertex list that make up this QUAD:
osg::DrawElementsUInt* pyramidBase =
new osg::DrawElementsUInt(osg::PrimitiveSet::QUADS, 0);
pyramidBase->push_back(3);
pyramidBase->push_back(2);
pyramidBase->push_back(1);
pyramidBase->push_back(0);
//Add this primitive to the geometry:
pyramidGeometry->addPrimitiveSet(pyramidBase);
// code to create other faces goes here!
// (removed to save space, see tutorial two)
osg::Vec4Array* colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) ); //index 0 red
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f) ); //index 1 green
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f) ); //index 2 blue
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); //index 3 white
osg::TemplateIndexArray
<unsigned int, osg::Array::UIntArrayType,4,4> *colorIndexArray;
colorIndexArray =
new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,4>;
colorIndexArray->push_back(0); // vertex 0 assigned color array element 0
colorIndexArray->push_back(1); // vertex 1 assigned color array element 1
colorIndexArray->push_back(2); // vertex 2 assigned color array element 2
colorIndexArray->push_back(3); // vertex 3 assigned color array element 3
colorIndexArray->push_back(0); // vertex 4 assigned color array element 0
pyramidGeometry->setColorArray(colors);
pyramidGeometry->setColorIndices(colorIndexArray);
pyramidGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
// Since the mapping from vertices to texture coordinates is 1:1,
// we don't need to use an index array to map vertices to texture
// coordinates. We can do it directly with the 'setTexCoordArray'
// method of the Geometry class.
// This method takes a variable that is an array of two dimensional
// vectors (osg::Vec2). This variable needs to have the same
// number of elements as our Geometry has vertices. Each array element
// defines the texture coordinate for the cooresponding vertex in the
// vertex array.
osg::Vec2Array* texcoords = new osg::Vec2Array(5);
(*texcoords)[0].set(0.00f,0.0f); // tex coord for vertex 0
(*texcoords)[1].set(0.25f,0.0f); // tex coord for vertex 1
(*texcoords)[2].set(0.50f,0.0f); // ""
(*texcoords)[3].set(0.75f,0.0f); // ""
(*texcoords)[4].set(0.50f,1.0f); // ""
pyramidGeometry->setTexCoordArray(0,texcoords);
return pyramidGeode;
}
渲染基本单位的方法是使用StateSet。这节代码演示了怎样从文件中加载纹理,产生此纹理起做用的一个StateSet,并将这个StateSet附加到场景中的一个节点上。前面开始的代码和上一节教程中的同样,初始化一个viewer并创建有一个pyramid的场景。
int main()
{
osgProducer::Viewer viewer;
// Declare a group to act as root node of a scene:
osg::Group* root = new osg::Group();
osg::Geode* pyramidGeode = createPyramid();
root->addChild(pyramidGeode);
如今,准备加纹理。这里咱们会声明一个纹理实例并将它的数据不一致性设为‘DYNAMIC'。(若是不把纹理声明为dynamic,osg的一些优化程序会删除它。)这个texture类包装了OpenGL纹理模式(wrap,filter,等等)和一个osg::Image。下面的代码说明了如何从文件里读取osg::Image实例并把这个图像和纹理关联起来。
osg::Texture2D* KLN89FaceTexture = new osg::Texture2D;
// protect from being optimized away as static state:
KLN89FaceTexture->setDataVariance(osg::Object::DYNAMIC);
// load an image by reading a file:
osg::Image* klnFace = osgDB::readImageFile("KLN89FaceB.tga");
if (!klnFace)
{
std::cout << " couldn't find texture, quiting." << std::endl;
return -1;
}
// Assign the texture to the image we read from file:
KLN89FaceTexture->setImage(klnFace);
纹理能够和渲染StateSet关联起来。下一步就产生一个StateSet,关联并启动咱们的纹理,并将这个StateSet附加到咱们的geometry上。
// Create a new StateSet with default settings:
osg::StateSet* stateOne = new osg::StateSet();
// Assign texture unit 0 of our new StateSet to the texture
// we just created and enable the texture.
stateOne->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
// Associate this state set with the Geode that contains
// the pyramid:
pyramidGeode->setStateSet(stateOne);
最后一步是仿真循环:
//The final step is to set up and enter a simulation loop.
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
return 0;
}
用osg::Shape实例构建场景。使用osg::StateSet控制shape的渲染。
Shape类是全部形状类别的基类。Shape既可用于剪裁和碰撞检测也可用于定义程序性地产生几何体的那些基本形状。下面的类继承自Shape类:
TriangleMesh
Sphere
InfinitePlane
HeightField
Cylinder
Cone
CompositeShape
Box
为了使这些形状能够被渲染,咱们须要把他们和Drawable类的实例关联起来。ShapeDrawable类提供了这样的功能。这个类继承自Drawable并容许咱们把Shape实例附加到能够被渲染的东西上。既然ShapeDrawable类继承自Drawable,ShapDrawable实例就能够被加到Geode类实例上。下面的步骤演示了将一个单位立方体加到空场景中时是如何作到这些的。
// Declare a group to act as root node of a scene:
osg::Group* root = new osg::Group();
// Declare a box class (derived from shape class) instance
// This constructor takes an osg::Vec3 to define the center
// and a float to define the height, width and depth.
// (an overloaded constructor allows you to specify unique
// height, width and height values.)
osg::Box* unitCube = new osg::Box( osg::Vec3(0,0,0), 1.0f);
// Declare an instance of the shape drawable class and initialize
// it with the unitCube shape we created above.
// This class is derived from 'drawable' so instances of this
// class can be added to Geode instances.
osg::ShapeDrawable* unitCubeDrawable = new osg::ShapeDrawable(unitCube);
// Declare a instance of the geode class:
osg::Geode* basicShapesGeode = new osg::Geode();
// Add the unit cube drawable to the geode:
basicShapesGeode->addDrawable(unitCubeDrawable);
// Add the goede to the scene:
root->addChild(basicShapesGeode);
产生一个球体和上面的代码基本类似。没有太多的注释的代码看起来是这个样子:
// Create a sphere centered at the origin, unit radius:
osg::Sphere* unitSphere = new osg::Sphere( osg::Vec3(0,0,0), 1.0);
osg::ShapeDrawable* unitSphereDrawable=new osg::ShapeDrawable(unitSphere);
如今,咱们可使用transform节点将这个球体加到场景中,以便让它离开原点。unitSphereDrawable不能直接添加到场景中(由于它不是继承自node类),因此咱们须要一个新的geode以便添加它。
osg::PositionAttitudeTransform* sphereXForm =
new osg::PositionAttitudeTransform();
sphereXForm->setPosition(osg::Vec3(2.5,0,0));
osg::Geode* unitSphereGeode = new osg::Geode();
root->addChild(sphereXForm);
sphereXForm->addChild(unitSphereGeode);
unitSphereGeode->addDrawable(unitSphereDrawable);
前面的教程讲解了如何生成纹理,将其指定为从文件加载的图像,生成一个带纹理的StateSet。下面的代码创建了两个状态集合——一个是BLEND纹理模式,另外一个是DECAL纹理模式。BLEND模式:
// Declare a state set for 'BLEND' texture mode
osg::StateSet* blendStateSet = new osg::StateSet();
// Declare a TexEnv instance, set the mode to 'BLEND'
osg::TexEnv* blendTexEnv = new osg::TexEnv;
blendTexEnv->setMode(osg::TexEnv::BLEND);
// Turn the attribute of texture 0 - the texture we loaded above - 'ON'
blendStateSet->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
// Set the texture texture environment for texture 0 to the
// texture envirnoment we declared above:
blendStateSet->setTextureAttribute(0,blendTexEnv);
重复这些步骤,产生DECAL纹理模式的状态集合。
osg::StateSet* decalStateSet = new osg::StateSet();
osg::TexEnv* decalTexEnv = new osg::TexEnv();
decalTexEnv->setMode(osg::TexEnv::DECAL);
decalStateSet->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
decalStateSet->setTextureAttribute(0,decalTexEnv);
产生了状态集合后咱们就能够把它们应用在场景中的节点上。在scene graph的绘制遍历(root->leaf)中状态是积累的。除非这个节点有一个它本身的状态,不然它会继承其父节点的状态。(若是一个节点有一个以上的父节点,它会使用一个以上的状态渲染。)
root->setStateSet(blendStateSet);
unitSphereGeode->setStateSet(decalStateSet);
最后一步是进入仿真循环。
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
return 0;
场景图管理器遍历scene graph,决定哪些几何体须要送到图形管道渲染。在遍历的过程当中,场景图管理器也搜集几何体如何被渲染的信息。这些信息存在osg::StateSet实例中。StateSet包含OpenGL的属性/数值对列表。这些StateSet能够和scenegraph中的节点关联起来。在预渲染的遍历中,StateSet从根节点到叶子节点是积累的。一个节点的没有变化的状态属性也是简单的遗传自父节点。
几个附加的特性容许更多的控制和弹性。一个状态的属性值能够被设为OVERRIDE。这意味着这个节点的全部孩子节点——无论其属性值是什么——会遗传其父节点的属性值。OVERRIDE意味着能够被覆盖。若是一个孩子节点把那个属性设为PROTECTED,那么它就能够设置本身的属性值,而不用理会父节点的设置。
下面的场景示范了state如何影响scene graph。根节点有一个BLEND模式纹理的基本状态。这个基本状态会被全部子节点继承,除非这个状态的参数改变。根节点的右子树就是这样的。右孩子没有被指定任何状态,因此它使用和根节点同样的状态来渲染。对于节点6,纹理的混合模式没有改变可是使用了一个新纹理。
根节点的左子树的纹理模式被设为DECAL。其余的参数是同样的,因此它们从根节点继承。在节点3中,FOG被打开了并设置为OVERRIDE。这个节点——节点3——的左孩子将FOG设为了OFF。既然它没有设置PROTECTED而且它父节点设置了OVERRIDE,因此就像父节点同样FOG仍然是ON。右孩子4设置FOG属性值为PROTECTED,所以能够覆盖其父节点的设置。
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);
加载几何模型并加入到场景中,调整其中一个模型在场景中的位置并经过安装仿真循环观察场景。
若是你下载了当前版本的Open Scene Graph,那么你就能够将在有相应插件的任何文件格式。包括如下的几何文件格式:3dc,3ds,flt,geo,iv,ive,lwo,md2,obj,osg和如下这些图像文件格式:bmp,gif,jpeg,rgb,tga,tif。
Open Scene Graph安装包里包含了不少open scene graph格式(.osg)的几何文件。咱们会加载其中一个,还有一个MPI Open Flight(.flt)文件。为了便于找到模型,创建一个models文件夹,并用OSG_DATA_PATH系统变量指向它。(一般为C:\Projects\OpenSceneGraph\OpenSceneGraph-Data\)。解压此文件到那个文件夹下。
几何模型使用scene graph的节点表示。所以,为了加载并操做一个几何模型文件,咱们须要声明一个句柄(或指针)指向osg::Node类型实例。(在一些要求的#include后)。
#include <osg/Node>
#include <osgDB/ReadFile>
...
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 <osgProducer/Viewer>
// 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’退出。
6.osg Text、HUD、RenderBins
6.1本章目标
添加文本到场景中——包括HUD风格的文本和做为场景一部分的文本。
本类继承自Drawable类。也就是说文本实例能够加到Geode类实例上而且能够和其它几何体同样被渲染。与文本类相关的方法的所有列在*这里*。‘osgExample Text’工程示范了许多方法。这个教程提供了文本类的几个有限的函数。绘制一个HUD牵扯到下面两个概念:
一、生成一个子树,它的根节点有合适的投影及观察矩阵...
二、将HUD子树中的几何体指定到合适的RenderBin上,这样HUD几何体就会在场景的其余部分以后按正确地状态设置绘制。
渲染HUD的子树涉及到一个投影矩阵和一个观察矩阵。对于投影矩阵,咱们会使用至关于屏幕维数水平和垂直扩展的正投影。根据这种模式,坐标至关于象素坐标。为了简单起见,观察矩阵使用单位矩阵。
为了渲染HUD,咱们把它里面的几何体附加到一个指定的RenderBin上。RenderBin容许用户在几何体绘制过程当中指定顺序。这对于HUD几何体须要最后绘制来讲颇有用。
首先,声明咱们须要的变量-osg::Text和osg::Projection。
osg::Group* root = NULL;
osg::Node* tankNode = NULL;
osg::Node* terrainNode = NULL;
osg::PositionAttitudeTransform* tankXform;
osgProducer::Viewer viewer;
// A geometry node for our HUD:
osg::Geode* HUDGeode = new osg::Geode();
// Text instance that wil show up in the HUD:
osgText::Text* textOne = new osgText::Text();
// Text instance for a label that will follow the tank:
osgText::Text* tankLabel = new osgText::Text();
// Projection node for defining view frustrum for HUD:
osg::Projection* HUDProjectionMatrix = new osg::Projection;
从文件里加载模型,和前面的教程同样创建scene graph(这里没什么新东东)。
// Initialize root of scene:
root = new osg::Group();
osgDB::FilePathList pathList = osgDB::getDataFilePathList();
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\T72-Tank\\");
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\JoeDirt\\");
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Textures\\");
osgDB::setDataFilePathList(pathList);
// Load models from files and assign to nodes:
tankNode = osgDB::readNodeFile("t72-tank_des.flt");
terrainNode = osgDB::readNodeFile("JoeDirt.flt");
// Initialize transform to be used for positioning the tank
tankXform = new osg::PositionAttitudeTransform());
tankXform->setPosition( osg::Vec3d(5,5,8) );
// Build the scene - add the terrain node directly to the root,
// connect the tank node to the root via the transform node:
root->addChild(terrainNode);
root->addChild(tankXform);
tankXform->addChild(tankNode);
下一步,创建场景来显示HUD组件。添加一个子树,它的根节点有一个投影和观察矩阵。
// Initialize the projection matrix for viewing everything we
// will add as descendants of this node. Use screen coordinates
// to define the horizontal and vertical extent of the projection
// matrix. Positions described under this node will equate to
// pixel coordinates.
HUDProjectionMatrix->setMatrix(osg::Matrix::ortho2D(0,1024,0,768));
// For the HUD model view matrix use an identity matrix:
osg::MatrixTransform* HUDModelViewMatrix = new osg::MatrixTransform;
HUDModelViewMatrix->setMatrix(osg::Matrix::identity());
// Make sure the model view matrix is not affected by any transforms
// above it in the scene graph:
HUDModelViewMatrix->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
// Add the HUD projection matrix as a child of the root node
// and the HUD model view matrix as a child of the projection matrix
// Anything under this node will be viewed using this projection matrix
// and positioned with this model view matrix.
root->addChild(HUDProjectionMatrix);
HUDProjectionMatrix->addChild(HUDModelViewMatrix);
如今创建几何体。咱们根据屏幕坐标创建一个四边形,并设置颜色和纹理坐标。
// Add the Geometry node to contain HUD geometry as a child of the
// HUD model view matrix.
HUDModelViewMatrix->addChild( HUDGeode );
// Set up geometry for the HUD and add it to the HUD
osg::Geometry* HUDBackgroundGeometry = new osg::Geometry();
osg::Vec3Array* HUDBackgroundVertices = new osg::Vec3Array;
HUDBackgroundVertices->push_back( osg::Vec3( 0,0,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(1024,0,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(1024,200,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(0,200,-1) );
osg::DrawElementsUInt* HUDBackgroundIndices =
new osg::DrawElementsUInt(osg::PrimitiveSet::POLYGON, 0);
HUDBackgroundIndices->push_back(0);
HUDBackgroundIndices->push_back(1);
HUDBackgroundIndices->push_back(2);
HUDBackgroundIndices->push_back(3);
osg::Vec4Array* HUDcolors = new osg::Vec4Array;
HUDcolors->push_back(osg::Vec4(0.8f,0.8f,0.8f,0.8f));
osg::Vec2Array* texcoords = new osg::Vec2Array(4);
(*texcoords)[0].set(0.0f,0.0f);
(*texcoords)[1].set(1.0f,0.0f);
(*texcoords)[2].set(1.0f,1.0f);
(*texcoords)[3].set(0.0f,1.0f);
HUDBackgroundGeometry->setTexCoordArray(0,texcoords);
osg::Texture2D* HUDTexture = new osg::Texture2D;
HUDTexture->setDataVariance(osg::Object::DYNAMIC);
osg::Image* hudImage;
hudImage = osgDB::readImageFile("HUDBack2.tga");
HUDTexture->setImage(hudImage);
osg::Vec3Array* HUDnormals = new osg::Vec3Array;
HUDnormals->push_back(osg::Vec3(0.0f,0.0f,1.0f));
HUDBackgroundGeometry->setNormalArray(HUDnormals);
HUDBackgroundGeometry->setNormalBinding(osg::Geometry::BIND_OVERALL);
HUDBackgroundGeometry->addPrimitiveSet(HUDBackgroundIndices);
HUDBackgroundGeometry->setVertexArray(HUDBackgroundVertices);
HUDBackgroundGeometry->setColorArray(HUDcolors);
HUDBackgroundGeometry->setColorBinding(osg::Geometry::BIND_OVERALL);
HUDGeode->addDrawable(HUDBackgroundGeometry);
为了正确的渲染HUD,咱们创建带有深度检测和透明度混合的osg::stateSet。咱们也要保证HUD几何体最后绘制。几何体在裁剪遍历时经过指定一个已编号的渲染箱能够控制渲染顺序。最后一行演示了这些:
// Create and set up a state set using the texture from above:
osg::StateSet* HUDStateSet = new osg::StateSet();
HUDGeode->setStateSet(HUDStateSet);
HUDStateSet->
setTextureAttributeAndModes(0,HUDTexture,osg::StateAttribute::ON);
// For this state set, turn blending on (so alpha texture looks right)
HUDStateSet->setMode(GL_BLEND,osg::StateAttribute::ON);
// Disable depth testing so geometry is draw regardless of depth values
// of geometry already draw.
HUDStateSet->setMode(GL_DEPTH_TEST,osg::StateAttribute::OFF);
HUDStateSet->setRenderingHint( osg::StateSet::TRANSPARENT_BIN );
// Need to make sure this geometry is draw last. RenderBins are handled
// in numerical order so set bin number to 11
HUDStateSet->setRenderBinDetails( 11, "RenderBin");
最后,使用文本时,因为osg::Text是继承自osg::Drawable的,osg::Text实例能够做为孩子加到osg::Geode类实例上。
// Add the text (Text class is derived from drawable) to the geode:
HUDGeode->addDrawable( textOne );
// Set up the parameters for the text we'll add to the HUD:
textOne->setCharacterSize(25);
textOne->setFont("C:/WINDOWS/Fonts/impact.ttf");
textOne->setText("Not so good");
textOne->setAxisAlignment(osgText::Text::SCREEN);
textOne->setPosition( osg::Vec3(360,165,-1.5) );
textOne->setColor( osg::Vec4(199, 77, 15, 1) );
// Declare a geode to contain the tank's text label:
osg::Geode* tankLabelGeode = new osg::Geode();
// Add the tank label to the scene:
tankLabelGeode->addDrawable(tankLabel);
tankXform->addChild(tankLabelGeode);
// Set up the parameters for the text label for the tank
// align text with tank's SCREEN.
// (for Onder: use XZ_PLANE to align text with tank's XZ plane.)
tankLabel->setCharacterSize(5);
tankLabel->setFont("/fonts/arial.ttf");
tankLabel->setText("Tank #1");
tankLabel->setAxisAlignment(osgText::Text::XZ_PLANE);
// Set the text to render with alignment anchor and bounding box around it:
tankLabel->setDrawMode(osgText::Text::TEXT |
osgText::Text::ALIGNMENT |
osgText::Text::BOUNDINGBOX);
tankLabel->setAlignment(osgText::Text::CENTER_TOP);
tankLabel->setPosition( osg::Vec3(0,0,8) );
tankLabel->setColor( osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) );
最后,创建viewer并进入仿真循环:
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
7.搜索并控制开关和DOF(自由度)节点
7.1搜索场景图形中的一个有名节点
模型文件可能包含了各类不一样的节点类型,用户经过对这些节点的使用来更新和表达模型的各个部分。使用osgSim::MultiSwitch多重节点能够在多个模型渲染状态间进行选择。例如,对坦克模型使用多重节点,用户便可自行选择与完整的或者损坏的坦克相关联的几何体以及渲染状态。模型中还能够包含DOF节点,以便清晰表达坦克的某个部分。例如炮塔节点能够旋转,机枪节点能够升高。炮塔旋转时,炮塔体(包括机枪)的航向角(heading)与坦克的航向角相关联,而机枪抬升时,机枪的俯仰角(pitch)与炮塔的俯仰角相关联。
对这些节点进行更新时,咱们须要一个指向节点的指针。而咱们首先要获取节点的名字,才能获得该节点的指针。而获取节点的名称,主要有这样一些方法:咨询建模人员;使用其它文件浏览器(对于.flt文件,可使用Creator或者Vega)浏览模型;或者使用OpenSceneGraph。用户能够根据本身的须要自由运用OSG的功能。例如在场景图形中载入flt文件,而且在仿真过程当中将整个场景保存成.osg文件。osg文件使用ASCII格式保存,所以用户可使用各类文本处理软件(写字板,记事本)对其进行编辑。在坦克模型文件中,你能够发现一个名为“sw1”的开关节点,它有两个子节点“good”和“bad”,分别指向坦克未损坏和损坏的状态。坦克模型的.osg文件能够从这里下载:
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/T72Tank.osg
如今咱们已经得到了须要控制的开关节点的名称(sw1),亦可获取其指针对象。获取节点指针的方法有两种:一是编写代码遍历整个场景图形;二是使用后面将会介绍的访问器(visitor)。在之前的教程中,咱们已经知道如何加载flight文件,将其添加到场景并进入仿真循环的方法。
#include <osg/PositionAttitudeTransform>
#include <osg/Group>
#include <osg/Node>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
#include <osgGA/TrackballManipulator>
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;
}
7.2按照“访问器”模式搜索有名节点(Finding a named node using the Visitor pattern)
“访问器”的设计容许用户将某个特定节点的指定函数,应用到当前场景遍历的全部此类节点中。遍历的类型包括NODE_VISITOR,UPDATE_VISITOR,COLLECT_OCCLUDER_VISITOR和CULL_VISITOR。因为咱们尚未讨论场景更新(updating),封闭节点(occluder node)和拣选(culling)的有关内容,所以这里首先介绍NODE_VISITOR(节点访问器)遍历类型。“访问器”一样容许用户指定遍历的模式,可选项包括TRAVERSE_NONE,TRAVERSE_PARENTS,TRAVERSE_ALL_CHILDREN和TRAVERSE_ACTIVE_CHILDREN。这里咱们将选择TRAVERSE_ALL_CHILDREN(遍历全部子节点)的模式。
而后,咱们须要定义应用到每一个节点的函数。这里咱们将会针对用户自定义的节点名称进行字符串比较。若是某个节点的名称与指定字符串相符,该节点将被添加到一个节点列表中。遍历过程结束后,列表中将包含全部符合指定的搜索字符串的节点。
为了可以充分利用“访问器”,咱们能够从基类osg::NodeVisitor派生一个特定的节点访问器(命名为findNodeVisitor)。这个类须要两个新的数据成员:一个std::string变量,用于和咱们搜索的有名节点进行字符串比较;以及一个节点列表变量(std::vector<osg::Node*>),用于保存符合搜索字符串的全部节点。为了实现上述的操做,咱们须要重载“apply”方法。基类的“apply”方法已经针对全部类型的节点(全部派生自osg::Node的节点)做了定义。用户能够重载apply方法来操做特定类型的节点。若是咱们但愿针对全部的节点进行一样的操做,那么能够重载针对osg::Node类型的apply方法。findNodeVisitor的头文件内容在下表中列出,相关的源代码能够在这里下载:
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/findNodeVisitor.zip
#include <osg/NodeVisitor>
#include <osg/Node>
#include <osgSim/DOFTransform>
#include <iostream>
#include <vector>
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<osg::Node*> nodeListType;
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<osg::Group*>
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankTwoGroup = dynamic_cast<osg::Group*>
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankThreeGroup = dynamic_cast<osg::Group*>
(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 <osgSim::MultiSwitch*> (findNode.getFirst());
更新开关节点
当咱们获取了一个合法的开关节点句柄后,下一步就是从一个模型状态变换到另外一个状态。咱们可使用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<osgSim::DOFTransform *> (findTurretNode.getFirst()) ;
// 若是节点句柄合法,则设置炮塔的航向角为向右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<osgSim::DOFTransform *> (findGunNode.getFirst()) ;
// 若是节点句柄合法,则设置机枪的俯仰角为向上22.5度。
if (gunDOF)
{
gunDOF->setCurrentHPR( osg::Vec3(0.0,3.14159/8.0,0.0) );
}
使用回调类实现对场景图形节点的更新。前一个教程介绍了在进入主仿真循环以前,更新DOF和开关节点的方法。本节将讲解如何使用回调来实如今每帧的更新遍历(update traversal)中进行节点的更新。
用户可使用回调来实现与场景图形的交互。回调能够被理解成是一种用户自定义的函数,根据遍历方式的不一样(更新update,拣选cull,绘制draw),回调函数将自动地执行。回调能够与个别的节点或者选定类型(及子类型)的节点相关联。在场景图形的各次遍历中,若是遇到的某个节点已经与用户定义的回调类和函数相关联,则这个节点的回调将被执行。若是但愿了解有关遍历和回调的更多信息,请参阅David Eberly所著的《3D Game Engine Design》第四章,以及SGI的《Performer Programmer's Guide》第四章。相关的示例请参见osgCallback例子。
更新回调将在场景图形每一次运行更新遍历时被执行。与更新回调相关的代码能够在每一帧被执行,且实现过程是在拣选回调以前,所以回调相关的代码能够插入到主仿真循环的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 <osgSim::DOFTransform*> (findTurret.getFirst());
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<tankDataType>指向用户数据。整个类的定义以下:
class tankNodeCallback : public osg::NodeCallback
{
public:
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::ref_ptr<tankDataType> tankData =
dynamic_cast<tankDataType*> (node->getUserData() );
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;
}
使程序具有将键盘事件与特定函数相关联的能力。在前面的教程中咱们已经可使用更新回调来控制炮塔的旋转。本章中咱们将添加一个键盘接口类,实现经过用户的键盘输入来更新炮塔的转角。
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),启动定时器并使本身被重复调用,以计算新的模型方向或者位置数据。
如下主要介绍如何建立一个用于将键盘输入关联到特定函数的键盘接口类。当用户将按键注册到接口类并设定相应的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 <osgGA/GUIEventHandler>
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<int, functionStatusType > keyFunctionMap;
// 保存已注册的“按下按键”方法及其键值。
keyFunctionMap keyFuncMap;
// 保存已注册的“松开按键”方法及其键值。
keyFunctionMap keyUPFuncMap;
};
#endif
下面的代码用于演示如何使用上面定义的类:
// 创建场景和视口。
// ……
// 声明响应函数:
// 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);
// 进入仿真循环
// ……
上一个教程咱们讲解了键盘事件处理器类,它用于注册响应函数。本章提供了用于键盘输入的更方便的方案。咱们将重载一个GUIEventHandler类,而没必要再建立和注册函数。在这个类中咱们将添加新的代码,以便执行特定的键盘和鼠标事件响应动做。咱们还将提出一种键盘事件处理器与更新回调通信的方法。
教程第8部分演示了如何将回调与DOF节点相关联,以实现场景中DOF节点位置的持续更新。那么,若是咱们但愿使用键盘输入来控制场景图形中的节点,应该如何处理呢?例如,若是咱们有一个基于位置变换节点的坦克模型,并但愿在按下w键的时候控制坦克向前运动,咱们须要进行以下一些操做:
1. 读取键盘事件;
2. 保存键盘事件的结果;
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<osg::PositionAttitudeTransform*> (node);
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本章目标
手动放置相机,以实现场景的观览。
咱们可使用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();
矩阵的位置设置为坦克模型后方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;
场景的视口类实例使用当前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键能够手动切换摄像机。
提示:自OSG 0.9.7发布以后,新的osgGA::MatrixManipulator类(TrackerManipulator)容许用户将摄相机“依附”到场景图形中的节点。这一新增的操纵器类能够高效地替代下面所述的方法。
本章教程将继续使用回调和节点路径(NodePath)来检索节点的世界坐标。
在一个典型的仿真过程当中,用户可能须要从场景中的各类车辆和人物里选择一个进行跟随。本章将介绍一种将摄像机“依附”到场景图形节点的方法。此时视口的摄像机将跟随节点的世界坐标进行放置。
视口类包括了一系列的矩阵控制器(osgGA::MatrixManipulator)。于是提供了“驱动控制(Drive)”,“轨迹球(Trackball)”,“飞行(Fly)”等交互方法。矩阵控制器类用于更新摄像机位置矩阵。它一般用于回应GUI事件(鼠标点击,拖动,按键,等等)。本文所述的功能须要依赖于相机位置矩阵,并参照场景图形节点的世界坐标。这样的话,相机就能够跟随场景图形中的节点进行运动了。
为了得到场景图形中节点的世界坐标,咱们须要使用节点访问器的节点路径功能来具现一个新的类。这个类将提供一种方法将本身的实例关联到场景图形,并所以提供访问任意节点世界坐标的方法。此坐标矩阵(场景中任意节点的世界坐标)将做为相机位置的矩阵,由osgGA::MatrixManipulator实例使用。
首先咱们建立一个类,计算场景图形中的多个变换矩阵的累加结果。很显然,全部的节点访问器都会访问当前的节点路径。节点路径本质上是根节点到当前节点的全部节点列表。有了节点路径的实例以后,咱们就可使用场景图形的方法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);
// 进入仿真循环……
建立回调,以实现用于沿轨道环绕,同时指向场景中某个节点的世界坐标矩阵的更新。使用此矩阵的逆矩阵来放置相机。
本章的回调类基于上一篇的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<osg::MatrixTransform *>(node); 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键来切换不一样的视口。