美国海军osg教程

中文教程网 http://www.osgchina.org/projects/osgcn/wikicn/Supportcn/Tutorials.php
Open Scene Graph教程1——使用Open Scene Graph几何
原文出处:http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgGeometry.htm

摘要

    本节涵盖了生成基本几何形状的一些方法。生成几何物体的方法有这么几种:在最底层对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。

 

// 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();

 

   }


 


    好运气!

Open Scene Graph教程2——使用StateSet产生有纹理的几何体
原文出处:http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgTexturedGeometry.htm

 


目标:

    为教程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;

 

}


 

编码愉快!


Open Scene Graph教程3——使用Shape,改变state

 


 


原文出处:http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgShape.htm

 

 


 


 


目标:

    用osg::Shape实例构建场景。使用osg::StateSet控制shape的渲染。

 

 

使用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;

 


 

 

Open Scene Graph教程4——更多的StateSet
原文出处:http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgStateSet.htm


StateSet如何工作?

    场景图管理器遍历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,因此可以覆盖其父节点的设置。

 


那很不错,但是代码怎么样呢?

 


    下面是操作状态配置并用节点将这些状态关联起来的代码。

 

 

 

   // 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教程5——从文件中加载模型并放入到场景中
原文出处:

 

 

http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgFileLoading.htm

 

 


 


 


目标

加载几何模型并加入到场景中,调整其中一个模型在场景中的位置并通过安装仿真循环观察场景。

 

 


 


 


加载几何模型并加入到场景中

    如果你下载了当前版本的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’退出。

 

 

 

Open Scene Graph教程6——osgText, HUD, RenderBins
原文出处:http://www.nps.navy.mil/cs/sullivan/osgtutorials/osgText.htm

 


 


目标

添加文本到场景中——包括HUD风格的文本和作为场景一部分的文本。

 


 


摘要

    文本类继承自Drawable类。也就是说文本实例可以加到Geode类实例上并且可以和其它几何体一样被渲染。与文本类相关的方法的全部列在*这里*。‘osgExample Text’工程示范了许多方法。这个教程提供了文本类的几个有限的函数。绘制一个HUD牵扯到下面两个概念:

 

1、生成一个子树,它的根节点有合适的投影及观察矩阵...

 

2、将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();

 

   }


 


好运!

11月5日
Navy07 - 搜索并控制开关和DOF节点(1)
开始翻译Navy的OSG教程

Navy,也就是美国海军……他们一直以来都是OSG的重要客户之一。在美国军方的支持和资助下,Navy研究学院还开发了基于OSG的著名游戏与仿真引擎Delta3D,它融合了OSG,OpenAL,ODE(物理仿真)和Cal3D(角色动画)等诸多开发库,形成了一个极其强大的可视化开发高级API库系统。同时,Delta3D也是开放源代码的。
Navy的官方网站提供了很好的OSG系列教程,截止到本文编写时为止,共有18个章节,每章以一个功能的实现为目的,并辅以简洁的文字介绍和源代码。对于初学者来说十分实用。
Navy OSG教程的官方链接为:
http://www.nps.navy.mil/cs/sullivan/osgTutorials/index.htm
国内的访客可能无法登陆这个网址(具体原因……不言而喻吧),可以使用网页代理来观看原文,并下载模型文件和例子源代码。也可以向我索取。

本教程的前几章已经有人翻译过了,具体请参见
http://blog.donews.com/kevinwhite/archive/2006/06.aspx
因为我并不知道这些文章是否允许转载(我不认识作者),所以只提供了他的链接,
由于Navy的教程很多是源代码的罗列,因此我会在翻译的同时给出对于代码的中文注解和我自己的见解~~其中有不妥当的地方,请大家参阅原文并提出,我会及时改正。
我的翻译和解释工作将从第7章“Finding nodes, DOFs and Switches”开始。
Navy教程的后面章节我会逐步解析并发布上来~~请大家多多支持,多多指正。

----------------------------------------------------------

Navy教程第七课 - 搜索并控制开关节点和DOF(自由度)节点
 

----------------------------------------------------------

搜索场景图形中的一个有名节点
模型文件可能包含了各种不同的节点类型,用户通过对这些节点的使用来更新和表达模型的各个部分。使用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;
}

(未完)

11月7日
Navy07 - 搜索并控制开关和DOF节点(2)
按照“访问器”模式搜索有名节点
“访问器”的设计允许用户将某个特定节点的指定函数,应用到当前场景遍历的所有此类节点中。遍历的类型包括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) );
   }

就是这样了!然后再编写仿真循环的代码即可。

11月11日
Navy08 - 使用更新回调来更改模型
本章目标:
使用回调类实现对场景图形节点的更新。前一个教程介绍了在进入主仿真循环之前,更新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;
}

祝各位好运

11月14日
Navy09 - 处理键盘输入
本章目标:
使程序具备将键盘事件与特定函数相关联的能力。在前面的教程中我们已经可以使用更新回调来控制炮塔的旋转。本章中我们将添加一个键盘接口类,实现通过用户的键盘输入来更新炮塔的转角。

 

----------------------------------------------------------------------

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),启动定时器并使自己被重复调用,以计算新的模型方向或者位置数据。

简单的键盘接口类
以下主要介绍如何创建一个用于将键盘输入关联到特定函数的键盘接口类。当用户将按键注册到接口类并设定相应的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);

// 进入仿真循环
// ……

11月17日
Navy09.1 - 处理键盘输入实现更新回调
本章目标:
上一个教程我们讲解了键盘事件处理器类,它用于注册响应函数。本章提供了用于键盘输入的更方便的方案。我们将重载一个GUIEventHandler类,而不必再创建和注册函数。在这个类中我们将添加新的代码,以便执行特定的键盘和鼠标事件响应动作。我们还将提出一种键盘事件处理器与更新回调通讯的方法。

----------------------------------------------------------------------

问题的提出:
教程08演示了如何将回调与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();
   }

完成了!不过用户还有更多可以扩展的地方:例如使坦克在按键松开的时候停止运动,转向,加速等等。
祝编程愉快!

11月23日
Navy10 - 使用自定义矩阵来放置相机
本章目标:
手动放置相机,以实现场景的观览。

 

----------------------------------------------------------------------

设置矩阵的方向和位置
我们可以使用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,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();

声明一个用于设置相机的矩阵。矩阵的位置设置为坦克模型后方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键可以手动切换摄像机。

11月26日
Navy11.1 - 实现跟随节点的相机
提示:
自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);

// 进入仿真循环……

祝你好运!

Navy11.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<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键来切换不同的视口。
祝您好运!

12月4日
Navy11.3 - 如何获取节点在世界坐标的位置
获取某个节点的世界坐标(相对世界原点的位置),有一种方法是创建可以更新矩阵的回调函数。有关这一方法的示例可以参照前面的例子。但是这一方法的弊病在于必须使用更新回调。回调被触发之后,无论用户需要与否,每帧都会自动计算矩阵坐标。如果你不喜欢这种方式的话,可以尝试使用访问器。

使用节点访问器计算世界原点的相对位置
对于场景图形中的一个OSG节点,在它和根节点之间可能会有其它的一些变换节点存在。那么应该如何获取节点在世界的坐标呢?从技术上讲是很难的。节点的每一个父节点都有且只有自己的变换矩阵。通常情况下,这些矩阵中包含了相对坐标数据。如果要计算目标节点的世界坐标的话,则需要将根节点和目标节点之间所有的矩阵相乘。访问器模式的使用解决了这一问题。访问器(Visitor)可以跟踪记录场景图形中节点遍历的路径。OSG提供了相应的函数,用于获取节点路径nodePath并计算基于路径上各个矩阵的世界坐标。

创建从叶节点到根节点的访问器。
对于目标节点,启动访问器。
访问器将遍历整个场景图形,以得到正确的节点路径。
到达根节点之后,计算得到目标节点的世界坐标。
使用访问器的nodePath计算节点世界坐标。
C++格式的代码如下:
// 该访问器类用于返回某个节点的世界坐标。
// 它从起始节点开始向父节点遍历,并随时将历经的节点记录到nodePath中。
// 第一次到达根节点之后,它将记录起始节点的世界坐标。连结起始节点到根节点路径上的
// 所有矩阵之后,即可获得节点的世界坐标。
class getWorldCoordOfNodeVisitor : public osg::NodeVisitor {
public:
   getWorldCoordOfNodeVisitor():
      osg::NodeVisitor(NodeVisitor::TRAVERSE_PARENTS), done(false)
      {
         wcMatrix= new osg::Matrixd();
      }
      virtual void apply(osg::Node &node)
      {
         if (!done)
         {
            if ( 0 == node.getNumParents() ) // 到达根节点,此时节点路径也已记录完整
            {
               wcMatrix->set( osg::computeLocalToWorld(this->getNodePath()) );
               done = true;
            }
            traverse(node);
         }
      }
      osg::Matrixd* giveUpDaMat()
      {
         return wcMatrix;
      }
private:
   bool done;
   osg::Matrix* wcMatrix;
};

// 对于场景中的合法节点,返回osg::Matrix格式的世界坐标。
// 用户创建用于更新世界坐标矩阵的访问器之后,既可获取该矩阵。
// (此函数也可以作为节点派生类的成员函数。)
osg::Matrixd* getWorldCoords( osg::Node* node) {
   getWorldCoordOfNodeVisitor* ncv = new getWorldCoordOfNodeVisitor();
   if (node && ncv)
   {
      node->accept(*ncv);
      return ncv->giveUpDaMat();
   }
   else
   {
      return NULL;
   }
}

编程愉快!

12月9日
Navy14.1 - 使用OpenGL顶点着色器和片元着色器(概述)
目的:
使用OpenGL着色语言中的顶点和片元着色器,替代原有的固化函数管道的光照和材质计算,用于场景图形中的节点选择。(砖块着色器程序来自3DLabs)

 

------------------------------------

概述:
OpenGL着色语言允许程序员编写自己的像素和顶点着色器。有关着色语言的更多信息,包括最低的硬件和软件需求,请参阅:
http://developer.3dlabs.com/openGL2/
OSG的osg::Program和osg::Shader类允许用户将着色器作为StateSet的一部分加入选定的场景图形子树。如果要在OpenSceneGraph中使用自定义的顶点和片元着色器,则需要使用下面的基类:

osg::Program - 在应用层级上封装了OpenGL着色语言的glProgramObject函数。Program类的对象继承自osg::StateAttribute类。即osg::Program类的实例可以关联到StateSet,并使用setAttributeAndModes()方法来许可自身的使用。开启Program渲染状态之后,与此渲染状态相关联的几何体均会使用该Program的着色器进行渲染。

osg::Shader - 在应用层级上封装了OpenGL着色语言的glShaderObject函数。这个类用于管理着色器源代码的加载和编译。osg::Shader类的实例可以与一个或多个osg::Program的实例相关联。Shader对象有两种类型:osg::Shader::FRAGMENT和osg::Shader::VERTEX。

代码:
要创建一个使用OpenGL像素和片元着色器的程序,可以按照下面的步骤:

创建一个osg::Program实例;
创建一个osg::Shader类的顶点(VERTEX)或片元(FRAGMENT)着色器实例;
加载并编译着色器代码;
将着色器添加到osg::Program实例;
将osg::Program实例关联给StateSet类,并将其激活;
下面的代码用于加载和应用基本的顶点和片元着色器:

  osg::StateSet* brickState = tankNode->getOrCreateStateSet();

   osg::Program* brickProgramObject = new osg::Program;
   osg::Shader* brickVertexObject =
      new osg::Shader( osg::Shader::VERTEX );
   osg::Shader* brickFragmentObject =
      new osg::Shader( osg::Shader::FRAGMENT );
   brickProgramObject->addShader( brickFragmentObject );
   brickProgramObject->addShader( brickVertexObject );
   loadShaderSource( brickVertexObject, "shaders/brick.vert" );
   loadShaderSource( brickFragmentObject, "shaders/brick.frag" );

   brickState->setAttributeAndModes(brickProgramObject, osg::StateAttribute::ON);

下面的函数可以方便地加载着色器代码,并将编译后的代码关联给着色器对象:

bool loadShaderSource(osg::Shader* obj, const std::string& fileName )
{
   std::string fqFileName = osgDB::findDataFile(fileName);
   if( fqFileName.length() == 0 )
   {
      std::cout << "File /"" << fileName << "/" not found." << std::endl;
      return false;
   }
   bool success = obj->loadShaderSourceFromFile( fqFileName.c_str());
   if ( !success  )
   {
      std::cout << "Couldn't load file: " << fileName << std::endl;
      return false;
   }
   else
   {
      return true;
   }
}

祝你好运!

Navy14.2 - 向着色器传递变量数据
目的:
使用OpenGL着色语言,实现从调用程序向着色器传递所需的参数。

 

-----------------------------

概述:


着色器有两种类型的参数:一致变量(Uniform variables)指得是在一帧当中保持恒定的数值,典型的参数包括视口的方向和灯光的方向。而易变变量(Varying variables)对于每一个执行单元(包括顶点着色器中的顶点,以及片元着色器中的片元)都是变化的。
一致变量用于从渲染程序向着色器传递参数。在《OpenGL着色语言》一书中,凹凸贴图着色器的例子使用了三个一致变量参数:三维向量LightPosition,以及两个2D材质变量baseTexture和normalMap。OSG类提供了一种直截了当的机制,使得OpenSceneGraph程序可以向着色器直接传递数据。


代码:
以下所述为OpenSceneGraph程序向凹凸贴图着色器传递参数的方法。其代码使用了Program对象类的方法addUniform,此方法有两个输入参数:一个用于声明着色器一致变量的字符串,以及一个用于记录参数值的变量。声明2D材质一致变量的方法与此类似,其输入参数为2D材质变量名称的字符串,以及所用材质单元的整数值。用于向凹凸贴图着色器传递所需变量的代码如下:

bool loadShaderSource(osg::Shader* obj, const std::string& fileName );

int main( int argc, char **argv )
{
   osg::Group* rootNode = new osg::Group();
   osg::Node* tankNode = NULL;
   osg::Node* terrainNode = NULL;
   osg::PositionAttitudeTransform* tankXform =
      new osg::PositionAttitudeTransform();

   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//");
   osgDB::setDataFilePathList(pathList);

   tankNode = osgDB::readNodeFile("T72-tank_des.flt");
   terrainNode = osgDB::readNodeFile("JoeDirt.flt");
   if (! (tankNode && terrainNode))
   {
      std::cout << "Couldn't load models, quitting." << std::endl;
      return -1;
   }
   rootNode->addChild(terrainNode);
   rootNode->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) ) );

   // 将一个“空”Program对象关联到rootNode,它相当于定义了一个缺省的
   // StateAttribute属性。“空”Program对象(不与任何Shader对象关联)是一种
   // 特殊情形,它表示程序将使用OpenGL 1.x固化功能的渲染管道。
   osg::StateSet* rootState = rootNode->getOrCreateStateSet();
   osg::Program* defaultProgramObject = new osg::Program;
   rootState->setAttributeAndModes(defaultProgramObject, osg::StateAttribute::ON);
   osg::StateSet* bumpMapState = tankNode->getOrCreateStateSet();

   osg::Program* bumpMapProgramObject = new osg::Program;

   osg::Shader* brickVertexObject =
      new osg::Shader( osg::Shader::VERTEX );
   osg::Shader* brickFragmentObject =
      new osg::Shader( osg::Shader::FRAGMENT );

   bool ok =
      loadShaderSource( brickVertexObject, "shaders/bumpmap.vert" )
      &&
      loadShaderSource( brickFragmentObject, "shaders/bumpmap.frag" ) ;

   if(!ok)
   {
      std::cout << "Couldn't load shaders" << std::endl;
      return -1;
   }

   bumpMapProgramObject->addShader( brickFragmentObject );
   bumpMapProgramObject->addShader( brickVertexObject );

   osg::Uniform* lightPosU = new osg::Uniform("LightPosition",osg::Vec3(0,0,1));
   osg::Uniform* normalMapU = new osg::Uniform("normalMap",1);
   osg::Uniform* baseTextureU = new osg::Uniform("baseTexture",0);
   bumpMapState->addUniform(lightPosU);
   bumpMapState->addUniform(normalMapU);
   bumpMapState->addUniform(baseTextureU);
   bumpMapState->setAttributeAndModes(bumpMapProgramObject, osg::StateAttribute::ON);

   osg::Texture2D* tankBodyNormalMap = new osg::Texture2D;
   // 避免在优化过程中静态量出错。
   tankBodyNormalMap->setDataVariance(osg::Object::DYNAMIC);
   osg::Image* tankBody = osgDB::readImageFile("TankBump.png");
   if (!tankBody)
   {
      std::cout << " couldn't find texture, quiting." << std::endl;
      return -1;
   }

   tankBodyNormalMap->setImage(tankBody);
   bumpMapState->setTextureAttributeAndModes(1,tankBodyNormalMap,osg::StateAttribute::ON);

   osgViewer::Viewer viewer;
   viewer.setCameraManipulator(new osgGA::TrackballManipulator());
   viewer.setSceneData( rootNode );
   viewer.realize();

   while( !viewer.done() )
   {
      viewer.frame();
   }
   return 0;
}

编程愉快!

12月10日
Navy14.3 - 更新着色器
目的:
持续更新OpenGL着色器程序所用的变量。

 

-----------------------------

概述:
本章演示了如何使用OpenGL着色器程序持续更新参数,从而实现对场景中对象的动态着色效果控制。我们将使用前述的凹凸贴图着色器,并更新其一致变量LightPosition,以模拟灯光环绕坦克模型移动的效果。我们将使用上一章所用的addUniform方法。不过,这一次我们将使用更新回调来实现变量更新的效果。回调类的主体用于持续改变灯光位置的向量。为了使用这个类的功能,我们简单地将其关联给场景中节点的更新回调。


代码:
着色器更新回调的定义如下:

class updateBumpShader : public osg::Uniform::Callback
{
public:
   virtual void operator()
      ( osg::Uniform* uniform, osg::NodeVisitor* nv )
   {
      float angle = nv->getFrameStamp()->getReferenceTime();
      float x = sinf(angle)*.2f;
      float y = cosf(angle)*.2f;
      osg::Vec3 lightPos(x,y,.6f);
      uniform->set(lightPos);
   }
};

其余的代码与上一章基本相同:设置场景和着色器的实例。唯一不同的是,我们需要将上面的回调类设置为场景中一个节点的更新回调。

int main( int argc, char **argv )
{
   osg::Group* rootNode = new osg::Group();
   osg::Node* tankNode = NULL;
   osg::Node* terrainNode = NULL;
   osg::PositionAttitudeTransform* tankXform =
      new osg::PositionAttitudeTransform();

   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//");
   osgDB::setDataFilePathList(pathList);

   tankNode = osgDB::readNodeFile("T72-tank_des.flt");
   terrainNode = osgDB::readNodeFile("JoeDirt.flt");
   if (! (tankNode && terrainNode))
   {
      std::cout << "Couldn't load models, quitting." << std::endl;
      return -1;
   }
   rootNode->addChild(terrainNode);
   rootNode->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) ) );

   osg::StateSet* bumpMapState = tankNode->getOrCreateStateSet();

   osg::Program* bumpMapProgramObject = new osg::Program;

   osg::Shader* bumpVertexObject =
      new osg::Shader( osg::Shader::VERTEX );
   osg::Shader* bumpFragmentObject =
      new osg::Shader( osg::Shader::FRAGMENT );

   bool ok =
      loadShaderSource( bumpVertexObject, "shaders/bumpmap.vert" )
      &&
      loadShaderSource
         ( bumpFragmentObject, "shaders/bumpmap.frag" ) ;

   if(!ok)
   {
      std::cout << "Couldn't load shaders" << std::endl;
      return -1;
   }

   bumpMapProgramObject->addShader( bumpFragmentObject );
   bumpMapProgramObject->addShader( bumpVertexObject );

   osg::Uniform* lightPosU =
      new osg::Uniform("LightPosition",osg::Vec3(0,0,1));
   osg::Uniform* normalMapU = new osg::Uniform("normalMap",1);
   osg::Uniform* baseTextureU = new osg::Uniform("baseTexture",0);

   bumpMapState->addUniform(lightPosU);
   bumpMapState->addUniform(normalMapU);
   bumpMapState->addUniform(baseTextureU);
   lightPosU->setUpdateCallback(new updateBumpShader() );

   bumpMapState->setAttributeAndModes
      (bumpMapProgramObject, osg::StateAttribute::ON);

   osg::Texture2D* tankBodyNormalMap = new osg::Texture2D;
   tankBodyNormalMap->setDataVariance(osg::Object::DYNAMIC);
   osg::Image* tankBody = osgDB::readImageFile("TankBump.png");
   if (!tankBody)
   {
      std::cout << " couldn't find texture, quiting." << std::endl;
      return -1;
   }

   tankBodyNormalMap->setImage(tankBody);
   bumpMapState->setTextureAttributeAndModes
      (1,tankBodyNormalMap,osg::StateAttribute::ON);

   osgViewer::Viewer viewer;

   viewer.setCameraManipulator(new osgGA::TrackballManipulator());
   viewer.setSceneData( rootNode );
   viewer.realize();

   while( !viewer.done() )
   {
      viewer.frame();
   }
   return 0;
}

编程愉快!

Navy15 - 向场景中添加告示牌(Billboard)节点 (1)
目的:
向场景中添加告示牌节点,以模拟沙漠中灌木的效果。

 

-----------------------------

概述:
告示牌(Billboard)节点的特性是围绕用户定义的轴(或者点)进行旋转,从而始终面向一个指定的方向。典型的告示牌可以绕Z轴正向旋转,同时面向视口方向,以实现树林的模拟。OSG发行版本的示例程序osgBillboard中通过指定旋转轴和法线(朝向方向)的方式,演示了各种模式的告示牌节点。本教程将创建多个树木样式的告示牌,它们环绕Z轴正向旋转并始终朝向视口。

Billboard类(osg::Billboard)包括可以绕一个轴或者参考点旋转并朝向指定方向的几何体。Billboard类继承自Geode类。也就是说,继承自Drawable类(包括Geometry类)的实例可以被添加给Billboard对象。我们可以将几何体关联给告示牌节点。因为Billboard类是继承自Geode类的,我们也可以将StateSet关联给告示牌节点。

除了继承自Geode类的方法和成员外,告示牌类还包括了用于控制其自身特性的成员和方法:告示牌是否环绕参考点或者轴;如果绕某个轴旋转的话,它应该朝向什么方向。为了控制告示牌的类型,可以使用其中的setMode(osg::Billboard::Mode)方法。其合法参数有:POINT_ROT_EYE(绕一个点旋转,相对于眼睛位置),POINT_ROT_WORLD(绕一个点旋转,相对于世界坐标),AXIAL_ROT(绕一个轴旋转)。如果指定了AXIAL_ROT模式,用户就可以使用setAxis(osg::Vec3)方法来设置告示牌绕之旋转的轴。用户还可以使用setNormal(osg::Vec3)方法定义告示牌朝向的法线。为了放置告示牌,用户还需重载osg::Geode的addGeometry方法。这个方法有两个参数:一个Drawable实例,以及一个标识位置的osg::Vec3实例。

创建告示牌还需要最后一步工作:当几何体根据法线的设置旋转并朝向视口之后,对其进行光照的计算将导致一种怪异的结果。(光照会随着视口的改变而突变)为了创建各个方向的形象均相同的告示牌,我们需要确信关闭告示牌的光照。综上所述,向场景中添加多个告示牌的基本步骤为:

创建一个告示牌实例并添加到场景;
创建光照关闭的渲染状态;
创建几何体(应用上面的渲染状态)并添加给告示牌节点。
代码:
需要以下的头文件:

#include <osg/Geometry>
#include <osg/Texture2D>
#include <osg/Billboard>
#include <osg/BlendFunc>
#include <osg/AlphaFunc>
#include <osgViewer/Viewer>
#include <osgGA/TrackballManipulator>
#include <osg/PositionAttitudeTransform>

首先,我们编写一个简便的函数用于生成与告示牌节点相关联的几何体。此函数有两个参数:一个浮点数用于表示放缩特性,以及一个关联给几何体实例的渲染状态指针。这个函数的返回值是一个几何体指针。我们使用简单的四边形来生成几何体。关于如何指定四边形的顶点和纹理坐标,请参照前面的教程。唯一需要注意的是,在创建该几何体的时候,我们需要它相对旋转轴置中。函数的定义如下所示:

osg::Drawable* createShrub(const float & scale, osg::StateSet* bbState)
{
   float width = 1.5f;
   float height = 3.0f;

   width *= scale;
   height *= scale;

   osg::Geometry* shrubQuad = new osg::Geometry;

   osg::Vec3Array* shrubVerts = new osg::Vec3Array(4);
   (*shrubVerts)[0] = osg::Vec3(-width/2.0f, 0, 0);
   (*shrubVerts)[1] = osg::Vec3( width/2.0f, 0, 0);
   (*shrubVerts)[2] = osg::Vec3( width/2.0f, 0, height);
   (*shrubVerts)[3] = osg::Vec3(-width/2.0f, 0, height);

   shrubQuad->setVertexArray(shrubVerts);

   osg::Vec2Array* shrubTexCoords = new osg::Vec2Array(4);
   (*shrubTexCoords)[0].set(0.0f,0.0f);
   (*shrubTexCoords)[1].set(1.0f,0.0f);
   (*shrubTexCoords)[2].set(1.0f,1.0f);
   (*shrubTexCoords)[3].set(0.0f,1.0f);
   shrubQuad->setTexCoordArray(0,shrubTexCoords);

   shrubQuad->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));

   // 创建一个颜色数组,并为所有的顶点添加单一的颜色。
   osg::Vec4Array* colorArray = new osg::Vec4Array;
   colorArray->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); // white, fully opaque
   // 创建颜色索引数组。
   osg::TemplateIndexArray
      <unsigned int, osg::Array::UIntArrayType,4,1> *colorIndexArray;
   colorIndexArray =
      new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,1>;
   colorIndexArray->push_back(0);
   // 使用索引数组将其中的第一个值关联给所有的顶点。
   shrubQuad->setColorArray( colorArray);
   shrubQuad->setColorIndices(colorIndexArray);
   shrubQuad->setColorBinding(osg::Geometry::BIND_OVERALL);

   shrubQuad->setStateSet(bbState);

   return shrubQuad;
}

12月18日
Navy15 - 向场景中添加告示牌(Billboard)节点 (2)
下一步,我们编写场景设置和仿真循环的执行代码。代码中包括我们刚刚编写的向告示牌添加四边形实例的函数(Billboard类继承自Geode类,因此可以向其添加Drawable实例),以及旋转轴和告示牌朝向方向的设置。

int main( int argc, char **argv )
{
   osgViewer::Viewer viewer;

   osg::Group* rootNode = new osg::Group();
   osg::Node* tankNode = NULL;
   osg::Node* terrainNode = NULL;
   osg::PositionAttitudeTransform* tankXform =
      new osg::PositionAttitudeTransform();

   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);

   tankNode = osgDB::readNodeFile("T72-tank_des.flt");
   terrainNode = osgDB::readNodeFile("JoeDirt.flt");
   if (! (tankNode && terrainNode))
   {
      std::cout << "Couldn't load models, quitting." << std::endl;
      return -1;
   }
   rootNode->addChild(terrainNode);
   rootNode->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) ) );

   viewer.setCameraManipulator(new osgGA::TrackballManipulator());
   viewer.setSceneData(rootNode);
   viewer.realize();

创建告示牌并设置其参数。我们希望告示牌可以环绕轴(0,0,1)旋转,并始终朝向视口。为了保证它始终面向视口,我们需要定义告示牌的法线方向为(0,-1,0)。

   osg::Billboard* shrubBillBoard = new osg::Billboard();
   rootNode->addChild(shrubBillBoard);

   shrubBillBoard->setMode(osg::Billboard::AXIAL_ROT);
   shrubBillBoard->setAxis(osg::Vec3(0.0f,0.0f,1.0f));
   shrubBillBoard->setNormal(osg::Vec3(0.0f,-1.0f,0.0f));

下面的代码将设置四边形几何体的渲染状态。我们将使用Alpha融合算法,使四边形看起来像是一个复杂的树木的几何形状。

osg::Texture2D *ocotilloTexture = new osg::Texture2D;
ocotilloTexture->setImage(osgDB::readImageFile("images//ocotillo.png"));

osg::AlphaFunc* alphaFunc = new osg::AlphaFunc;
alphaFunc->setFunction(osg::AlphaFunc::GEQUAL,0.05f);

osg::StateSet* billBoardStateSet = new osg::StateSet;

billBoardStateSet->setMode( GL_LIGHTING, osg::StateAttribute::OFF );
billBoardStateSet->setTextureAttributeAndModes
      (0, ocotilloTexture, osg::StateAttribute::ON );
billBoardStateSet->setAttributeAndModes
      (new osg::BlendFunc, osg::StateAttribute::ON );
osg::AlphaFunc* alphaFunction = new osg::AlphaFunc;
alphaFunction->setFunction(osg::AlphaFunc::GEQUAL,0.05f);
billBoardStateSet->setAttributeAndModes( alphaFunc, osg::StateAttribute::ON );

下面,我们需要创建一些Drawable几何体并将其添加给告示牌节点实例。我们将使用上面定义的函数来创建几何体;并使用Billboard类重载的addDrawable方法来添加并放置这些四边形。

// 创建不同放缩系数的几何体,并赋予它们已定义好的渲染状态。
osg::Drawable* shrub1Drawable = createShrub( 1.0f, billBoardStateSet);
osg::Drawable* shrub2Drawable = createShrub( 2.0f, billBoardStateSet);
osg::Drawable* shrub3Drawable = createShrub( 1.2f, billBoardStateSet);

// 将这些几何体添加到告示牌节点,并设置其不同位置。
shrubBillBoard->addDrawable( shrub1Drawable , osg::Vec3(12,3,8) );
shrubBillBoard->addDrawable( shrub2Drawable , osg::Vec3(10,18,8));
shrubBillBoard->addDrawable( shrub3Drawable , osg::Vec3(6,10,8) )

这之后的代码就很寻常了。

   while( !viewer.done() )
   {
      viewer.frame();
   }
   return 0;
}

祝你好运!

Navy16 - 交集测试 (1)
目的:
向场景中添加一系列随机放置着的灌木。使用osgUtil库的交集函数来获取正确的地形高度值,以便正确安置灌木告示牌节点。

 

-----------------------------

概述:


如果我们希望扩展前一个教程的内容,向场景中的地形添加一些随意放置的告示牌节点,那么我们最好能够获取任意经纬(x,y)位置的地形高度值。我们可以使用osgUtil库提供的交集运算模块函数来实现这一功能。我们可能会用到的基本类描述如下:

线段(osg::LineSegment)- 交集测试的基础是场景中的射线。线段类提供了一种定义射线的方法。它包括两个osg::Vec3实例:一个用于定义线段的起点,另一个用于定义终点。当交集测试被触发时,它将检测射线的相交情况并执行相应的操作。

交点(osgUtil::Hit)- 这个类向程序员提供了获取交集检测的基本数据的方法。交点类包括一条射线与场景中几何体相交的状态信息。尤为重要的是,它以Vec3的形式提供了局部和世界坐标的位置以及法线数据。它的成员方法getLocalIntersectPoint,getLocalIntersectNormal,getWorldIntersectPoint和getworldIntersectNormal分别以osg::Vec3作为返回值,返回局部/世界坐标的相交点/法线数值。

交点列表(osgUtil::IntersectVisitor::HitList)- 一条单一的线段可能与场景中的多个几何体实例(或者多次与同一个几何体)产生交集。对于每一条参与交集测试的线段,系统均会产生一个列表。这个列表包含了所有交集测试产生的Hit实例。如果没有监测到任何交集,该列表保持为空。

交集访问器(osgUtil::IntersectVisitor)- 射线与场景中几何体的交集测试由交集访问器来创建并实现初始化。IntersectionVisitor类继承自NodeVisitor类,因此其创建和触发机制与NodeVisitor实例大致相似。访问器需要维护一个进行交集测试的线段列表。而对于其中的每一条线段,访问器都会创建一个交点列表(osgUtil::IntersectVisitor::HitList实例)。

代码:
为了正确获取放置灌木模型所需的地形高度值,我们需要遵循下面的基本步骤:

创建一个LineSegment实例,它使用两个Vec3实例来定义交集测试所用射线的起点和终点。
创建一个IntersectVisitor实例。
将LineSegment实例添加到IntersectVisitor实例。
初始化IntersectVisitor实例,使其从场景图形中适当的节点开始遍历。
获取交集测试结果的世界坐标。
下面的代码演示了上述步骤的两种实现方式。第一部分是将单个LineSegment添加到IntersectVisitor,以判断坦克模型的放置高度是否正确。第二部分是将与告示牌节点相关的多个线段添加到IntersectVisitor,每一个线段实例均对应一个灌木告示牌对象。针对每个LineSegment执行与其关联的交集测试之后,我们既可正确地放置告示牌的位置了。

#include <ctime>
#include <cstdlib>
#include <iostream>
#include <osg/Geometry>
#include <osg/Texture2D>
#include <osg/Billboard>
#include <osg/BlendFunc>
#include <osg/AlphaFunc>
#include <osg/PositionAttitudeTransform>
#include <osgUtil/IntersectVisitor>
#include <osgDB/Registry>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
#include <osgGA/TrackballManipulator>
#include <osgDB/FileUtils>

#define NUMBER_O_SHRUBS 1000

osg::Drawable* createShrub(const float & scale, osg::StateSet* bbState)
{
   float width = 1.5f;
   float height = 3.0f;

   width *= scale;
   height *= scale;

   osg::Geometry* shrubQuad = new osg::Geometry;

   osg::Vec3Array* shrubVerts = new osg::Vec3Array(4);
   (*shrubVerts)[0] = osg::Vec3(-width/2.0f, 0, 0);
   (*shrubVerts)[1] = osg::Vec3( width/2.0f, 0, 0);
   (*shrubVerts)[2] = osg::Vec3( width/2.0f, 0, height);
   (*shrubVerts)[3] = osg::Vec3(-width/2.0f, 0, height);

   shrubQuad->setVertexArray(shrubVerts);

   osg::Vec2Array* shrubTexCoords = new osg::Vec2Array(4);
   (*shrubTexCoords)[0].set(0.0f,0.0f);
   (*shrubTexCoords)[1].set(1.0f,0.0f);
   (*shrubTexCoords)[2].set(1.0f,1.0f);
   (*shrubTexCoords)[3].set(0.0f,1.0f);
   shrubQuad->setTexCoordArray(0,shrubTexCoords);

   shrubQuad->addPrimitiveSet(new osg::DrawArrays osg::PrimitiveSet::QUADS,0,4));

   shrubQuad->setStateSet(bbState);

   return shrubQuad;
}

int main( int argc, char **argv )
{
   osgViewer::Viewer viewer;

   osg::Group* rootNode = new osg::Group();
   osg::Node* tankNode = NULL;
   osg::Node* terrainNode = NULL;
   osg::PositionAttitudeTransform* tankXform =
      new osg::PositionAttitudeTransform();

   tankNode = osgDB::readNodeFile("//Models//T72-Tank//T72-tank_des.flt");
   terrainNode = osgDB::readNodeFile("//Models//JoeDirt//JoeDirt.flt");
   if (! (tankNode && terrainNode))
   {
      std::cout << "Couldn't load models, quitting." << std::endl;
      return -1;
   }
   rootNode->addChild(terrainNode);
   rootNode->addChild(tankXform);
   tankXform->addChild(tankNode);

 

 

12月19日
Navy16 - 交集测试 (2)
下面将获取地形模型在给定X,Y位置的高度值,并放置坦克模型。

   double tankXPosition = -10.0;
   double tankYPosition = -10.0;

   osg::LineSegment* tankLocationSegment = new osg::LineSegment();
   tankLocationSegment->set(
      osg::Vec3(tankXPosition, tankYPosition, 999) ,
      osg::Vec3(tankXPosition, tankYPosition, -999) );

   osgUtil::IntersectVisitor findTankElevationVisitor;
   findTankElevationVisitor.addLineSegment(tankLocationSegment);
   terrainNode->accept(findTankElevationVisitor);

   osgUtil::IntersectVisitor::HitList tankElevationLocatorHits;
   tankElevationLocatorHits =
      findTankElevationVisitor.getHitList(tankLocationSegment);
   osgUtil::Hit heightTestResults;
   if ( tankElevationLocatorHits.empty() )
   {
      std::cout << " couldn't place tank on terrain" << std::endl;
      return -1;
   }
   heightTestResults = tankElevationLocatorHits.front();
   osg::Vec3d terrainHeight = heightTestResults.getWorldIntersectPoint();

   tankXform->setPosition( terrainHeight );
   tankXform->setAttitude(
      osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) );

下面的代码将设置视口,告示牌节点,以及渲染状态。

   viewer.setCameraManipulator(new osgGA::TrackballManipulator());
   viewer.setSceneData(rootNode);
   viewer.realize();

   osg::Billboard* shrubBillBoard = new osg::Billboard();
   rootNode->addChild(shrubBillBoard);

   shrubBillBoard->setMode(osg::Billboard::AXIAL_ROT);
   shrubBillBoard->setAxis(osg::Vec3(0.0f,0.0f,1.0f));
   shrubBillBoard->setNormal(osg::Vec3(0.0f,-1.0f,0.0f));

   osg::Texture2D *ocotilloTexture = new osg::Texture2D;
   ocotilloTexture->setImage(osgDB::readImageFile("images//ocotillo.png"));

   osg::StateSet* billBoardStateSet = new osg::StateSet;

   billBoardStateSet->setMode( GL_LIGHTING, osg::StateAttribute::OFF );
   billBoardStateSet->setTextureAttributeAndModes(0, ocotilloTexture, osg::StateAttribute::ON );
   billBoardStateSet->setAttributeAndModes( new osg::BlendFunc, osg::StateAttribute::ON );

   osg::AlphaFunc* alphaFunction = new osg::AlphaFunc;
   alphaFunction->setFunction(osg::AlphaFunc::GEQUAL,0.05f);
   billBoardStateSet->setAttributeAndModes( alphaFunction, osg::StateAttribute::ON );

按照上面所述的检测射线和地形模型碰撞交点的基本步骤,现在我们将向IntersectVisitor添加多个LineSegment实例。每条射线的交集测试结果(交点位置)将用于放置相应的告示牌。

   srand(time(0)); // 初始化随机数生成器。

   osgUtil::IntersectVisitor isectVisitor;
   osg::LineSegment* terrainIsect[NUMBER_O_SHRUBS];

   int randomX, randomY;

   for (int i=0; i< NUMBER_O_SHRUBS; i++ )
   {
      randomX = (rand() % 100) + 1;
      randomY = (rand() % 100) + 1;
      terrainIsect[i] = new osg::LineSegment(
         osg::Vec3(randomX, randomY, 999) ,
         osg::Vec3(randomX, randomY, -999) );
      isectVisitor.addLineSegment(terrainIsect[i]);
   }
   terrainNode->accept(isectVisitor);

   osg::Drawable* shrubDrawable[NUMBER_O_SHRUBS];

   for (int j = 0 ; j < NUMBER_O_SHRUBS; j ++)
   {
      float randomScale = ((rand() % 15) + 1 ) / 10.0;
      shrubDrawable[j] = createShrub( randomScale, billBoardStateSet);
      osgUtil::IntersectVisitor::HitList hitList = isectVisitor.getHitList(terrainIsect[j]);
      if (! hitList.empty() )
      {
         osgUtil::Hit firstHit = hitList.front();
         osg::Vec3d shrubPosition = firstHit.getWorldIntersectPoint();

         // osg::Vec3d shrubPosition =
         // isectVisitor.getHitList(terrainIsect[j]).front().getWorldIntersectPoint();

         shrubBillBoard->addDrawable( shrubDrawable[j] , shrubPosition );
      }
   }

   while( !viewer.done() )
   {
      viewer.frame();
   }
   return 0;
}

编程愉快!

 

Navy17.1 - 向场景中添加osgParticle粒子效果 (1)
目的:
向场景中添加自定义的osgParticle实例,模拟坦克模型在地形上运动时产生的烟尘。

 

-----------------------------

概述:
添加粒子效果可以有效提高仿真程序的外观和真实性。粒子引擎一般用于模拟烟雾,火焰,尘埃以及其他一些类似的效果。如果要向OSG场景中添加粒子效果,通常可以使用下面的一些类:

粒子系统(osgParticle::ParticleSystem)- 维护并管理一系列粒子的生成,更新,渲染和销毁。粒子系统类继承自Drawable类。粒子的渲染控制因此与其它Drawable对象的渲染类似:控制其渲染属性StateAttribute即可。OSG提供了一个方便的函数以允许用户控制三个常用的渲染状态属性。方法setDefaultAttributes可以用于指定材质(或者指定为NULL以禁用材质),允许/禁止附加的图像融合,允许/禁止光照。

粒子(osgParticle::Particle)- 粒子系统的基本单元。粒子类同时具有物理属性和图像属性。它的形状可以是任意的点(POINT),四边形(QUAD),四边形带(QUAD_TRIPSTRIP),六角形(HEXAGON)或者线(LINE)。每个粒子都有自己的生命周期。生命周期也就是每个粒子可以存活的秒数。(生命周期为负数的粒子可以存活无限长时间)所有的粒子都具有大小(SIZE),ALPHA值和颜色(COLOR)属性。每一组粒子都可以指定其最大和最小值。为了便于粒子生命周期的管理,粒子系统通过改变生命周期的最大和最小值来控制单个粒子的渲染。(根据已经消耗的时间,在最小和最大值之间进行线性插值)
程序员也可以自行指定最小到最大值的插值方法。(参见osgParticle::Interpolator的代码)

放置器(osgParticle::Placer)- 设置粒子的初始位置。用户可以使用预定义的放置器或者定义自己的放置器。已定义的放置器包括:点放置器PointPlacer(所有的粒子从同一点出生),扇面放置器SectorPlacer(所有的粒子从一个指定中心点,半径范围和角度范围的扇面出生),以及多段放置器MultiSegmentPlacer(用户指定一系列的点,粒子沿着这些点定义的线段出生)。

发射器(osgParticle::Shooter)- 指定粒子的初始速度。RadialShooter类允许用户指定一个速度范围(米/秒)以及弧度值表示的方向。方向由两个角度指定:theta角 - 与Z轴夹角,phi角 - 与XY平面夹角。

计数器(osgParticle::Counter)- 控制每一帧产生的粒子数。RandomRateCounter类允许用户指定每帧产生粒子的最大和最小数目。

标准放射极(osgParticle::ModularEmitter)- 一个标准放射极包括一个计数器,一个放置器和一个发射器。它为用户控制粒子系统中多个元素提供了一个标准机制。

粒子系统更新器(osgParticle::ParticleSystemUpdater)- 用于自动更新粒子。将其置于场景中时,它会在拣选遍历中调用所有“存活”粒子的更新方法。

标准编程器(osgParticle::ModularProgram)- 在单个粒子的生命周期中,用户可以使用ModularProgram实例控制粒子的位置。ModularProgram需要与Operator对象组合使用。

计算器(osgParticle::Operator)- 提供了控制粒子在其生命周期中的运动特性的方法。用户可以改变现有Operator类实例的参数,或者定义自己的Operator类。OSG提供的Operator类包括:AccelOperator(应用常加速度),AngularAccelOperator(应用常角加速度),FluidFrictionOperator(基于指定密度和粘性的流体运动进行计算),以及ForceOperator(应用常力)。

代码:
为了使用上面的类创建高度自定义化的粒子系统,我们可以遵循以下的步骤。(括号中的步骤是可选的,如果所建立的粒子系统比较基本,也可以忽略)

创建粒子系统实例并将其添加到场景。
(设置粒子系统的渲染状态属性。)
创建粒子对象并将其关联到粒子系统。
(设置粒子的参数。)
创建粒子系统更新器,将其关联到粒子系统实例,并添加到场景中。
(创建标准放射极,以定义:计数器 - 每帧创建的粒子数,放置器 - 粒子的出生位置,发射器 - 初始速度。)
(将标准放射极关联到粒子系统。)
(创建ModularProgram标准编程器实例,以控制粒子在生命周期中的运动:首先创建并定义Operator计算器;然后添加计算器到标准编程器。)
下面的代码将完成上述的步骤。首先,我们需要建立基本的坦克和地形模型。(稍后我们再添加坦克模型到场景中)

osg::Group* rootNode = new osg::Group();

osg::Node* terrainNode = new osg::Node();
osgViewer::Viewer viewer;

terrainNode = osgDB::readNodeFile("//Models//JoeDirt//JoeDirt.flt");
if (! terrainNode)
{
   std::cout << "Couldn't load models, quitting." << std::endl;
   return -1;
}
rootNode->addChild(terrainNode);

osg::Node* tankNode = osgDB::readNodeFile("//Models//T72-Tank//T72-tank_des.flt");
if ( ! tankNode)
{
   std::cout << "no tank" << std::endl;
   return -1;
}

 

12月21日
Navy17.1 - 向场景中添加osgParticle粒子效果 (2)
建立粒子系统的基本步骤是:创建一个粒子系统对象,一个更新器对象,以及一个粒子对象,同时设置场景。

// 创建并初始化粒子系统。
osgParticle::ParticleSystem *dustParticleSystem = new osgParticle::ParticleSystem;

// 设置材质,是否放射粒子,以及是否使用光照。
dustParticleSystem->setDefaultAttributes(dust2.rgb", false, false);

// 由于粒子系统类继承自Drawable类,因此我们可以将其作为Geode的子节点加入场景。
osg::Geode *geode = new osg::Geode;

rootNode->addChild(geode);
geode->addDrawable(dustParticleSystem);

// 添加更新器,以实现每帧的粒子管理。
osgParticle::ParticleSystemUpdater *dustSystemUpdater = new osgParticle::ParticleSystemUpdater;

// 将更新器与粒子系统对象关联。
dustSystemUpdater->addParticleSystem(dustParticleSystem);
// 将更新器节点添加到场景中。
rootNode->addChild(dustSystemUpdater);

// 创建粒子对象,设置其属性并交由粒子系统使用。
osgParticle::Particle smokeParticle;
smokeParticle.setSizeRange(osgParticle::rangef(0.01,20.0)); // 单位:米
smokeParticle.setLifeTime(4); // 单位:秒
smokeParticle.setMass(0.01); // 单位:千克
// 设置为粒子系统的缺省粒子对象。
dustParticleSystem->setDefaultParticleTemplate(smokeParticle);

下面的代码将使用标准放射极对象来设置粒子的一些基本参数:例如每帧创建的粒子数,新生粒子的产生位置,以及新生粒子的速度。

// 创建标准放射极对象。(包括缺省的计数器,放置器和发射器)
osgParticle::ModularEmitter *emitter = new osgParticle::ModularEmitter;

// 将放射极对象与粒子系统关联。
emitter->setParticleSystem(dustParticleSystem);

// 获取放射极中缺省计数器的句柄,调整每帧增加的新粒子数目。
osgParticle::RandomRateCounter *dustRate =
   static_cast<osgParticle::RandomRateCounter *>(emitter->getCounter());
dustRate->setRateRange(5, 10); // 每秒新生成5到10个新粒子。

// 自定义一个放置器,这里我们创建并初始化一个多段放置器。
osgParticle::MultiSegmentPlacer* lineSegment = new osgParticle::MultiSegmentPlacer();

// 向放置器添加顶点,也就是定义粒子产生时所处的线段位置。
// (如果将坦克和标准放射极定义与同一位置,那么我们可以实现一种
// 灰尘粒子从坦克模型后下方的延长线上产生的效果。)
lineSegment->addVertex(0,0,-2);
lineSegment->addVertex(0,-2,-2);
lineSegment->addVertex(0,-16,0);

// 为标准放射极设置放置器。
emitter->setPlacer(lineSegment);

// 自定义一个发射器,这里我们创建并初始化一个RadialShooter弧度发射器。
osgParticle::RadialShooter* smokeShooter = new osgParticle::RadialShooter();

// 设置发射器的属性。
smokeShooter->setThetaRange(0.0, 3.14159/2); // 弧度值,与Z轴夹角。
smokeShooter->setInitialSpeedRange(50,100); // 单位:米/秒

// 为标准放射极设置发射器。
emitter->setShooter(smokeShooter);

现在我们将把放射极和坦克模型作为Transform变换节点的子节点添加到场景中。放射极和坦克均由变换节点决定其位置。刚才定义的放置器将会根据变换的参量安排粒子的位置。

osg::MatrixTransform * tankTransform = new osg::MatrixTransform();
tankTransform->setUpdateCallback( new orbit() ); // 回调函数,使节点环向运动。

// 把放射极和坦克模型添加为变换节点的子节点。
tankTransform->addChild(emitter);
tankTransform->addChild(tankNode);
rootNode->addChild(tankTransform);

下面的代码将创建一个标准编程器ModularProgram实例,用于控制粒子在生命周期中的更新情况。标准编程器对象使用Operator计算器来实现对粒子的控制。

// 创建标准编程器对象并与粒子系统相关联。
osgParticle::ModularProgram *moveDustInAir = new osgParticle::ModularProgram;
moveDustInAir->setParticleSystem(dustParticleSystem);

// 创建计算器对象,用于模拟重力的作用,调整其参数并添加给编程器对象。
osgParticle::AccelOperator *accelUp = new osgParticle::AccelOperator;
accelUp->setToGravity(-1); // 设置重力加速度的放缩因子。
moveDustInAir->addOperator(accelUp);

// 向编程器再添加一个计算器对象,用于计算空气阻力。
osgParticle::FluidFrictionOperator *airFriction = new osgParticle::FluidFrictionOperator;
airFriction->setFluidToAir();
moveDustInAir->addOperator(airFriction);

// 最后,将编程器添加到场景中。
rootNode->addChild(moveDustInAir);

下面的代码就是仿真循环的内容了。

viewer.setCameraManipulator(new osgGA::TrackballManipulator());
viewer.setSceneData(rootNode);
viewer.realize();

while( !viewer.done() )
{
   viewer.frame();
}
return 0;

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/doing5552/archive/2008/09/01/2859545.aspx

你可能感兴趣的:(null,Graph,Blend,图形,Matrix,shader)