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。然后将那个代码融合到这个代码中,使机器人行走在地面上,而不是虚空中。

    增加代码,使每次你点击场景中的一点时,机器人移动到那个位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值