中级教程2 射线场景查询及基本鼠标运用 (第1部分,共2部分)

引言

在本教程中我们将创建一个初级的场景编辑器。在此过程中,我们将介绍:

1,如何使用 RaySceneQueries 让摄像机不下沉并穿过地形块
2,如何使用 MouseListener和MouseMotionListener接口
3,使用鼠标在地形中选中一点(包含x和y坐标)

Prerequisites(略)

Getting Started(略)

创建场景

转到ITutorial02::createScene方法,相信大家对下列代码都已经很熟悉了。如有任何不明白的地方,请在继续学习之前阅读Ogre API参考。把以下代码添加至createScene函数中:

        // Set ambient light
        mSceneMgr->setAmbientLight(Ogre::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(Ogre::Degree(-30));
        mCamera->yaw(Ogre::Degree(-45));        

现在我们完成了基本的世界几何体的创建。我们通过调用一些CEGUI函数来激活光标。但是开始之前必须先启动CEGUI。非常简单 - bootstrapSystem()为我们做了所有的工作。注意这也将使CEGUI使用Ogre的资源管理系统。

// CEGUI setup
        mGUIRenderer = &CEGUI::OgreRenderer::bootstrapSystem();

真正显示光标的代码:

// Mouse
        CEGUI::SchemeManager::getSingleton().create((CEGUI::utf8*)"TaharezLook.scheme");
        CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");

如果编译并运行当前代码,你将会在屏幕中间看到一个光标,但是还不能移动它。

很可能你还需要把CEGUI相关的资源传递给Ogre资源管理器。如果你遇到运行时异常,尝试添加下列代码至resources.cfg:

[CEGUI]
 FileSystem=/usr/share/CEGUI/schemes
 FileSystem=/usr/share/CEGUI/fonts
 FileSystem=/usr/share/CEGUI/imagesets
 FileSystem=/usr/share/CEGUI/layouts
 FileSystem=/usr/share/CEGUI/looknfeel
 FileSystem=/usr/share/CEGUI/lua_scripts
 FileSystem=/usr/share/CEGUI/schemes
 FileSystem=/usr/share/CEGUI/xml_schemas

FrameListener介绍

以下是本应用程序需要完成的所有事情。FrameListener 是所有代码中较复杂的一部份。所以我将花一些时间概括我们要尝试完成什么工作,并让你在着手实现它之前心中有数。

首先,我们想让鼠标右键与“鼠标环视”模式绑定,不能使用鼠标四处观察是一件很烦人的事情,所以首先我们得让鼠标的控制权回到应用程序中(虽然只在按住右键时)。注意:得益于 OgreBites命名空间里的sdkCameraMan类,教程框架已经控制了摄像机,但出于学习的目的我们将重新实现对摄像机的控制。

第二,我们想让摄像机不至于下沉并穿透地形,这将使它以更接近于我们期望的程序的工作方式。

第三,我们想当点击左键的时候在场景中随处添加实体。

最后,我们想四处拖动实体,即点击并按住左键,我们想看到实体并移动至我们想放置它的地方。松开按键后将在该处真正固定它。

为完成这些我们需要使用一些受保护的变量(这已经在类中添加了):


Ogre::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
     Ogre::SceneNode *mCurrentObject;         // The newly created object
     CEGUI::Renderer *mGUIRenderer;     // cegui renderer
     float mRotateSpeed;


mRaySceneQuery 变量存储了一份RaySceneQuery的拷贝。我们将用它来查找地形上的坐标。mLMouseDown和mRMouseDown变量将跟踪我们是否按下了鼠标按键(即mLMouseDown为true则用户按下了鼠标左键,否则为false).mCount统计屏幕上的实体数目。mCount 保存一个我们最近健建的场景节点的指针(我们将用它来四处"拖放"实体)。最后mGUIRenderer 保存CEGUI 渲染器的指针,我们将用它来更新CEGUI 。

注意许多函数和Mouse listeners关联,我们将重写它来提供对摄像机的控制。


创建FrameListener

转到createFrameListener 方法并添加下列初始化代码于BaseApplication::createFrameListener()的调用之后。注意我们降低了旋转速度因为地形非常的小

// Setup default variables
         mCount = 0;
         mCurrentObject = NULL;
         mLMouseDown = false;
         mRMouseDown = false;
 
         // Reduce rotate speed
         mRotateSpeed =.1;

最后我们要创建RaySceneQuery 对象,通过调用SceneManager来实现:


// Create RaySceneQuery
         mRaySceneQuery = mSceneMgr->createRayQuery(Ogre::Ray());

这些是createFrameListener()全部需要的。但是如果我们创建了一个RaySceneQuery,稍后我们也需要销毁它。转到ITutorial02析构函数(ITutorial02)并添加以下各行

// We created the query, and we are also responsible for deleting it.
         mSceneMgr->destroyQuery(mRaySceneQuery);

在进入下一节之前确保你能顺利编译。

我们要绑定鼠标环视模式至鼠标右键,为完成这个任务我们需:

当鼠标移动时更新CEGUI(让光标也能跟着移动)
当鼠标右键被按住时设置mRMouseButton 为true
当释放鼠标右键时设置mRMouseButton 为false
当"拖动"鼠标时(即当按住一个按键并移动鼠标)转换视图模式
当鼠标正被拖动时隐藏光标

查找ITutorial02::mouseMoved方法,我们将添加代码实现每一次鼠标移动时移动光标。添加代码到函数中:

// Update CEGUI with the mouse motion
        CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);

查找ITutorial02::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 。转到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用同样的方法旋转摄像机。转到ITutorial::mouseMoved并在return语句之前添加下列代码:


// If we are dragging the left mouse button.
        if (mLMouseDown)
        {
        } // if
 
        // If we are dragging the right mouse button.
        else if (mRMouseDown)
        {
            mCamera->yaw(Ogre::Degree(-arg.state.X.rel * mRotateSpeed));
            mCamera->pitch(Ogre::Degree(-arg.state.Y.rel * mRotateSpeed));
        } // else if

 
如果你现在编译并运行,你将能通过按住鼠标右键来控制摄像机转到哪里。

地形碰撞检测

接下来我们要实现的是,当我们朝着地形块移动而不会穿过它。因为BaseApplication::createFrameListener()方法已经处理了摄像机的移动,我们就不用再去碰那些代码了。反之,在BaseApplication::createFrameListener()移动摄像机之后我们要保证摄像机处于离地形块10个单位之上。如果不是这样,我们要移动它至那里。请紧跟这段代码,这个教程快结束时我们将用RaySceneQuery来做一些别的事情,但是完成这一节前我不会讲述更多的细节。
转到ITutorial02::frameRenderingQueued()方法并删除它的内容。我们要做的第一件事是调用BaseApplication::frameRenderingQueued方法来完成它的所有默认功能。如果它返回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 (!BaseApplication::frameRenderingQueued(evt))
             return false;

我们在frameRenderingQueued的最前面这么做是因为BaseApplication::frameRenderingQueued 方法用from OgreBites处理了TrayManager窗口的更新(FPS窗口和Ogre logo)。所以在这个函数中处理完这些之后我们要做剩下的其它事情。我们的目标是找到摄像机的当前位置,并朝着地形块径直向下。调用RaySceneQuery它将告诉我们下方地形块的高度。获取摄像机的当前位置之后,我们需要创建一条射线,一条射线有一个原点(射线开始的地方),还有一个方向,目前情况我们的方向是NEGATIVE_UNIT_Y,因为我们让射线垂直向下射出。一旦我们创建好了射线,就可以通知RaySceneQuery 来使用它:


// Setup the scene query
        Ogre::Vector3 camPos = mCamera->getPosition();
        Ogre::Ray cameraRay(Ogre::Vector3(camPos.x, 5000.0f, camPos.z), Ogre::Vector3::NEGATIVE_UNIT_Y);
        mRaySceneQuery->setRay(cameraRay);

注意我们用 5000.0f的高度来代替摄像头的实际位置。如果我们用摄像机的Y分量代替这个高度,当我们处于地形块下方时将漏掉整个地形块。现在我们要执行查询并得到结果。查询结果以std::iterator的形式返咽,我将简单来描述它。

// Perform the scene query
         Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
         Ogre::RaySceneQueryResult::iterator itr = result.begin();

查询结果是一个简单的worldFragments(当前情况是地形块) 列表和一个movables 列表(我们将在后续的教程介绍movables )。如果你不熟悉STL 迭代器。则只需知道用begin 方法获取迭代器的第一个元素,如果result.begin() == result.end()则不再返咽任何结果。下一个教程我们将处理SceneQuerys返回多个值的情况。现在我们只需向它打个招呼并移动它。下列几行代码确保查询至少返回一个结果(itr != result.end()),此结果是一个地形块(itr->worldFragment)。

// Get the results, set the camera height
         if (itr != result.end() && itr->worldFragment)
         {

worldFragment 结构包含一个表示射线与地形相交的 singleIntersection变量(Vector3类型)。我们要通过把这个向量的y值赋值给一个局部变量来获取地形块的高度。一旦我们得到了高度,就能知道摄像机是否在此高度下面。如果是这种情况我们要移动摄像机至这个高度。注意实际上我向上移动摄像机10个单位。这样做确保当我们过于接近地形块时不会穿过它。

Ogre::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函数当你按住鼠标左键时做一些不同的事情。在ITutorial02::mousePressed 函数找到下列代码,我们将在if分支里添加后面的代码。

// Left mouse button down
        if (id == OIS::MB_Left)
        {
            mLMouseDown = true;
        } // if

 


第一部份代码看起来可能很熟悉,我们创建一条射线用于mRaySceneQuery 对象。并设置了射线。Ogre为我们提供了Camera::getCameraToViewportRay;一个不错的函数来转换点击屏幕的点(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();
                Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
                mRaySceneQuery->setRay(mouseRay);

接下来我们将执行查询并确保它返回一个结果集。

// Execute query
                Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
                Ogre::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"... 等等。首先构造名字(查阅关于C的书籍获取更多关于sprintf的资料):


char name[16];
                sprintf( name, "Robot%d", mCount++ );


接下来我们创建实体和场景节点。注意我们用itr->worldFragment->singleIntersection作为机器人的默认位置,并缩放为1/10的尺寸,因为地形相当的小。注意我们把新建的对象赋值给成员变量mCurrentObject。我们将在下一节用到它。

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


下面一段代码是自解释的,我们创建了一条基于鼠标当前位置的射线,然后执行RaySceneQuery 并并移动对象至一个新的位置。注意我们没有检查mCurrentObject 对象是否是最新创建的对象,因为mLMouseDown 和mCurrentObject 在mousePressed()函数里同时被设置:mLMouseDown设为true,mCurrentObject 设置为最近一次创建的场景节点。

if (mLMouseDown)
        {
            CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
            Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));
            mRaySceneQuery->setRay(mouseRay);
 
            Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
            Ogre::RaySceneQueryResult::iterator itr = result.begin();
 
            if (itr != result.end() && itr->worldFragment)
                mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
        } // if

 
编译并运行程序,我们终于完成了!