osg_beginer_guider_Chapter 5: Managing Scene Graph

場景圖是表示圖形與狀態對象的空間佈局的節點的層次圖結構圖。他封裝了最底層的圖像基元與狀態組合,可以通過底層的圖像API創建可視化事物。OpenSceneGraph釋放了場景圖的威力,並且開發優化機制來管理與渲染3D場景,從而允許開發者以標準方式使用簡單但強大的代碼實現如對象組裝,遍歷,傳輸棧,場景裁剪,細節管理以及其他基本或是高級圖像特性等事情。

在本章中,我們將會探討下列主題:

  • 理解組合節點與葉子節點的概念
  • 如何處理父節點與子節點接口
  • 使用各種節點,包括轉換節點,切換節點,細節節點與代理節點
  • 如何由基本的節點類派生我們自己的節點
  • 如何遍歷已載入模式的場景圖結構

The Group interface

osg::Group類型表示OSG場景圖的組合節點。他可以具有任意數量的子節點,包括osg::Geode葉子節點以及其他的osg::Group節點。他是最常用到的各種NodeKits的基類-也就是具有各種功能的節點。

osg::Group類派生自osg::Node,因而間接派生自osg::Referenced。osg::Group類包含一個子節點列表,每一個子節點由智能指針osg::ref_ptr<>進行管理。這可以確保刪除場景圖中的級聯節點集合時不會存在內存泄露。

osg::Group類提供了一個公共方法集合用於定義處理子節點的接口。這些方法非常類似於osg::Geode的可繪製元素的管理方法,但是大多數的輸入參數是osg::Node指針。

  1. 公共方法addChild()將一個節點關聯到子節點列表的結尾。同時,有一個insertChild()方法用於將節點插入到osg::Group的指定位置處,該方法接受一個整數索引與一個節點指針作爲參數。
  2. 公共方法removeChild()與removeChildren()將會由當前的osg::Group對象移除一個或是多個子節點。後者使用兩個參數:基於零的起始元素索引,以及要移除的元素數目。
  3. getChild()方法在指定索引處存儲的osg::Node指針。
  4. getNumChildren()返回子節點的總數。

由於我們前面處理osg::Geode與可繪製元素的經驗,我們可以很容易處理osg::Group子節點接口。

Managing parent nodes

我們已經瞭解到osg::Group被用作組合節點,而osg::Geode被用作場景圖的葉子節點。其方法在上一章中進行了介紹,而在本章中也同樣會用到。另外,兩個類都應具有用於管理父節點的接口。

正如稍後將會解釋的,OSG允許一個節點具有多個父節點。在本節中,我們將會首先概略瞭解一個父節點管理方法,這些方法直接聲明在osg::Node類中:

  1. getParent()方法返回一個osg::Group指針作爲父節點。他需要一個表示父節點列表中索引的整數參數。
  2. getNumParents()方法返回父節點的總數。如果節點只有一個父節點,該方法將會返回1,而此時只有getParent(0)是正確可用的。
  3. getParentalNodePaths()方法返回由場景的根節點到當前節點(但是不包含當前節點)的所有可能路徑。他返回一個osg::NodePath變量的列表。

osg::NodePath實際上是一個節點指針的std::vector對象,例如,假定我們有如下一個場景圖:

_images/osg_parentalnodepath.png

下面的代碼片段將會找到由場景根節點到節點child3的唯一路徑:

osg::NodePath& nodePath = child3->getParentalNodePaths()[0];
for ( unsigned int i=0; i<nodePath.size(); ++i )
{
    osg::Node* node = nodePath[i];
    // Do something...
}

我們可以在循環中成功獲取節點Root,Child1與Child2。

我們並不需要使用內存管理系統來引用節點的父節點。當父節點被刪除時,他會自動由子節點的記錄中移除。

沒有任何父節點的節點只能被看作場景圖的根節點。在這種情況下,getNumParents()方法將會返回0,並且不會獲取到父節點。

Time for action - adding models to the scene graph

在前面的示例中,我們只是通過osgDB::readNodeFile()函數載入一個模型,例如Cessna。這次我們將會嘗試載入並管理多個模型。每一個模型將會被賦值給一個節點指針,然後添加到組合節點。組合節點被定義爲根節點,將會被程序用來在最後渲染整個場景圖:

  1. 包含必需的頭文件:
#include <osg/Group>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
  1. 在主函數中,我們首先載入兩個不同的模型,然後將其賦值給osg::Node指針。載入的模型同時也是一個通過組合節點與葉子節點構建的子場景圖。osg::Node類能夠表示任意類型的子場景圖,如果需要,他們可以被轉換爲osg::Group或是osg::Geode,或者通過C++ dynamic_cast<>操作符實現,或者是如asGroup()與asGeode()這樣的便利方法,這要比dynamic_cast<>節省時間。
osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile(
    "cessna.osg" );
osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile( "cow.osg" );
  1. 通過addChild()方法向osg::Group節點添加兩個模型:
osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild( model1.get() );
root->addChild( model2.get() );
  1. 初始化並啓動查看器:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 現在我們將會看到一頭牛與Cessna模型粘合在一起。實際上不可能看到這樣的場景,但是在虛擬世界中,這兩個模型屬性於由一個組合節點管理的不相關的子節點,因而可以由場景查看器進行單獨渲染。
_images/osg_group_node.png

What just happened?

osg::Group與osg::Geode都由osg::Node基類派生。osg::Group允許添加任意類型的子節點,包括osg::Group自身。然而,osg::Geode類不包含組合節點或是葉子節點。他只接受用於渲染的可繪製元素。

如果我們能夠確定一個節點的類型是osg::Group,osg::Geode還是其他的派生類型將會非常方便,特別是由文件讀取並由osg::Node類所管理的節點,例如:

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile( "cessna.osg" );

dynamic_cast<>操作符與如asGroup(),asGeode()以及其他的便利方法,會有助於將一個指針或引用類型轉換爲另一種指針或是引用類型。首先,我們以dynamic_cast<>爲例。這可以用來在類的繼承層次結構中向下轉換,例如:

osg::ref_ptr<osg::Group> model =
    dynamic_cast<osg::Group*>( osgDB::readNodeFile("cessna.osg") );

osgDB::readNodeFile()函數的返回值總是osg::Node*,但是我們也可以嘗試使用osg::Group指針進行管理。如果Cessna子圖的根節點是一個組合節點,那麼轉換就會成功,否則轉換失敗,而變量model將會爲NULL。

我們也可以執行向上轉換,這實際上是隱式轉換:

osg::ref_ptr<osg::Group> group = ...;
osg::Node* node1 = dynamic_cast<osg::Node*>( group.get() );
osg::Node* node2 = group.get();

在大多數編譯器上,node1與node2都會通過編譯並正常工作。

轉換方法也會完成類似的工作。事實上,如果我們所需要的類型存在一個這樣的轉換方法,則推薦使用轉換方法,而不是dynamic_cast<>,特別是在性能要求較高的代碼中:

// Assumes the Cessna's root node is a group node.
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
osg::Group* convModel1 = model->asGroup();  // OK!
osg::Geode* convModel2 = model->asGeode();  // Returns NULL.

Pop quiz - fast dynamic casting

在C++程序中,dynamic_cast<>會以運行時檢測的安全性執行類型轉換,這會要求允許運行時類型信息(RTTI)。有時並不推薦與osg::Node類的轉換方法相比較,後者已經由子類,例如osg::Group與osg::Geode進行了重寫。我們知道其中的原因嗎?何時我們應該使用asGroup()與asGeode(),而何時應該使用dynamic_cast<>呢?

Traversing the scene graph

一個通常的遍歷由下列步驟組成:

  1. 首先,由任意節點開始(例如,根節點)。
  2. 遞歸沿場景圖向下(或向上)到子節點,直到葉子節點或是沒有子節點的節點。
  3. 反向到達沒有完成探索的最近節點,重複上述步驟。這被稱爲場景圖的嘗試優先搜索。

在遍歷過程中,可以對所有的場景節點應用不同的更新與渲染操作,從而使得遍歷成爲場景圖的關鍵特性。有不同目的的多種遍歷類型:

  1. 事件(event)遍歷在遍歷節點時首先處理鼠標與鍵盤輸入以及其他的用戶事件。
  2. 更新遍歷(或應用遍歷)允許用戶應用修改場景圖,例如設置節點與幾何屬性,應用節點功能,執行回調等。
  3. 裁剪遍歷(cull)測試一個節點是否位於一個視口內並可進行渲染。他會裁剪不可見與不可用的節點,並且向內部渲染列表輸出優化的場景圖。
  4. 繪製遍歷(draw)(或渲染遍歷)執行底層的OpenGL API調用來真正的渲染場景。注意,他與場景圖並沒有關係,而僅是作用在由裁剪遍歷所生成的渲染列表上。

在通常情況下,這些遍歷應依次爲每一幀所執行。但是對於具有多處理器與圖形卡的系統,OSG可以並行執行從而提高渲染效率。

訪問者模式可以用來實現遍歷。該模式會在本章稍後進行討論。

Transformation nodes

osg::Group節點除了向下遍歷到子節點外不做任何事情。然而,OSG同時支持osg::Transform類家庭,這是在應用到幾何體的遍歷相關轉換過程中創建的。osg::Transform派生自osg::Group。他不能被直接實例化。相反,他提供了一個用於實現不同轉換接口的子類集合。

當向下遍歷場景圖層次結構時,osg::Transform節點總是將其自己的操作添加到當前的變換矩陣,也就是,OpenGL模型-視圖矩陣(model-view matrix)。他等同於如glMultMatrix()這樣的連接OpenGL矩陣命令,例如:

上面的示例場景圖可以翻譯爲如下的OpenGL代碼:

glPushMatrix();
    glMultMatrix( matrixOfTransform1 );
    renderGeode1();  // Assume this will render Geode1
    glPushMatrix();
        glMultMatrix( matrixOfTransform2 );
        renderGeode2();    // Assume this will render Geode2
    glPopMatrix();
glPopMatrix();

要使用座標幀(coordinate frmae)的概念來描述上述過程,我們可以說Geode1與Transform2位於Transform1的相對引用幀之下,Geode2位於Transform2的相對引用幀之下。然而,OSG同時也允許設置絕對引用幀,從而導致與OpenGL命令glLoadMatrix()等同的行爲:

transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF );

而要切換到默認的座標幀,可以使用如下的代碼:

transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF );

Understanding the matrix

osg::Matrix是一個基本的OSG數據類型,而不需要使用智能指針進行管理。他支持4x4矩陣變換接口,例如變換,旋轉,縮放與投影操作。他可以顯式設置:

osg::Matrix mat( 1.0f, 0.0f, 0.0f, 0.0f,
                 0.0f, 1.0f, 0.0f, 0.0f,
                 0.0f, 0.0f, 1.0f, 0.0f,
                 0.0f, 0.0f, 0.0f, 1.0f ); // Just an identity matrix

其他的方法與操作包括:

  1. 公共方法postMult()與operator*()將當前的矩陣對象與輸入矩陣或向量參數執行後乘運算。而方法preMult()執行前乘運算。
  2. makeTranslate(),makeRotate()與makeScale()方法重置當前矩陣並且創建一個4x4變換,旋轉或是縮放矩陣。其靜態版本,translate(),rotate()與scale()可以使用特定的參數分配一個新的矩陣對象。
  3. 公共方法invert()反轉矩陣。其靜態版本inverse()需要一個矩陣參數並且返回一個新的反轉osg::Matrix對象。

我們將會注意到OSG使用行爲主(row-major)矩陣來表示變換。這意味着OSG會將向量看作行,並使用行向量執行前乘矩陣操作。所以將變換矩陣mat應用到座標vec的方法爲:

osg::Matrix mat = …;
osg::Vec3 vec = …;
osg::Vec3 resultVec = vec * mat;

當連接矩陣時,OSG行爲主矩陣操作的順序也很容易理解:

osg::Matrix mat1 = osg::Matrix::scale(sx, sy, sz);
osg::Matrix mat2 = osg::Matrix::translate(x, y, z);
osg::Matrix resultMat = mat1 * mat2;

開發者總是可以由左向右讀取變換過程,也就是,resultMat意味着首先使用mat1縮放向量,然而使用mat2進行反轉。這種解釋聽起來更爲清晰與合適。

osg::Matrix類表示一個4x4浮點類型矩陣。他可以通過直接使用osg::Matrix重載方法set()進行轉換。

The Matrix Transform class

osg::MatrixTransform類派生自osg::Transform。他在內部使用一個osg::Matrix變量來應用4x4雙精度浮點類型變換。公共方法setMatrix()與getMatrix()將osg::Matrix參數賦值給osg::MatrixTransform的成員變量。

Time for action - performing translations of child nodes

現在我們要利用變換節點。osg::MatrixTransform節點,將當前的模型視圖矩陣與指定的矩陣直接相乘,可以將我們的模型移動到視圖空間中的其他位置。

  1. 包含必需的頭文件:
#include <osg/MatrixTransform>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
  1. 首先載入Cessna模型:
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile(
  "cessna.osg" );
  1. osg::MatrixTransform類由osg::Group類派生,所以他可以使用addChild()方法來添加多個子節點。所有的子節點都會受到osg::MatrixTransform節點的影響,並且會依據當前的矩陣進行變換。在這裏,我們將會兩次載入模型,以同時單獨顯示兩個實例:
osg::ref_ptr<osg::MatrixTransform> transformation1 = new
osg::MatrixTransform;
transform1->setMatrix( osg::Matrix::translate(
  -25.0f, 0.0f, 0.0f) );
transform1->addChild( model.get() );
osg::ref_ptr<osg::MatrixTransform> transform2 = new
osg::MatrixTransform;
transform2->setMatrix( osg::Matrix::translate(
  25.0f, 0.0f, 0.0f) );
transform2->addChild( model.get() );
  1. 向根節點添加兩個變換節點並啓動查看器:
osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild( transformation1.get() );
root->addChild( transformation2.get() );
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. Cessna模型,最初位於座標原點,該模型被複制並在不同的位置顯示。一個被變換到座標(-25.0, 0.0, 0.0)處,而另一個被變換到(25.0,0.0,0.0)處:
_images/osg_matrix.png

What just happened?

我們也許會場景圖的結構感到迷惑,因爲model指針被關聯到兩個不同的父節點。在一個典型的樹結構中,一個節點至多隻有一個父節點,因而共享子節點是不可能的。然而,OSG支持對象共享機制,也就是,一個子節點(model指針)可以爲不同的祖先節點(transformation1與transformation2)實例化。然後當遍歷並渲染場景圖時,由根節點到實例化節點會有多條路徑,從而導致實例節點被顯示多次。

_images/osg_multi_parent.png

這對於減少場景內存非常有用,因爲程序只會保存一份共享數據的拷貝,並且在由其多個父節點管理的不同環境中簡單的多次調用實現方法(例如,osg::Drawable派生類的drawImplementation())。

共享子節點的每個父節點會保存有其自己的指向子節點的osg::ref_ptr<>指針。在這種情況下,引用計數不會減少到0,而該節點在其所有的父節點解引用之前不會被釋放。我們將會發現在管理節點的多個父節點時getParent()與getNumParents()方法將會非常有用。

建議在一個程序中儘可能的共享葉子節點,幾何體,紋理以及OpenGL渲染狀態。

Pop quiz - matrix multiplications

正如我們已經討論的,OSG使用行向量與行爲主矩陣在右側原則(right-hand rule)下來執行前相乘(vector*matrix)。然而,OpenGL使用列爲主矩陣與列向量來執行後相乘(matrix*vector)。所以,當將OpenGL變換轉換爲OSG變換時,我們會認爲哪一個重要呢?

Have a go hero - making use of the PositionAttitudeTransform class

osg::MatrixTransform類的執行類似於OpenGL的glMultMatrix()與glLoadMatrix()函數,該函數幾乎可以實現所有的空間變換類型,但是並不容易使用。然而,osg::PositionAttitudeTransform類的作用類似於OpenGL的glTranslate(),glScale()與glRotate()函數的組合。他提供了公共方法在3D世界中變換子節點,包括setPosition(),setScale()與setAttitue()。前兩個需要osg::Vec3輸入值,而setAttitude()使用osg::Quat變量作爲參數。osg::Quat是一個四元數類,該類被用來表示朝向。其構造函數可以接受一個浮點角度與一個osg::Vec3向量作爲參數。歐拉旋轉(關於三個固定座標的旋轉)也是可以接受的,但要使用osg::Quat的重載構造函數:

osg::Quat quat(xAngle, osg::X_AXIS,
               yAngle, osg::Y_AXIS,
               zangle, osg::Z_AXIS); // Angles should be radians!

現在讓我們重寫前面的示例,使用osg::PositionAttitudeTransform類來替換osg::MatrixTransform節點。使用setPosition()來指定變換,使用setRotate()來指定子模型的旋轉,體驗一下在某些情況下對於是否更爲方便。

Switch nodes

osg::Switch節點能夠渲染或是略過某些特定條件的子節點。他繼承了超類osg::Group的方法,並且可以爲每一個子節點關聯一個布爾值。他有一些非常有用的公共方法:

  1. 重載的addChild()方法除了osg::Node指針以外還可以有一個布爾參數。當布爾參數被設置爲假時,所添加的節點對於查看器不可見。
  2. setValue()方法可以設置指定索引處子節點的可見性值。他有兩個參數:基於零的索引與布爾值。getValue()可以獲取輸入索引處子節點的值。
  3. setNewChildDefaultValue()方法爲新子節點設置默認可見性。如果一個子節點只是簡單的被添加而沒有指定值,則其值由setNewChildDefaultValue()決定,例如:
switchNode->setNewChildDefaultValue( false );
switchNode->addChild( childNode ); // Turned off by default now!

Time for action - switching between the normal and damaged Cessna

我們將要使用osg::Switch節點來構建場景。他甚至可以用來實現狀態切換動畫與更爲複雜的工作,但是目前我們僅是演示如何在場景查看器啓動之前預先定義子節點的可見性。

  1. 包含必需的頭文件:
#include <osg/Switch>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
  1. 我們由文件中讀取兩個模型並使用開關進行控制。我們可以在OSG示例數據目錄中找到一個正常的Cessna與一個損壞的Cessna。他們非常適於模擬飛機的不同狀態:
osg::ref_ptr<osg::Node> model1= osgDB::readNodeFile("cessna.osg");
osg::ref_ptr<osg::Node> model2= osgDB::readNodeFile("cessnafire.
osg");
  1. osg::Switch節點能夠顯示一個或多個子節點並隱藏其他的子節點。其作用不同於osg::Group父類,後者會在渲染場景時顯示所有的子節點。如果我們要開發一個戰鬥遊戲,並且要在任何時刻管理某些飛機對象時,這個功能將會非常有用。下面的代碼會在將model2添加到根節點時設置爲可見,並同時隱藏model1:
osg::ref_ptr<osg::Switch> root = new osg::Switch;
root->addChild( model1.get(), false );
root->addChild( model2.get(), true );
  1. 啓動查看器:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 現在我們將會看到一個燃燒的Cessna而不是正常的Cessna:
_images/osg_switch.png

What just happened?

osg::Switch類在由其超類osg::Group管理的子節點列表之處添加一個開關值列表。兩個列表具有相同的大小,而列表中的每個元素與另一個列表中的元素具有一對一的關係。所以,開關值列表中的任何變化將會影響到相關的子節點,打開或關閉其可見性。

當OSG後端遍歷場景圖並應用不同的NodeKit功能時,由addChild()或setValue()所觸發的開關值變化將會被保存爲屬性並在下一個渲染幀中執行。在下面的代碼片段中,只有位於索引0與1處的後兩個子節點的開關值會實際起作用:

switchNode->setValue( 0, false );
switchNode->setValue( 0, true );
switchNode->setValue( 1, true );
switchNode->setValue( 1, false );

setValue()方法的重複調用會被簡單覆蓋且不會影響場景圖。

Level-of-detail nodes

詳細級別技術爲指定的對象創建詳細或是複雜性級別,並且提供一定的線索來自動選擇相應的對象級別,例如,依據距離觀看者的距離。他會減少3D世界中對象表示的複雜性,並且在遠距離對象的外觀上具有不被注意到的質量損失。

osg::LOD節點派生自osg::Group,並且使用子節點來表示可變詳細級別上的相同對象,由最高級別到最低級別。每一個級別需要一個最小與最大可視範圍來指定在相鄰級別之間切換的合理機會。osg::LOD節點的結果是子節點的離散量作爲級別,也被稱之爲離散LOD。

osg::LOD類可以配合子節點指定範圍,或是在已有的子節點上使用setRange()方法:

osg::ref_ptr<osg::LOD> lodNode = new osg::LOD;
lodNode->addChild( node2, 500.0f, FLT_MAX );
lodNode->addChild( node1 );
...
lodNode->setRange( 1, 0.0f, 500.0f );

在前面的代碼片段中,我們首先添加一個節點,node2,當距離眼睛超過500單位時纔會顯示該節點。在這這後,我們添加一個高分辨率模型,node1,並且通過使用setRange()方法爲近距離觀察重置其可視範圍。

Time for action - constructing a LOD Cessna

我們將使用一個預定義對象的集合創建一個離散LOD節點來表示相同的模型。這些對象被用作osg::LOD節點的子節點並且在不同的距離上顯示。我們將內部多邊形減少技術類osgUtil::Simplifier來由源始模型生成各種細節對象。我們也可以由磁盤文件讀取低多邊形與高多邊形模型。

  1. 包含必需的頭文件:
#include <osg/LOD>
#include <osgDB/ReadFile>
#include <osgUtil/Simplifier>
#include <osgViewer/Viewer>
  1. 我們要構建三級模型細節。首先,我們需要創建原始模型的三份拷貝。可以由文件三次讀取Cessna,但是在這裏調用clone()方法來複制所載入的模型以立即使用:
osg::ref_ptr<osg::Node> modelL3 = osgDB::readNodeFile("cessna.
osg");
osg::ref_ptr<osg::Node> modelL2 = dynamic_cast<osg::Node*>(
    modelL3->clone(osg::CopyOp::DEEP_COPY_ALL) );
osg::ref_ptr<osg::Node> modelL1 = dynamic_cast<osg::Node*>(
    modelL3->clone(osg::CopyOp::DEEP_COPY_ALL) );
  1. 我們希望級別三將是原始Cessna,該級別具有最大的多邊形數以用於近距離查看。級別二具有較少的可顯示的多邊形數,而級別一是細節最少的,該級別只在較遠的距離上顯示。osgUtil::Simplifier類在這裏用來減少頂點數與面數。我們使用不同的值爲級別一與級別二應用setSampleRation()方法,從而會導致不同的縮放比率:
osgUtil::Simplifier simplifier;
simplifier.setSampleRatio( 0.5 );
modelL2->accept( simplifier );
simplifier.setSampleRatio( 0.1 );
modelL1->accept( simplifier );
  1. 向LOD節點添加級別模型並且以遞減順序設置其可見範圍。當我們使用addChild()與setRange()方法配置最小與最大範圍值時,不要有重疊的範圍,否則就會在相同的位置上顯示多個級別模型,從而導致不正確的行爲:
osg::ref_ptr<osg::LOD> root = new osg::LOD;
root->addChild( modelL1.get(), 200.0f, FLT_MAX );
root->addChild( modelL2.get(), 50.0f, 200.0f );
root->addChild( modelL3.get(), 0.0f, 50.0f );
  1. 啓動查看器。這次程序會需要一些時間來計算並減少模型面數:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 再次出現Cessna模型。嘗試持續按下鼠標右鍵來放大與縮小。當近距離查看時我們會發現模型依然顯示很好,如下圖中的左側圖片所示。然而,當由遠距離查看時,模型會有簡化。如下圖中的右側兩幅圖所示。距離並不會嚴重影響渲染結果,但如果正確使用將會增強系統效率。
_images/osg_lod.png

What just happened?

我們是否注意到Cessna被拷貝兩次來準備不同的多邊形級別?modelL3在這裏不能被共享,因爲簡化器會直接在程序內存中操作幾何體數據,從而會影響共享相同內存的所有指針。事實上,這被稱爲淺拷貝(shallow copy)。

在這個示例中,我們引入了clone()方法,該方法可以爲所有的場景節點,可繪製元素與對象所用。他能夠執行深拷貝(deep copy),也就是,拷貝源對象所用的所有動態分配的內存。所以modelL2與modelL1管理新分配的內存,這兩個指針使用與modelL3相同的數據進行填充。

然後osgUtil::Simplifier類開始簡化模型,從而減輕圖形管理的負載。要應用該簡化器,我們必須調用節點的accept()方法。在Visiting scene graph structures一節,我們會瞭解到該類以及訪問者模式的更多信息。

Proxy and paging nodes

代理節點osg::ProxyNode與分頁節點osg::PagedLOD是爲場景負載均衡而提供的。這兩個類都是直接或是間接由osg::Group類派生的。

如果有大量的模型要載入並在場景圖中顯示時,osg::ProxyNode節點將會減少查看器的啓動時間。他能夠作爲外部文件的接口,幫助程序儘快啓動,然後使用一個獨立數據線程讀取這些等待的模型。他使用setFileName()而不是addChile()來設置模型文件並動態載入作爲子節點。

osg::PagedLOD節點同時繼承了osg::LOD的方法,但是爲了避免圖像管線的負載並使得渲染過程儘可能平滑而動態載入或是卸載詳細級別。

Time for action - loading a model at runtime

我們將通過使用osg::ProxyNode來演示模型文件的載入。代理將會記錄原始模型的文件名,並延遲載入直到查看已經運行併發送相應的請求。

  1. 包含必需的頭文件:
#include <osg/ProxyNode>
#include <osgViewer/Viewer>
  1. 我們並沒有直接載入模型文件作爲子節點,而是爲特定索引處的子節點設置文件名。這類似於insertChild()方法,後者會將節點放置在子節點列表的特定索引處,但是列表不會被填充,直到動態載入過程已經完成。
osg::ref_ptr<osg::ProxyNode> root = new osg::ProxyNode;
root->setFileName( 0, "cow.osg" );
  1. 啓動查看器:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 模型看起來像通常一樣被載入,但是我們會注意到他是突然出現的,而且查看點並沒有被調整到最佳位置。這是因爲不可見的代理節點的使用就如同在渲染開始時他並沒有包含子節點。然後cow模型會在運行時由文件載入,並且會自動添加爲代理的子節點並渲染:
_images/osg_proxy.png

What just happened?

osg::ProxyNode與osg::PagedLOD本身非常小巧;他們主要是作爲容器。OSG的內部數據載入管理器osgDB::DatabasePager將會在新文件或是詳細級別可用時,或是回退到下一個可用的子節點時,會實際完成發送請法度與載入場景圖的工作。

數據分頁器在多個後臺線程中運行,並且驅動靜態數據庫(由代理與分佈節點管理的數據生成文件)與動態數據庫數據(在運行時生成成並添加的分佈節點)的載入。

數據庫分佈器自動回收在當前視口中不再出現的分佈節點,並且會在渲染後端幾乎超負載時將其由場景圖中移除,也就是他需要提供大量渲染數據的多線程分頁支持時。然而,這並不會影響osg::ProxyNode節點。

Have a go hero - working with the PagedLOD class

類似於代理節點,osg::PagedLOD類也有一個setFileName()方法來設置要載入到特定子節點位置處的文件。然而,作爲一個LOD節點,他還需要設置每一個動態載入子節點的最小與最大可視範圍。假定我們有一個cessna.osg文件以及一個低多邊形版本modelL1,我們可以像下面的樣子組織分頁節點:

osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD;
pagedLOD->addChild( modelL1, 200.0f, FLT_MAX );
pagedLOD->setFileName( 1, "cessna.osg" );
pagedLOD->setRange( 1, 0.0f, 200.0f );

注意,modelL1指針不會由內存中卸載,因爲他是一個直接子節點,而不是一個文件代理。

我們會看到如果只顯一個詳細級別的節點,使用osg::LOD與osg::PagedLOD之間並沒有區別。一個更好的主意是嘗試使用osg::MatrixTransform來構建一個大的Cessna集。例如,我們可以使用一個獨立的函數來構建一個可變換的LOD Cessna:

osg::Node* createLODNode( const osg::Vec3& pos )
{
    osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD;
    …
    osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform;
    mt->setMatrix( osg::Matrix::translate(pos) );
    mt->addChild( pagedLOD.get() );
    return mt.release();
}

設置不同的位置參數並向場景根節點添加多個createLODNode()節點。可以看一下分佈節點是如何被渲染的。再嘗試使用osg::LOD,來比對一下在性能與內存使用上的不同。

Customizing your own NodeKits

在自定義節點與擴展新特性中最重要的步驟就是重寫虛方法traverse()。該方法是由OSG渲染後端爲每一幀所調用的。traverse()方法有一個輸入參數,osg::NodeVisitor&,該參數實際上指明瞭遍歷類型(更新,事件或剪裁)。大多數的OSG NodeKits重寫traverse()來實現其自己的功能,以及其他一些屬性與方法。

注意,有時重寫traverse()方法有一些危險,因爲如果開發者不能足夠細心,他就會影響遍歷過程並有可能導致不正確的渲染結果。如果我們希望通過將每一個節點類型擴展爲一個新的自定義類來爲多個節點類型添加新功能時,他會顯得笨拙難用。在這些情況下,考慮使用節點回調,我們會在第8章中進行討論。

Time for action - animating the switch node

osg::Switch類可以顯示特定的子節點而隱藏他的子節點。他可以用來表示各種對象的動畫狀態,例如,信號燈。然而,一個典型的osg::Switch節點並不能在不同時刻自動在子節點之間切換。基於這一思想,我們將開發一個新的AnimatingSwitch節點,該類會一次顯示一個子節點,並且依據用戶定義的內部計數器反轉切換狀態。

  1. 包含必需的頭文件:
#include <osg/Switch>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
  1. 聲明AnimatingSwitch類。該類將會由osg::Switch類派生並利用setValue()方法。我們同時使用一個OSG宏定義,META_Node,該宏類似於在上一章所介紹的定義節點基本屬性的META_Object宏:
class AnimatingSwitch : public osg::Switch
{
public:
    AnimatingSwitch() : osg::Switch(), _count(0) {}
    AnimatingSwitch( const AnimatingSwitch& copy,
             const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY )
    : osg::Switch(copy, copyop), _count(copy._count) {}
    META_Node( osg, AnimatingSwitch );

    virtual void traverse( osg::NodeVisitor& nv );

protected:
    unsigned int _count;
};
  1. 在traverse()實現中,我們將會增加內部計數器並且測試他是否到達60的倍數,並且反轉第一個與第二子節點的狀態:
void AnimatingSwitch::traverse( osg::NodeVisitor& nv )
{
    if ( !((++_count)%60) )
    {
        setValue( 0, !getValue(0) );
        setValue( 1, !getValue(1) );
    }
    osg::Switch::traverse( nv );
}
  1. 再次載入Cessna模型與燃燒的Cessna模型,並將其添加到自定義的AnimatingSwitch實例:
osg::ref_ptr<osg::Node> model1= osgDB::readNodeFile("cessna.osg");
osg::ref_ptr<osg::Node> model2= osgDB::readNodeFile("cessnafire.
osg");
osg::ref_ptr<AnimatingSwitch> root = new AnimatingSwitch;
root->addChild( model1.get(), true );
root->addChild( model2.get(), false );
  1. 啓動查看器:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 因爲硬件的刷新速率通常是60Hz,traverse()中的if條件將會每分鐘變爲真,從而實現動畫。那麼我們就會在前一分鐘內看到Cessna,而在下一分鐘內看到燃燒的Cessna,依次循環:
_images/osg_animating_switch.png

What just happened?

因爲traverse()方法被廣泛重新實現來擴展不同的節點類型,他涉及到爲實際使用讀取變換矩陣與渲染狀態的機制。例如,osg::LOD節點必須計算由子節點的中心到查看者眼睛的距離,從而用作不同級別之間切換的可視範圍。

輸入參數osg::NodeVisitor&是各種節點操作的關鍵。他表示訪問節點的遍歷類型,例如更新,事件與裁剪遍歷。前兩者與回調相關,我們會在第8章中進行詳細討論。

裁剪遍歷,名爲osgUtil::CullVisitor,可以使用下面的代碼片段由osg::NodeVisitor&參數獲取:

osgUtil::CullVisitor* cv = dynamic_cast<osgUtil::CullVisitor*>(&nv);
if ( cv )
{
    // Do something
}

我們應該在程序的開始處包含<osgUtil/CullVisitor>頭文件。裁剪訪問器通過不同的方法能夠獲取大量的場景狀態,甚至是改變內部渲染列的結構與順序。osgUtil::CullVisitor的概念與使用超出了本書的範圍,但是依然值得由OSG NodeKits的源碼進行理解與學習。

Have a go hero - creating a tracker node

我們是否想過實現一個跟蹤器節點,該節點會總是跟蹤其他節點的位置?跟蹤器是一個更好的osg::MatrixTransform派生子類。他可以使用智能指針成員來記錄要跟蹤的節點並在traverse()重寫方法中獲取3D世界中的位置。然後跟蹤器將會使用setMatrix()方法來將其自身設置到一個相對位置,以實現跟蹤操作。

我們可以通過使用osg::computeLocalToWorld()函數計算絕對座標幀中的頂點:

osg::Vec3 posInWorld = node->getBound().center() *
            osg::computeLocalToWorld(node->getParentalNodePaths()[0]);

這裏的getBound()方法將會返回一個osg::BoundingSphere對象。osg::BoundingSphere類表示一個節點的邊界圓,用來確定在視圖截面裁剪過程中節點是否可見與可裁剪。他有兩個主要方法:center()方法簡單讀取本地座標中邊界圓的中心點;而radius()方法返回半徑。

使用Managing parent nodes一節中所提供的getParentalNodePaths()方法,我們可以獲得父節點路徑並且計算由節點的相對引用幀到世界引用幀的變換矩陣。

The visitor design pattern

訪問者模式用來表示在一個圖結構的元素上所執行的用戶操作,而無需修改這些元素的類。訪問者類實現了所有要應用各種元素類型上的相應虛函數,並且通過雙分派(double dispatch)機制來實現該目標,也就是,依據接收者元素與訪問本身的運行時類型,分派一定的虛函數調用。

基於雙分派理論,開發者可以使用特定的操作請求自定義其訪問者,並且在運行時將訪問者綁定到不同的元素類型而不修改元素接口。這是一種無需定義多個新元素子類來擴展元素功能的好方法。

OSG支持osg::NodeVisitor類來實現訪問者模式。也就是,一個osg::NodeVisitor派生類遍歷一個場景圖,訪問每一個節點,並應用用戶定義的操作。他是更新,事件與裁剪遍歷(例如osgUtil::CullVisitor)以及其他一些場景圖工具,包括osgUtil::SmoothingVisitor,osgUtil::Simplifier與osgUtil::TriStripVisitor的實現的基類,所有這些類都會遍歷指定的子場景圖並且在osg::Geode節點中的幾何體上應用多邊形修改。

Visiting scene graph structures

要創建一個訪問者子類,我們必須重新實現osg::NodeVisitor基類中所聲明的一個或是多個apply()虛重載方法。這些方法是爲大多數主要的OSG節點類型所設計的。訪問者會在遍歷過程中爲他所訪問的每一個節點自動調用相應的apply()方法。用戶自定義的訪問者類應只爲所要求的節點類型重寫apply()方法。

在apply()方法的實現中,開發者需要在適當的時候調用osg::NodeVisitor的traverse()方法。他會指示訪問者遍歷到下一個節點,也許是一個子節點,或者如果當前節點沒有子節點要訪問,則爲兄弟節點。不調用traverse()方法則意味着立即停止遍歷,而場景圖的其他部分會被忽略而不執行任何操作。

apply()方法具有如下的統一格式:

virtual void apply( osg::Node& );
virtual void apply( osg::Geode& );
virtual void apply( osg::Group& );
virtual void apply( osg::Transform& );

要遍歷指定節點的子場景圖並調用這些方法,我們首先需要爲訪問對象選擇一個遍歷節點。以假定的ExampleVisitor類作爲例子,在特定的節點上初始化並啓動訪問需要兩個步驟:

ExampleVisitor visitor;
visitor->setTraversalMode( osg::NodeVisitor::TRAVERSE_ALL_CHILDREN );
node->accept( visitor );

枚舉器TRAVERSE_ALL_CHILDREN意味着遍歷節點的所有子節點。還有兩個其他選項:TRAVERSE_PARENTS,該選項會由當前節點回溯直到根節點,以及TRAVERSE_ACTIVE_CHILDREN,該選項只訪問活動子節點,例如,osg::Switch節點的可見子節點。

Time for action - analyzing the Cessna structure

用戶程序也許總是會在載入模型文件後在載入的場景圖中查找感興趣的節點。例如,如果根節點是osg::Transform或osg::Switch,我們也許會希望接管載入模型的變換或可見性。我們也許會對收集所有骨骼連接處的變換節點感興趣,從而用來在稍後執行特徵動畫。

在這種情況下,載入模型結構的分析非常重要。在這裏我們將會實現一個信息輸出訪問器,該訪問器會輸出所訪問節點的基本信息並將其排列在樹結構中。

  1. 包含必需的頭文件:
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
#include <iostream>
  1. 聲明InfoVisitor類,並定義必需要虛方法。我們僅處理葉子節點與普通的osg::Node對象。內聯函數spaces()用來在節點信息之前輸出空格,來表示其在樹結構中的級別:
class InfoVisitor : public osg::NodeVisitor
{
public:
    InfoVisitor() : _level(0)
    { setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); }

    std::string spaces()
    { return std::string(_level*2, ' '); }

    Virtual void apply( osg::Node& node );
    virtual void apply( osg::Geode& geode );

protected:
    unsigned int _level;
};
  1. 我們將會介紹兩個方法,className()與libraryName(),這兩個方法都會返回const char*值,例如,作爲類名的”Node”以及作爲庫名的”osg”。META_Object與META_Node宏定義會在內部完成這些工作:
void InfoVisitor::apply( osg::Node& node )
{
    std::cout << spaces() << node.libraryName() << "::"
      << node.className() << std::endl;

    _level++;
    traverse( node );
    _level--;
}
  1. 以osg::Geode&爲參數的apply()重載方法的實現與前面的實現略爲不同。他會遍歷所有關聯到osg::Geode節點的可繪製元素並輸出其信息。在這裏要小心traverse()的調用時,從而保證樹中每個節點的級別都是正確的。
void apply( osg::Geode& geode )
{
    std::cout << spaces() << geode.libraryName() << "::"
      << geode.className() << std::endl;

    _level++;
    for ( unsigned int i=0; i<geode.getNumDrawables(); ++i )
    {
        osg::Drawable* drawable = geode.getDrawable(i);
        std::cout << spaces() << drawable->libraryName() << "::"
          << drawable->className() << std::endl;
    }

    traverse( geode );
    _level--;
}
  1. 在主函數中,使用osgDB::readNodeFiles()由命令行參數讀取文件:
osg::ArgumentParser arguments( &argc, argv );
osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles( arguments );
if ( !root )
{
    OSG_FATAL << arguments.getApplicationName() <<": No data
      loaded." << std::endl;
    return -1;
}
  1. 現在使用自定義的InfoVisitor來訪問載入的模型。爲了允許其所有子節點的遍歷,我們會注意到在訪問器的構造函數中調用了setTraversalMode()方法:
InfoVisitor infoVisitor;
root->accept( infoVisitor );
  1. 是否啓動查看器,這取決於我們自己,因爲我們的訪問器已完成其任務:
osgViewer::Viewer viewer;
viewer.setSceneData( root.get() );
return viewer.run();
  1. 假定我們的可執行文件爲MyProject.ext,在命令行輸入:
# MyProject.exe cessnafire.osg
  1. 我們會在控制檯看到下列信息:
_images/osg_visitor.png

What just happened?

現在我們可以很容易繪製輸入的燃燒Cessna模型的結構。他顯式包含一個帶有幾何體對象的osg::Geode節點,該節點包含Cessna的幾何數據。幾何體節點可以通過其父節點osg::MatrixTransform進行變換。整個模型由osg::Group節點所管理,該模型是由osgDB::readNodeFile()或osgDB::readNodeFiles()函數返回的。

其他以osgParticle爲前綴的類現在看起來有些奇怪。他們實際上表示Cessna的煙與火粒子效果,我們會在第8章進行介紹。

現在我們能夠基於訪問場景圖的結果修改模型的基元集合,或是控制粒子系統。要實現該目的,現在我們將指定的節點指針保存在我們自己的訪問者類的成員變量中,並在未來的代碼中重用。

Summary

本章探討了如何通過使用OSG實現一個典型的場景圖,顯示了各種場景圖節點類型的使用,特別關注了圖樹的組裝以及如何添加狀態對象,例如常用到的;osg::Transform,osg::Switch,osg::LOD以及osg::ProxyNode類。我們特別探討了:

  • 如何實例化osg::Group與osg::Geode節點來組裝一個基本的層次結構圖並處理父節點與子節點。
  • 如何使用osg::Transform,基於對矩陣及其實現-osg::Matrix變量-的理解實現空間變換。
  • 如何使用osg::Switch節點來切換場景節點的渲染狀態。
  • 如何通過使用osg::LOD類來場景節點確定渲染複雜性的細節。
  • 使用osg::ProxyNode與osg::PagedLOD類來平衡運行時場景載入。
  • 如何自定義節點並強化其特性。
  • 訪問者設計模式的基本概念及其在OSG中的實現
  • 使用osg::NodeVisitor派生類遍歷節點及其子場景圖
發佈了9 篇原創文章 · 獲贊 9 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章