https://github.com/mylxiaoyi/osg3/blob/master/source/ch08.rst
OSG提供了一系列的工具集支持實時動畫的實現,包括變換動畫,關鍵幀動畫,骨骼動畫以及幾乎所有我們可以在本章中發現的其他動畫。在本章中我們將會首先解釋場景對象動畫的基本概念,然后介紹大多數常用場景動畫類型的實現細節,從而可以應用到各種場景。
在本章中我們將會討論:
- 回調的概念并使用回調
- 在不同的條件下實現簡單的動畫
- 如何創建簡單的路徑動畫
- 如何構建復雜的關鍵幀以及動畫管道系統
- 如何使用預先設置的骨骼系統生成特征動畫
- 如何實現渲染狀態與紋理動畫
Taking references to functions
在上一章中,我們嘗試了將子場景圖動態渲染到紋理。一個不推薦的方法就是在后幀事件(post-frame events)中更新渲染到紋理(render-to-textures)相機的視圖矩陣,其主要問題在于多線程環境。后幀事件也許會獨立的裁剪或繪制線程相重疊,從而導致數據訪問沖突。
為了避免數據訪問沖突的出現,我們可以考慮為更新遍歷部署動畫功能引用,并由OSG決定執行順序以及何時依據引用調用這些功能。傳遞給可執行代碼段的引用就被稱為回調。
在更新遍歷中被觸發的回調被稱為更新回調。還有分別在事件遍歷與裁剪遍歷中執行的事件回調與裁剪回調。OSG并沒有使用函數的地址作為其引用,而是提供了其自己的執行操作的實現,被稱為算符。為了自定義執行代碼,我們必須重寫回調算符的關鍵操作符與方法,并將其關聯到相應的場景對象,例如,節點或是可繪制元素。
List of callbacks
在OSG場景圖與后端有多種回調類型。其中,osg::NodeCallback類是更新,事件與裁剪回調的一個重要實現。他只能被關聯到節點。對于可繪制元素,我們有osg::Drawable::UpdateCallback,osg::Drawable::EventCallback與osg::Drawable::CullCallback來實現相同的目的。
osg::NodeCallback類有一個虛operator()方法用于用戶重寫以自定義其執行代碼。為了使其工作,我們必須使用setUpdateCallback()或addUpdateCallback()方法將回調對象關聯到場景圖中的特定節點。然而,operator()方法會在每幀的更新遍歷中被自動調用。
下表提供了OSG中所定義的主要回調的一個簡要介紹,其中的每一個都有一個可以為用戶子類重寫的虛方法,以及一個到屬性的關聯來表明他被關聯到特定類的相應方法。


Time for action - switching nodes in the update traversal
我們是否還記得在第5章中我們設計了一動畫開關節點?他由osg::Switch派生,但是通過重寫traverse()虛方法,依據一個內部計數器,自動改變其前兩個子節點的狀態。
現在我們要重做相同的任務,但是這次使用更新回調機制。這需要由osg::NodeCallback基類派生一個自定義類,并且重寫operator()來執行回調實現中的操作。
- 包含必需的頭文件:
#include <osg/Switch> #include <osgDB/ReadFile> #include <osgViewer/Viewer>
- 聲明SwitchingCallback類。他是一個基于osg::NodeCallback的派生類,很快將會用作場景節點的更新,事件與裁剪回調。唯一要實現的虛方法是operator()。該方法會在場景圖的更新,事件或裁剪遍歷中被自動調用。另外,我們同時初始化成員變量_counter作為內部計數器:
class SwitchingCallback : public osg::NodeCallback { public: SwitchingCallback() : _count(0) {} virtual void operator()( osg::Node* node, osg::NodeVisitor* nv ); protected: unsigned int _count; };
- operator()有兩個輸入參數:與回調相關聯的節點,以及在遍歷中調用函數的節點訪問器。要實現兩個子節點狀態切換動畫,我們需要將節點指針轉換為osg::Switch類型。在這里使用static_cast<>,因為我們確定相關聯的節點是開關節點。同時,traverse()方法應在特定的位置處執行,以確保更新遍歷訪問器能繼續遍歷場景圖。
void SwitchingCallback::operator()( osg::Node* node, osg::NodeVisitor* nv ) { osg::Switch* switchNode = static_cast<osg::Switch*>( node ); if ( !((++_count)%60) && switchNode ) { switchNode->setValue( 0, !switchNode->getValue(0) ); switchNode->setValue( 1, !switchNode->getValue(1) ); } traverse( node, nv ); }
- 接下來的步驟已經在第5章中介紹過了。載入顯示兩個顯示不同Cessna狀態的模型,并將其放置在switch節點之下,該節點將會被用在自定義更新回調SwitchingCallback中:
SwitchingCallback: osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile( "cessna.osg" ); osg::ref_ptr<osg::Node> model2= osgDB::readNodeFile("cessnafire. osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild( model1.get(), false ); root->addChild( model2.get(), true );
- 不要忘記將更新回調關聯到節點。如果我們厭倦了在每一幀中執行該回調,僅需要向setUpdateCallback()方法傳遞一個NULL參數。如果回調對象的引用計數減少到0,則該對象會被刪除:
root->setUpdateCallback( new SwitchingCallback );
- 現在啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
- 渲染結果完全類似于第5章中的Animating the switch node示例。Cessna將會交替處于完整與燃燒狀態。與重寫一個新節點類型相比,使用回調的解決方案對場景圖影響更少,而且可以很容易的在運行時移除回調或是替換為其他的回調。
What just happened?
目前為止我們已經處理了神奇的traverse()方法用于兩個目的:通過重寫traverse()方法自定義節點以用于我們自己的執行代碼;調用osg::NodeVisitor類的traverse()方法以在實現節點訪問器時繼續遍歷。盡管這兩個操作具有不同的參數,他們實際上表示相同的處理管線。
首先,節點訪問器的traverse()方法有一個osg::Node參數,簡單調用traverse()虛方法并傳遞其自身作為參數。
其次,節點的遍歷方法必須要實現的結束處調用超類的traverse()方法。然而他將確定是否有要使用當前訪問器對象(使用子節點的accept()方法)遍歷的子節點。
最后,訪問器依次調用apply()虛方法來接收各種節點類型作為其參數,然后實現自定義的訪問行為。因為每一個apply()方法必須調用訪問器的traverse()方法來結束其自身,循環會回到第一步,直到整個場景圖遍歷完成。整個過程可以通過下面的圖來解釋:

回調的operator()方法以第三種形式調用其traverse()方法,使用訪問器與節點作為參數。然而,沒有必要擔心其復雜性,因為他所執行的唯一操作就是調用訪問器的traverse()方法并繼續遍歷。如果我們在回調方法中調用失敗,回調會簡單停止并立即由當前節點返回。
Pop quiz - adding or setting callbacks
除了setUpdateCallback()之外,addUpdateCallback()方法也可以用來將回調關聯到場景節點。他會將新回調對象添加到主回調對象之后,從而使得在一個節點中存在多個回調成為可能。我們喜歡哪一種方式呢?我們能否確定在主回調對象的operator()方法中,嵌套回調將會何時執行呢?
Avoding confilicting modifications
我們以一種非常簡單而容易的方法討論了OSG的多線程實現與線程安全。處理結構的理論超出了本書的范圍。但是了顯示維護場景對象數據多新性的重要性,我們需要簡要討論一個線程模型。
OSG可以使得繪制遍歷,也就是將數據傳送給OpenGL管線,在一個單獨的線程中運行。他必須與每一幀中的其他繪制遍歷相同步,但是繪制遍歷的部分可以與來自下一幀的更新遍歷相重疊,從而改善渲染效率并減少幀延遲。這意味著osgViewer::Viewer的frame()方法會在繪制工作依然處于活動狀態時返回。那么更新回調中的數據變化也許會與未完成的渲染操作相沖突,從而導致不可預期的行為,甚至崩潰。
OSG在setDataVariance()方法中提供了解決方法,該方法屬于osg::Object類,這是所有場景對象的基類。這可以設置為三個枚舉值之一:UNSPECIFIED(默認),STATIC與DYNAMIC。場景圖中的DYNAMIC對象必須在繪制遍歷的開始進行處理。也就是,渲染后端應確保所有節點以及被指定為DYNAMIC的場景對象在下一幀的更新與裁剪遍歷開始之前已完成繪制。然而,STATIC對象,在更新與繪制過程中會保持不變,從而會被稍后渲染且不會阻塞幀速率。
默認情況下,所有新分配的對象都被指定為UNSPECIFIED,包括節點,可繪制元素,狀態集以及屬性。這允許OSG預測數據變化。另一方面,我們總是可以重置該值并使其由下一幀開始工作,例如:
node->setDataVariance( osg::Object::DYNAMIC );
Time for action - drawing a geometry dynamically
動態修改幾何體的頂點與基元屬性是很常見的。我們可以改變每個頂點的位置,法線,顏色與紋理坐標,以及每一幀相關的基元,以實現各種動畫類型。在修改過程中,關注數據的變化是很重要的,因為繪制遍歷也許會與更新頂點與基元的更新遍歷同時運行,從而會導致沖突甚至是崩潰。
在這個示例中,我們將會使用在第4章中所創建的四邊形幾何體。我們會簡單的修改其最后一個頂點,并使其圍繞X軸旋轉,從而生成一個簡單的動畫效果。
- 包含必需的頭文件:
#include <osg/Geometry> #include <osg/Geode> #include <osgViewer/Viewer>
- 四邊形的創建對于我們非常熟悉。指定頂點,法線以及顏色數組,并添加基元集合來表示要安排的所有頂點,并使用GL_QUAD類型進行渲染。最后,返回新分配的幾何體對象:
osg::Geometry* createQuad() { osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array; vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 0.0f) ); vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.0f) ); vertices->push_back( osg::Vec3(0.0f, 0.0f, 1.0f) ); osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array; normals->push_back( osg::Vec3(0.0f,-1.0f, 0.0f) ); osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array; colors->push_back( osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f) ); colors->push_back( osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f) ); colors->push_back( osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f) ); colors->push_back( osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f) ); osg::ref_ptr<osg::Geometry> quad = new osg::Geometry; quad->setVertexArray( vertices.get() ); quad->setNormalArray( normals.get() ); quad->setNormalBinding( osg::Geometry::BIND_OVERALL ); quad->setColorArray( colors.get() ); quad->setColorBinding( osg::Geometry::BIND_PER_VERTEX ); quad->addPrimitiveSet( new osg::DrawArrays(GL_QUADS, 0, 4) ); return quad.release(); }
- 借助于osg::Drawable::UpdateCallback,我們可以很容易獲取每一幀要修改的幾何體指針。唯一要覆蓋的方法是update(),該方法有一個節點訪問器與一個可繪制元素指針作為參數。其超類,osg::Drawable::UpdateCallback,類似于osg::NodeCallback類,不同的是可繪制元素的回調不必遍歷到所有子節點(沒有子節點的可繪制元素)。
class DynamicQuadCallback : public osg::Drawable::UpdateCallback { public: virtual void update( osg::NodeVisitor*, osg::Drawable* drawable ); };
- 在update()方法的實現中,我們使用static_cast<>操作符讀取所創建四邊形幾何體的頂點數組。如果DynamicQuadCallback類不僅被應用于osg::Geometry,而是同時應用于其他自定義的可繪制元素,則dynamic_cast<>關鍵字也許更為安全。然后,我們使用osg::Quat四元數類快速圍繞原點(0,0,0)旋轉數組中的最后一個頂點。退出方法之前的最后一步工作是重新計算當前幾何體的顯示列表對象與邊界盒子,當任何一個頂點被修改時,這些元素需要進行更新:
void DynamicQuadCallback::update( osg::NodeVisitor*, osg::Drawable* drawable ) { osg::Geometry* quad = static_cast<osg::Geometry*>( drawable ); if ( !quad ) return; osg::Vec3Array* vertices = static_cast<osg::Vec3Array*>( quad->getVertexArray() ); if ( !vertices ) return; osg::Quat quat(osg::PI*0.01, osg::X_AXIS); vertices->back() = quat * vertices->back(); quad->dirtyDisplayList(); quad->dirtyBound(); }
- 我們將幾何體定義為DYNAMIC,從而OSG后端的繪制遍歷會自動指示動態對象來執行穩健的場景圖遍歷。另外,可繪制元素的修改回調是通過osg::Drawable類的setUpdateCallback()方法指定的:
osg::Geometry* quad = createQuad(); quad->setDataVariance( osg::Object::DYNAMIC ); quad->setUpdateCallback( new DynamicQuadCallback );
- 現在將四邊形幾何體添加到osg::Geode節點,并將根節點關聯到查看器:
osg::ref_ptr<osg::Geode> root = new osg::Geode; root->addDrawable( quad ); osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
- 這次四邊形動起來了。借助于osg::Quat類,其第四個頂點圍繞X軸旋轉。這要比僅是在屏幕上顯示一個靜態的圖像動態得多:

What just happened?
試著移除setDataVariance()行并看一下會發生什么。奇怪的是示例依然能夠正確運行,就如同他沒有受到線程模型的影響。這是因為UNSPECIFIED對象能夠確定他們是否在回調中被動態修改,并自動將數據變化重置為DYNAMIC。
試著將枚舉DYNAMIC修改為STATIC,而我們會發現渲染會閃爍且在控制中中有OpenGL錯誤消息"invalid operation"。這實際上是由線程沖突引起的。
如果沒有調用dirtyDisplayList()方法,OSG將會忽略所有動態可繪制元素的變化并利用顯示列表命令來存儲前一個頂點與基元數據。同時,如果沒有調用dirtyBound()方法,OSG不會知道邊界盒子是否適合可繪制元素的尺寸,并且會在執行視圖裁剪時出現錯誤。
Have a go hero - dirtying geometry objects
為了進行正確渲染,我們需要調用dirtyDisplayList()方法來激活可繪制元素數據的更新。但是一個重要的先決條件是可繪制元素應支持顯示列表模式,這是可繪制元素的默認行為,并且可以通過setUseDisplayList()方法打開或關閉。
當使用VBO模型時,OSG允許使用更好的機制,這會更為高效。打開setUseVertexBufferOjbects()并禁止setUseDisplayList()可以起作用。我們將會發現在該情況下dirtyDisplayList()方法沒有起作用。通過執行dirty()方法可以污染數組數據,例如:
osg::Vec3Array* vertices = ; // Dynamically modify the vertex array data vertices->dirty();
看一下我們的修改是否起作用,并在污染相同的幾何體時標識兩種策略之間的區別。事實上,在這里顯示列表不起作用是因為他會在每一幀中重新生成。所以,對于渲染變化的幾何體數據,我們更喜歡VBO。
Understanding ease motions
假定有一列火車在15分鐘內由A站運行到B站。我們將會在更新回調中通過修改列車的變換矩陣來模擬這一場景。最簡單的方法是將位于A站的火車放置在時間點0處,而位于B站的火車位于時間點15(分鐘)處,并在變換過程中進行移動。在這里將會著重使用的方法是線性插值。該方法會在兩個相鄰采樣點P0與P1之間繪制一條直線,并且返回直線上的相應點P,從而可以用來表示節點的變換與縮放操作。通常可以使用下面的形式進行表達:
P = (1 - t) * P0 + t * P1
這里t是一個0到1之間的數。
不幸的是,列車的運動通常更為復雜。他由站點A出發,慢慢加速,以平滑的速度運行,減速,最終停靠在站點B。在這種情況下,線性插值總是有些不自然。
所以我們有簡單的方法,或是簡單的函數。這些是用來在兩點這宰插值的數學函數。為了獲得更為自然的效果,一個簡單的函數通常不會生成非線性的結果。osgAnmination庫定義了大量內建的簡單函數。其中的每一個至少有兩個參數:起如值(通常為0)與過程(通常為1),并生成該范圍[起始值,起始值+過程]之內的結果。他們可以被應用起始(InMotion),結束(OutMotion)或同時應用到直動畫的起始與結束(InOutMotion)。我們將會在下表中列出這些函數:

要創建一個線性插值運動對象,我們可以輸入:
// Start value is 0.0, and duration time is 1.0. osg::ref_ptr<osgAnimation::LinearMotion> motion = new osgAnimation::LinearMotion(0.0f, 1.0f);
OSG源碼中的examples/osganimationeasemotion文件有助于我們以圖形方式理解這些簡單運動。要了解詳細內容可以嘗試編譯并運行。
Animating the transformation nodes
路徑動畫是圖形程序中最廣為使用的動畫。他們可以用來描述運動的汽車,飛機,旋轉的球,或是相機運動。路徑應總是被首先設置,包括位置,旋轉以及不同關鍵時刻節點的縮放值。當模擬循環運行時,使用為位置與縮放向量使用線性插值以及為旋轉四元數據使用球形插值的方法計算每一幀的變換狀態。這里內部使用osg::Quat的slerp()方法。
OSG提供了osg::AnimationPath類來封裝時間變化變換路徑。他有一個insert()方法可以用來在指定的時間點播放一個控制點。控制點由osg::AnimationPath::ControlPoint類所聲明,接受一個位置值,一個可選的旋轉與縮放值以構建動畫路徑。例如:
osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath; path->insert(t1, osg::AnimationPath::ControlPoint(pos1,rot1,scale1)); path->insert(t2, …);
這里,t1與t2是以秒計的時間節點,而rot1是一個表示對象旋轉的osg::Quat變量。
除此之外,我們可以使用setLoopMode()方法設置動畫的循環模式。默認值為LOOP,也就是動畫將會在設定好的路徑上連續運行。這個參數可以修改為NO_LOOPING(運行一次)或是SWING(創建一個往復路徑)以用于其他目的。
然后,我們將osg::AnimationPath對象關聯到內建的osg::AnimationPathCallback對象,該類實例上派生自osg::NodeCallback,并幫助開發者以直觀的方式控制其動畫場景。
Time for action - making use of the animation path
現在我們要使得我們的Cessna繞著一個圓運動。他將在一個圓心位于(0,0,0)的圓內運動。通過關鍵幀之間的線性插值,路徑被用來持續更新模型的位置與朝向。為了實現動畫時間線,唯一的工作就是添加控制點,包括位置,可選擇的旋轉以及縮放關鍵值。
- 包含必需的頭文件:
#include <osg/AnimationPath> #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgViewer/Viewer>
- 創建動畫路徑。這實際上是XOY平面上具有指定半徑的圓。time參數被用來指定完成一圈所需要的時間。osg::AnimationPath對象被設置為無限循環動畫。他包含32個控制點來構成圓路徑,這是由局部變量numSamples來定義的:
osg::AnimationPath* createAnimationPath( float radius, float time) { osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath; path->setLoopMode( osg::AnimationPath::LOOP ); unsigned int numSamples = 32; float delta_yaw = 2.0f * osg::PI / ((float)numSamples - 1.0f); float delta_time = time / (float)numSamples; for ( unsigned int i=0; i<numSamples; ++i ) { float yaw = delta_yaw * (float)i; osg::Vec3 pos( sinf(yaw)*radius, cosf(yaw)*radius, 0.0f ); osg::Quat rot( -yaw, osg::Z_AXIS ); path->insert( delta_time * (float)i, osg::AnimationPath::ControlPoint(pos, rot)); } return path.release(); }
- 載入Cessna模型。我們將會注意到這次與之前的文件名之間有著明顯的區別。在這里字符串"0,0,90.rot"看起來是多余的。這是一種偽載入器,作為文件名的一部分,但實際上是使模型cessna.osg繞Z軸旋轉90度。我們會在第10章中進行詳細討論:
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile( "cessna.osg.0,0,90.rot" ); osg::ref_ptr<osg::MatrixTransform> root = new osg::MatrixTransform; root->addChild( model.get() );
- 將動畫路徑添加到osg::AnimationPathCallback對象,并將回調關聯到節點。注意,動畫路徑僅影響osg::MatrixTransform與osg::PositionAttitudeTransform節點,在更新遍歷中更新其變換矩陣或是位置與旋轉屬性:
osg::ref_ptr<osg::AnimationPathCallback> apcb = new osg::AnimationPathCallback; apcb->setAnimationPath( createAnimationPath(50.0f, 6.0f) ); root->setUpdateCallback( apcb.get() );
- 現在簡單啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
- 現在Cessna開始做圓周運動。其運動也許會超出屏幕范圍,所以我們需要使用相機操作器來切換到一個比初始位置更好的查看位置。使用鼠標按鈕來調整視圖矩陣從而全面觀看所創建的動畫路徑:

What just happened?
osg::AnimationPath類使用getMatrix()方法依據指定時間點前與后的兩個控制點來計算并返回運動變換矩陣。然而將其應用到主osg::MatrixTransform,osg::PositionAttitudeTransform或osg::Camera節點以使其沿著路徑運行。這是由osg::AnimationPathCallback類完成的,該類實際上是用于特定目的的更新回調。
如果osg::AnimationPathCallback對象被關聯到其他類型的節點,而不是前面所描述的變換節點,則他會變得無效。同時也不建議將動畫路徑回調用作事件或裁剪回調,因為這會導致不可預料的結果。
Have a go hero - more controls over the animation path
動畫必須能夠被停止,重置與快進,從而使得用戶的控制更為容易。osg::AnimationPathCallback類提供了reset(),setPause(),setTimeMultiplier()與setTimeOffset()方法來實現這些常見的操作。例如,要重置當前的動畫路徑,在任意時刻調用apcb:
apcb->setPause( false ); apcb->reset();
為了將時間偏移設置為4.0s,并且以2x倍速度快速前進動畫,可以使用:
apcb->setTimeOffset( 4.0f ); apcb->setTimeMultiplier( 2.0f );
現在是我們是否明白應如何創建我們自己的路徑動畫層了嗎?
Changing rendering states
渲染狀態也可以進行動畫。通過修改一個或是多個渲染屬性可以生成大量的效果,包括漸進與漸出,大氣的密度與變化,霧,修改光柱的方向等。我們可以很容易在更新回調中實現狀態動畫。我們可以由重載方法的參數中獲取屬性對象,或是僅將對象作為用戶定義回調的成員變量。記住要使用智能指針來確保成員變量在不再被引用時會被自動銷毀。
簡單運動類可以用來改善動畫質量。我們必須使用起始值與過程參數來分配一個簡單運動對象,并使用間隔時間進行更新。例如:
osg::ref_ptr<osgAnimation::LinearMotion> motion = new osgAnimation::LinearMotion(0.0, 10.0); motion->update( dt ); float value = motion->getValue();
這會使用由0.0到10.0范圍內的X軸創建一個線性運動對象。getValue()方法在當前的X值上使用特定的公式,并獲取相應的Y值。
如果我們希望在我們的工程中使用簡單運動以及更多的功能,我們磚雕要將osgAnimation庫作為依賴添加進來。
我們已經體驗過使用osg::BlendFunc類與渲染順序來使得場景對象半透明。被稱為alpha值的顏色向量的第四個組成部分會為我們提供技巧。但是如果我們有一個連續變化的alpha值時會發生什么呢?當alpha為0時將會完全透明(不可見),而當為1.0時則會完全不透明。因而由0.0到1.0的動畫過程將會導致對象逐漸對查看者可見,也就是淡入效果。
更新回調可以用在該任務中。創建一個基于osg::NodeCallback的類并將其設置給將要淡入的類沒有任何問題。但是狀態屬性回調,osg::StateAttributeCallback,在該示例中也可用。
在這里,osg::Material類被用來提供每一個幾何頂點的alpha位,而不僅是設置顏色數組。
- 包含必需的頭文件:
#include <osg/Geode> #include <osg/Geometry> #include <osg/BlendFunc> #include <osg/Material> #include <osgAnimation/EaseMotion> #include <osgDB/ReadFile> #include <osgViewer/Viewer>
- 要實例化osg::StateAttributeCallback,我們需要重寫operator()方法,并利用其參數:狀態屬性本身與進行遍歷的訪問器。這里的另一個任務是使用立體函數在動畫曲線的進入出位置聲明一個簡單運動插值器:
class AlphaFadingCallback : public osg::StateAttributeCallback { public: AlphaFadingCallback() { _motion = new osgAnimation::InOutCubicMotion(0.0f, 1.0f); } virtual void operator()(osg::StateAttribute*, osg::NodeVisitor*); protected: osg::ref_ptr<osgAnimation::InOutCubicMotion> _motion; };
- 在operator()中,我們將會獲取場景對象的材質屬性,該屬性可以被用來模擬透明與半透明效果。這需要兩步:首先,使用自定義的時間值差量更新簡單運動對象;然后,獲取0到1之間的運動結果,并將其應用到材質的混合顏色的alpha部分:
void AlphaFadingCallback::operator()( osg::StateAttribute* sa, osg::NodeVisitor* nv ) { osg::Material* material = static_cast<osg::Material*>( sa ); if ( material ) { _motion->update( 0.005 ); float alpha = _motion->getValue(); material->setDiffuse( osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, alpha)); } }
- 這就是我們在osg::StateAttribute回調中的全部操作。現在,在示例的主函數中,我們要創建一個四邊形并將回調應用于其材質。我們可以拷貝第4章與第6章中的代碼來自己創建四邊形幾何體。OSG支持一個更為方便的名為osg::createTextureQuadGeometry()函數。他需要一個角點,一個寬度向量以及一個高度向量,并返回一個使用預設頂點,法線與紋理坐標數據的新創建的osg::Geometry對象:
osg::ref_ptr<osg::Drawable> quad = osg::createTexturedQuadGeomet ry( osg::Vec3(-0.5f, 0.0f, -0.5f), osg::Vec3(1.0f, 0.0f, 0.0f), osg::Vec3(0.0f, 0.0f, 1.0f) ); osg::ref_ptr<osg::Geode> geode = new osg::Geode; geode->addDrawable( quad.get() );
- 配置材質屬性并沒有什么特別的。如果有使用OpenGL glMaterial()的經驗,我們可以很容易想見osg::Material類是如何使用類似的成員方法設置周邊與混合顏色的。此時需要注意的是將AlphaFadingCallback對象關聯到材質,并使其在每一幀的所有更新遍歷中起作用:
osg::ref_ptr<osg::Material> material = new osg::Material; material->setAmbient( osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f) ); material->setDiffuse( osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, 0.5f) ); material->setUpdateCallback( new AlphaFadingCallback );
- 將材質屬性及相關的模式添加到geode的狀態集合。同時,我們需要使能OpenGL混合函數來實現我們的淡入效果,并且確保透明對象以順序方式進行渲染:
geode->getOrCreateStateSet()->setAttributeAndModes( material.get() ); geode->getOrCreateStateSet()->setAttributeAndModes( new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ); geode->getOrCreateStateSet()->setRenderingHint( osg::StateSet::TRANSPARENT_BIN );
- 將四邊形添加到根節點。我們同時添加滑翔機模型作為參考模型,其中的一半為四邊形所覆蓋,從而指示四邊形是否淡入淡出:
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild( geode.get() ); root->addChild( osgDB::readNodeFile("glider.osg") );
- 現在啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
- 出入立體簡單運動使得alpha的變以一種平滑的方式出現。我們將會發現他更適合于實現真實的動畫而不是簡單的線性插值運動。現在,我們是否知道如何使用相同的結構實現淡出效果呢?這兩種效果被經常用于表示動態模型與大城市場景構建中:

What just happened?
osgAnimation::InOutCubicMotion類基于時間的立體形式生成值。結果曲線如下圖所示:

假定當前的時間值為t(位于X軸),運動對象會依據立體形式返回Y值。他會由零加速至半路,然后減速到零。這使得對象看起來更為自然,而不僅使用簡單的常速。試著將其以及更多的簡單運動應用到材質值,以及osg::MatrixTransform節點的路徑動畫(在自定義的節點回調中修改其矩陣)。
Pop quiz - choosing the alpha setter and the callback
除了osg::Material類與osg::Geometry的顏色數組之外,還有哪些可以設置頂點的alpha值呢?除了osg::StateAttributeCallback之外,我們知道還有哪些回調可以用來控制淡入效果呢,例如,節點與可繪制元素回調?我們是否能以最簡單的方式修改上面的示例來實現淡出效果呢?
Have a go hero - animating in graphics shaders
在圖形陰影器中使用狀態動畫會很酷。在大多數情況下,他要比固定管線更容易控制,并為我們提供各種效果的自由想像,例如海浪,波紋,火焰,陰影以及復雜的實際效果。
osg::Uniform類可以使用setUpdateCallback()方法以及osg::Uniform::Callback派生對象來定義其自己的更新回調。其虛方法operator()有兩個輸入參數:uniform指針與遍歷訪問器。使用set()方法來修改uniform值(必須與之前的類型相同)并且查看是否可以在圖形陰影器中工作。
Playing movies on textures
如果我們能夠在3D世界中觀察影片或是存儲影片內容將會非常有趣。我們可以將一個大的方塊幾何體放置為電影屏幕,并將一個動態2D紋理與其表面相關聯。紋理包含構成影像的一系列圖像。圖像序列可以隨時添加新的圖像是所必需要,該圖像可以來自文件或是微型相機。
OSG使用osg::ImageStream類來支持圖像流,該類管理數據緩沖區中的子圖像。他可以被派生從而由視頻文件或是網絡讀取數據。事實上,OSG已經有一些內建的插件支持AVI,MPG,MOV以及其他文件格式的載入與播放。我們將會在第10章進行詳細描述。
在這里,我們將會介紹另一個osg::ImageSequence類,該類存儲多個圖像對象并依次渲染。他具有下列的公共方法:
- addImage()方法向序列添加一個osg::Image對象。同時還有setImage()與getImage()方法操作指定索引處的子圖像,以及getNumImages()方法統計子圖像的數量。
- addImageFile()與setImageFile()方法可以將圖像對象壓入子圖像列表的結尾處。但是無需指定指針,這兩個方法都接受一個文件名參數,從而由磁盤讀取子圖像。
- setLength()方法設置以秒計的圖像序列總時間。該時間在動畫過程中每一個子圖像之間平均分配的。
- setTimeMultiplier()方法設置時間乘數。默認為1.0,而更大的值指示序列應快進。
- play(),pause(),rewind()與seek()方法為開發者提供了對序列的基本控制。seek()方法接受一個時間參數,該參數應小于總時間長度。
Time for action - rendering a flashing spotlight
渲染動態紋理的關鍵是提供多個圖像作為源,并依次進行繪制。這些圖像可以由一個視頻文件獲取,或是由開發者與藝術人員創建。在下面的示例中,我們將會使用變量的半徑創建一系列的點光,并將其輸出到osg::Image對象,然后使用osg::ImageSequence類將其關聯到紋理屬性來在特定的模型上生成閃爍效果。
- 包含必需的頭文件:
#include <osg/ImageSequence> #include <osg/Texture2D> #include <osg/Geometry> #include <osg/Geode> #include <osgViewer/Viewer>
- 點光可以定義為將光束投影在空間上的一系列點。他通常生成圍繞中心點的一個暈輪,而且可以被修改來使用不同的顏色與強度范圍。這里,函數createSpotLight()使用中心顏色,背景顏色與強度參數簡單生成一個osg::Image對象。size參數被用來定義圖像本身的最終大小。在這里,data()方法接受列與行索引,并返回一個相對應的起始地址用于賦值:
osg::Image* createSpotLight( const osg::Vec4& centerColor, const osg::Vec4& bgColor, unsigned int size, float power ) { osg::ref_ptr<osg::Image> image = new osg::Image; image->allocateImage( size, size, 1, GL_RGBA, GL_UNSIGNED_BYTE ); float mid = (float(size)-1) * 0.5f; float div = 2.0f / float(size); for( unsigned int r=0; r<size; ++r ) { unsigned char* ptr = image->data(0, r); for( unsigned int c=0; c<size; ++c ) { float dx = (float(c) - mid)*div; float dy = (float(r) - mid)*div; float r = powf(1.0f - sqrtf(dx*dx+dy*dy), power); if ( r<0.0f ) r = 0.0f; osg::Vec4 color = centerColor*r + bgColor*(1.0f - r); *ptr++ = (unsigned char)((color[0]) * 255.0f); *ptr++ = (unsigned char)((color[1]) * 255.0f); *ptr++ = (unsigned char)((color[2]) * 255.0f); *ptr++ = (unsigned char)((color[3]) * 255.0f); } } return image.release(); }
- 通過使得的createSpotLight()函數,我們可以使用不同的強度值快速生成多個圖像。然后我們將所有這些圖像添加到osg::ImageSequence對象用于統一管理:
osg::Vec4 centerColor( 1.0f, 1.0f, 0.0f, 1.0f ); osg::Vec4 bgColor( 0.0f, 0.0f, 0.0f, 1.0f ); osg::ref_ptr<osg::ImageSequence> sequence = new osg::ImageSequence; sequence->addImage( createSpotLight(centerColor, bgColor, 64, 3.0f) ); sequence->addImage( createSpotLight(centerColor, bgColor, 64, 3.5f) ); sequence->addImage( createSpotLight(centerColor, bgColor, 64, 4.0f) ); sequence->addImage( createSpotLight(centerColor, bgColor, 64, 3.5f) );
- 由于osg:ImageSequence是由osg::Image類派生的,他可以直接關聯一個紋理作為數據源。這使得在模型表面持續顯示圖像成為可能:
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D; texture->setImage( imageSequence.get() );
- 再次使用osg::createTextureQuadGeometry()函數生成一個四邊形。這被用來表示最終的圖像序列。如果所有的圖像均是由一個視頻源中獲取,他甚至是可以被看作是在電影中用于顯示電影的屏幕。
osg::ref_ptr<osg::Geode> geode = new osg::Geode; geode->addDrawable( osg::createTexturedQuadGeometry( osg::Vec3(), osg::Vec3(1.0,0.0,0.0), osg::Vec3(0.0,0.0,1.0)) ); geode->getOrCreateStateSet()->setTextureAttributeAndModes( 0, texture.get(), osg::StateAttribute::ON );
- 我們需要配置osg::ImageSequence對象來確定總長度(以秒計),并開始以順序方式播放序列。這也可以在一個更新回調中完成:
imageSequence->setLength( 0.5 ); imageSequence->play();
- 啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( geode.get() ); return viewer.run();
- 我們可以看到一個點光在四邊形中心閃爍。這是因為我們將具有不同半徑的點光圖像應用到序列,并循環播放(默認情況)。現在我們可以基于這個基本實現想像一些更為真實的效果:

What just happened?
osg::ImageSequence類在每一幀中由存儲的圖像更新當前的渲染數據。他使用setImage()方法來配置維度,格式以及像素數據,同時會污染自身-這會使得保存有圖像的所有紋理對象更新圖形內存并向渲染管線輸出新數據。這并不高效,因為如果切換頻繁,這會導致較高的CPU-GPU帶寬使用的增加。
另一個有趣的地方是addFileName()與setFileName()方法。這兩個方法使用磁盤上的圖像文件來構成圖像序列,并且在默認情況下所有這些文件一次載入。這可以通過setMode()方法進行修改。該方法接受下列參數中的一個:
- PRE_LOAD_ALL_IMAGES會導致默認行為
- PAGE_AND_RETAIN_IMAGES將會按需由文件載入圖像
- PAGE_AND_DISCARD_USED_IMAGES會當影片重置時移除所有使用的圖像并重新載入
所以,如果強制以分頁機制載入圖像,在啟動循環之間設置模式:
imageSequence->setMode( osg::ImageSequence::PAGE_AND_RETAIN_IMAGES );
Creating complex key-frame animations
現在我們可以進一步探討osgAnimation庫了。除了簡單的運動實現,osgAnimation支持更多通用的動畫特性,包括固體動畫,變形動畫,骨骼動畫,基本的動畫管理器以及時間線高度器。他定義了大量的概念與模板類,這些類看起來非常復雜,但是可以為開發者提供極大的靈活性來構建他們自己的高級動畫。
具有了使用動畫路徑的基礎之后,我們可以快速地理解osgAnimation的重要概念,并且由一個實現了與動畫路徑示例相同效果的示例入手。
動畫的基本元素是關鍵幀。這定義了所有平滑動畫的結束點。osg::AnimationPath使用ControlPoint類來創建位置,旋轉與縮放值的關鍵幀。
一個關鍵幀通常需要兩個參數:時間點以及要實現的時間。osgAnimation::TexmplateKeyframe<>類被用來定義osgAnimation庫中的一個普通關鍵幀,而osgAnimation::TemplateKeyframeContainer<>類管理一個相同數據類型的關鍵幀列表。他派生自std::vector類并且繼承了所有的vector方法,例如push_back(),pop_back()與迭代器。所以,要向一個位置關鍵幀添加到相應的容器對象,我們可以使用:
osgAnimation::TemplateKeyframe<osg::Vec3> kf(0.0, osg::Vec3()); osgAnimation::TemplateKeyframeContainer<osg::Vec3>* container = new osgAnimation::TemplateKeyframeContainer<osg::Vec3>; container->push_back( keyframe );
這里,osg::Vec3是關鍵幀與容器的模板參數。為了簡化代碼,我們可以簡單的將模板類名替換為osgAnimation::Vec3KeyFrame與osgAnimation::Vec3KeyFrameContainer,也就是:
osgAnimation::Vec3KeyframeContainer* container = new osgAnimation::Vec3KeyframeContainer; container->push_back( osgAnimation::Vec3Keyframe(0.0, osg::Vec3()) );
容器對象實際上由osg::Referenced派生,所以他也可以由智能指針進行管理。然后可以使用一個采樣器使用定義了插值方法的算符在關鍵幀容器中插入元素。
osgAnimation::TemplateSampler<>定義了底層采樣器模板。他包含一個內部插值器對象以及一個具有相同模板參數的osgAnimation::TemplateKeyframeContainer<>。采樣器也具有別名。例如,osgAnimation::Vec3LinearSampler定義了一個包含osg::Vec3數據與線性插值器的采樣器。其公共方法getOrCreateKeyframeContainer()可以隨時返回一個正確的3D向量關鍵幀容器。
下表列出了osgAnimation名字空間內的采樣器類型及其相關聯的容器與關鍵幀類:

為了將關鍵幀添加到一個指定的采樣器對象,只需要輸入:
// Again, assume it is a 3D vector sampler sampler->getOrCreateKeyframeContainer()->push_back( osgAnimation::Vec3Keyframe(0.0, osg::Vec3()) ); // Frame at 0s sampler->getOrCreateKeyframeContainer()->push_back( osgAnimation::Vec3Keyframe(2.0, osg::Vec3()) ); // Frame at 2s
Channels and animation managers
現在是處理滿是關鍵幀的采樣器的時候了。osgAnimation::TemplateChannel<>類接受一個特定的采樣器類作為參數,并表示采樣器與目標的關聯。通道的名字是通過setName()方法設置的,而其所查找的目標是由setTargetName()方法所定義的。
目標對象經常是osgAnimation內建更新回調。他們應使用setUpdateCallback()方法關聯到特定節點。osgAnimation::UpdateMatrixTransform就是一個典型。他更新宿主osg::MatrixTransform節點,并使用每一幀的通道結果修改變換矩陣。我們可以在下面的示例中看到其用法。
一個包含3D向量采樣器的通道可以替換為osgAnimation::Vec3LinearChannel類,而具有球形四元數據采樣器的類被稱為osgAnimation::QuatSphericalLinearChannel,等等。
在完成設計所有的關鍵幀與動畫通道以后,構建我們動畫場景的最后一步就是為所有通道聲明管理器類。在此之前,我們定義osgAnimation::Animation類來包含一系列的動畫通道,就如同他們位于相同的層。通道可以使用addChannel()方法被添加到動畫對象。
osgAnimation::BasicAnimationManager類是所有動畫對象的最終管家。他通過registerAnimation(),unregisterAnimation()與getAnimationList()方法管理osgAnimation::Animation對象,并通過playAnimation(),stopAnimation()與isgPlaying()方法控制一個或多個動畫對象的播放狀態。同時他也是一個更新回調,但是為了提供對整個場景圖動畫的完全控制,他應被設置到根節點。
整個過程可以通過下圖進行描述:

Time for action - managing animation channels
為了實現與我們已經完成的動畫路徑示例相同的動畫效果,我們需要創建兩個通道,一個為位置動畫目標,而另一個旋轉動畫目標。
圍繞原點生成圓路徑的createAnimationPath()函數可以被重用。但是并不能將位置與旋轉值組合到一個控制點結構中,這兩種類型的關鍵幀應被添加到屬于不同動畫通道的單獨容器中。
- 包含必需的頭文件:
#include <osg/MatrixTransform> #include <osgAnimation/BasicAnimationManager> #include <osgAnimation/UpdateMatrixTransform> #include <osgAnimation/StackedTranslateElement> #include <osgAnimation/StackedQuaternionElement> #include <osgDB/ReadFile> #include <osgViewer/Viewer>
- createAnimationPath()的算法依然有用。唯一的區別在于所計算的值應被放置在不同類型的關鍵幀中(VecKeyFrame與QuatKeyFrame),然后添加到輸入容器中:
void createAnimationPath( float radius, float time, osgAnimation::Vec3KeyframeContainer* container1, osgAnimation::QuatKeyframeContainer* container2 ) { unsigned int numSamples = 32; float delta_yaw = 2.0f * osg::PI/((float)numSamples - 1.0f); float delta_time = time / (float)numSamples; for ( unsigned int i=0; i<numSamples; ++i ) { float yaw = delta_yaw * (float)i; osg::Vec3 pos( sinf(yaw)*radius, cosf(yaw)*radius, 0.0f ); osg::Quat rot( -yaw, osg::Z_AXIS ); container1->push_back( osgAnimation::Vec3Keyframe(delta_time * (float)i, pos)); container2->push_back( osgAnimation::QuatKeyframe(delta_time * (float)i, rot)); } }
- 在主函數中,我們首先聲明一個“位置動畫”通道與一個“旋轉動畫”通道(QuatSphericalChannel可以實現與osg::Quat的slerp()方法相同的效果)。其名字應是唯一的,而目的名字應與其更新器相同。否則,通道將不會被正確識別:
osg::ref_ptr<osgAnimation::Vec3LinearChannel> ch1 = new osgAnimation::Vec3LinearChannel; ch1->setName( "position" ); ch1->setTargetName( "PathCallback" ); osg::ref_ptr<osgAnimation::QuatSphericalLinearChannel> ch2 = new osgAnimation::QuatSphericalLinearChannel; ch2->setName( "quat" ); ch2->setTargetName( "PathCallback" );
- 如前面所描述的,通道的關鍵幀容器將會在createAnimationPath()函數中接收正確的動畫數據:
createAnimationPath( 50.0f, 6.0f, ch1->getOrCreateSampler()->getOrCreateKeyframeContainer(), ch2->getOrCreateSampler()->getOrCreateKeyframeContainer() );
- 現在我們來創建一個osg::Animation對象來包含這兩個通道并定義其通用行為。setPlayMode()方法與osg::AnimationPath的setLoopMode()方法等同:
osg::ref_ptr<osgAnimation::Animation> animation = new osgAnimation::Animation; animation->setPlayMode( osgAnimation::Animation::LOOP ); animation->addChannel( ch1.get() ); animation->addChannel( ch2.get() );
- 動畫已完成設置,但還沒有被關聯到任何場景元素。因為他將要影響變換節點,在這里我們需要創建一個變換更新器目標,來匹配動畫的所有通道。其元素與通道通過相同的名字字符串處于一對一的關系:
osg::ref_ptr<osgAnimation::UpdateMatrixTransform> updater = new osgAnimation::UpdateMatrixTransform("PathCallback"); updater->getStackedTransforms().push_back( new osgAnimation::StackedTranslateElement("position") ); updater->getStackedTransforms().push_back( new osgAnimation::StackedQuaternionElement("quat") );
- Cessna借助于偽載入器被載入,并被位于osg::MatrixTransform父節點之下。變換動畫可以應用于其上的變換父節點將會接受更新器作為更新回調。在這里數據變化確保動畫處理總是安全的:
osg::ref_ptr<osg::MatrixTransform> animRoot= new osg::MatrixTransform; animRoot->addChild( osgDB::readNodeFile("cessna.osg.0,0,90.rot") ); animRoot->setDataVariance( osg::Object::DYNAMIC ); animRoot->setUpdateCallback( updater.get() );
- 因為我們只有一個要插入的動畫對象,一個基本管理器就足夠了。下一步是創建一個osgAnimation::BasicAnimationManager對象并向其注冊動畫:
osg::ref_ptr<osgAnimation::BasicAnimationManager> manager = new osgAnimation::BasicAnimationManager; manager->registerAnimation( animation.get() );
- 管理器也是一個更新回調,所以將其關聯到場景圖的根節點:
osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild( animRoot.get() ); root->setUpdateCallback( manager.get() );
- 現在播放動畫。當然,我們也可以將下面的代碼放在一個自定義回調中:
manager->playAnimation( animation.get() );
- 啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( root.get() ); return viewer.run();
- 結果與動畫路徑完全相同。使用如此多的對象實現這樣的一個簡單的動畫似乎有一些復雜。我們這里介紹這個示例僅是演示osgAnimation元素的整體結構,并希望能夠激發更多的靈感。
What just happened?
這里osgAnimation::UpdateMatrixTransform對象是兩個動畫通道的目標,因為其名字PathCallback是在構造函數中設置,也被用于通道的setTargetName()方法。
但是這并不夠。更新器應該知道每一個通道將會執行哪一個動作,并將通道鏈接到正確的動作處理器。例如,osgAnimation::Vec3LinearChannel對象可以被用來表示3D位置,或是實現旋轉的歐拉角。要判斷他將會被應用于實際任務,我們需要將某些堆疊的元素壓入更新,每一個元素與一個預定義的通道相關聯。這是通過添加到由getStackedTransforms()方法所返回的列表來實現的,該列表間接派生于std::vector。
可用的堆疊元素包含StackedTranslateElement(變換動作),StackedScaleElement(縮放動作),StackedRotateAxisElement(歐拉旋轉動作),StackedQuaternionElement(四元數旋轉操作)以及StackedMatrixElement(矩陣賦值操作)。所有這些類定義在osgAnimation名字空間中,并且被鏈接到相同名字的通道。
Loading and rendering characters
osgAnimation庫具有實現特征動畫的特定類。osgAnimation::Bone與osgAnimation::Skeleton類被用來構建場景圖中的完整骨骼。osgAnimation::UpdateBone類定義了如何由動畫通道更新骨骼。
不幸的是,在OSG中構建我們自己的特征并不容易,特別是完全從頭開始時。一個較為簡單的方法就是由文件載入特征模式并在我們的OSG程序中進行播放。Collada DAE是一個初學者可以用來創建并保存動畫特征的格式。我們可以在https://collada.org找到更多關于開放標準與工具的信息。
Autodesk FBX也是一個很好的文件格式,但是他只能為商業軟件所支持。
OSG可以通過osgDB::readNodeFile()函數同時讀取兩種格式,假定我們有第三方庫并且編譯了相應的OSG插件。要詳細了解如何實現可以參考第10章。
Time for action - creating and driving a character system
現在我們要載入并播放已有的OSG人物,bignathan,動畫。這是由osgAnimation作者創建的,并包含一系列的滑稽動畫。在這里要執行的主要操作是由根節點獲取動畫管理器,列出所有可用的動畫,并在其中播放特定的動畫。
- 包含必需的頭文件:
#include <osgAnimation/BasicAnimationManager> #include <osgDB/ReadFile> #include <osgViewer/Viewer> #include <iostream>
- 我們要為程序配置兩個參婁。參數--animation指定要在程序中播放的動畫,而--listall在控制臺列出所有可用的動畫:
osg::ArgumentParser arguments( &argc, argv ); bool listAll = false; std::string animationName; arguments.read( "--animation", animationName ); if ( arguments.read("--listall") ) listAll = true;
- 確保載入bignathan.osg;否則,我們不能繼續該示例。他應位于由環境變量OSG_FILE_PATH所定義的示例數據目錄中。我們可以通過運行安裝器或是通過查找OSG網站來獲取:
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("bignathan.osg"); if ( !model ) return 1;
- 試著由根模型的更新回調中獲取動畫管理器:
osgAnimation::BasicAnimationManager* manager = dynamic_cast<osgAnimation::BasicAnimationManager*> ( model->getUpdateCallback() ); if ( !manager ) return 1;
- 現在迭代管理中所記錄的所有動畫。如果由命令行讀取了--listall參數,則每個動畫的名字也要輸出到屏幕上。播放與--animation參數后的輸入參數匹配的動畫:
const osgAnimation::AnimationList& animations = manager->getAnimationList(); if ( listAll ) std::cout << "**** Animations ****" << std::endl; for ( unsigned int i=0; i<animations.size(); ++i ) { const std::string& name = animations[i]->getName(); if ( name==animationName ) manager->playAnimation( animations[i].get() ); if ( listAll ) std::cout << name << std::endl; } if ( listAll ) { std::cout << "********************" << std::endl; return 0; }
- 啟動查看器:
osgViewer::Viewer viewer; viewer.setSceneData( model.get() ); return viewer.run();
- 啟動命令行。第一步是列出所有的動畫并了解一下哪一個更為有趣。輸入下面的命令并查看輸出(假定可執行文件名為MyProject.exe):
# MyProject.exe --listall
輸出結果如下所示:

- 使用--animation參數輸入下面命令:
# MyProject.exe --animation Idle_Head_Scratch_01
- 現在我們會看到一個一直在抓頭的多邊形男孩:

What just happened?
也許我們更希望創建能夠運動的人物,而不是將其載入OSG中并開始渲染。但是這超出了本書的范圍。有大量的3D建模軟件可供我們使用:Autodesk 3dsmax,Autodesk Maya,Blender等。可以試著將我們的工作輸出為FBX格式,或是我們可以選擇通過某些導出工具,例如Cedric Pinson的Blender Exporter,將其轉換為OSG原生格式:http://hg.plopbyte.net/osgexport/ 。這是由Mercurial控制的,這是一個非常流行的源碼控制管理工具。
除了osgAnimation中的人物解決方案以外,還有更多處理人物動畫的第三方工程。其中一個就是所謂的Cal3D工程。這有一個名為osgCal2的OSG封裝工程。建議我們可以查看下面的網站來了解一下他們是否更適合我們的程序:
Have a go hero - analyzing the structure of your character
我們是否對bignathan的結構感興趣?正如前面所介紹的,他應該是由osgAnimation::Bone與osgAnimation::Skeleton類構成的,這兩者實際上是節點。所以,節點訪問器可以用來分析場景圖并了解一下他是如何組織與遍歷的。
修改第5章中的訪問器示例,并用來查看與操作人物文件中的所有骨骼節點。一個建議就是我們可以讀取與每一個osgAnimation::Bone節點相關聯的更新回調,而如果可能,將bignathan用作參數構建我們自己的biped。他們通常具有相同的骨架。
Summary
OSG支持可以應用到3D程序中的所有動畫類型。最常見的是在時間上變換,這可以通過修改空間狀態或是3D對象的渲染來實現,而所謂的關鍵幀動畫被設計用來通過在幀之間進行插值實現平滑運動。骨骼系統是特征動畫的關鍵,其中mesh被用來配置預構建的骨骼。
在本章中我們介紹了OSG動畫類的功能,特殊探討了:
- 避免沖突修改的原因與方法,特別是當創建動態幾何體時。
- 派生回調基類,包括osg::nodeCallback,osg::StateAttributeCallback等。
- 通過使用osg::AnimationPath與osg::AnimationPathCallback類在路徑動畫中插入變換值。
- 使用簡單運動類,例如osgAnimation::LinearMotion與osgAnimation::InOutCubicMotion來實現自然運動效果。
- 使用osg::ImageSequence類生成動畫紋理。
- 如何通過使用osgAnimation庫以及動畫通道與控制方法創建復雜通用的關鍵幀動畫。