例:geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));php
來指定要利用這些數據生成一個怎麼樣的形狀。html
該行代碼中,使用DrawArrays類向Geometry類送入了新幾何體的信息,即,該幾何體是一個QUADS,它的頂點座標從索引數組中讀入,從第1個索引值開始,共讀入4個索引值,組成一個四邊形圖形。node
幾何體的形狀參數除了QUADS以外,還有數種方式,以用於不一樣的用戶需求,列表以下:ios
POINTSc++ |
繪製點算法 |
繪製用戶指定的全部頂點。數據庫 |
LINESwindows |
繪製直線api |
直線的起點、終點由數組中前後相鄰的兩個點決定;用戶提供的點不止兩個時,將嘗試繼續繪製新的直線。數組 |
LINE_STRIP |
繪製多段直線 |
多段直線的第一段由數組中的前兩個點決定;其他段的起點位置爲上一段的終點座標,而終點位置由數組中隨後的點決定。 |
LINE_LOOP |
繪製封閉直線 |
繪圖方式與多段直線相同,可是最後將自動封閉該直線。 |
TRIANGLES |
繪製三角形 |
三角形的三個頂點由數組中相鄰的三個點決定,並按照逆時針的順序進行繪製;用戶提供的點不止三個時,將嘗試繼續繪製新的三角形。 |
TRIANGLE_STRIP |
繪製多段三角形 |
第一段三角形的由數組中的前三個點決定;其他段三角形的繪製,起始邊由上一段三角形的後兩個點決定,第三點由數組中隨後的一點決定。 |
TRIANGLE_FAN |
繪製三角扇面 |
第一段三角形的由數組中的前三個點決定;其他段三角形的繪製,起始邊由整個數組的第一點和上一段三角形的最後一個點決定,第三點由數組中隨後的一點決定。 |
QUADS |
繪製四邊形 |
四邊形的四個頂點由數組中相鄰的四個點決定,並按照逆時針的順序進行繪製;用戶提供的點不止四個時,將嘗試繼續繪製新的四邊形。 |
QUAD_STRIP |
繪製多段四邊形 |
第一段四邊形的起始邊由數組中的前兩個點決定,邊的矢量方向由這兩點的延伸方向決定;起始邊的對邊由其後的兩個點決定,若是起始邊和對邊的矢量方向不一樣,那麼四邊形將會扭曲;其他段四邊形的繪製,起始邊由上一段決定,其對邊由隨後的兩點及其延伸方向決定。 |
POLYGON |
繪製任意多邊形 |
根據用戶提供的頂點的數量,繪製多邊形。 |
和opengl對比:
osg::PrimitiveSet::POINTS對應OpenGL中的GL_POINTS繪製單獨的點
osg::PrimitiveSet::LINES對應OpenGL中的GL_LINES繪製每兩點鏈接的線
osg::PrimitiveSet::LINE_STRIP對應OpenGL中的GL_LINE_STRIP繪製依次鏈接各點的線
osg::PrimitiveSet::LINE_LOOP對應OpenGL中的GL_LINE_LOOP繪製依次鏈接各點的線,首尾相連
osg::PrimitiveSet::POLYGON對應OpenGL中的GL_POLYGON繪製依次鏈接各點的多邊形
osg::PrimitiveSet::QUADS對應OpenGL中的GL_QUADS繪製依次鏈接每四點的四邊形
如:一、二、三、四、五、六、七、8點 繪製結果一、二、三、4組成四邊形,五、六、七、8組成四邊形
osg::PrimitiveSet::QUAD_STRIP對應OpenGL中的GL_QUAD_STRIP繪製四邊形
如:一、二、三、四、五、六、七、8點 繪製結果一、二、三、4組成四邊形,三、四、五、6組成四邊形、五、
六、七、8組成四邊形
osg::PrimitiveSet::TRIANGLES對應OpenGL中的GL_TRIANGLES繪製每三點鏈接的三角形
如:一、二、三、四、五、6點 繪製結果一、二、3組成三角形,四、五、6組成三角形
osg::PrimitiveSet::TRIANGLE_STRIP對應OpenGL中的GL_TRIANGLE_STRIP
如:一、二、三、四、五、6點 繪製結果一、二、3組成三角形,二、三、4組成三角形,三、四、5組成三角
形四、五、6組成三角形
osg::PrimitiveSet::TRIANGLE_FAN對應OpenGL中的GL_TRIANGLE_FAN
如:一、二、三、四、五、6點 繪製結果一、二、3組成三角形,一、三、4組成三角形,一、四、5組成三角
形,一、五、6組成三角形
目錄
7.搜索並控制開關和DOF(自由度)節點(Finding and Manipulating a Switch and DOF Node)... 20
10.使用自定義矩陣來放置相機(Positioning a Camera with a User-Defined Matrix)... 36
本節涵蓋了生成基本幾何形狀的一些方法。生成幾何物體的方法有這麼幾種:在最底層對OpenGL基本幾何進行鬆散的包裝,中級是使用Open Scene Graph的基本形狀,以及更高級一些的從文件讀取。這篇教程涵蓋的是最低層的。這種方法彈性最大但最費力。一般在Scene Graph級別,幾何形狀是從文件加載的。文件加載器完成了跟蹤頂點的大部分工做。
對一下幾個類的簡單解釋:
Geode類:
geode類繼承自node類。在一個Scene Graph中,node(固然包含geode)能夠做爲葉子節點。Geode實例能夠有多個相關的drawable。
Drawable類層次:
基類drawable是一個有六個具體子類的抽象類。
geometry類能夠直接有vertex和vertex數據,或者任意個primitiveSet實例。
vertex和vertex屬性數據(顏色、法線、紋理座標)存放在數組中。既然多個頂點能夠共享相同的顏色、法線或紋理座標,那麼數組索引就能夠用來將頂點數組映射到顏色、法線、或紋理座標數組。
PrimitiveSet類:
這個類鬆散的包裝了OpenGL的基本圖形-POINTS,LINES,LINE_STRIP,LINE_LOOP,...,POLYGON.
如下這節代碼安裝了一個viewer來觀察咱們建立的場景,一個‘group’實例做爲scene graph的根節點,一個幾何節點(geode)來收集drawable,和一個geometry實例來關聯頂點和頂點數據。(這個例子中渲染的形狀是一個四面體)
...
int main()
{
...
osgProducer::Viewer viewer;
osg::Group* root = new osg::Group();
osg::Geode* pyramidGeode = new osg::Geode();
osg::Geometry* pyramidGeometry = new osg::Geometry();
下一步,須要將錐體geometry和錐體geode關聯起來,並將pyramid geode加到scene graph的根節點上。
pyramidGeode->addDrawable(pyramidGeometry);
root->addChild(pyramidGeode);
聲明一個頂點數組。每一個頂點由一個三元組表示——vec3類的實例。這些三元組用osg::Vec3Array類的實例存貯。既然osg::Vec3Array繼承自STL的vector類,那麼咱們就可使用push_back方法來添加數組成員。push_back將元素加到向量的尾端,所以第一個元素的索引是0,第二個是1,依此類推。
使用‘z’軸向上的右手座標系系統,下面的0...4數組元素表明着產生一個簡單錐體所需的5個點。
osg::Vec3Array* pyramidVertices = new osg::Vec3Array;
pyramidVertices->push_back( osg::Vec3( 0, 0, 0) ); // front left
pyramidVertices->push_back( osg::Vec3(10, 0, 0) ); // front right
pyramidVertices->push_back( osg::Vec3(10,10, 0) ); // back right
pyramidVertices->push_back( osg::Vec3( 0,10, 0) ); // back left
pyramidVertices->push_back( osg::Vec3( 5, 5,10) ); // peak
將這個頂點集合和與咱們加到場景中的geode相關的geometry關聯起來。
pyramidGeometry->setVertexArray( pyramidVertices );
下一步,產生一個基本集合並將其加入到pyramid geometry中。使用pyramid的前四個點經過DrawElementsUint類的實例來定義基座。這個類也繼承自STL的vector,因此push_back方法會順序添加元素。爲了保證合適的背面剔除,頂點的順序應當是逆時針方向的。構造器的參數是基本的枚舉類型(和opengl的基本枚舉類型一致),和起始的頂點數組索引。
osg::DrawElementsUInt* pyramidBase =
new osg::DrawElementsUInt(osg::PrimitiveSet::QUADS, 0);
pyramidBase->push_back(3);
pyramidBase->push_back(2);
pyramidBase->push_back(1);
pyramidBase->push_back(0);
pyramidGeometry->addPrimitiveSet(pyramidBase);
對每一個面重復相同的動做。頂點仍要按逆時針方向指定。
osg::DrawElementsUInt* pyramidFaceOne =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceOne->push_back(0);
pyramidFaceOne->push_back(1);
pyramidFaceOne->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceOne);
osg::DrawElementsUInt* pyramidFaceTwo =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceTwo->push_back(1);
pyramidFaceTwo->push_back(2);
pyramidFaceTwo->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceTwo);
osg::DrawElementsUInt* pyramidFaceThree =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceThree->push_back(2);
pyramidFaceThree->push_back(3);
pyramidFaceThree->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceThree);
osg::DrawElementsUInt* pyramidFaceFour =
new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES, 0);
pyramidFaceFour->push_back(3);
pyramidFaceFour->push_back(0);
pyramidFaceFour->push_back(4);
pyramidGeometry->addPrimitiveSet(pyramidFaceFour)
聲明並加載一個vec4爲元素的數組來存儲顏色。
osg::Vec4Array* colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) ); //index 0 red
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f) ); //index 1 green
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f) ); //index 2 blue
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); //index 3 white
聲明的這個變量能夠將頂點數組元素和顏色數組元素匹配起來。這個容器的元素數應當和頂點數一致。這個容器是頂點數組和顏色數組的鏈接。這個索引數組中的條目就對應着頂點數組中的元素。他們的值就是顏色數組中的索引。頂點數組元素與normal和紋理座標數組的匹配也是遵循這種模式。
注意,這種狀況下,咱們將5個頂點指定4種顏色。頂點數組的0和4元素都被指定爲顏色數組的0元素。
osg::TemplateIndexArray
<unsigned int, osg::Array::UIntArrayType,4,4> *colorIndexArray;
colorIndexArray =
new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,4>;
colorIndexArray->push_back(0); // vertex 0 assigned color array element 0
colorIndexArray->push_back(1); // vertex 1 assigned color array element 1
colorIndexArray->push_back(2); // vertex 2 assigned color array element 2
colorIndexArray->push_back(3); // vertex 3 assigned color array element 3
colorIndexArray->push_back(0); // vertex 4 assigned color array element 0
下一步,將顏色數組和geometry關聯起來,將上面產生的顏色索引指定給geometry,設定綁定模式爲_PER_VERTEX。
pyramidGeometry->setColorArray(colors);
pyramidGeometry->setColorIndices(colorIndexArray);
pyramidGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
osg::Vec2Array* texcoords = new osg::Vec2Array(5);
(*texcoords)[0].set(0.00f,0.0f);
(*texcoords)[1].set(0.25f,0.0f);
(*texcoords)[2].set(0.50f,0.0f);
(*texcoords)[3].set(0.75f,0.0f);
(*texcoords)[4].set(0.50f,1.0f);
pyramidGeometry->setTexCoordArray(0,texcoords);
注:一下部分可能錯誤。
// Declare and initialize a transform node.
osg::PositionAttitudeTransform* pyramidTwoXForm =
new osg::PositionAttitudeTransform();
// Use the 'addChild' method of the osg::Group class to
// add the transform as a child of the root node and the
// pyramid node as a child of the transform.
root->addChild(pyramidTwoXForm);
pyramidTwoXForm->addChild(pyramidGeode);
// Declare and initialize a Vec3 instance to change the
// position of the tank model in the scene
osg::Vec3 pyramidTwoPosition(15,0,0);
pyramidTwoXForm->setPosition( pyramidTwoPosition );
既然咱們生成了一個geometry節點並將它加到了場景中,咱們就能夠重用這個geometry。例如,若是咱們想讓另外一個pyramid在第一個的右側15個單位處,咱們就能夠在咱們的scene graph中將這個geode加到transform節點的子節點上。
最後一步,創建並進入一個仿真循環。
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
爲教程1中介紹的由OpenGL基本繪製單位定義的幾何體添加紋理。
前一節教程介紹了包含由OpenGL基本單位產生的基本形狀的視景。本節講解如何爲這些形狀添加紋理。爲了使代碼更方便使用,咱們將pyramid的代碼放到一個函數中,產生geode並返回它的指針。下面的代碼來自教程1。
osg::Geode* createPyramid()
{
osg::Geode* pyramidGeode = new osg::Geode();
osg::Geometry* pyramidGeometry = new osg::Geometry();
pyramidGeode->addDrawable(pyramidGeometry);
// Specify the vertices:
osg::Vec3Array* pyramidVertices = new osg::Vec3Array;
pyramidVertices->push_back( osg::Vec3(0, 0, 0) ); // front left
pyramidVertices->push_back( osg::Vec3(2, 0, 0) ); // front right
pyramidVertices->push_back( osg::Vec3(2, 2, 0) ); // back right
pyramidVertices->push_back( osg::Vec3( 0,2, 0) ); // back left
pyramidVertices->push_back( osg::Vec3( 1, 1,2) ); // peak
// Associate this set of vertices with the geometry associated with the
// geode we added to the scene.
pyramidGeometry->setVertexArray( pyramidVertices );
// Create a QUAD primitive for the base by specifying the
// vertices from our vertex list that make up this QUAD:
osg::DrawElementsUInt* pyramidBase =
new osg::DrawElementsUInt(osg::PrimitiveSet::QUADS, 0);
pyramidBase->push_back(3);
pyramidBase->push_back(2);
pyramidBase->push_back(1);
pyramidBase->push_back(0);
//Add this primitive to the geometry:
pyramidGeometry->addPrimitiveSet(pyramidBase);
// code to create other faces goes here!
// (removed to save space, see tutorial two)
osg::Vec4Array* colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) ); //index 0 red
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f) ); //index 1 green
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f) ); //index 2 blue
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); //index 3 white
osg::TemplateIndexArray
<unsigned int, osg::Array::UIntArrayType,4,4> *colorIndexArray;
colorIndexArray =
new osg::TemplateIndexArray<unsigned int, osg::Array::UIntArrayType,4,4>;
colorIndexArray->push_back(0); // vertex 0 assigned color array element 0
colorIndexArray->push_back(1); // vertex 1 assigned color array element 1
colorIndexArray->push_back(2); // vertex 2 assigned color array element 2
colorIndexArray->push_back(3); // vertex 3 assigned color array element 3
colorIndexArray->push_back(0); // vertex 4 assigned color array element 0
pyramidGeometry->setColorArray(colors);
pyramidGeometry->setColorIndices(colorIndexArray);
pyramidGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
// Since the mapping from vertices to texture coordinates is 1:1,
// we don't need to use an index array to map vertices to texture
// coordinates. We can do it directly with the 'setTexCoordArray'
// method of the Geometry class.
// This method takes a variable that is an array of two dimensional
// vectors (osg::Vec2). This variable needs to have the same
// number of elements as our Geometry has vertices. Each array element
// defines the texture coordinate for the cooresponding vertex in the
// vertex array.
osg::Vec2Array* texcoords = new osg::Vec2Array(5);
(*texcoords)[0].set(0.00f,0.0f); // tex coord for vertex 0
(*texcoords)[1].set(0.25f,0.0f); // tex coord for vertex 1
(*texcoords)[2].set(0.50f,0.0f); // ""
(*texcoords)[3].set(0.75f,0.0f); // ""
(*texcoords)[4].set(0.50f,1.0f); // ""
pyramidGeometry->setTexCoordArray(0,texcoords);
return pyramidGeode;
}
渲染基本單位的方法是使用StateSet。這節代碼演示了怎樣從文件中加載紋理,產生此紋理起做用的一個StateSet,並將這個StateSet附加到場景中的一個節點上。前面開始的代碼和上一節教程中的同樣,初始化一個viewer並創建有一個pyramid的場景。
int main()
{
osgProducer::Viewer viewer;
// Declare a group to act as root node of a scene:
osg::Group* root = new osg::Group();
osg::Geode* pyramidGeode = createPyramid();
root->addChild(pyramidGeode);
如今,準備加紋理。這裏咱們會聲明一個紋理實例並將它的數據不一致性設爲‘DYNAMIC'。(若是不把紋理聲明爲dynamic,osg的一些優化程序會刪除它。)這個texture類包裝了OpenGL紋理模式(wrap,filter,等等)和一個osg::Image。下面的代碼說明了如何從文件裏讀取osg::Image實例並把這個圖像和紋理關聯起來。
osg::Texture2D* KLN89FaceTexture = new osg::Texture2D;
// protect from being optimized away as static state:
KLN89FaceTexture->setDataVariance(osg::Object::DYNAMIC);
// load an image by reading a file:
osg::Image* klnFace = osgDB::readImageFile("KLN89FaceB.tga");
if (!klnFace)
{
std::cout << " couldn't find texture, quiting." << std::endl;
return -1;
}
// Assign the texture to the image we read from file:
KLN89FaceTexture->setImage(klnFace);
紋理能夠和渲染StateSet關聯起來。下一步就產生一個StateSet,關聯並啓動咱們的紋理,並將這個StateSet附加到咱們的geometry上。
// Create a new StateSet with default settings:
osg::StateSet* stateOne = new osg::StateSet();
// Assign texture unit 0 of our new StateSet to the texture
// we just created and enable the texture.
stateOne->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
// Associate this state set with the Geode that contains
// the pyramid:
pyramidGeode->setStateSet(stateOne);
最後一步是仿真循環:
//The final step is to set up and enter a simulation loop.
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
return 0;
}
用osg::Shape實例構建場景。使用osg::StateSet控制shape的渲染。
Shape類是全部形狀類別的基類。Shape既可用於剪裁和碰撞檢測也可用於定義程序性地產生幾何體的那些基本形狀。下面的類繼承自Shape類:
TriangleMesh
Sphere
InfinitePlane
HeightField
Cylinder
Cone
CompositeShape
Box
爲了使這些形狀能夠被渲染,咱們須要把他們和Drawable類的實例關聯起來。ShapeDrawable類提供了這樣的功能。這個類繼承自Drawable並容許咱們把Shape實例附加到能夠被渲染的東西上。既然ShapeDrawable類繼承自Drawable,ShapDrawable實例就能夠被加到Geode類實例上。下面的步驟演示了將一個單位立方體加到空場景中時是如何作到這些的。
// Declare a group to act as root node of a scene:
osg::Group* root = new osg::Group();
// Declare a box class (derived from shape class) instance
// This constructor takes an osg::Vec3 to define the center
// and a float to define the height, width and depth.
// (an overloaded constructor allows you to specify unique
// height, width and height values.)
osg::Box* unitCube = new osg::Box( osg::Vec3(0,0,0), 1.0f);
// Declare an instance of the shape drawable class and initialize
// it with the unitCube shape we created above.
// This class is derived from 'drawable' so instances of this
// class can be added to Geode instances.
osg::ShapeDrawable* unitCubeDrawable = new osg::ShapeDrawable(unitCube);
// Declare a instance of the geode class:
osg::Geode* basicShapesGeode = new osg::Geode();
// Add the unit cube drawable to the geode:
basicShapesGeode->addDrawable(unitCubeDrawable);
// Add the goede to the scene:
root->addChild(basicShapesGeode);
產生一個球體和上面的代碼基本類似。沒有太多的註釋的代碼看起來是這個樣子:
// Create a sphere centered at the origin, unit radius:
osg::Sphere* unitSphere = new osg::Sphere( osg::Vec3(0,0,0), 1.0);
osg::ShapeDrawable* unitSphereDrawable=new osg::ShapeDrawable(unitSphere);
如今,咱們可使用transform節點將這個球體加到場景中,以便讓它離開原點。unitSphereDrawable不能直接添加到場景中(由於它不是繼承自node類),因此咱們須要一個新的geode以便添加它。
osg::PositionAttitudeTransform* sphereXForm =
new osg::PositionAttitudeTransform();
sphereXForm->setPosition(osg::Vec3(2.5,0,0));
osg::Geode* unitSphereGeode = new osg::Geode();
root->addChild(sphereXForm);
sphereXForm->addChild(unitSphereGeode);
unitSphereGeode->addDrawable(unitSphereDrawable);
前面的教程講解了如何生成紋理,將其指定爲從文件加載的圖像,生成一個帶紋理的StateSet。下面的代碼創建了兩個狀態集合——一個是BLEND紋理模式,另外一個是DECAL紋理模式。BLEND模式:
// Declare a state set for 'BLEND' texture mode
osg::StateSet* blendStateSet = new osg::StateSet();
// Declare a TexEnv instance, set the mode to 'BLEND'
osg::TexEnv* blendTexEnv = new osg::TexEnv;
blendTexEnv->setMode(osg::TexEnv::BLEND);
// Turn the attribute of texture 0 - the texture we loaded above - 'ON'
blendStateSet->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
// Set the texture texture environment for texture 0 to the
// texture envirnoment we declared above:
blendStateSet->setTextureAttribute(0,blendTexEnv);
重複這些步驟,產生DECAL紋理模式的狀態集合。
osg::StateSet* decalStateSet = new osg::StateSet();
osg::TexEnv* decalTexEnv = new osg::TexEnv();
decalTexEnv->setMode(osg::TexEnv::DECAL);
decalStateSet->setTextureAttributeAndModes(0,KLN89FaceTexture,
osg::StateAttribute::ON);
decalStateSet->setTextureAttribute(0,decalTexEnv);
產生了狀態集合後咱們就能夠把它們應用在場景中的節點上。在scene graph的繪製遍歷(root->leaf)中狀態是積累的。除非這個節點有一個它本身的狀態,不然它會繼承其父節點的狀態。(若是一個節點有一個以上的父節點,它會使用一個以上的狀態渲染。)
root->setStateSet(blendStateSet);
unitSphereGeode->setStateSet(decalStateSet);
最後一步是進入仿真循環。
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
return 0;
場景圖管理器遍歷scene graph,決定哪些幾何體須要送到圖形管道渲染。在遍歷的過程當中,場景圖管理器也蒐集幾何體如何被渲染的信息。這些信息存在osg::StateSet實例中。StateSet包含OpenGL的屬性/數值對列表。這些StateSet能夠和scenegraph中的節點關聯起來。在預渲染的遍歷中,StateSet從根節點到葉子節點是積累的。一個節點的沒有變化的狀態屬性也是簡單的遺傳自父節點。
幾個附加的特性容許更多的控制和彈性。一個狀態的屬性值能夠被設爲OVERRIDE。這意味着這個節點的全部孩子節點——無論其屬性值是什麼——會遺傳其父節點的屬性值。OVERRIDE意味着能夠被覆蓋。若是一個孩子節點把那個屬性設爲PROTECTED,那麼它就能夠設置本身的屬性值,而不用理會父節點的設置。
下面的場景示範了state如何影響scene graph。根節點有一個BLEND模式紋理的基本狀態。這個基本狀態會被全部子節點繼承,除非這個狀態的參數改變。根節點的右子樹就是這樣的。右孩子沒有被指定任何狀態,因此它使用和根節點同樣的狀態來渲染。對於節點6,紋理的混合模式沒有改變可是使用了一個新紋理。
根節點的左子樹的紋理模式被設爲DECAL。其餘的參數是同樣的,因此它們從根節點繼承。在節點3中,FOG被打開了並設置爲OVERRIDE。這個節點——節點3——的左孩子將FOG設爲了OFF。既然它沒有設置PROTECTED而且它父節點設置了OVERRIDE,因此就像父節點同樣FOG仍然是ON。右孩子4設置FOG屬性值爲PROTECTED,所以能夠覆蓋其父節點的設置。
a.jpg (86.96 KB)
2007-11-23 01:05 PM
下面是操做狀態配置並用節點將這些狀態關聯起來的代碼。
// Set an osg::TexEnv instance's mode to BLEND,
// make this TexEnv current for texture unit 0 and assign
// a valid texture to texture unit 0
blendTexEnv->setMode(osg::TexEnv::BLEND);
stateRootBlend->setTextureAttribute(0,blendTexEnv,osg::StateAttribute::ON);
stateRootBlend->setTextureAttributeAndModes(0,ocotilloTexture,
osg::StateAttribute::ON);
// For state five, change the texture associated with texture unit 0
//all other attributes will remain unchanged as inherited from above.
// (texture mode will still be BLEND)
stateFiveDustTexture->setTextureAttributeAndModes(0,dustTexture,
osg::StateAttribute::ON);
// Set the mode of an osg::TexEnv instance to DECAL
//Use this mode for stateOneDecal.
decalTexEnv->setMode(osg::TexEnv::DECAL);
stateOneDecal->setTextureAttribute(0,decalTexEnv,osg::StateAttribute::ON);
// For stateTwo, turn FOG OFF and set to OVERRIDE.
//Descendants in this sub-tree will not be able to change FOG unless
//they set the FOG attribute value to PROTECTED
stateTwoFogON_OVRD->setAttribute(fog, osg::StateAttribute::ON);
stateTwoFogON_OVRD->setMode(GL_FOG,
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
// For stateThree, try to turn FOG OFF.
Since the attribute is not PROTECTED, and
// the parents set this attribute value to OVERRIDE, the parent's value will be used.
// (i.e. FOG will remain ON.)
stateThreeFogOFF->setMode(GL_FOG, osg::StateAttribute::OFF);
// For stateFour, set the mode to PROTECTED, thus overriding the parent setting
stateFourFogOFF_PROT->setMode(GL_FOG,
osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
// apply the StateSets above to appropriates nodes in the scene graph.
root->setStateSet(stateRootBlend);
mtOne->setStateSet(stateOneDecal);
mtTwo->setStateSet(stateTwoFogON_OVRD);
mtThree->setStateSet(stateThreeFogOFF);
mtSix->setStateSet(stateFiveDustTexture);
mtFour->setStateSet(stateFourFogOFF_PROT);
加載幾何模型並加入到場景中,調整其中一個模型在場景中的位置並經過安裝仿真循環觀察場景。
若是你下載了當前版本的Open Scene Graph,那麼你就能夠將在有相應插件的任何文件格式。包括如下的幾何文件格式:3dc,3ds,flt,geo,iv,ive,lwo,md2,obj,osg和如下這些圖像文件格式:bmp,gif,jpeg,rgb,tga,tif。
Open Scene Graph安裝包裏包含了不少open scene graph格式(.osg)的幾何文件。咱們會加載其中一個,還有一個MPI Open Flight(.flt)文件。爲了便於找到模型,創建一個models文件夾,並用OSG_DATA_PATH系統變量指向它。(一般爲C:\Projects\OpenSceneGraph\OpenSceneGraph-Data\)。解壓此文件到那個文件夾下。
幾何模型使用scene graph的節點表示。所以,爲了加載並操做一個幾何模型文件,咱們須要聲明一個句柄(或指針)指向osg::Node類型實例。(在一些要求的#include後)。
#include <osg/Node>
#include <osgDB/ReadFile>
...
osg::Node* cessnaNode = NULL;
osg::Node* tankNode = NULL;
...
cessnaNode = osgDB::readNodeFile("cessna.osg");
tankNode = osgDB::readNodeFile("Models/T72-tank/t72-tank_des.flt");
這就是加載數據庫須要作的事。下一步咱們把它做爲scene graph的一部分加入。將模型加載到transform節點的子節點上,這樣咱們就能夠從新定位它了。
// Declare a node which will serve as the root node
// for the scene graph. Since we will be adding nodes
// as 'children' of this node we need to make it a 'group'
// instance.
// The 'node' class represents the most generic version of nodes.
// This includes nodes that do not have children (leaf nodes.)
// The 'group' class is a specialized version of the node class.
// It adds functions associated with adding and manipulating
// children.
osg::Group* root = new osg::Group();
root->addChild(cessnaNode);
// Declare transform, initialize with defaults.
osg::PositionAttitudeTransform* tankXform =
new osg::PositionAttitudeTransform();
// Use the 'addChild' method of the osg::Group class to
// add the transform as a child of the root node and the
// tank node as a child of the transform.
root->addChild(tankXform);
tankXform->addChild(tankNode);
// Declare and initialize a Vec3 instance to change the
// position of the tank model in the scene
osg::Vec3 tankPosit(5,0,0);
tankXform->setPosition( tankPosit );
如今,咱們的scene graph由一個根節點和兩個子節點組成。一個是cessna的幾何模型,另外一個是一個右子樹,由一個僅有一個tank的幾何模型的transform節點組成。爲了觀察場景,須要創建一個viewer和一個仿真循環。就像這樣作的:
#include <osgProducer/Viewer>
// Declare a 'viewer'
osgProducer::Viewer viewer;
// For now, we can initialize with 'standard settings'
// Standard settings include a standard keyboard mouse
// interface as well as default drive, fly and trackball
// motion models for updating the scene.
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
// Next we will need to assign the scene graph we created
// above to this viewer:
viewer.setSceneData( root );
// create the windows and start the required threads.
viewer.realize();
// Enter the simulation loop. viewer.done() returns false
// until the user presses the 'esc' key.
// (This can be changed by adding your own keyboard/mouse
// event handler or by changing the settings of the default
// keyboard/mouse event handler)
while( !viewer.done() )
{
// wait for all cull and draw threads to complete.
viewer.sync();
// Initiate scene graph traversal to update nodes.
// Animation nodes will require update. Additionally,
// any node for which an 'update' callback has been
// set up will also be updated. More information on
// settting up callbacks to follow.
viewer.update();
// initiate the cull and draw traversals of the scene.
viewer.frame();
}
你應當能編譯並執行上面的代碼(保證調用的順序是正確的,已經添加了main等等)。執行代碼的時候,按h鍵會彈出一個菜單選項(彷佛沒有這個功能——譯者)。按‘esc’退出。
6.osg Text、HUD、RenderBins
6.1本章目標
添加文本到場景中——包括HUD風格的文本和做爲場景一部分的文本。
本類繼承自Drawable類。也就是說文本實例能夠加到Geode類實例上而且能夠和其它幾何體同樣被渲染。與文本類相關的方法的所有列在*這裏*。‘osgExample Text’工程示範了許多方法。這個教程提供了文本類的幾個有限的函數。繪製一個HUD牽扯到下面兩個概念:
一、生成一個子樹,它的根節點有合適的投影及觀察矩陣...
二、將HUD子樹中的幾何體指定到合適的RenderBin上,這樣HUD幾何體就會在場景的其餘部分以後按正確地狀態設置繪製。
渲染HUD的子樹涉及到一個投影矩陣和一個觀察矩陣。對於投影矩陣,咱們會使用至關於屏幕維數水平和垂直擴展的正投影。根據這種模式,座標至關於象素座標。爲了簡單起見,觀察矩陣使用單位矩陣。
爲了渲染HUD,咱們把它裏面的幾何體附加到一個指定的RenderBin上。RenderBin容許用戶在幾何體繪製過程當中指定順序。這對於HUD幾何體須要最後繪製來講頗有用。
首先,聲明咱們須要的變量-osg::Text和osg::Projection。
osg::Group* root = NULL;
osg::Node* tankNode = NULL;
osg::Node* terrainNode = NULL;
osg::PositionAttitudeTransform* tankXform;
osgProducer::Viewer viewer;
// A geometry node for our HUD:
osg::Geode* HUDGeode = new osg::Geode();
// Text instance that wil show up in the HUD:
osgText::Text* textOne = new osgText::Text();
// Text instance for a label that will follow the tank:
osgText::Text* tankLabel = new osgText::Text();
// Projection node for defining view frustrum for HUD:
osg::Projection* HUDProjectionMatrix = new osg::Projection;
從文件里加載模型,和前面的教程同樣創建scene graph(這裏沒什麼新東東)。
// Initialize root of scene:
root = new osg::Group();
osgDB::FilePathList pathList = osgDB::getDataFilePathList();
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\T72-Tank\\");
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Models\\JoeDirt\\");
pathList.push_back
("C:\\Projects\\OpenSceneGraph\\OpenSceneGraph-Data\\NPSData\\Textures\\");
osgDB::setDataFilePathList(pathList);
// Load models from files and assign to nodes:
tankNode = osgDB::readNodeFile("t72-tank_des.flt");
terrainNode = osgDB::readNodeFile("JoeDirt.flt");
// Initialize transform to be used for positioning the tank
tankXform = new osg::PositionAttitudeTransform());
tankXform->setPosition( osg::Vec3d(5,5,8) );
// Build the scene - add the terrain node directly to the root,
// connect the tank node to the root via the transform node:
root->addChild(terrainNode);
root->addChild(tankXform);
tankXform->addChild(tankNode);
下一步,創建場景來顯示HUD組件。添加一個子樹,它的根節點有一個投影和觀察矩陣。
// Initialize the projection matrix for viewing everything we
// will add as descendants of this node. Use screen coordinates
// to define the horizontal and vertical extent of the projection
// matrix. Positions described under this node will equate to
// pixel coordinates.
HUDProjectionMatrix->setMatrix(osg::Matrix::ortho2D(0,1024,0,768));
// For the HUD model view matrix use an identity matrix:
osg::MatrixTransform* HUDModelViewMatrix = new osg::MatrixTransform;
HUDModelViewMatrix->setMatrix(osg::Matrix::identity());
// Make sure the model view matrix is not affected by any transforms
// above it in the scene graph:
HUDModelViewMatrix->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
// Add the HUD projection matrix as a child of the root node
// and the HUD model view matrix as a child of the projection matrix
// Anything under this node will be viewed using this projection matrix
// and positioned with this model view matrix.
root->addChild(HUDProjectionMatrix);
HUDProjectionMatrix->addChild(HUDModelViewMatrix);
如今創建幾何體。咱們根據屏幕座標創建一個四邊形,並設置顏色和紋理座標。
// Add the Geometry node to contain HUD geometry as a child of the
// HUD model view matrix.
HUDModelViewMatrix->addChild( HUDGeode );
// Set up geometry for the HUD and add it to the HUD
osg::Geometry* HUDBackgroundGeometry = new osg::Geometry();
osg::Vec3Array* HUDBackgroundVertices = new osg::Vec3Array;
HUDBackgroundVertices->push_back( osg::Vec3( 0,0,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(1024,0,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(1024,200,-1) );
HUDBackgroundVertices->push_back( osg::Vec3(0,200,-1) );
osg::DrawElementsUInt* HUDBackgroundIndices =
new osg::DrawElementsUInt(osg::PrimitiveSet::POLYGON, 0);
HUDBackgroundIndices->push_back(0);
HUDBackgroundIndices->push_back(1);
HUDBackgroundIndices->push_back(2);
HUDBackgroundIndices->push_back(3);
osg::Vec4Array* HUDcolors = new osg::Vec4Array;
HUDcolors->push_back(osg::Vec4(0.8f,0.8f,0.8f,0.8f));
osg::Vec2Array* texcoords = new osg::Vec2Array(4);
(*texcoords)[0].set(0.0f,0.0f);
(*texcoords)[1].set(1.0f,0.0f);
(*texcoords)[2].set(1.0f,1.0f);
(*texcoords)[3].set(0.0f,1.0f);
HUDBackgroundGeometry->setTexCoordArray(0,texcoords);
osg::Texture2D* HUDTexture = new osg::Texture2D;
HUDTexture->setDataVariance(osg::Object::DYNAMIC);
osg::Image* hudImage;
hudImage = osgDB::readImageFile("HUDBack2.tga");
HUDTexture->setImage(hudImage);
osg::Vec3Array* HUDnormals = new osg::Vec3Array;
HUDnormals->push_back(osg::Vec3(0.0f,0.0f,1.0f));
HUDBackgroundGeometry->setNormalArray(HUDnormals);
HUDBackgroundGeometry->setNormalBinding(osg::Geometry::BIND_OVERALL);
HUDBackgroundGeometry->addPrimitiveSet(HUDBackgroundIndices);
HUDBackgroundGeometry->setVertexArray(HUDBackgroundVertices);
HUDBackgroundGeometry->setColorArray(HUDcolors);
HUDBackgroundGeometry->setColorBinding(osg::Geometry::BIND_OVERALL);
HUDGeode->addDrawable(HUDBackgroundGeometry);
爲了正確的渲染HUD,咱們創建帶有深度檢測和透明度混合的osg::stateSet。咱們也要保證HUD幾何體最後繪製。幾何體在裁剪遍歷時經過指定一個已編號的渲染箱能夠控制渲染順序。最後一行演示了這些:
// Create and set up a state set using the texture from above:
osg::StateSet* HUDStateSet = new osg::StateSet();
HUDGeode->setStateSet(HUDStateSet);
HUDStateSet->
setTextureAttributeAndModes(0,HUDTexture,osg::StateAttribute::ON);
// For this state set, turn blending on (so alpha texture looks right)
HUDStateSet->setMode(GL_BLEND,osg::StateAttribute::ON);
// Disable depth testing so geometry is draw regardless of depth values
// of geometry already draw.
HUDStateSet->setMode(GL_DEPTH_TEST,osg::StateAttribute::OFF);
HUDStateSet->setRenderingHint( osg::StateSet::TRANSPARENT_BIN );
// Need to make sure this geometry is draw last. RenderBins are handled
// in numerical order so set bin number to 11
HUDStateSet->setRenderBinDetails( 11, "RenderBin");
最後,使用文本時,因爲osg::Text是繼承自osg::Drawable的,osg::Text實例能夠做爲孩子加到osg::Geode類實例上。
// Add the text (Text class is derived from drawable) to the geode:
HUDGeode->addDrawable( textOne );
// Set up the parameters for the text we'll add to the HUD:
textOne->setCharacterSize(25);
textOne->setFont("C:/WINDOWS/Fonts/impact.ttf");
textOne->setText("Not so good");
textOne->setAxisAlignment(osgText::Text::SCREEN);
textOne->setPosition( osg::Vec3(360,165,-1.5) );
textOne->setColor( osg::Vec4(199, 77, 15, 1) );
// Declare a geode to contain the tank's text label:
osg::Geode* tankLabelGeode = new osg::Geode();
// Add the tank label to the scene:
tankLabelGeode->addDrawable(tankLabel);
tankXform->addChild(tankLabelGeode);
// Set up the parameters for the text label for the tank
// align text with tank's SCREEN.
// (for Onder: use XZ_PLANE to align text with tank's XZ plane.)
tankLabel->setCharacterSize(5);
tankLabel->setFont("/fonts/arial.ttf");
tankLabel->setText("Tank #1");
tankLabel->setAxisAlignment(osgText::Text::XZ_PLANE);
// Set the text to render with alignment anchor and bounding box around it:
tankLabel->setDrawMode(osgText::Text::TEXT |
osgText::Text::ALIGNMENT |
osgText::Text::BOUNDINGBOX);
tankLabel->setAlignment(osgText::Text::CENTER_TOP);
tankLabel->setPosition( osg::Vec3(0,0,8) );
tankLabel->setColor( osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) );
最後,創建viewer並進入仿真循環:
viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.sync();
viewer.update();
viewer.frame();
}
7.搜索並控制開關和DOF(自由度)節點
7.1搜索場景圖形中的一個有名節點
模型文件可能包含了各類不一樣的節點類型,用戶經過對這些節點的使用來更新和表達模型的各個部分。使用osgSim::MultiSwitch多重節點能夠在多個模型渲染狀態間進行選擇。例如,對坦克模型使用多重節點,用戶便可自行選擇與完整的或者損壞的坦克相關聯的幾何體以及渲染狀態。模型中還能夠包含DOF節點,以便清晰表達坦克的某個部分。例如炮塔節點能夠旋轉,機槍節點能夠升高。炮塔旋轉時,炮塔體(包括機槍)的航向角(heading)與坦克的航向角相關聯,而機槍擡升時,機槍的俯仰角(pitch)與炮塔的俯仰角相關聯。
對這些節點進行更新時,咱們須要一個指向節點的指針。而咱們首先要獲取節點的名字,才能獲得該節點的指針。而獲取節點的名稱,主要有這樣一些方法:諮詢建模人員;使用其它文件瀏覽器(對於.flt文件,可使用Creator或者Vega)瀏覽模型;或者使用OpenSceneGraph。用戶能夠根據本身的須要自由運用OSG的功能。例如在場景圖形中載入flt文件,而且在仿真過程當中將整個場景保存成.osg文件。osg文件使用ASCII格式保存,所以用戶可使用各類文本處理軟件(寫字板,記事本)對其進行編輯。在坦克模型文件中,你能夠發現一個名爲「sw1」的開關節點,它有兩個子節點「good」和「bad」,分別指向坦克未損壞和損壞的狀態。坦克模型的.osg文件能夠從這裏下載:
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/T72Tank.osg
如今咱們已經得到了須要控制的開關節點的名稱(sw1),亦可獲取其指針對象。獲取節點指針的方法有兩種:一是編寫代碼遍歷整個場景圖形;二是使用後面將會介紹的訪問器(visitor)。在之前的教程中,咱們已經知道如何加載flight文件,將其添加到場景並進入仿真循環的方法。
#include <osg/PositionAttitudeTransform>
#include <osg/Group>
#include <osg/Node>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
#include <osgGA/TrackballManipulator>
int main()
{
osg::Node* tankNode = NULL;
osg::Group* root = NULL;
osgViewer::Viewer viewer;
osg::Vec3 tankPosit;
osg::PositionAttitudeTransform* tankXform;
tankNode = osgDB::readNodeFile("../NPS_Data/Models/t72-tank/t72-tank_des.flt");
root = new osg::Group();
tankXform = new osg::PositionAttitudeTransform();
root->addChild(tankXform);
tankXform->addChild(tankNode);
tankPosit.set(5,0,0);
tankXform->setPosition( tankPosit );
viewer.setCameraManipulator(new osgGA::TrackballManipulator());
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.frame();
}
}
如今咱們須要修改上述代碼,以添加查找節點的函數。下面的遞歸函數有兩個參數值:用於搜索的字符串,以及用於指定搜索開始位置的節點。函數的返回值是指定節點子樹中,第一個與輸入字符串名稱相符的節點實例。若是沒有找到這樣的節點,函數將返回NULL。特別要注意的是,使用訪問器將提供更爲靈活的節點訪問方式。而下面的代碼只用於展現如何手動編寫場景圖形的遍歷代碼。
osg::Node* findNamedNode(const std::string& searchName,
osg::Node* currNode)
{
osg::Group* currGroup;
osg::Node* foundNode;
// 檢查輸入的節點是不是合法的,
// 若是輸入節點爲NULL,則直接返回NULL。
if ( !currNode)
{ return NULL; }
// 若是輸入節點合法,那麼先檢查該節點是否就是咱們想要的結果。
// 若是確爲所求,那麼直接返回輸入節點。
if (currNode->getName() == searchName)
{ return currNode; }
// 若是輸入節點並不是所求,那麼檢查它的子節點(不包括葉節點)狀況。
// 若是子節點存在,則使用遞歸調用來檢查每一個子節點。
// 若是某一次遞歸的返回值非空,說明已經找到所求的節點,返回其指針。
// 若是全部的節點都已經遍歷過,那麼說明不存在所求節點,返回NULL。
currGroup = currNode->asGroup();
if ( currGroup )
{
for (unsigned int i = 0 ; i < currGroup->getNumChildren(); i ++)
{
foundNode = findNamedNode(searchName, currGroup->getChild(i));
if (foundNode)
return foundNode; // 找到所求節點。
}
return NULL; // 遍歷結束,不存在所求節點。
}
else
{
return NULL; // 該節點不是組節點,返回NULL。
}
}
如今咱們能夠在代碼中添加這個函數,用於查找場景中指定名稱的節點並獲取其指針。注意這是一種深度優先的算法,它返回第一個符合的節點指針。
咱們將在設置場景以後,進入仿真循環以前調用該函數。函數返回的開關節點指針能夠用於更新開關的狀態。下面的代碼用於模型載入後,執行查找節點的工做。
osg::Switch* tankStateSwitch = NULL;
osg::Node* foundNode = NULL;
foundNode = findNamedNode("sw1",root);
tankStateSwitch = (osg::Switch*) foundNode;
if ( !tankStateSwitch)
{
std::cout << "tank state switch node not found, quitting." << std::endl;
return -1;
}
7.2按照「訪問器」模式搜索有名節點(Finding a named node using the Visitor pattern)
「訪問器」的設計容許用戶將某個特定節點的指定函數,應用到當前場景遍歷的全部此類節點中。遍歷的類型包括NODE_VISITOR,UPDATE_VISITOR,COLLECT_OCCLUDER_VISITOR和CULL_VISITOR。因爲咱們尚未討論場景更新(updating),封閉節點(occluder node)和揀選(culling)的有關內容,所以這裏首先介紹NODE_VISITOR(節點訪問器)遍歷類型。「訪問器」一樣容許用戶指定遍歷的模式,可選項包括TRAVERSE_NONE,TRAVERSE_PARENTS,TRAVERSE_ALL_CHILDREN和TRAVERSE_ACTIVE_CHILDREN。這裏咱們將選擇TRAVERSE_ALL_CHILDREN(遍歷全部子節點)的模式。
而後,咱們須要定義應用到每一個節點的函數。這裏咱們將會針對用戶自定義的節點名稱進行字符串比較。若是某個節點的名稱與指定字符串相符,該節點將被添加到一個節點列表中。遍歷過程結束後,列表中將包含全部符合指定的搜索字符串的節點。
爲了可以充分利用「訪問器」,咱們能夠從基類osg::NodeVisitor派生一個特定的節點訪問器(命名爲findNodeVisitor)。這個類須要兩個新的數據成員:一個std::string變量,用於和咱們搜索的有名節點進行字符串比較;以及一個節點列表變量(std::vector<osg::Node*>),用於保存符合搜索字符串的全部節點。爲了實現上述的操做,咱們須要重載「apply」方法。基類的「apply」方法已經針對全部類型的節點(全部派生自osg::Node的節點)做了定義。用戶能夠重載apply方法來操做特定類型的節點。若是咱們但願針對全部的節點進行一樣的操做,那麼能夠重載針對osg::Node類型的apply方法。findNodeVisitor的頭文件內容在下表中列出,相關的源代碼能夠在這裏下載:
http://www.nps.navy.mil/cs/sullivan/osgTutorials/Download/findNodeVisitor.zip
#include <osg/NodeVisitor>
#include <osg/Node>
#include <osgSim/DOFTransform>
#include <iostream>
#include <vector>
class findNodeVisitor : public osg::NodeVisitor {
public:
findNodeVisitor();
findNodeVisitor(const std::string &searchName) ;
virtual void apply(osg::Node &searchNode);
virtual void apply(osg::Transform &searchNode);
void setNameToFind(const std::string &searchName);
osg::Node* getFirst();
typedef std::vector<osg::Node*> nodeListType;
nodeListType& getNodeList() { return foundNodeList; }
private:
std::string searchForName;
nodeListType foundNodeList;
};
如今,咱們建立的類能夠作到:啓動一次節點訪問遍歷,訪問指定場景子樹的每一個子節點,將節點的名稱與用戶指定的字符串做比較,並創建一個列表用於保存名字與搜索字符串相同的節點。那麼如何啓動這個過程呢?咱們可使用osg::Node的「accept」方法來實現節點訪問器的啓動。選擇某個執行accept方法的節點,咱們就能夠控制遍歷開始的位置。(遍歷的方向是經過選擇遍歷模式來決定的,而節點類型的區分則是經過重載相應的apply方法來實現)「accpet」方法將響應某一類的遍歷請求,並執行用戶指定節點的全部子類節點的apply方法。在這裏咱們將重載通常節點的apply方法,並選擇TRAVERSE_ALL_CHILDREN的遍歷模式,所以,觸發accept方法的場景子樹中全部的節點,均會執行這一apply方法。
在這個例子中,咱們將讀入三種不一樣狀態的坦克。第一個模型沒有任何變化,第二個模型將使用多重開關(multSwitch)來關聯損壞狀態,而第三個模型中,坦克的炮塔將旋轉不一樣的角度,同時槍管也會升高。
下面的代碼實現了從文件中讀入三個坦克模型並將其添加到場景的過程。其中兩個坦克將做爲變換節點(PositionAttitudeTransform)的子節點載入,以便將其位置設置到座標原點以外。
// 定義場景樹的根節點,以及三個獨立的坦克模型節點
osg::Group* root = new osg::Group();
osg::Group* tankOneGroup = NULL;
osg::Group* tankTwoGroup = NULL;
osg::Group* tankThreeGroup = NULL;
// 從文件中讀入坦克模型
tankOneGroup = dynamic_cast<osg::Group*>
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankTwoGroup = dynamic_cast<osg::Group*>
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
tankThreeGroup = dynamic_cast<osg::Group*>
(osgDB::readNodeFile("t72-tank/t72-tank_des.flt"));
// 將第一個坦克做爲根節點的子節點載入
root->addChild(tankOneGroup);
// 爲第二個坦克定義一個位置變換
osg::PositionAttitudeTransform* tankTwoPAT =
new osg::PositionAttitudeTransform();
// 將第二個坦克向右移動5個單位,向前移動5個單位
tankTwoPAT->setPosition( osg::Vec3(5,5,0) );
// 將第二個坦克做爲變換節點的子節點載入場景
root->addChild(tankTwoPAT);
tankTwoPAT->addChild(tankTwoGroup);
// 爲第三個坦克定義一個位置變換
osg::PositionAttitudeTransform* tankThreePAT =
new osg::PositionAttitudeTransform();
// 將第二個坦克向右移動10個單位
tankThreePAT->setPosition( osg::Vec3(10,0,0) );
// 將坦克模型向左旋轉22.5度(爲此,炮塔的旋轉應當與坦克的頭部關聯)
tankThreePAT->setAttitude( osg::Quat(3.14159/8.0, osg::Vec3(0,0,1) ));
// 將第三個坦克做爲變換節點的子節點載入場景
root->addChild(tankThreePAT);
tankThreePAT->addChild(tankThreeGroup);
咱們準備將第二個模型設置爲損壞的狀態,所以咱們使用findNodeVisitor類獲取控制狀態的多重開關(multiSwitch)的句柄。這個節點訪問器須要從包含了第二個坦克的組節點開始執行。下面的代碼演示了聲明和初始化一個findNodeVisitor實例並執行場景遍歷的方法。遍歷完成以後,咱們便可獲得節點列表中符合搜索字符串的第一個節點的句柄。這也就是咱們準備使用multiSwitch來進行控制的節點句柄。
// 聲明一個findNodeVisitor類的實例,設置搜索字符串爲「sw1」
findNodeVisitor findNode("sw1");
// 開始執行訪問器實例的遍歷過程,起點是tankTwoGroup,搜索它全部的子節點
// 並建立一個列表,用於保存全部符合搜索條件的節點
tankTwoGroup->accept(findNode);
// 聲明一個開關類型,並將其關聯給搜索結果列表中的第一個節點。
osgSim::MultiSwitch* tankSwitch = NULL;
tankSwitch = dynamic_cast <osgSim::MultiSwitch*> (findNode.getFirst());
更新開關節點
當咱們獲取了一個合法的開關節點句柄後,下一步就是從一個模型狀態變換到另外一個狀態。咱們可使用setSingleChildOn方法來實現這個操做。setSingleChildOn()方法包括兩個參數:第一個無符號整型量至關於多重開關組(switchSet)的索引號;第二個無符號整型量至關於開關的位置。在這個例子中,咱們只有一個多重開關,其值能夠設置爲未損壞狀態或者損壞狀態,以下所示:
// 首先確認節點是否合法,而後設置其中的第一個(也是惟一的)多重開關
if (tankSwitch)
{
//tankSwitch->setSingleChildOn(0,false); // 未損壞的模型
tankSwitch->setSingleChildOn(0,true); // 損壞的模型
}
更新DOF節點
坦克模型還包括了兩個DOF(自由度)節點「turret」和「gun」。這兩個節點的句柄也可使用上文所述的findNodeVisitor來獲取。(此時,訪問器的場景遍歷應當從包含第三個模型的組節點處開始執行)一旦咱們獲取了某個DOF節點的合法句柄以後,便可使用setCurrentHPR方法來更新與這些節點相關的變換矩陣。setCurrentHPR方法只有一個參數:這個osg::Vec3量至關於三個歐拉角heading,pitch和roll的弧度值。(若是要使用角度來描述這個值,可使用osg::DegreesToRadians方法)
// 聲明一個findNodeVisitor實例,設置搜索字符串爲「turret」
findNodeVisitor findTurretNode("turret");
// 遍歷將從包含第三個坦克模型的組節點處開始執行
tankThreeGroup->accept(findTurretNode);
// 確認咱們找到了正確類型的節點
osgSim::DOFTransform * turretDOF =
dynamic_cast<osgSim::DOFTransform *> (findTurretNode.getFirst()) ;
// 若是節點句柄合法,則設置炮塔的航向角爲向右22.5度。
if (turretDOF)
{
turretDOF->setCurrentHPR( osg::Vec3(-3.14159/4.0,0.0,0.0) );
}
同理,機槍的自由度也能夠以下設置:
// 聲明一個findNodeVisitor實例,設置搜索字符串爲「gun」
findNodeVisitor findGunNode("gun");
// 遍歷將從包含第三個坦克模型的組節點處開始執行
tankThreeGroup->accept(findGunNode);
// 確認咱們找到了正確類型的節點
osgSim::DOFTransform * gunDOF =
dynamic_cast<osgSim::DOFTransform *> (findGunNode.getFirst()) ;
// 若是節點句柄合法,則設置機槍的俯仰角爲向上22.5度。
if (gunDOF)
{
gunDOF->setCurrentHPR( osg::Vec3(0.0,3.14159/8.0,0.0) );
}
使用回調類實現對場景圖形節點的更新。前一個教程介紹了在進入主仿真循環以前,更新DOF和開關節點的方法。本節將講解如何使用回調來實如今每幀的更新遍歷(update traversal)中進行節點的更新。
用戶可使用回調來實現與場景圖形的交互。回調能夠被理解成是一種用戶自定義的函數,根據遍歷方式的不一樣(更新update,揀選cull,繪製draw),回調函數將自動地執行。回調能夠與個別的節點或者選定類型(及子類型)的節點相關聯。在場景圖形的各次遍歷中,若是遇到的某個節點已經與用戶定義的回調類和函數相關聯,則這個節點的回調將被執行。若是但願瞭解有關遍歷和回調的更多信息,請參閱David Eberly所著的《3D Game Engine Design》第四章,以及SGI的《Performer Programmer's Guide》第四章。相關的示例請參見osgCallback例子。
更新回調將在場景圖形每一次運行更新遍歷時被執行。與更新回調相關的代碼能夠在每一幀被執行,且實現過程是在揀選回調以前,所以回調相關的代碼能夠插入到主仿真循環的viewer.update()和viewer.frame()函數之間。而OSG的回調也提供了維護更爲方便的接口來實現上述的功能。善於使用回調的程序代碼也能夠在多線程的工做中更加高效地運行。
從前一個教程展開來講,若是咱們須要自動更新與坦克模型的炮塔航向角和機槍傾角相關聯的DOF(自由度)節點,咱們能夠採起多種方式來完成這一任務。譬如,針對咱們將要操做的各個節點編寫相應的回調函數:包括一個與機槍節點相關聯的回調,一個與炮塔節點相關聯的回調,等等。這種方法的缺陷是,與不一樣模型相關聯的函數沒法被集中化,所以增長了代碼閱讀、維護和更新的複雜性。另外一種(極端的)方法是,只編寫一個更新回調函數,來完成整個場景的節點操做。本質上來講,這種方法和上一種具備一樣的問題,由於全部的代碼都會集中到仿真循環當中。當仿真的複雜程度不斷增長時,這個惟一的更新回調函數也會變得愈發難以閱讀、維護和修改。關於編寫場景中節點/子樹回調函數的方法,並無必定之規。在本例中咱們將建立單一的坦克節點回調,這個回調函數將負責更新炮塔和機槍的自由度節點。
爲了實現這一回調,咱們須要在節點類原有的基礎上添加新的數據。咱們須要得到與炮塔和機槍相關聯的DOF節點的句柄,以更新炮塔旋轉和機槍俯仰的角度值。角度值的變化要創建在上一次變化的基礎上。由於回調是做爲場景遍歷的一部分進行初始化的,咱們所需的參數一般只有兩個:一個是與回調相關聯的節點指針,一個是用於執行遍歷的節點訪問器指針。爲了得到更多的參數數據(炮塔和機槍DOF的句柄,旋轉和俯仰角度值),咱們可使用節點類的userData數據成員。userData是一個指向用戶定義類的指針,其中包含了關聯某個特定節點時所需的一切數據集。而對於用戶自定義類,只有一個條件是必需的,即,它必須繼承自osg::Referenced類。Referenced類提供了智能指針的功能,用於協助用戶管理內存分配。智能指針記錄了分配給一個類的實例的引用計數值。這個類的實例只有在引用計數值到達0的時候纔會被刪除。有關osg::Referenced的更詳細敘述,請參閱本章後面的部分。基於上述的需求,咱們向坦克節點添加以下的代碼:
class tankDataType : public osg::Referenced
{
public:
// 公有成員……
protected:
osgSim::DOFTransform* tankTurretNode;
osgSim::DOFTransform* tankGunNode;
double rotation;
double elevation;
};
爲了正確實現tankData類,咱們須要獲取DOF節點的句柄。這一工做能夠在類的構造函數中使用前一教程所述的findNodeVisitor類完成。findNodeVisitor將從一個起始節點開始遍歷。本例中咱們將從表示坦克的子樹的根節點開始執行遍歷,所以咱們須要向tankDataType的構造函數傳遞坦克節點的指針。所以,tankDataType類的構造函數代碼應當編寫爲:(向特定節點分配用戶數據的步驟將隨後給出)
tankDataType::tankDataType(osg::Node* n)
{
rotation = 0;
elevation = 0;
findNodeVisitor findTurret("turret");
n->accept(findTurret);
tankTurretNode =
dynamic_cast <osgSim::DOFTransform*> (findTurret.getFirst());
findNodeVisitor findGun("gun");
n->accept(findGun);
tankGunNode =
dynamic_cast< osgSim::DOFTransform*> (findGun.getFirst());
}
咱們也能夠在tankDataType類中定義更新炮塔旋轉和機槍俯仰的方法。如今咱們只須要簡單地讓炮塔和機槍角度每幀改變一個固定值便可。對於機槍的俯仰角,咱們須要判斷它是否超過了實際狀況的限制值。若是達到限制值,則重置仰角爲0。炮塔的旋轉能夠在一個圓周內自由進行。
void tankDataType::updateTurretRotation()
{
rotation += 0.01;
tankTurretNode->setCurrentHPR( osg::Vec3(rotation,0,0) );
}
void tankDataType::updateGunElevation()
{
elevation += 0.01;
tankGunNode->setCurrentHPR( osg::Vec3(0,elevation,0) );
if (elevation > .5)
elevation = 0.0;
}
將上述代碼添加到類的內容後,咱們新定義的類以下所示:
class tankDataType : public osg::Referenced
{
public:
tankDataType(osg::Node*n);
void updateTurretRotation();
void updateGunElevation();
protected:
osgSim::DOFTransform* tankTurretNode;
osgSim::DOFTransform* tankGunNode;
double rotation; //(弧度值)
double elevation; //(弧度值)
};
下一個步驟是建立回調,並將其關聯到坦克節點上。爲了建立這個回調,咱們須要重載「()」操做符,它包括兩個參數:節點的指針和節點訪問器的指針。在這個函數中咱們將執行DOF節點的更新。所以,咱們須要執行tankData實例的更新方法,其中tankData實例使用坦克節點的userData成員與坦克節點相關聯。坦克節點的指針能夠經過使用getUserData方法來獲取。因爲這個方法的返回值是一個osg::Referenced基類的指針,所以須要將其安全地轉換爲tankDataType類的指針。爲了保證用戶數據的引用計數值是正確的,咱們使用模板類型osg::ref_ptr<tankDataType>指向用戶數據。整個類的定義以下:
class tankNodeCallback : public osg::NodeCallback
{
public:
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::ref_ptr<tankDataType> tankData =
dynamic_cast<tankDataType*> (node->getUserData() );
if(tankData)
{
tankData->updateTurretRotation();
tankData->updateGunElevation();
}
traverse(node, nv);
}
};
下一步的工做是「安裝」回調:將其關聯給咱們要修改的坦克節點,以實現每幀的更新函數執行。所以,咱們首先要保證坦克節點的用戶數據(tankDataType類的實例)是正確的。而後,咱們使用osg::Node類的setUpdateCallback方法將回調與正確的節點相關聯。代碼以下所示:
// 初始化變量和模型,創建場景……
tankDataType* tankData = new tankDataType(tankNode);
tankNode->setUserData( tankData );
tankNode->setUpdateCallback(new tankNodeCallback);
建立了回調以後,咱們進入仿真循環。仿真循環的代碼不用加以改變。當咱們調用視口類實例的frame()方法時,咱們即進入一個更新遍歷。當更新遍歷及至坦克節點時,將觸發tankNodeCallback類的操做符「()」函數。
// 視口的初始化,等等……
while( !viewer.done() )
{
viewer.frame();
}
return 0;
}
使程序具有將鍵盤事件與特定函數相關聯的能力。在前面的教程中咱們已經可使用更新回調來控制炮塔的旋轉。本章中咱們將添加一個鍵盤接口類,實現經過用戶的鍵盤輸入來更新炮塔的轉角。
GUI事件適配器GUIEventAdapter和GUI動做適配器GUIActionAdapter。
GUIEventHandler類向開發者提供了窗體系統的GUI事件接口。這一事件處理器使用GUIEventAdapter實例來接收更新。事件處理器還可使用GUIActionAdapter實例向GUI系統發送請求,以實現一些特定的操做。
GUIEventAdapter實例包括了各類事件類型(PUSH,RELEASE,DOUBLECLICK,DRAG,MOVE,KEYDOWN,KEYUP,FRAME,RESIZE,SCROLLUP,SCROLLDOWN,SCROLLLEFT)。依據GUIEventAdapter事件類型的不一樣,其實例可能還有更多的相關屬性。例如X,Y座標與鼠標事件相關。KEYUP和KEYDOWN事件則與一個按鍵值(例如「a」,「F1」)相關聯。
GUIEventHandler使用GUIActionAdapter來請求GUI系統執行動做。這些動做包括重繪請求requestRedraw(),屢次更新請求requestContinuousUpdate(),光標位置重置請求requestWarpPointer(x,y)。
GUIEventHandler類主要經過handle方法來實現與GUI的交互。handle方法有兩個參數:一個GUIEventAdapter實例用於接收GUI的更新,以及一個GUIActionAdapter用於向GUI發送請求。handle方法用於檢查GUIEventAdapter的動做類型和值,執行指定的操做,並使用GUIActionAdapter向GUI系統發送請求。若是事件已經被正確處理,則handle方法返回的布爾值爲true,不然爲false。
一個GUI系統可能與多個GUIEventAdapter相關聯(GUIEventAdapter的順序保存在視口類的eventHandlerList中),所以這個方法的返回值能夠用於控制單個鍵盤事件的屢次執行。若是一個GUIEventHandler返回false,下一個GUIEventHandler將繼續響應同一個鍵盤事件。
後面的例子將演示GUIEventHandler與GUI系統交互的方法:TrackballManipulator類(繼承自GUIEventHandler)以GUIEventAdapter實例的形式接收鼠標事件的更新。鼠標事件的解析由TrackballManipulator類完成,並能夠實現「拋出」的操做(所謂拋出,指的是用戶按下鍵拖動模型並忽然鬆開,以實現模型的持續旋轉或移動)。解析事件時,TrackBallManipulator將發送請求到GUI系統(使用GUIActionAdapter),啓動定時器並使本身被重複調用,以計算新的模型方向或者位置數據。
如下主要介紹如何建立一個用於將鍵盤輸入關聯到特定函數的鍵盤接口類。當用戶將按鍵註冊到接口類並設定相應的C++響應函數以後,便可創建相應的表格條目。該表格用於保存鍵值(「a」,「F1」等等),按鍵狀態(按下,鬆開)以及C++響應函數。本質上講,用戶能夠由此實現形同「按下f鍵,即執行functionOne」的交互操做。因爲新的類將繼承自GUIEventHandler類,所以每當GUI系統捕獲到一個GUI事件時,這些類的handle方法都會被觸發。而handle方法觸發後,GUI事件的鍵值和按鍵狀態(例如,鬆開a鍵)將與表格中的條目做比較,若是發現相符的條目,則執行與此鍵值和狀態相關聯的函數
用戶經過addFunction方法能夠註冊按鍵條目。這個函數有兩種形式。第一種把鍵值和響應函數做爲輸入值。這個函數主要用於用戶僅處理KEY_DOWN事件的情形。例如,用戶能夠將「a」鍵的按下事件與一個反鋸齒效果的操做函數相關聯。可是用戶不能用這個函數來處理按鍵鬆開的動做。
另外一個情形下,用戶可能須要區分由單個按鍵的「按下」和「鬆開」事件產生的不一樣動做。例如控制第一人稱視角的射擊者動做。按下w鍵使模型加速向前。鬆開w鍵以後,運動模型逐漸中止。一種可行的設計方法是,爲按下按鍵和鬆開按鍵分別設計不一樣的響應函數。二者中的一個用來實現按下按鍵的動做。
#ifndef KEYBOARD_HANDLER_H
#define KEYBOARD_HANDLER_H
#include <osgGA/GUIEventHandler>
class keyboardEventHandler : public osgGA::GUIEventHandler
{
public:
typedef void (*functionType) ();
enum keyStatusType
{
KEY_UP, KEY_DOWN
};
// 用於保存當前按鍵狀態和執行函數的結構體。
// 記下當前按鍵狀態的信息以免重複的調用。
// (若是已經按下按鍵,則沒必要重複調用相應的方法)
struct functionStatusType
{
functionStatusType() {keyState = KEY_UP; keyFunction = NULL;}
functionType keyFunction;
keyStatusType keyState;
};
// 這個函數用於關聯鍵值和響應函數。若是鍵值在以前沒有註冊過,它和
// 它的響應函數都會被添加到「按下按鍵」事件的映射中,並返回true。
// 不然,不進行操做並返回false。
bool addFunction(int whatKey, functionType newFunction);
// 重載函數,容許用戶指定函數是否與KEY_UP或者KEY_DOWN事件關聯。
bool addFunction(int whatKey, keyStatusType keyPressStatus,
functionType newFunction);
// 此方法將比較當前按下按鍵的狀態以及註冊鍵/狀態的列表。
// 若是條目吻合且事件較新(即,按鍵還未按下),則執行響應函數。
virtual bool handle(const osgGA::GUIEventAdapter& ea,
osgGA::GUIActionAdapter&);
// 重載函數,用於實現GUI事件處理訪問器的功能。
virtual void accept(osgGA::GUIEventHandlerVisitor& v)
{ v.visit(*this); };
protected:
// 定義用於保存已註冊鍵值,響應函數和按鍵狀態的數據類型。
typedef std::map<int, functionStatusType > keyFunctionMap;
// 保存已註冊的「按下按鍵」方法及其鍵值。
keyFunctionMap keyFuncMap;
// 保存已註冊的「鬆開按鍵」方法及其鍵值。
keyFunctionMap keyUPFuncMap;
};
#endif
下面的代碼用於演示如何使用上面定義的類:
// 創建場景和視口。
// ……
// 聲明響應函數:
// startAction(),stopAction(),toggleSomething()
// ……
// 聲明並初始化鍵盤事件處理器的實例。
keyboardEventHandler* keh = new keyboardEventHandler();
// 將事件處理器添加到視口的事件處理器列表。
// 若是使用push_front且列表第一項的handle方法返回true,則其它處理器
// 將不會再響應GUI同一個GUI事件。咱們也可使用push_back,將事件的
// 第一處理權交給其它的事件處理器;或者也能夠設置handle方法的返回值
// 爲false。OSG 2.x版還容許使用addEventHandler方法來加以替代。
//viewer.getEventHandlers().push_front(keh);
viewer.addEventHandler(keh);
// 註冊鍵值,響應函數。
// 按下a鍵時,觸發toggelSomething函數。
// (鬆開a鍵則沒有效果)
keh->addFunction('a',toggleSomething);
// 按下j鍵時,觸發startAction函數。(例如,加快模型運動速度)
// 注意,也能夠不添加第二個參數。
keh->addFunction('j',keyboardEventHandler::KEY_DOWN, startAction);
// 鬆開j鍵時,觸發stopAction函數。
keh->addFunction('j',keyboardEventHandler::KEY_UP, stopAction);
// 進入仿真循環
// ……
上一個教程咱們講解了鍵盤事件處理器類,它用於註冊響應函數。本章提供了用於鍵盤輸入的更方便的方案。咱們將重載一個GUIEventHandler類,而沒必要再建立和註冊函數。在這個類中咱們將添加新的代碼,以便執行特定的鍵盤和鼠標事件響應動做。咱們還將提出一種鍵盤事件處理器與更新回調通信的方法。
教程第8部分演示瞭如何將回調與DOF節點相關聯,以實現場景中DOF節點位置的持續更新。那麼,若是咱們但願使用鍵盤輸入來控制場景圖形中的節點,應該如何處理呢?例如,若是咱們有一個基於位置變換節點的坦克模型,並但願在按下w鍵的時候控制坦克向前運動,咱們須要進行以下一些操做:
1. 讀取鍵盤事件;
2. 保存鍵盤事件的結果;
3. 在更新回調中響應鍵盤事件。
第一步:基類osgGA::GUIEventHandler用於定義用戶本身的GUI鍵盤和鼠標事件動做。咱們能夠從基類派生本身的類並重載其handle方法,以建立自定義的動做。同時還編寫accept方法來實現GUIEventHandlerVisitor(OSG 2.0版本中此類已經廢棄)的功能。其基本的框架結構以下所示:
class myKeyboardEventHandler : public osgGA::GUIEventHandler
{
public:
virtual bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter&);
virtual void accept(osgGA::GUIEventHandlerVisitor& v) { v.visit(*this); };
};
bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter&ea,osgGA::GUIActionAdapter& aa)
{
switch(ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
switch(ea.getKey())
{
case 'w':
std::cout << " w key pressed" << std::endl;
return false;
break;
default:
return false;
}
}
default:
return false;
}
}
上述類的核心部分就是咱們從基類中重載的handle方法。這個方法有兩個參數:一個GUIEventAdapter類的實例,用於接收GUI事件;另外一個是GUIActionAdapter類的實例,用於生成並向GUI系統發送請求,例如重繪請求和持續更新請求。
咱們須要根據第一個參數編寫代碼以包含更多的事件,例如KEYUP,DOUBLECLICK,DRAG等。若是要處理按下按鍵的事件,則應針對KEYDOWN這個分支條件來擴展相應的代碼。
事件處理函數的返回值與事件處理器列表中當前處理器觸發的鍵盤和鼠標事件相關。若是返回值爲true,則系統認爲事件已經處理,再也不傳遞給下一個事件處理器。若是返回值爲false,則傳遞給下一個事件處理器,繼續執行對事件的響應。
爲了「安裝」咱們的事件處理器,咱們須要建立它的實例並添加到osgViewer::Viewer的事件處理器列表。代碼以下:
myKeyboardEventHandler* myFirstEventHandler = new myKeyboardEventHandler();
viewer.getEventHandlerList().push_front(myFirstEventHandler);
第二步:到目前爲止,咱們的鍵盤處理器還並不完善。它的功能僅僅是在每次按下w鍵時向控制窗口輸出。若是咱們但願按下鍵時能夠控制場景圖形中的元素,則須要在鍵盤處理器和更新回調之間創建一個通信結構。爲此,咱們將建立一個用於保存鍵盤狀態的類。這個事件處理器類用於記錄最近的鍵盤和鼠標事件狀態。而更新回調類也須要創建與鍵盤處理器類的接口,以實現場景圖形的正確更新。如今咱們開始建立基本的框架結構。用戶能夠在此基礎上進行自由的擴展。下面的代碼是一個類的定義,用於容許鍵盤事件處理器和更新回調之間通信。
class tankInputDeviceStateType
{
public:
tankInputDeviceStateType::tankInputDeviceStateType() :
moveFwdRequest(false) {}
bool moveFwdRequest;
};
下一步的工做是確認鍵盤事件處理器和更新回調都有正確的數據接口。這些數據將封裝到tankInputdeviceStateType的實例中。由於咱們僅使用一個事件處理器來控制坦克,所以能夠在事件處理器中提供指向tankInputDeviceStateType實例的指針。咱們將向事件處理器添加一個數據成員(指向tankInputDeviceStateType的實例)。同時咱們還會將指針設置爲構造函數的輸入參量。以上所述的改動,即指向tankInputDeviceStateType實例的指針,以及新的構造函數以下所示:
class myKeyboardEventHandler : public osgGA::GUIEventHandler {
public:
myKeyboardEventHandler(tankInputDeviceStateType* tids)
{
tankInputDeviceState = tids;
}
// ……
protected:
tankInputDeviceStateType* tankInputDeviceState;
};
咱們還須要修改handle方法,以實現除了輸出到控制檯以外更多的功能。咱們經過修改標誌參量的值,來發送坦克向前運動的請求。
bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& aa)
{
switch(ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
switch(ea.getKey())
{
case 'w':
tankInputDeviceState->moveFwdRequest = true;
return false;
break;
default:
return false;
}
}
default:
return false;
}
}
第三步:用於更新位置的回調類也須要編寫鍵盤狀態數據的接口。咱們爲更新回調添加與上述相同的參數。這其中包括一個指向同一tankInputDeviceStateType實例的指針。類的構造函數則負責將這個指針傳遞給成員變量。得到指針以後,咱們就能夠在回調內部使用其數值了。目前的回調只具有使坦克向前運動的代碼,前提是用戶執行了相應的鍵盤事件。回調類的內容以下所示:
class updateTankPosCallback : public osg::NodeCallback {
public:
updateTankPosCallback::updateTankPosCallback(tankInputDeviceStateType* tankIDevState)
: rotation(0.0) , tankPos(-15.,0.,0.)
{
tankInputDeviceState = tankIDevState;
}
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::PositionAttitudeTransform* pat =
dynamic_cast<osg::PositionAttitudeTransform*> (node);
if(pat)
{
if (tankInputDeviceState->moveFwdRequest)
{
tankPos.set(tankPos.x()+.01,0,0);
pat->setPosition(tankPos);
}
}
traverse(node, nv);
}
protected:
osg::Vec3d tankPos;
tankInputDeviceStateType* tankInputDeviceState;
};
如今,鍵盤和更新回調之間的通信框架已經基本完成。下一步是建立一個tankInputDeviceStateType的實例。這個實例將做爲事件處理器構造函數的參數傳入。同時它也是模型位置更新回調類的構造函數參數。當事件處理器添加到視口的事件處理器列表中以後,咱們就能夠進入仿真循環並執行相應的功能了。
// 定義用於記錄鍵盤事件的類的實例。
tankInputDeviceStateType* tIDevState = new tankInputDeviceStateType;
// 設置坦克的更新回調。
// 其構造函數將傳遞上面的實例指針做爲實參。
tankPAT->setUpdateCallback(new updateTankPosCallback(tIDevState));
// 鍵盤處理器類的構造函數一樣傳遞上面的實例指針做爲實參。
myKeyboardEventHandler* tankEventHandler = new myKeyboardEventHandler(tIDevState);
// 將事件處理器壓入處理器列表。
viewer.getEventHandlerList().push_front(tankEventHandler);
// 設置視口並進入仿真循環。
viewer.setSceneData( root );
viewer.realize();
while( !viewer.done() )
{
viewer.frame();
}
不過用戶還有更多能夠擴展的地方:例如使坦克在按鍵鬆開的時候中止運動,轉向,加速等等。
10.使用自定義矩陣來放置相機(Positioning a Camera with a User-Defined Matrix)
10.1本章目標
手動放置相機,以實現場景的觀覽。
咱們可使用osg::Matrix類來設置矩陣的數據。本章中咱們將使用雙精度類型的矩陣類osg::Matrixd。要設置矩陣的位置和方向,咱們可使用矩陣類的makeTranslate()和makeRotate()方法。爲了方便起見,這兩個方法均提供了多種可重載的類型。本例中咱們使用的makeRotate()方法要求三對角度/向量值做爲輸入參數。旋轉量由圍繞指定向量軸所旋轉的角度(表示爲弧度值)決定。這裏咱們簡單地選用X,Y,Z直角座標系做爲旋轉參照的向量軸。將平移矩陣右乘旋轉矩陣後,便可建立一個單一的表示旋轉和平移的矩陣。代碼以下:
以下是設置場景的代碼。此場景包括一個小型的地形和坦克模型。坦克位於(10,10,8)的位置。
int main()
{
osg::Node* groundNode = NULL;
osg::Node* tankNode = NULL;
osg::Group* root = new osg::Group();
osgProducer::Viewer viewer;
osg::PositionAttitudeTransform* tankXform;
groundNode = osgDB::readNodeFile("\\Models\\JoeDirt\\JoeDirt.flt");
tankNode = osgDB::readNodeFile("\\Models\\T72-Tank\\T72-tank_des.flt");
// 建立綠色的天空佈景。
osg::ClearNode* backdrop = new osg::ClearNode;
backdrop->setClearColor(osg::Vec4(0.0f,0.8f,0.0f,1.0f));
root->addChild(backdrop);
root->addChild(groundNode);
tankXform = new osg::PositionAttitudeTransform();
root->addChild(tankXform);
tankXform->addChild(tankNode);
tankXform->setPosition( osg::Vec3(10,10, );
tankXform->setAttitude(
osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) );
osgGA::TrackballManipulator *Tman = new osgGA::TrackballManipulator();
viewer.setCameraManipulator(Tman);
viewer.setSceneData( root );
viewer.realize();
矩陣的位置設置爲坦克模型後方60個單元,上方7個單元。同時設置矩陣的方向。
osg::Matrixd myCameraMatrix;
osg::Matrixd cameraRotation;
osg::Matrixd cameraTrans;
cameraRotation.makeRotate(
osg::DegreesToRadians(-20.0), osg::Vec3(0,1,0), // 滾轉角(Y軸)
osg::DegreesToRadians(-15.0), osg::Vec3(1,0,0) , // 俯仰角(X軸)
osg::DegreesToRadians( 10.0), osg::Vec3(0,0,1) ); // 航向角(Z軸)
// 相機位於坦克以後60個單元,之上7個單元。
cameraTrans.makeTranslate( 10,-50,15 );
myCameraMatrix = cameraRotation * cameraTrans;
場景的視口類實例使用當前MatrixManipulator控制器類(TrackballManipulator,DriveManipulator等)矩陣的逆矩陣來設置主攝像機的位置。爲了在視口中使用咱們自定義的攝像機位置和方向矩陣,咱們須要首先計算自定義矩陣的逆矩陣。
除了求取逆矩陣以外,咱們還須要提供世界座標系的方向。一般osgGA::MatrixManipulator矩陣(osgProducer::Viewer中使用)使用的座標系爲Z軸向上。可是Producer和osg::Matrix(也就是上文所建立的)使用Y軸向上的座標系系統。所以,在得到逆矩陣以後,咱們須要將其從Y軸向上旋轉到Z軸向上的形式。這一要求能夠經過沿X軸旋轉-90度來實現。其實現代碼以下所示:
while( !viewer.done() )
{
if (manuallyPlaceCamera)
{
osg::Matrixd i = myCameraMatrix.inverse(myCameraMatrix);
Tman->setByInverseMatrix(
osg::Matrix(i.ptr() )
* osg::Matrix::rotate( -3.1415926/2.0, 1, 0, 0 ) );
}
viewer.frame();
}
注意:按下V鍵能夠手動切換攝像機。
提示:自OSG 0.9.7發佈以後,新的osgGA::MatrixManipulator類(TrackerManipulator)容許用戶將攝相機「依附」到場景圖形中的節點。這一新增的操縱器類能夠高效地替代下面所述的方法。
本章教程將繼續使用回調和節點路徑(NodePath)來檢索節點的世界座標。
在一個典型的仿真過程當中,用戶可能須要從場景中的各類車輛和人物裏選擇一個進行跟隨。本章將介紹一種將攝像機「依附」到場景圖形節點的方法。此時視口的攝像機將跟隨節點的世界座標進行放置。
視口類包括了一系列的矩陣控制器(osgGA::MatrixManipulator)。於是提供了「驅動控制(Drive)」,「軌跡球(Trackball)」,「飛行(Fly)」等交互方法。矩陣控制器類用於更新攝像機位置矩陣。它一般用於迴應GUI事件(鼠標點擊,拖動,按鍵,等等)。本文所述的功能須要依賴於相機位置矩陣,並參照場景圖形節點的世界座標。這樣的話,相機就能夠跟隨場景圖形中的節點進行運動了。
爲了得到場景圖形中節點的世界座標,咱們須要使用節點訪問器的節點路徑功能來具現一個新的類。這個類將提供一種方法將本身的實例關聯到場景圖形,並所以提供訪問任意節點世界座標的方法。此座標矩陣(場景中任意節點的世界座標)將做爲相機位置的矩陣,由osgGA::MatrixManipulator實例使用。
首先咱們建立一個類,計算場景圖形中的多個變換矩陣的累加結果。很顯然,全部的節點訪問器都會訪問當前的節點路徑。節點路徑本質上是根節點到當前節點的全部節點列表。有了節點路徑的實例以後,咱們就可使用場景圖形的方法computeWorldToLocal( osg::NodePath)來獲取表達節點世界座標的矩陣了。
這個類的核心是使用更新回調來獲取某個給定節點以前全部節點的矩陣和。整個類的定義以下:
struct updateAccumulatedMatrix : public osg::NodeCallback
{
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
matrix = osg::computeWorldToLocal(nv->getNodePath() );
traverse(node,nv);
}
osg::Matrix matrix;
};
下一步,咱們須要在場景圖形的更新遍歷中啓動回調類。所以,咱們將建立一個類,其中包括一個osg::Node實例做爲數據成員。此節點數據成員的更新回調是上述updateAccumulatedMatrix類的實例,同時此節點也將設置爲場景的一部分。爲了讀取用於描繪節點世界座標的矩陣(該矩陣與節點實例相關聯),咱們須要爲矩陣提供一個「get」方法。咱們還須要提供添加節點到場景圖形的方法。咱們須要注意的是,用戶應如何將節點關聯到場景中。此節點應當有且只有一個父節點。所以,爲了保證這個類的實例只有一個相關聯的節點,咱們還須要記錄這個類的父節點。類的定義以下面的代碼所示:
struct transformAccumulator
{
public:
transformAccumulator();
bool attachToGroup(osg::Group* g);
osg::Matrix getMatrix();
protected:
osg::ref_ptr<osg::Group> parent;
osg::Node* node;
updateAccumulatedMatrix* mpcb;
};
類的實現代碼以下所示:
transformAccumulator::transformAccumulator()
{
parent = NULL;
node = new osg::Node;
mpcb = new updateAccumulatedMatrix();
node->setUpdateCallback(mpcb);
}
osg::Matrix transformAccumulator::getMatrix()
{
return mpcb->matrix;
}
bool transformAccumulator::attachToGroup(osg::Group* g)
// 注意不要在回調中調用這個函數。
{
bool success = false;
if (parent != NULL)
{
int n = parent->getNumChildren();
for (int i = 0; i < n; i++)
{
if (node == parent->getChild(i) )
{
parent->removeChild(i,1);
success = true;
}
}
if (! success)
{
return success;
}
}
g->addChild(node);
return true;
}
如今,咱們已經提供了類和方法來獲取場景中節點的世界座標矩陣,咱們所需的只是學習如何使用這個矩陣來變換相機的位置。osgGA::MatrixManipulator類便可提供一種更新相機位置矩陣的方法。咱們能夠從MatrixManipulator繼承一個新的類,以實現利用場景中某個節點的世界座標矩陣來改變相機的位置。爲了實現這一目的,這個類須要提供一個數據成員,做爲上述的accumulateTransform實例的句柄。新建類同時還須要保存相機位置矩陣的相應數據。
MatrixManipulator類的核心是「handle」方法。這個方法用於檢查選中的GUI事件並做出響應。對咱們的類而言,惟一須要響應的GUI事件就是「FRAME」事件。在每個「幀事件」中,咱們都須要設置相機位置矩陣與transformAccumulator矩陣的數值相等。咱們能夠在類的成員中建立一個簡單的updateMatrix方法來實現這一操做。因爲咱們使用了虛基類,所以某些方法必須在這裏進行定義(矩陣的設置及讀取,以及反轉)。綜上所述,類的實現代碼以下所示:
class followNodeMatrixManipulator : public osgGA::MatrixManipulator
{
public:
followNodeMatrixManipulator( transformAccumulator* ta);
bool handle (const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa);
void updateTheMatrix();
virtual void setByMatrix(const osg::Matrixd& mat) {theMatrix = mat;}
virtual void setByInverseMatrix(const osg::Matrixd&mat) {}
virtual osg::Matrixd getInverseMatrix() const;
virtual osg::Matrixd getMatrix() const;
protected:
~followNodeMatrixManipulator() {}
transformAccumulator* worldCoordinatesOfNode;
osg::Matrixd theMatrix;
};
The class implementation is as follows:
followNodeMatrixManipulator::followNodeMatrixManipulator( transformAccumulator* ta)
{
worldCoordinatesOfNode = ta; theMatrix = osg::Matrixd::identity();
}
void followNodeMatrixManipulator::updateTheMatrix()
{
theMatrix = worldCoordinatesOfNode->getMatrix();
}
osg::Matrixd followNodeMatrixManipulator::getMatrix() const
{
return theMatrix;
}
osg::Matrixd followNodeMatrixManipulator::getInverseMatrix() const
{
// 將矩陣從Y軸向上旋轉到Z軸向上
osg::Matrixd m;
m = theMatrix * osg::Matrixd::rotate(-M_PI/2.0, osg::Vec3(1,0,0) );
return m;
}
void followNodeMatrixManipulator::setByMatrix(const osg::Matrixd& mat)
{
theMatrix = mat;
}
void followNodeMatrixManipulator::setByInverseMatrix(const osg::Matrixd& mat)
{
theMatrix = mat.inverse();
}
bool followNodeMatrixManipulator::handle
(const osgGA::GUIEventAdapter&ea, osgGA::GUIActionAdapter&aa)
{
switch(ea.getEventType())
{
case (osgGA::GUIEventAdapter::FRAME):
{
updateTheMatrix();
return false;
}
}
return false;
}
上述的全部類都定義完畢以後,咱們便可直接對其進行使用。咱們須要聲明一個transformAccumulator類的實例。該實例應當與場景圖形中的某個節點相關聯。而後,咱們須要聲明nodeFollowerMatrixManipulator類的實例。此操縱器類的構造函數將獲取transformAccumulator實例的指針。最後,將新的矩陣操縱器添加到視口操控器列表中。上述步驟的實現以下:
// 設置場景和視口(包括tankTransform節點的添加)……
transformAccumulator* tankWorldCoords = new transformAccumulator();
tankWorldCoords->attachToGroup(tankTransform);
followNodeMatrixManipulator* followTank =
new followNodeMatrixManipulator(tankWorldCoords);
osgGA::KeySwitchMatrixManipulator *ksmm = new osgGA::KeySwitchMatrixManipulator();
if (!ksmm)
return -1;
// 添加跟隨坦克的矩陣控制器的。按下「m」鍵便可實現視口切換到該控制器。
ksmm->addMatrixManipulator('m',"tankFollower",followTank);
viewer.setCameraManipulator(ksmm);
// 進入仿真循環……
建立回調,以實現用於沿軌道環繞,同時指向場景中某個節點的世界座標矩陣的更新。使用此矩陣的逆矩陣來放置相機。
本章的回調類基於上一篇的osgFollowMe教程。本章中,咱們將添加一個新的矩陣數據成員,以保存視口相機所需的世界座標。每次更新遍歷啓動時,咱們將調用環繞節點的當前軌道世界座標矩陣。爲了實現環繞節點的效果,咱們將添加一個「angle」數據成員,其值每幀都會增長。矩陣的相對座標基於一個固定數值的位置變換,而旋轉量基於每幀更新的角度數據成員。爲了實現相機的放置,咱們還將添加一個方法,它將返回當前的軌道位置世界座標。類的聲明以下所示: class orbit : public osg::NodeCallback {public: orbit(): heading(M_PI/2.0) {} osg::Matrix getWCMatrix(){return worldCoordMatrix;} virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) { osg::MatrixTransform *tx = dynamic_cast<osg::MatrixTransform *>(node); if( tx != NULL ) { heading += M_PI/180.0; osg::Matrixd orbitRotation; orbitRotation.makeRotate( osg::DegreesToRadians(-10.0), osg::Vec3(0,1,0), // 滾轉角(Y軸) osg::DegreesToRadians(-20.0), osg::Vec3(1,0,0) , // 俯仰角(X軸) heading, osg::Vec3(0, 0, 1) ); // 航向角(Z軸) osg::Matrixd orbitTranslation; orbitTranslation.makeTranslate( 0,-40, 4 ); tx->setMatrix ( orbitTranslation * orbitRotation); worldCoordMatrix = osg::computeLocalToWorld( nv->getNodePath() ); } traverse(node, nv); }private: osg::Matrix worldCoordMatrix; float heading;}; 使用回調時,咱們須要向場景添加一個矩陣變換,並將更新回調設置爲「orbit」類的實例。咱們使用前述osgManualCamera教程中的代碼來實現用矩陣世界座標來放置相機。咱們還將使用前述鍵盤接口類的代碼來添加一個函數來更新全局量,該全局量用於容許用戶自行選擇缺省和「環繞」的視口。 int main(){ osg::Node* groundNode = NULL; osg::Node* tankNode = NULL; osg::Group* root = NULL; osgViewer::Viewer viewer; osg::PositionAttitudeTransform* tankXform = NULL; groundNode = osgDB::readNodeFile("\\Models\\JoeDirt\\JoeDirt.flt"); tankNode = osgDB::readNodeFile("\\Models\\T72-Tank\\T72-tank_des.flt"); root = new osg::Group(); // 建立天空。 osg::ClearNode* backdrop = new osg::ClearNode; backdrop->setClearColor(osg::Vec4(0.0f,0.8f,0.0f,1.0f)); root->addChild(backdrop); tankXform = new osg::PositionAttitudeTransform(); root->addChild(groundNode); root->addChild(tankXform); tankXform->addChild(tankNode); tankXform->setPosition( osg::Vec3(10,10,8) ); tankXform->setAttitude( osg::Quat(osg::DegreesToRadians(-45.0), osg::Vec3(0,0,1) ) ); osgGA::TrackballManipulator *Tman = new osgGA::TrackballManipulator(); viewer.setCameraManipulator(Tman); viewer.setSceneData( root ); viewer.realize(); // 建立矩陣變換節點,以實現環繞坦克節點。 osg::MatrixTransform* orbitTankXForm = new osg::MatrixTransform(); // 建立環繞軌道回調的實例。 orbit* tankOrbitCallback = new orbit(); // 爲矩陣變換節點添加更新回調的實例。 orbitTankXForm->setUpdateCallback( tankOrbitCallback ); // 將位置軌道關聯給坦克的位置,即,將其設置爲坦克變換節點的子節點。 tankXform->addChild(orbitTankXForm); keyboardEventHandler* keh = new keyboardEventHandler(); keh->addFunction('v',toggleTankOrbiterView); viewer.addEventHandler(keh); while( !viewer.done() ) { if (useTankOrbiterView) { Tman->setByInverseMatrix(tankOrbitCallback->getWCMatrix() *osg::Matrix::rotate( -3.1415926/2.0, 1, 0, 0 )); } viewer.frame(); } return 0;} 提示:按下V鍵來切換不一樣的視口。