Ogre中級教程(一): 動畫, 兩點間移動, 和四元數基礎

Ogre中級教程
中級教程一
來自 Ogre wiki
動畫, 兩點間移動, 和四元數基礎
作者: Culver.
內容
      目錄
      [隱藏]
        1 介紹
        2 前期準備
        3 準備開始
        4 設置場景
        5 動畫
        6 移動角色
        7 鞏固練習
          7.1 簡單問題
          7.2 中級問題
          7.3 困難問題
          7.4 專家問題


介紹
這個教程裏包括怎麼樣得到一個模型,並添加模型動畫,最後讓模型可以在兩個預先定義的點之間走動。在此將講述如何用基本的四元數方法保持模型移動的時候正面一直朝着我們指定的方向。你必須一點點的將代碼加入到你的項目中,並在每次加入新代碼後編譯並察看demo運行的結果。

本課的最終代碼在這裏。
前期準備
首先,這個指南假設你已經知道如何設置Ogre的項目環境以及如何正確編譯項目。該例子同樣使用STL
中的queue數據結構。那麼預先了解如何使用queue是必要的,至少你需要知道什麼是模版。如果你不熟悉STL,那麼我像你推薦STL參考[ISBN
0596005563],它可以幫助你在將來花費更少的時間。
準備開始
首先,你需要爲這個Demo創建一個新項目,在項目中添加一個名爲"MoveDemo.cpp"的文件並加入如下代碼:
#include "ExampleApplication.h"

#include <deque>
using namespace std;

class MoveDemoListener : public ExampleFrameListener
{
public:

    MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,
        Entity *ent, deque<Vector3> &walk)
        : ExampleFrameListener(win, cam, false, false), mNode(sn), mEntity(ent), mWalkList( walk )
    {
    } // MoveDemoListener

    /* This function is called to start the object moving to the next position
       in mWalkList.
    */
    bool nextLocation( )
    {
        return true;
    } // nextLocation( )

    bool frameStarted(const FrameEvent &evt)
    {
        return ExampleFrameListener::frameStarted(evt);
    }
protected:
    Real mDistance;                  // The distance the object has left to travel
    Vector3 mDirection;              // The direction the object is moving
    Vector3 mDestination;            // The destination the object is moving towards

    AnimationState *mAnimationState; // The current animation state of the object

    Entity *mEntity;                 // The Entity we are animating
    SceneNode *mNode;                // The SceneNode that the Entity is attached to
    std::deque<Vector3> mWalkList;   // The list of points we are walking to

    Real mWalkSpeed;                 // The speed at which the object is moving
};


class MoveDemoApplication : public ExampleApplication
{
protected:
public:
    MoveDemoApplication()
    {
    }

    ~MoveDemoApplication()
    {
    }
protected:
    Entity *mEntity;                // The entity of the object we are animating
    SceneNode *mNode;               // The SceneNode of the object we are moving
    std::deque<Vector3> mWalkList;  // A deque containing the waypoints

    void createScene(void)
    {
    }

    void createFrameListener(void)
    {
        mFrameListener= new MoveDemoListener(mWindow, mCamera, mNode, mEntity, mWalkList);
        mFrameListener->showDebugOverlay(true);
        mRoot->addFrameListener(mFrameListener);
    }

};


#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"


INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
#endif
{
    // Create application object
    MoveDemoApplication app;

    try {
        app.go();
    } catch( Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
        MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!",
            MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
        fprintf(stderr, "An exception has occured: %s/n",
                e.getFullDescription().c_str());
#endif
    }


    return 0;
}
在我們繼續講解之前,你可以編譯這部分代碼看下效果。
設置場景
在我們開始之前,需要注意的是已經在MoveDemoApplication中預先定義的三個變量。我們創建的entity實例保存在變量mEntity中,我們創建的node實例保存在mNode中,另外mWalkList包含了所有我們希望對象行走到的節點。

定位到MoveDemoApplication::createScene函數並且加入以下代碼。首先,我們來設置環境光(ambient
light)到最大,這樣可以讓我們看到我們放在場景中的所有對象。
       // Set the default lighting.
       mSceneMgr->setAmbientLight( ColourValue( 1.0f, 1.0f, 1.0f ) );
接下來我們來在屏幕上創建一個可以使用的機器人。要做到這點我們需要在創建SceneNode之前先爲機器人創建一個entity使得我們可以對其進行旋轉。
       // Create the entity
       mEntity = mSceneMgr->createEntity( "Robot", "robot.mesh" );

       // Create the scene node
       mNode = mSceneMgr->getRootSceneNode( )->
           createChildSceneNode( "RobotNode", Vector3( 0.0f, 0.0f, 25.0f ) );
       mNode->attachObject( mEntity );
以上這些都是非常基礎的,所以我認爲不需要再對以上的描述做任何解釋。在接下來的代碼片斷,我們將開始告訴機器人那些地方是它需要到達的。這裏需要你們瞭解一些STL的知識,deque對象是一個高效的雙端對列。我們只需要使用它的幾個簡單的方法。push_front和push_back方法分別將對象放入隊列的前端和後端,front和back方法分別返回當前隊列前端和後端的元素(PS:注意,這裏最好有判空的習慣,用if(
empty() )
)pop_front和pop_back兩個方法分別從隊列兩端移除對象。最後,empty方法返回該隊列是否爲空。下面這些代碼添加了兩個Vector到隊列中,在後面我們移動robot的時候會用到它們。

       // Create the walking list
       mWalkList.push_back( Vector3( 550.0f,  0.0f,  50.0f  ) );
       mWalkList.push_back( Vector3(-100.0f,  0.0f, -200.0f ) );
接下來,我們在場景裏放置一些物體,以標記這個機器人應該朝哪走去。這樣使我們能看見機器人在場景裏相對於其它物體進行移動。注意它們的位置的負Y部分,這些物體被放在機器人移動目的地的正下方,當它到達指定地點時,它就站在這些物體上面。

       // Create objects so we can see movement
       Entity *ent;
       SceneNode *node;

       ent = mSceneMgr->createEntity( "Knot1", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot1Node",
           Vector3(  0.0f, -10.0f,  25.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );

       ent = mSceneMgr->createEntity( "Knot2", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot2Node",
           Vector3( 550.0f, -10.0f,  50.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );

       ent = mSceneMgr->createEntity( "Knot3", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot3Node",
           Vector3(-100.0f, -10.0f,-200.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );
最後,我們要創建一個攝像機從適合的角度來觀察它。我們來把攝像機移動到更多的位置。
       // Set the camera to look at our handywork
       mCamera->setPosition( 90.0f, 280.0f, 535.0f );
       mCamera->pitch( Degree(-30.0f) );
       mCamera->yaw( Degree(-15.0f) );
現在編譯並運行代碼。你應該能看到這個樣子: [[1]]
在進入下一個部分之前,注意一下MoveDemoListener的構造器,它在MoveDemoApplication::createFrameListener方法裏的第一行被調用。除了傳入BaseFrameListener的標準參數,還有場景節點、實體、雙端隊列。

動畫
現在我們來設置一些基本的動畫。在Ogre裏動畫是非常簡單的。要做的話,你需要從實體對象裏獲取AnimationState,設置它的選項,並激活它。這樣就能使動畫活動起來,但你還必須在每一幀後給它添加時間,才能讓動畫動起來。我們設置成每次移動一步。首先,找到MoveDemoListener的構造器,並添加以下代碼:

      // Set idle animation
      mAnimationState = ent->getAnimationState("Idle");
      mAnimationState->setLoop(true);
      mAnimationState->setEnabled(true);
第二行從實體中獲取到了AnimationState。第三行我們調用setLoop( true
),讓動畫不停地循環。而在一些動畫裏(比如死亡動畫),我們可能要把這個設置爲false。第四行才把這個動畫真正激活。但等等...我們從哪裏獲取的“Idle”?這個魔術般的常量是怎麼飛到這裏來的?每個mesh都有它們自己定義的動畫集。爲了能夠查看某個mesh的全部動畫,你需要下載OgreMeshViewer才能看到。

現在,如果我們編譯並運行這個demo,我們看見了...nothing!
這是因爲我們還需要在每一幀里根據時間來更新這個動畫的狀態。找到MoveDemoListener::frameStarted方法,在方法的開頭添加這一行:
       mAnimationState->addTime(evt.timeSinceLastFrame);
現在來編譯並運行程序。你應該可以看了一個機器人正在原地踏步了。
移動角色
現在我們執行棘手的任務,開始讓這個機器人從一點走到另一點。在我們開始之前,我想介紹一下保存在MoveDemoListener類裏的成員變量。我們將使用4個變量來完成移動機器人的任務。首先,我們把機器人移動的方向保存到mDirection裏面。我們再把當前機器人前往的目的地保存在mDestination裏。然後在mDistance保存機器人離目的地的距離。最後,在mWalkSpeed裏我們保存機器人的移動速度。

首先清空MoveDemoListener構造器,我們會用稍微不同的代碼來替換。我們要做的第一件事是設置這個類的變量。我們將把行走速度設爲每秒35個單位。有一個大問題要注意,我們故意把mDirection設成零向量,因爲後面我們會用它來判斷機器人是否正在行走。

       // Set default values for variables
       mWalkSpeed = 35.0f;
       mDirection = Vector3::ZERO;
好了,搞定了。我們要讓機器人動起來。爲了讓機器人移動,我們只須告訴它改變動畫。然而,我們只想要若存在另一個要移動到的地點,就讓機器人開始移動。爲了這個目的,我們調用nextLocation
函數。把代碼加到MoveDemoListener::frameStarted方法的頂部,在調用AnimationState::addTime之前:
      if (mDirection == Vector3::ZERO)
      {
          if (nextLocation())
          {
              // Set walking animation
              mAnimationState = mEntity->getAnimationState("Walk");
              mAnimationState->setLoop(true);
              mAnimationState->setEnabled(true);
          }
      }
如果你現在編譯並運行,這個機器人將原地行走。這是由於機器人是以ZERO方向出發的,而我們的MoveDemoListener::nextLocation函數總是返回true。在後面的步驟中,我們將給MoveDemoListener::nextLocation函數添加更多的一點智能。

現在,我們準備要真正在場景裏移動機器人了。爲了這樣做,我們需要在每一幀裏讓我移動一點點。找到MoveDemoListener::frameStarted方法,我們將在調用AnimationState::addTime之前,我們先前的if語句之後,添加以下代碼。這段代碼將處理當機器人實際移動的情況;mDirection
!= Vector3::ZERO。
       else
       {
           Real move = mWalkSpeed * evt.timeSinceLastFrame;
           mDistance -= move;
現在,我們要檢測一下我們是否“走過”了目標地點。即,如果現在mDistance小於0,我們需要“跳”到這點上,並設置移動到下一個地點。注意,我們把mDirection設置成零向量。如果nextLocation方法不改變mDirection(即沒有其它地方可去),我們就不再四處移動了。

           if (mDistance <= 0.0f)
           {
               mNode->setPosition(mDestination);
               mDirection = Vector3::ZERO;
現在我們移動到了這個點,我們需要設置運動到下一個點。只要我們知道有否需要移動到下一個地點,我們就能設置正確的動畫;如果有其它地點要去,就行走。如果沒有其它目的地,則停滯。

              // Set animation based on if the robot has another point to walk to.
              if (! nextLocation())
              {
                  // Set Idle animation                    
                  mAnimationState = mEntity->getAnimationState("Idle");
                  mAnimationState->setLoop(true);
                  mAnimationState->setEnabled(true);
              }
              else
              {
                  // Rotation Code will go here later
              }
          }
注意,如果queue裏已經沒有更多的地點要走的話,我們沒有必要再次設置行走動畫。既然機器人已經在行走了,沒有必要再告訴他這麼做。然而,如果機器人還要走向另一個地點,我們就要把它旋轉以面對那個地點。現在,我們在else括號旁邊留下注釋佔位符;記住這個地點,因爲我們後面還要回來。

這裏考慮的是當我們離目標地點很近的時候。現在我們需要處理一般情況,當我們正在到達而沒有到達的時候。爲此,我們在機器人的行走方向上對它進行平移,用move變量指定的值。通過添加以下代碼來實現:

           else
           {
               mNode->translate(mDirection * move);
           } // else
       } // if
我們差不多做完了,除了還要設置運動需要的變量。如果我們正確地設置了運動變量,我們的機器人就會朝它該去的方向行走。看看MoveDemoListener::nextLocation方法,如果我們用完了所有的地點,它返回false。這是函數的第一行。(注意你要保留函數底部的return
true語句)
       if (mWalkList.empty())
           return false;
現在我們來設置變量。首先我們從雙端隊列裏取出一個向量。通過目標向量減去場景節點的當前向量,我們得取方向向量。然而我們仍有一個問題,還記得我們要在frameStarted方法裏用mDirection乘以移動量嗎?如果我們這麼做,我們必須把方向向量轉換成單位向量(即,它的長度等於一)。normalise函數爲我們做了這些事,並返回向量的原始長度。唾手可得,我們需要設置到目的地的距離。

      mDestination = mWalkList.front();  // this gets the front of the deque
      mWalkList.pop_front();             // this removes the front of the deque
      mDirection = mDestination - mNode->getPosition();
      mDistance = mDirection.normalise();
編譯並運行代碼。搞定! 現在機器人行走到每一個地點,但它總是面朝着Vector3::UNIT_X方向(它的默認)。我們需要當它向地點移動時,改變它的朝向。
我們需要做的是獲得機器人臉的方向,然後用旋轉函數將它旋轉到正確的位置。在我們上一次留下注釋佔位符的地方,插入如下代碼。第一行獲得了機器人臉的朝向。第二行建立了一個四元組,它表示從當前方向到目標方向的旋轉。第三行纔是真正旋轉了這個機器人。

       Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
       Ogre::Quaternion quat = src.getRotationTo(mDirection);
       mNode->rotate(quat);
我們在基礎教程4裏已經對四元組進行過簡單的介紹,但在這裏纔是我們對它的第一次使用。基本上說,四元組就是在三維空間裏對旋轉的表示。它們被用來跟蹤物體是如何在空間裏放置的,也可能被用來在Ogre裏對物體進行旋轉。我們在第一行裏調用getOrientation方法,返回了一個表示機器人在空間裏面向方向的四元組。因爲Ogre不知道機器人的哪一面纔是它的正面,所以我們必須用UNIT_X方向乘以這個朝向,以取得機器人當前的朝向。我們把這個方向保存在src變量裏。在第二行,getRotationTo方法返回給我們一個四元組,它表示機器人從目前的朝向到我們想讓它朝向方向的旋轉。第三行,我們旋轉節點,以讓它面向一個新的方向。

我們創建的代碼只剩下一個問題了。這裏有一種特殊情況將會使SceneNode::rotate失敗。如果我們正試圖讓機器人旋轉180度,旋轉代碼會因爲除以0的錯誤而崩掉。爲了解決這個問題,我們需要測試我們是否執行180度的旋轉。如果是,我們只要用yaw來將機器人旋轉180度,而不是用rotate。爲此,刪除我們剛纔放入的代碼,並用這些代替:

      Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
      if ((1.0f + src.dotProduct(mDirection)) < 0.0001f)
      {
          mNode->yaw(Degree(180));
      }
      else
      {
          Ogre::Quaternion quat = src.getRotationTo(mDirection);
          mNode->rotate(quat);
      } // else
這些代碼的意思應該是比較清楚的,除了包在if語句裏的內容。如果兩個向量是互相反向的(即,它們之間的角度是180度),它們的點乘就將是-1。所以,如果我們把兩個向量點乘而且結果等於-1.0f,則我們用yaw旋轉180度,否則我們用rotate代替。爲什麼我加上1.0f,並檢查它是否小於0.0001f?
不要忘了浮點舍入錯誤。你應該從來不直接比較兩個浮點數的大小。最後,需要注意的是,這兩個向量的點乘永遠是落在[-1,1]這個區域之間的。如果還不太清楚的話,你應該先去學一學最基本的線性代數再來做圖像編程!
至少你應該複習一下四元組與旋轉基礎,查閱關於一本關於基礎的向量及矩陣運算的書籍。
好了,我們的代碼完成了! 編譯並運行這個Demo,你會看見一個機器人朝着指定的地點走動。
鞏固練習
簡單問題
1. 添加更多的點到路徑中。同時在他點的位置放上Knonts來觀察他想去哪裏。
2. 機器人走完他的有效路程後就應該不存在了!當機器人完成行走,他就應該用執行死亡動畫來代替待機動畫。死亡的動畫叫“Die”。
中級問題
1.
看完教程後,你注意到了mWalkSpeed有點問題嗎?我們只是一次性設置了一個值,然後就再也沒變過。就好像是一個類的不變的靜態變量。試着改變一下這個變量。(提示:可以定義鍵盤的+和-分別表示加速和減速)

2.
代碼中有些地方非常取巧,例如跟蹤機器人是否正在走,用了mDirection向量跟Vector3::ZERO比較。如果我們換用一個bool型變量mWalking來跟蹤機器人是否在移動也許會更好。實現這個改變。

困難問題
1.
這個類的一個侷限是你不能在創建對象後再給機器人行走的路線增加新的目的地點。修補這個問題,實現一個帶有一個Vector3參數的新方法,並且將它插入mWalkList隊列。(提示:如果機器人還未完成行走過程,你就只需要將目的地點插入隊列尾即可。如果機器人已經走完全程,你將需要讓它再次開始行走,然後調用nextLocation開始再次行走。)

專家問題
1.
這個類的另一個主要侷限是它只跟蹤一個物體。重新實現這個類,使之可以彼此獨立地移動任意數量的物體。(提示:你可以再創建一個類,這個類包含移動一個物體所需要知道的全部東西。把它存儲在一個STL對象中,以便以後可以通過key獲取數據。)如果可以不註冊附加的framelistener,你會得到加分。

2. 做完上面的改變,你也許注意到了機器人可能會彼此發生碰撞。修復它,或者創建一個聰明的尋路函數,或者當機器人碰撞時檢測,阻止它們彼此穿透而過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章