Chia´s Small Shop

Chia小鋪,陳列隨手的文字

與Ogre共舞:第五步,轉轉乳酪方塊

在這步中,將要介紹Ogre在描繪場景時,一個參與描繪流程的重要的類別:FrameListener,顧名思義就是Ogre每描繪一張畫面時,在旁邊「聽候通知」的類別。在行為上,FrameListener與回呼函數(callback function)相當,每當Ogre描繪一張畫面,會在適當事件發生的時候通知FrameListener來處理。我們可以利用FrameListener來處理每一個畫面中,物件的操作、攝影機視角的改變、以及其他輸出輸入的處理,讓使用者能夠與Ogre的場景有所互動。

Ogre提供的FrameListener類別不能直接使用,必須產生一個衍生類別後,實做FrameListener提供的虛擬函式。先來看看一個簡單的衍生FrameListener的例子:

class MyOgreFrameListener: public FrameListener
{
public:
    MyOgreFrameListener( Camera *_cam ) :
        m_camera( _cam ), m_scene_mgr( _cam->getSceneManager() ) {}
    ~MyOgreFrameListener() {}

public: // FrameListener virtual functions
    bool frameStarted( const FrameEvent &_evt );
    bool frameRenderingQueued( const FrameEvent &_evt );
    bool frameEnded( const FrameEvent &_evt );

private:
    Camera*         m_camera;
    SceneManager*   m_scene_mgr;
};

在第1行,我們定義MyOgreFrameListener繼承了FrameListener類別。我們希望在描繪過程中,要能操作物件的位置與姿態,因此在data member中宣告Camera與SceneManager的指標,並在第4行建構子的地方初始化。第9~11行,就是FrameListener提供的三個事件通知的函式。函式所傳入的參數FrameEvent,包含了事件發生到目前為止的時間,與上一張畫面描繪的時間。

顯然的,FrameListener提供的三種「事件通知」就是今天的重點,這三個函式被執行的時機為:

  • frameStarted() – 在描繪一張新畫面的開始之前。
  • frameRenderQueued() – 在描繪指令已經傳送出去(到顯示卡)後,並且在切換畫面的緩衝區之前。
  • frameEnded() – 在描繪這張畫面完成/結束後(切換畫面緩衝區之後)。

在先前版本的Ogre僅包含frameStarted()與frameEnded()兩個函式,frameRenderQueued()是1.6新增的。在Ogre提供的範例程式”ExampleFrameListener.h”中,就使用了frameRenderQueued()代替frameStarted(),處理使用者輸入的部份,到底frameRenderQueued()有什麼過人之處呢?

ogre-framelistener

上圖是FrameListener三種事件通知的執行時機,與GPU繪圖的時間關係示意圖。每一張畫面的描繪,Ogre (CPU)會順序執行:

  1. frameStarted()
  2. Ogre更新所有描繪的目標(render target)
  3. 傳送描繪指令到底層硬體(GPU)
  4. frameRenderQueued()
  5. 切換畫面緩衝區(這個時候,GPU已經把畫面描繪好,存在畫面緩衝區中,只要切換緩衝區,描繪好的畫面就會顯示在螢幕上)
  6. frameEnded()

在這個流程中,在3.執行完成後,CPU就暫時沒事了,描繪場景的工作交由顯示卡(GPU)處理。而frameRenderQueued()就是為了避免CPU閒置而出現的,進而提昇每秒鐘能處理的畫面數量。譬如說,原先在frameStarted()裡的處理步驟,搬到frameRenderQueued()裡面執行,就可以縮短每個畫面描繪的時間,這對注重效能的應用(像是遊戲)是個重要的設計。在Ogre的API參考文件中也說明,frameRenderQueued()很適合用來進行「每個畫面都需要」的處理。當然,這也有個小小小的缺點,那就是在frameRenderQueued()中所做的處理,必須等到下一張畫面才會出現。不過,對每秒能處理幾十到上百張畫面的情況來說,差這一張畫面的時間間隔似乎沒有太大的影響。

接下來就是這次的工作了!我們要在frameRenderQueued()中,設計物件的出現方式 – 旋轉。下面是讓12個乳酪方塊轉啊轉的程式碼:

bool MyOgreFrameListener::frameRenderingQueued( const FrameEvent &_evt )
{
    // set the rotation angle in every frame
    float angle = 0.2;
    // get the named entity in the scene manager
    Entity *ent = m_scene_mgr->getEntity( "cube0" );

    // get the scene node which the entity's scene node is attached to
    // e.g. [SceneNode]< ---[SceneNode]==[Entity]
    SceneNode *center_node = ent->getParentSceneNode()->getParentSceneNode();

    // rotate this center_node along z-axis (in TS_LOCAL) and x-axis (in TS_PARENT)
    center_node->rotate( Vector3::UNIT_Z, Degree( angle ) );
    center_node->rotate( Vector3::UNIT_X, Degree( angle * 0.5), SceneNode::TS_PARENT );

    // keep the rendering loop going
    return true;
}

還記得在上一步中提到的物件操作嗎?對,就是透過SceneNode來改變物件的位置與姿態。首先,我們要用SceneManager取得要操作的SceneNode。可是,在上一步的範例中,只有幫Entity取了”cube0”, “cube1”,…之類的名稱,沒有SceneNode的名稱怎麼辦?沒關係,可以透過Entity提供的getParentSceneNode()取得所依附的SceneNode。根據之前乳酪方塊們建立的關係,在第6行,任意選擇一個乳酪方塊的名稱,找出這個名字的Entity。接著,第10行,呼叫兩次getParentSceneNode()得到12個乳酪方塊的共同父節點(第一次呼叫取得該Entity的節點,第二次就是該節點的父節點),用center_node指標來表示。第13行,我們讓center_node以他自己(TS_LOCAL)的z軸方向,每個畫面旋轉angle角度。這個步驟會讓12個子節點跟著「旋轉」,但事實上是因為12個節點必須相對於父節點(center_node)保持位置與姿態的不變。而為了增加一點點華麗度(?),第14行中center_node會以它的父節點(TS_PARENT)的x軸方向,每個畫面旋轉一半的angle角度(轉得慢一點)。這裡的angle定義在第4行,表示每個畫面要旋轉的角度量。最後,在第17行,回傳true讓描繪迴圈持續執行下去。

在實際執行時,如果你有處理能力較強的硬體,那麼乳酪方塊就會轉得比較快一點;反之,在較慢的電腦上,就會像慢動作那樣的轉啊轉。或許可以來點什麼辦法,讓不同的硬體都可以擁有同樣的表現。嗯…注意到那個傳進frameRenderingQueued()的參數了嗎?

float angle = 90 * _evt.timeSinceLastFrame;

我們用FrameEvent::timeSinceLastFrame作為調整旋轉角度使用。在較快的電腦上,畫一張畫面的時間短,因此相對的旋轉角度會較小。這個timeSinceLastFrame的單位是秒,上面的敘述是讓angle設定為每秒鐘可以轉90度的數值。

好啦!FrameListener設計完成,該來看看成果了。不過,別忘了在開始執行前,要讓Ogre知道FrameListener的存在。在Ogre執行startRendering()之前,加上

/*
 *  10. Create the FrameListener
 */
m_frame_listener = new MyOgreFrameListener( m_camera );
m_root->addFrameListener( m_frame_listener );

第4行先產生一個自己定義的FrmaeListener,然後利用Root::addFrameListener()將我們的FrameListener加入Root。Ogre不限制FrameListener的數量,如果有需要,可以產生多個FrameListener分別擔任不同的任務。不過要使用FrameListener,僅限於使用自動描繪迴圈的方式(就是用startRendering()開始描繪迴圈)。如果是利用RenderTarget::update()自行描繪場景,則FrameListener不會收到任何的呼叫。

轉轉乳酪方塊

[下載] 範例程式 (VC2008 專案)
ogre_tutorial_5.zip 5.6 Kb

在這個範例裡,多了兩個檔案,分別是”MyOgreFrameListener.h”與”MyOgreFrameListener.cpp”,宣告與實做MyOgreFrameListener類別。另外,參數與旋轉使用的參考座標系,可以自行改變,看看各有什麼效果。例如:將前面提到的center_node對自己的x軸旋轉,會發生什麼事呢?由於center_node的z軸也會旋轉,因此它的x軸是會改變方向的(相對於其父節點的座標系),所以…趕快自己改改看吧!

三月 28, 2009 - 發文者為 chia0418 | Ogre | , | 3 則留言

3 則留言 »

  1. 原來timeSinceLastFrame是這樣用的阿
    之前還在想說這個到底是要怎麼用XD

    Comment 由 艾薩克 | 三月 29, 2009

  2. 沒錯,Ogre的範例中,也用了timeSinceLastFrame來控制攝影機的移動速度喔!

    Comment 由 chia0418 | 三月 31, 2009

  3. 不知為甚麼我跟著做畫面只要靜止不動

    Comment 由 a | 八月 6, 2009


留言