Ogre中级教程(四): 成批选择和基本手动对象

来源:互联网 发布:基因对比软件 编辑:程序博客网 时间:2024/05/29 03:17

  中级教程四
    成批选择和基本手动对象 目录
          [隐藏]
            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查询(比如射线场景查询),然后再用物理引擎逐个检测查询返回的结果,检查网格的几何形状来看看是否真的击中了,还是仅仅非常的接近。

原创粉丝点击