Ogre中級教程(二):射線場景查詢及基礎鼠標用法

中級教程2:射線場景查詢及基礎鼠標用法
有關這篇教程,無論遇到任何問題,都可以到論壇發帖尋求幫助。
      目錄
      [隱藏]
        1 介紹
        2 前期準備
        3 開始
        4 創建場景
        5 幀監聽器介紹
        6 創建幀監聽器
        7 增加鼠標查看
        8 地形碰撞檢測
        9 地形選擇
        10 進階練習
          10.1 簡單練習
          10.2 中級練習
          10.3 高級練習
          10.4 進階練習


介紹
本教程中,我們會初步創建一個基礎場景編輯器。在過程之中,我們會涉及到:
    如何使用RaySceneQueries阻止鏡頭穿透地面
    如何使用MouseListener和MouseMotionListener接口
    使用鼠標選取地面上的x和y座標
你可以在這裏找到完整代碼。跟隨着教程,你會慢慢地向你自己的工程項目中增加代碼,並且隨着編譯看到結果。
前期準備
本教程假設你已經知道了如何創建Ogre工程,並且可以成功編譯。假設你已經瞭解了基本的Ogre對象(場景節點,實體,等等)。你也應該熟悉STL迭代器基本的使用方法,因爲本教程會用到。(Ogre也大量用到STL,如果你還不熟悉STL,那麼你需要花些時間學習一下。)

開始
首先,你需要爲此演示程序創建一個新工程。在創建工程時,選空工程、自己的框架,以及初始化進度條和CEGUI支持,不選編譯後拷貝。向工程中,增加一個名叫“MouseQuery.cpp”的文件,並向其中添加如下代碼:

#include <CEGUI/CEGUISystem.h>
#include <CEGUI/CEGUISchemeManager.h>
#include <OgreCEGUIRenderer.h>

#include "ExampleApplication.h"

class MouseQueryListener : public ExampleFrameListener, public OIS::MouseListener
{
public:

 MouseQueryListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager, CEGUI::Renderer *renderer)
  : ExampleFrameListener(win, cam, false, true), mGUIRenderer(renderer)
 {
 } // MouseQueryListener

 ~MouseQueryListener()
 {
 }

 bool frameStarted(const FrameEvent &evt)
 {
  return ExampleFrameListener::frameStarted(evt);
 }

 /* MouseListener callbacks. */
 bool mouseMoved(const OIS::MouseEvent &arg)
 {
  return true;
 }

 bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
 {
  return true;
 }

 bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
 {
  return true;
 }


protected:
 RaySceneQuery *mRaySceneQuery;     // The ray scene query pointer
 bool mLMouseDown, mRMouseDown;     // True if the mouse buttons are down
 int mCount;                        // The number of robots on the screen
 SceneManager *mSceneMgr;           // A pointer to the scene manager
 SceneNode *mCurrentObject;         // The newly created object
 CEGUI::Renderer *mGUIRenderer;     // CEGUI renderer
};

class MouseQueryApplication : public ExampleApplication
{
protected:
 CEGUI::OgreCEGUIRenderer *mGUIRenderer;
 CEGUI::System *mGUISystem;         // cegui system
public:
 MouseQueryApplication()
 {
 }

 ~MouseQueryApplication()
 {
 }
protected:
 void chooseSceneManager(void)
 {
  // Use the terrain scene manager.
  mSceneMgr = mRoot->createSceneManager(ST_EXTERIOR_CLOSE);
 }

 void createScene(void)
 {
 }

 void createFrameListener(void)
 {
  mFrameListener = new MouseQueryListener(mWindow, mCamera, mSceneMgr, mGUIRenderer);
  mFrameListener->showDebugOverlay(true);
  mRoot->addFrameListener(mFrameListener);
 }
};


#if OGRE_PLATFORM == PLATFORM_WIN32 || 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
 MouseQueryApplication app;

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

 return 0;
}

在繼續下面教程以前,先確保上面代碼可以正常編譯。
創建場景
找到MouseQueryApplication::createScene方法。下面的代碼應該都很熟悉了。如果你不知道其中某些是做什麼用的,請在繼續本教程前,參考Ogre
API。向createScene中,增加如下代碼:
        // Set ambient light
       mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));
       mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);

       // World geometry
       mSceneMgr->setWorldGeometry("terrain.cfg");

       // Set camera look point
       mCamera->setPosition(40, 100, 580);
       mCamera->pitch(Degree(-30));
       mCamera->yaw(Degree(-45));
既然我們建立了基本的世界空間,那麼就要打開光標。打開光標,要使用CEGUI函數調用。不過在此之前,我們需要啓用CEGUI。我們首先創建一個OgreCEGUIRenderer,然後創建系統對象並將剛創建的Renderer傳給它。創建CEGUI我們會專門留待後續教程介紹,現在只要知道創建mGUIRenderer時必須以最後一個參數告訴CEGUI你要用那個場景管理器。

       // CEGUI setup
       mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
       mGUISystem = new CEGUI::System(mGUIRenderer);
現在我們需要實際顯示光標了。同樣地,我不打算過多解釋這些代碼。我們會在後面的教程中詳細介紹。(其實也沒什麼,就是設置了一下CEGUI的窗口和鼠標的樣式。——Aaron註釋)

       // Mouse
       CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
       CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
如果你編譯並運行這個程序,你會發現一個光標出現在屏幕中央,但它還動不了。
幀監聽器介紹
這是程序要做的全部事情。FrameListener是代碼中複雜的部分,所以我會花一些時間強調我們要完成的東西,以便在我們開始實現它之前,使你有一個大體的印象。
    首先,我們想要將鼠標右鍵綁定到“鼠標觀察”模式。不能使用鼠標四下看看是相當鬱悶的,所以我們首先對程序增加鼠標控制(儘管只是在我們保持鼠標右鍵按下時)。
    第二,我們想要讓鏡頭不會穿過地表。這會使它更接近我們期望的樣子。
    第三,我們想要在地表上用鼠標左鍵點擊一下,就在那裏增加一個實體。
    最後,我們想要能“拖拽”實體。即選中我們想要看到的實體,按住鼠標左鍵不放,將它移動到我們想要放置的地方。鬆開鼠標左鍵,就又會將它鎖定在原地。
要做到這幾點,我們要使用幾個受保護的變量(這些已經加到類中了):
    RaySceneQuery *mRaySceneQuery;      // 射線場景查詢指針
    bool mLMouseDown, mRMouseDown;     // 如果按下鼠標按鈕,返回True
    int mCount;                        // 屏幕上機器人的數量
    SceneManager *mSceneMgr;           // 指向場景管理器的指針
    SceneNode *mCurrentObject;         // 新創建的物休
    CEGUI::Renderer *mGUIRenderer;     // CEGUI渲染器
變量mRaySceneQuery握有RaySceneQuery的一個拷貝,我們會它來尋找地面上的座標。變量mLMouseDown和mRMouseDon會追蹤我們是否按下鼠標鍵(例如:如果按下鼠標左鍵,則mLMouseDown爲true;否則,爲false)。mCount計數屏幕上有的實體數。mCurrentObject握有指向最近創建的場景節點的指針(我們將用這個“拖拽”實體)。最後,mGUIRenderer握有指向CEGUI
Renderer的指針,我們將用它更新CEGUI。
還要注意的是,有許多和鼠標監聽器相關的函數。在本演示程序中,我們不會全部用到,但是它們必須全部在那兒,否則編譯會報錯說你沒定義它們。
創建幀監聽器
找到MouseQueryListener構造函數,增加如下初始化代碼。注意,由於地形相當小,所以我們也要減少移動和旋轉速度。
        // Setup default variables
        mCount = 0;
        mCurrentObject = NULL;
        mLMouseDown = false;
        mRMouseDown = false;
        mSceneMgr = sceneManager;

        // Reduce move speed
        mMoveSpeed = 50;
        mRotateSpeed /= 500;
爲了MouseQueryListener能收到鼠標事件,我們必須把它註冊爲一個鼠標監聽器。如果對此不太熟悉,請參考基礎教程5。
        // Register this so that we get mouse events.
        mMouse->setEventCallback(this);
最後,在構造函數中我們需要創建一個RaySceneQuery對象。用場景管理器的一個調用創建:
        // Create RaySceneQuery
        mRaySceneQuery = mSceneMgr->createRayQuery(Ray());
這是我們需要的全部構造函數了,但是如果我們創建一個RaySceneQuery,以後我們就必須銷燬它。找到MouseQueryListener析構函數(~MouseQueryListener),增加如下代碼:

        // We created the query, and we are also responsible for deleting it.
        mSceneMgr->destroyQuery(mRaySceneQuery);
在進入下一階段前,請確保你的代碼可以正常編譯。
增加鼠標查看
我們要將鼠標查看模式綁定到鼠標右鍵上,需要:
    當鼠標被移動時,更新CEGUI(以便光標也移動)
    當鼠標右鍵被按下時,設置mRMouseButton爲true
    當鼠標右鍵被鬆開時,設置mRMouseButton爲false
    當鼠標被“拖拽”時,改變視圖
    當鼠標被“拖拽”時,隱藏鼠標光標
找到MouseQueryListener::mouseMoved方法。我們將要增加代碼使每次鼠標移動時移動鼠標光標。向函數中增加代碼:
       // Update CEGUI with the mouse motion
       CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
現在找到MouseQueryListener::mousePressed方法。這段代碼當鼠標右鍵按下時,隱藏光標,並設置變量mRMouseDown爲true。
       // Left mouse button down
       if (id == OIS::MB_Left)
       {
           mLMouseDown = true;
       } // if

       // Right mouse button down
       else if (id == OIS::MB_Right)
       {
           CEGUI::MouseCursor::getSingleton().hide();
           mRMouseDown = true;
       } // else if
接下來,當鼠標右鍵擡起時,我們需要再次顯示光標,並將mRMouseDown設置爲false。找到mouseReleased函數,增加如下代碼:
       // Left mouse button up
       if (id == OIS::MB_Left)
       {
           mLMouseDown = false;
       } // if

       // Right mouse button up
       else if (id == OIS::MB_Right)
       {
           CEGUI::MouseCursor::getSingleton().show();
           mRMouseDown = false;
       } // else if
現在,我們有了全部準備好的代碼,我們想要在按住鼠標右鍵移動鼠標時改變視圖。我們要做的就是,讀取他自上次調用方法後移動的距離。這可以用與基礎教程5中旋轉攝像機鏡頭一樣的方法實現。找到TutorialFrameListener::mouseMoved函數,就在返回狀態前,增加如下代碼:

       // If we are dragging the left mouse button.
       if (mLMouseDown)
       {
       } // if

       // If we are dragging the right mouse button.
       else if (mRMouseDown)
       {
           mCamera->yaw(Degree(-arg.state.X.rel * mRotateSpeed));
           mCamera->pitch(Degree(-arg.state.Y.rel * mRotateSpeed));
       } // else if
現在如果你編譯並運行這些代碼,你將能夠通過按住鼠標右鍵控制攝像機往哪裏看。
地形碰撞檢測
我們現在要實現它,以便當我們向着地面移動時,能夠不穿過地面。因爲BaseFrameListener已經處理了攝像機移動,所以我們就不用碰那些代碼了。替代地,在BaseFrameListener移動了攝像機後,我們要確保攝像機在地面以上10個單位處。如果它不在,我們要把它移到那兒。請跟緊這段代碼。我們將在本教程結束前使用RaySceneQuery做幾件別的事,而且在這段結束後,我不會再做如此詳細的介紹。

找到MouseQueryListener::frameStarted方法,移除該方法的全部代碼。我們首先要做的事是調用ExampleFrameListener::frameStarted方法。如果它返回false,則我們也會返回false。

        // Process the base frame listener code.  Since we are going to be
        // manipulating the translate vector, we need this to happen first.
        if (!ExampleFrameListener::frameStarted(evt))
            return false;
我們在frameStarted函數的最開始處做這些,是因爲ExampleFrameListener的frameStarted成員函數移動攝像機,並且在此發生後我們需要在函數中安排我們的剩餘行動。我們的目標及時找到攝像機的當前位置,並沿着它向地面發射一條射線。這被稱爲射線場景查詢,它會告訴我們我們下面的地面的高度。得到了攝像機的當前位置後,我們需要創建一條射線。這條射線有一個起點(射線開始的地方),和一個方向。在本教程的情況下,我們的方向是Y軸負向,因爲我們指定射線一直向下。一旦我們創建了射線,我們就告訴RaySceneQuery對象使用它。

       // Setup the scene query
       Vector3 camPos = mCamera->getPosition();
       Ray cameraRay(Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y);
       mRaySceneQuery->setRay(cameraRay);
注意,我們已經使用了5000.0f高度代替了攝像機的實際位置。如果我們使用攝像機的Y座標代替這個高度,如果攝像機在地面以下,我們會錯過整個地面。現在我們需要執行查詢,得到結果。查詢結果是std::iterator類型的。

        // Perform the scene query
        RaySceneQueryResult &result = mRaySceneQuery->execute();
        RaySceneQueryResult::iterator itr = result.begin();
在本教程中的這個地形條件下,查詢結果基本上是一個worldFragment的列表和一個可移動物體(稍後的教程會介紹到)的列表。如果你對STL迭代器不太熟悉,只要知道調用begin方法獲得迭代器的第一個元素。如果result.begin()
==
result.end(),那麼無返回結果。在下一個演示程序裏,我們將處理SceneQuery的多個返回值。目前,我們只要揮揮手,在其間移動。下面的這行代碼保證了至少返回一個查詢結果(itr
!= result.end()),那個結果是地面(itr->worldFragment)。
        // Get the results, set the camera height
        if (itr != result.end() && itr->worldFragment)
        {
worldFragment結構包含有在變量singleIntersection(一個Vector3)中射線擊中地面的位置。我們要得到地面的高度,依靠將這個向量的Y值賦值給一個本地變量。一旦我們有了高度,我們就要檢查攝像機是否低於這一高度,如果低於這一高度,那麼我們要將攝像機向上移動至地面高度。注意,我們實際將攝像機多移動了10個單位。這樣保證我們不能由於太靠近地面而看穿地面。

            Real terrainHeight = itr->worldFragment->singleIntersection.y;
            if ((terrainHeight + 10.0f) > camPos.y)
                mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
        }

        return true;
最後,我們返回true,繼續渲染。此時,你應該編譯測試你的程序了。
地形選擇
在這部分中,每次點擊鼠標左鍵,我們將向屏幕上創建和添加對象。每次你點擊、按住鼠標左鍵,就會創建一個對象並跟隨你的光標。你可以移動對象,直到你鬆開鼠標左鍵,同時對象也鎖定在那一點上。要做到這些,我們需要改變mousePressed函數。在MouseQueryLlistener::mousePressed函數中,找到如下代碼。我們將要在這個if語句中增加一些代碼。

       // Left mouse button down
       if (id == OIS::MB_Left)
       {
           mLMouseDown = true;
       } // if
第一段代碼看起來會很熟悉。我們會創建一條射線以供mRaySceneQuery對象使用,設置射線。Ogre給我們提供了Camera::getCameraToViewpointRay;一個將屏幕上的點擊(X和Y座標)轉換成一條可供RaySceneQuery對象使用的射線的好用函數。

           // Left mouse button down
           if (id == OIS::MB_Left)
           {
               // Setup the ray scene query, use CEGUI's mouse position
               CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
               Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
               mRaySceneQuery->setRay(mouseRay);
接下來,我們將執行查詢,並確保它返回一個結果。
               // Execute query
               RaySceneQueryResult &result = mRaySceneQuery->execute();
               RaySceneQueryResult::iterator itr = result.begin( );

               // Get results, create a node/entity on the position
               if (itr != result.end() && itr->worldFragment)
               {
既然我們有了worldFragment(也就是點擊的位置),我們就要創建對象並把它放到位。我們的第一個難題是,Ogre中每個實體和場景節點都需要一個唯一的名字。要完成這一點,我們要給每個實體命名爲“Robot1”,“Robot2”,“Robot3”……同樣將每個場景節點命名爲“Robot1Node”,“Robot2Node”,“Robot3Node”……等等。首先,我們創建名字(更多關於sprintf的信息,請參考C語言)。

               char name[16];
               sprintf( name, "Robot%d", mCount++ );
接下來,我們創建實體和場景節點。注意,我們使用itr->worldFragment->singleIntersection作爲我們的機器人的默認位置。由於地形太小所以我們也把他縮小爲原來的十分之一。注意我們要將這個新建的對象賦值給成員變量mCurrentObject。我們將在下一段要用到它。

                   Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
                   mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", itr->worldFragment->singleIntersection);
                   mCurrentObject->attachObject(ent);
                   mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
               } // if

               mLMouseDown = true;
           } // if
現在編譯運行程序。你可以在場景中點擊地形上任意地點放置機器人。我們幾乎完全控制了我們的程序,但是在結束前,我們需要實現對象拖拽。我們要在這個if語句段中添加代碼:

       // If we are dragging the left mouse button.
 if (mLMouseDown)
 {
 } // if
接下來的代碼段現在應該是不言而喻的。我們創建了一條基於鼠標當前位置的射線,然後我們執行了射線場景查詢且將對象移動到新位置。注意我們不必檢查mCurrentObject看看它是不是有效的,因爲如果mCurrentObject未被mousePressed設置,那麼mLMouseDown不會是true。

       if (mLMouseDown)
       {
           CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
           Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));
           mRaySceneQuery->setRay(mouseRay);

           RaySceneQueryResult &result = mRaySceneQuery->execute();
           RaySceneQueryResult::iterator itr = result.begin();

           if (itr != result.end() && itr->worldFragment)
               mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
       } // if
編譯運行程序。現在全部都完成了。點擊幾次後,你得到的結果應該看起來如下圖所示。 [image:Intermediate_Tutorial_2.jpg]
進階練習
簡單練習
    要阻止攝像機鏡頭看穿地形,我們選擇地形上10個單位。這一選擇是任意的。我們可以改進這一數值使之更接近地表而不穿過嗎?如果可以,設定此變量爲靜態類成員並給它賦值。

    有時我們確實想要穿越地形,特別是在場景編輯器中。創建一個標記控制碰撞檢測開關,並綁定到一個鍵盤上的按鍵上。確保碰撞檢測被關閉時,你不會在frameStarted中進行SceneQuery場景查詢。

中級練習
    當前我們每幀都要做場景查詢,無論攝像機是否實際移動過。修補這個問題,如果攝像機移動了,只做一次場景查詢。(提示:找到ExampleFrameListener中的移動向量,調用函數後,測試它是否爲Vector3::ZERO。)

高級練習
    注意到,每次我們執行一個場景查詢調用時,有許多代碼副本。將所有場景查詢相關功能打包到一個受保護的函數中。確保處理地形一點不交叉的情況。
進階練習
    在這個教程中,我們使用了RaySceneQueries來放置地形上的對象。我們也許可以用它做些別的事情。拿來中級教程1的代碼,完成困難問題1和專家問題1。然後將那個代碼融合到這個代碼中,使機器人行走在地面上,而不是虛空中。

    增加代碼,使每次你點擊場景中的一點時,機器人移動到那個位置。

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