Ogre中級教程(四): 成批選擇和基本手動對象

  中級教程四
    成批選擇和基本手動對象 目錄
          [隱藏]
            1 介紹
            2 先決條件
            3 ManualObject對象
              3.1 3D對象的快速入門
              3.2 介紹
              3.3 代碼
            4 體積選取
              4.1 設置
              4.2 鼠標處理
              4.3 PlaneBoundedVolumeListSceneQuery
            5 最後關於包圍盒的注意事項


    介紹
    在這一課裏,我們將涉及如何進行成批選取。意思就是,當你在屏幕上點擊並且拖拽鼠標時,一個白色矩形會追蹤你正在選擇的區域。當鼠標移動時,所有在選擇區域裏的物體都會被高亮。爲了實現它,我們將學習兩種對象:ManualObject(創建矩形)和PlaneBoundedVolumeListSceneQuery。注意,當我們涉及ManualObject的基本用法時,只是對它的簡單介紹,而不是教你如何完全用它創建3D物體。我們只會涉及我們所需要的。

    你能在這裏找到本課的代碼。當你學習本課時,你應該逐個地往你的工程裏添加代碼,編譯後觀察相應的結果。
    先決條件
    用你喜歡的IDE創建一個cpp,並添加以下代碼:
   #include <CEGUI/CEGUI.h>
   #include <OgreCEGUIRenderer.h>
  
   #include "ExampleApplication.h"
  
   class SelectionRectangle : public ManualObject
   {
   public:
       SelectionRectangle(const String &name)
           : ManualObject(name)
       {
       }
  
       /**
       * Sets the corners of the SelectionRectangle.  Every parameter should be in the
       * range [0, 1] representing a percentage of the screen the SelectionRectangle
       * should take up.
       */
       void setCorners(float left, float top, float right, float bottom)
       {
       }
  
       void setCorners(const Vector2 &topLeft, const Vector2 &bottomRight)
       {
           setCorners(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
       }
   };
  
   class DemoListener : public ExampleFrameListener, public OIS::MouseListener
   {
   public:
       DemoListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager)
           : ExampleFrameListener(win, cam, false, true), mSceneMgr(sceneManager), mSelecting(false)
       {
           mMouse->setEventCallback(this);
       } // DemoListener
  
       ~DemoListener()
       {
       }
  
       /* MouseListener callbacks. */
       bool mouseMoved(const OIS::MouseEvent &arg)
       {
           CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
           return true;
       }
  
       bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
       {
           return true;
       }
  
       bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
       {
           return true;
       }
  
       void performSelection(const Vector2 &first, const Vector2 &second)
       {
       }
  
      void deselectObjects()
      {
          std::list<MovableObject*>::iterator itr;
          for (itr = mSelected.begin(); itr != mSelected.end(); ++itr)
              (*itr)->getParentSceneNode()->showBoundingBox(false);
      }
  
      void selectObject(MovableObject *obj)
      {
          obj->getParentSceneNode()->showBoundingBox(true);
          mSelected.push_back(obj);
      }
  
   private:
       Vector2 mStart, mStop;
       SceneManager *mSceneMgr;
       PlaneBoundedVolumeListSceneQuery *mVolQuery;
       std::list<MovableObject*> mSelected;
       SelectionRectangle *mRect;
       bool mSelecting;
  
  
       static void swap(float &x, float &y)
       {
           float tmp = x;
           x = y;
           y = tmp;
       }
   };
  
   class DemoApplication : public ExampleApplication
   {
   public:
       DemoApplication()
           : mRenderer(0), mSystem(0)
       {
       }
  
       ~DemoApplication()
       {
           if (mSystem)
               delete mSystem;
  
           if (mRenderer)
               delete mRenderer;
       }
  
   protected:
       CEGUI::OgreCEGUIRenderer *mRenderer;
       CEGUI::System *mSystem;
  
       void createScene(void)
       {
           mRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
           mSystem = new CEGUI::System(mRenderer);
  
           CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
           CEGUI::MouseCursor::getSingleton().setImage((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow");
  
           mCamera->setPosition(-60, 100, -60);
           mCamera->lookAt(60, 0, 60);
  
           mSceneMgr->setAmbientLight(ColourValue::White);
           for (int i = 0; i < 10; ++i)
               for (int j = 0; j < 10; ++j)
               {
                   Entity *ent = mSceneMgr->createEntity("Robot" + StringConverter::toString(i + j * 10), "robot.mesh");
                   SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(i * 15, 0, j * 15));
                   node->attachObject(ent);
                   node->setScale(0.1, 0.1, 0.1);
               }
       }
  
       void createFrameListener(void)
       {
           mFrameListener = new DemoListener(mWindow, mCamera, mSceneMgr);
           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
       DemoApplication app;
  
       try {
           app.go();
       } catch(Exception& e) {
   #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
           MessageBoxA(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;
   }
繼續之前,請確保這段代碼能夠編譯。當你運行它時,你應該能夠移動鼠標指針,但程序目前不能做其它的。按ESC退出。
    ManualObject對象
    3D對象的快速入門
    在我們開始進入製作網格(mesh)之前,有必要講一下mesh是什麼,以及它的構成。儘管非常簡化,mesh大致由兩部分組成:頂點緩存(Vertex
    buffer)和索引緩存(Index buffer)。
    頂點緩存在3D空間裏定義點集。頂點緩存裏的每一個元素由若干你能設置的屬性來定義。唯一一個你必須設置的屬性就是頂點的座標。除了這個,你還有設置其它可選的屬性,比如頂點顏色、紋理座標等。你實際需要的屬性取決於mesh的用途。

    索引緩存通過從頂點緩存選取頂點,以“把點連起來”。索引緩存裏每三個頂點定義了一個由GPU繪製的三角形。你在索引緩存裏選取頂點的順序,告訴了顯卡這個三角形的朝向。逆時針繪製的三角形是朝向你的,順時針繪製的三角形是背向你的。一般情況下只有三角形的正面才被繪製,所以確保你的三角形被正確載入是很重要的。

    雖然所有的mesh都有頂點緩存,但不一定都有索引緩存。比如,我們只要創建一個空的三角形(而不是實心的),我們創建的mesh就不需要索引緩存。最後要注意,頂點緩存和索引緩存通常保存在顯卡自己的內存裏,所以你的軟件只要發送一些離散的命令,告訴它使用預定義的緩存來一口氣渲染整個3D網格。

    介紹
    在Ogre裏有兩種方法來創建你自己的網格。第一種是繼承SimpleRenderable,並直接提供給它頂點和索引緩存。這是最直接的創建方式,但也是最不直觀的。爲了使事情更簡單,Ogre提供一個更棒的接口叫做ManualObject,它能讓你用一些簡單的函數來定義一個網格,而不用往緩存裏寫原始數據。你僅僅調用"position"和"colour"函數,而不用往緩存裏丟位置、顏色等數據。

    在本課裏,當我們拖動鼠標去選擇物體時,我們要創建並顯示一個白色矩形。在Ogre裏並沒有真正的用來顯示2D矩形的類。我們必須自己找一個解決辦法。我們可以使用一個Overlay並縮放它,以顯示一個矩形選擇框,但這樣做帶來的問題是,選擇框的圖像可能會隨着拉昇而難看變形。取而代之,我們將生成一個非常簡單的2D網格,來作爲我們的選擇矩形。

    代碼
    當我們創建選擇矩形的時候,我們想讓它以2D的形式呈現。我們還想保證當在屏幕裏發生重疊時,它顯示在所有其它物體之上。實現這個非常簡單。找到SelectionRectangle的構造器,並添加如下代碼:

      setRenderQueueGroup(RENDER_QUEUE_OVERLAY);
      setUseIdentityProjection(true);
      setUseIdentityView(true);
第一個函數把這個物體的渲染隊列設置成重疊隊列(Overlay queue)。接下來的兩個函數把投影矩陣(projection
    matrix)和視圖矩陣(view
    matrix)設置成identity。投影矩陣和視圖矩陣被很多渲染系統所使用(比如OpenGL和DirectX),以定義物體在世界中的座標。既然Ogre爲我們做了抽象,我們不必深究這些矩陣是什麼樣的或他們幹了些什麼。然而,你需要知道如果你把投影矩陣和視圖矩陣設置成identity,就像剛纔那樣,我們基本上就是在繪製2D物體。這樣定義之後,座標系統發生了一些改變。我們不再需要Z軸(若你被要求提供Z軸,設置成-1)。取而代之,我們有一個新的座標系統,X和Y的範圍分別都是-1到1。最後,我們將把這個物體的查詢標記設置成0,如下:

      setQueryFlags(0);
現在,對象設置好了,我們來實際構建這個矩形。我們開始之前還有一個小小阻礙,我們將使用鼠標座標來調用這個函數。也就是,傳給我們一個0到1之間的數字爲每個座標軸,然而我們需要把這個數字轉換成範圍[-1,1]的。還有更復雜的,y座標要反向。在CEGUI裏,鼠標指針在屏幕頂部時,值爲+1,在底部時,值爲-1。感謝上帝,用一個快速轉換就能解決這個問題。找到setCorners函數並添加如下代碼:

      left = left * 2 - 1;
      right = right * 2 - 1;
      top = 1 - top * 2;
      bottom = 1 - bottom * 2;
現在轉換成新座標系統了。下面,我們來真正創建這個對象。爲此,我們首先調用begin方法。它需要兩個參數,物體的這一部分所使用的材質,以及它所使用的渲染操作。因爲我們不使用紋理,把這個材質置空。第二個參數是渲染操作(RenderOperation)。我們可以使用點、線、三角形來渲染這個網格。如果我們要渲染一個實心的網格,可以用三角形。但我們只需要一個空的矩形,所以我們使用線條(line
    strip)。從你定義的前一個頂點到現在的頂點,線條繪製一條直線。所以爲了創建我們的矩形,需要定義5個點(第一個和最後一個是相同的,這樣才能連接成整個矩形):

      clear();
      begin("", RenderOperation::OT_LINE_STRIP);
          position(left, top, -1);
          position(right, top, -1);
          position(right, bottom, -1);
          position(left, bottom, -1);
          position(left, top, -1);
      end();
注意,因爲我們將在後面多次調用它,我們在最前面加入clear函數,在重新繪製矩形之前移除上次的矩形。當定義一個手動物體時,你可能要多次調用begin/end來創建多個子網格(它們可能有不同的材質/渲染操作)。注意,我們把Z參數設成-1,因爲我們只定義一個2D對象而不必使用Z軸。把它設置爲-1,可以保證當渲染時我們不處在攝像機之上或之後。

    最後我們還要爲這個物體設置包圍盒。許多場景管理器會把遠離屏幕的物體剔除掉。儘管我們創建的差不多是一個2D物體,但Ogre仍是一個3D引擎,它把2D物體當作在3D空間裏對待。這意味着,如果我們創建這個物體,並把它綁在場景節點上(正如我們下面要做的那樣),當我們遠一點觀看時會消失。爲了修正這個問題,我們將把這個物體的包圍盒設置成無限大,這樣攝像機就永遠在它裏面:

      AxisAlignedBox box;
      box.setInfinite();
      setBoundingBox(box);
請注意,我們在調用clear()之後添加這段代碼的。當每你調用ManualObject::clear,包圍盒都會被重置,所以當你創建經常清空的ManualObject時要格外小心,每當你重新創建它的時候,也要重新設置包圍盒。

    好了,我們要爲SelectionRectangle類所做的全部就是這些。繼續下去之前請保證能編譯你的代碼,但目前還沒有爲程序添加功能。
    體積選取
    設置
    在我們進入選取操作的代碼之前,先來設置一些東西。首先,我們要創建一個SelectionRectangle類的實例,然後讓SceneManager來爲我們創建一個體積查詢:

      mRect = new SelectionRectangle("Selection SelectionRectangle");
      mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mRect);

      mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(PlaneBoundedVolumeList());
再來,我們要保證結束時幀監聽器做一些清理。把下面的代碼加到~DemoListener:
      mSceneMgr->destroyQuery(mVolQuery);
      delete mRect;
注意,我們讓SceneManager爲我們進行清理,而不是直接刪除。
    鼠標處理
    我們要展示的特性是體積選取。這意味着當用戶點擊鼠標並拖拽時,屏幕裏將繪製一個矩形。隨着鼠標的移動,所有在矩形內的物體將被選取。首先,我們要處理鼠標的點擊事件。我們要保存鼠標的起始位置,並且把SelectionRectangle設置成可見的。找到mousePressed函數並添加如下代碼:

      if (id == OIS::MB_Left)
      {
          CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
          mStart.x = mouse->getPosition().d_x / (float)arg.state.width;
          mStart.y = mouse->getPosition().d_y / (float)arg.state.height;
          mStop = mStart;

          mSelecting = true;
          mRect->clear();
          mRect->setVisible(true);
      }
注意,我們使用的是CEGUI::MouseCursor的x和y座標,而不是OIS的鼠標座標。這是因爲有時OIS反映的座標與CEGUI實際顯示的不一樣。爲了保證我們與用戶所看到的相一致,我們使用CEGUI的鼠標座標。

    接下來我們要做的是,當用戶釋放鼠標按鈕時,停止顯示選擇框,並執行這個選取查詢。在mouseReleased里加入以下代碼:
      if (id == OIS::MB_Left)
      {
          performSelection(mStart, mStop);
          mSelecting = false;
          mRect->setVisible(false);
      }
最後,每當鼠標移動時,我們需要更新矩形的座標:
      if (mSelecting)
      {
          CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
          mStop.x = mouse->getPosition().d_x / (float)arg.state.width;
          mStop.y = mouse->getPosition().d_y / (float)arg.state.height;

          mRect->setCorners(mStart, mStop);
      }
每當鼠標移動時,我們都調整mStop向量,這樣我們就能輕鬆地使用setCorners成員函數了。編譯並運行你的程序,現在你能用鼠標繪製一個矩形了。
    PlaneBoundedVolumeListSceneQuery
    現在,我們可以讓SelectionRectangle正確地渲染了,我們還想執行一個體積選擇。找到performSelection函數,並添加如下代碼:
      float left = first.x, right = second.x,
          top = first.y, bottom = second.y;

      if (left > right)
          swap(left, right);

      if (top > bottom)
          swap(top, bottom);
在這段代碼裏,我們分別爲left、right、top、botton變量賦予向量參數。if語句保證了我們實際的left和top值最小。(如果這個矩形是“反向”畫出來的,意味着從右下角到左上角,我們就要進行這種交換。)

    接下來,我們要檢查並瞭解矩形區域的實際小大。如果這個矩形太小了,我們的創建平面包圍體積的方法就會失敗,並且導致選取太多或太少的物體。如果這個矩形小於屏幕的某個百分比,我們只將它返回而不執行這個選取。我隨意地選擇0.0001作爲取消查詢的臨界點,但在你的程序裏你應該自己決定它的值。還有,在真實的應用裏,你應該找到這個矩形的中心,並執行一個標準查詢,而不是什麼都不做:

      if ((right - left) * (bottom - top) < 0.0001)
          return;
現在,我們進入了這個函數的核心,我們要執行這個查詢本身。PlaneBoundedVolumeQueries使用平面來包圍一個區域,所以所有在區域裏的物體都被選取。我們將創建一個被五個平面包圍的區域,它是朝向裏面的。爲了創建這些平面,我們建立了4條射線,每一條都是矩形的一個角產生的。一旦我們有四條射線,

    For this example we will build an area enclosed by five planes which face
    inward. To create these planes out of our rectangle, we will create 4 rays,
    one for each corner of the rectangle. Once we have these four rays, we will
    grab points along the rays to create the planes:
      Ray topLeft = mCamera->getCameraToViewportRay(left, top);
      Ray topRight = mCamera->getCameraToViewportRay(right, top);
      Ray bottomLeft = mCamera->getCameraToViewportRay(left, bottom);
      Ray bottomRight = mCamera->getCameraToViewportRay(right, bottom);
現在我們來創建平面。注意,我們沿着射線走100個單位抓取一個點。這是隨便選擇的,我們也可以選擇2而不是100。在這裏唯一重要的是前平面,它在攝像機前面3個單位的位置。

      PlaneBoundedVolume vol;
      vol.planes.push_back(Plane(topLeft.getPoint(3), topRight.getPoint(3), bottomRight.getPoint(3)));         // 前平面
      vol.planes.push_back(Plane(topLeft.getOrigin(), topLeft.getPoint(100), topRight.getPoint(100)));         // 頂平面
      vol.planes.push_back(Plane(topLeft.getOrigin(), bottomLeft.getPoint(100), topLeft.getPoint(100)));       // 左平面
      vol.planes.push_back(Plane(topLeft.getOrigin(), bottomRight.getPoint(100), bottomLeft.getPoint(100)));   // 底平面
      vol.planes.push_back(Plane(topLeft.getOrigin(), topRight.getPoint(100), bottomRight.getPoint(100)));     // 右平面
這些平面定義了一個在攝像機前無限伸展的“開放盒子”。你可以把我們用鼠標繪製的矩形,想像成在鏡頭跟前,這個盒子的終點。好了,我們已經創建了平臺,我們還需要執行這個查詢:

      PlaneBoundedVolumeList volList;
      volList.push_back(vol);

      mVolQuery->setVolumes(volList);
      SceneQueryResult result = mVolQuery->execute();
最後我們需要處理查詢返回的結果。首先我們要取消所有先前選取的物體,然後選取所有查詢得到的物體。deselectObjects和selectObject函數已經爲你寫好了,因爲在前面的教程裏我們就已經介紹了這些函數:

      deselectObjects();
      SceneQueryResultMovableList::iterator itr;
      for (itr = result.movables.begin(); itr != result.movables.end(); ++itr)
          selectObject(*itr);
這就是全部我們要爲查詢所做的。注意,我們在體積查詢裏也使用查詢標記,雖然本課我們還沒有這麼做。想了解更多關於查詢標記,請參考上一課。
    翻譯並運行程序。你現在可以在場景裏選取物體了!
    最後關於包圍盒的注意事項
    也許你可能注意到了,在這一課裏以及前面兩課中,Ogre的選取依賴於物體的包圍盒而不是網格本身。這意味着RaySceneQuery和PlaneBoundedVolumeQuery總是承認這個查詢實際上接觸的東西。存在一些方法,可以進行基於像素的完美射線選取(比如在FPS遊戲裏,需要判斷一個射擊是否命中目標,你就需要這麼做)。而出於速度考慮,使用體積選取也能爲你提供非常精確的結果。不幸的是,這超出了本課的範圍。更多關於如何在純Ogre裏實現,請參考多面體級別的射線構建。

    如果你爲Ogre整合了物理引擎,比如OgreNewt,它們也會爲你提供一些方法。但你仍然不會白學了射線查詢和體積查詢。做一個基於網格的選取是非常費時的,而且如果你嘗試檢測所有在場景裏的東西,會大大影響你的幀率。事實上,進行一個鼠標選取最常用的方法是,首先執行一個Ogre查詢(比如射線場景查詢),然後再用物理引擎逐個檢測查詢返回的結果,檢查網格的幾何形狀來看看是否真的擊中了,還是僅僅非常的接近。

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