Ogre中级教程

中级教程一

来自 Ogre wiki

动画, 两点间移动, 和四元数基础

作者: Culver.

内容

目录

[隐藏]

介绍

这个教程里包括怎么样得到一个模型,并添加模型动画,最后让模型可以在两个预先定义的点之间走动。在此将讲述如何用基本的四元数方法保持模型移动的时候正面一直朝着我们指定的方向。你必须一点点的将代码加入到你的项目中,并在每次加入新代码后编译并察看demo运行的结果。

本课的最终代码在这里

前期准备

首先,这个指南假设你已经知道如何设置Ogre的项目环境以及如何正确编译项目。该例子同样使用STL 中的queue数据结构。那么预先了解如何使用queue是必要的,至少你需要知道什么是模版。如果你不熟悉STL,那么我像你推荐STL参考[ISBN 0596005563],它可以帮助你在将来花费更少的时间。

准备开始

首先,你需要为这个Demo创建一个新项目,在项目中添加一个名为"MoveDemo.cpp"的文件并加入如下代码:

#include "ExampleApplication.h"

#include <deque>
using namespace std;

class MoveDemoListener : public ExampleFrameListener
{
public:

    MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,
        Entity *ent, deque<Vector3> &walk)
        : ExampleFrameListener(win, cam, false, false), mNode(sn), mEntity(ent), mWalkList( walk )
    {
    } // MoveDemoListener

    /* This function is called to start the object moving to the next position
       in mWalkList.
    */
    bool nextLocation( )
    {
        return true;
    } // nextLocation( )

    bool frameStarted(const FrameEvent &evt)
    {
        return ExampleFrameListener::frameStarted(evt);
    }
protected:
    Real mDistance;                  // The distance the object has left to travel
    Vector3 mDirection;              // The direction the object is moving
    Vector3 mDestination;            // The destination the object is moving towards

    AnimationState *mAnimationState; // The current animation state of the object

    Entity *mEntity;                 // The Entity we are animating
    SceneNode *mNode;                // The SceneNode that the Entity is attached to
    std::deque<Vector3> mWalkList;   // The list of points we are walking to

    Real mWalkSpeed;                 // The speed at which the object is moving
};


class MoveDemoApplication : public ExampleApplication
{
protected:
public:
    MoveDemoApplication()
    {
    }

    ~MoveDemoApplication() 
    {
    }
protected:
    Entity *mEntity;                // The entity of the object we are animating
    SceneNode *mNode;               // The SceneNode of the object we are moving
    std::deque<Vector3> mWalkList;  // A deque containing the waypoints

    void createScene(void)
    {
    }

    void createFrameListener(void)
    {
        mFrameListener= new MoveDemoListener(mWindow, mCamera, mNode, mEntity, mWalkList);
        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
    MoveDemoApplication app;

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


    return 0;
}

在我们继续讲解之前,你可以编译这部分代码看下效果。

设置场景

在我们开始之前,需要注意的是已经在MoveDemoApplication中预先定义的三个变量。我们创建的entity实例保存在变量mEntity中,我们创建的node实例保存在mNode中,另外mWalkList包含了所有我们希望对象行走到的节点。

定位到MoveDemoApplication::createScene函数并且加入以下代码。首先,我们来设置环境光(ambient light)到最大,这样可以让我们看到我们放在场景中的所有对象。

       // Set the default lighting.
       mSceneMgr->setAmbientLight( ColourValue( 1.0f, 1.0f, 1.0f ) );

接下来我们来在屏幕上创建一个可以使用的机器人。要做到这点我们需要在创建SceneNode之前先为机器人创建一个entity使得我们可以对其进行旋转。

       // Create the entity
       mEntity = mSceneMgr->createEntity( "Robot", "robot.mesh" );

       // Create the scene node
       mNode = mSceneMgr->getRootSceneNode( )->
           createChildSceneNode( "RobotNode", Vector3( 0.0f, 0.0f, 25.0f ) );
       mNode->attachObject( mEntity );

以上这些都是非常基础的,所以我认为不需要再对以上的描述做任何解释。在接下来的代码片断,我们将开始告诉机器人那些地方是它需要到达的。这里需要你们了解一些STL的知识,deque对象是一个高效的双端对列。我们只需要使用它的几个简单的方法。push_front和push_back方法分别将对象放入队列的前端和后端,front和back方法分别返回当前队列前端和后端的元素(PS:注意,这里最好有判空的习惯,用if( empty() ) )pop_front和pop_back两个方法分别从队列两端移除对象。最后,empty方法返回该队列是否为空。下面这些代码添加了两个Vector到队列中,在后面我们移动robot的时候会用到它们。

       // Create the walking list
       mWalkList.push_back( Vector3( 550.0f,  0.0f,  50.0f  ) );
       mWalkList.push_back( Vector3(-100.0f,  0.0f, -200.0f ) );

接下来,我们在场景里放置一些物体,以标记这个机器人应该朝哪走去。这样使我们能看见机器人在场景里相对于其它物体进行移动。注意它们的位置的负Y部分,这些物体被放在机器人移动目的地的正下方,当它到达指定地点时,它就站在这些物体上面。

       // Create objects so we can see movement
       Entity *ent;
       SceneNode *node;

       ent = mSceneMgr->createEntity( "Knot1", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot1Node",
           Vector3(  0.0f, -10.0f,  25.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );

       ent = mSceneMgr->createEntity( "Knot2", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot2Node",
           Vector3( 550.0f, -10.0f,  50.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );

       ent = mSceneMgr->createEntity( "Knot3", "knot.mesh" );
       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot3Node",
           Vector3(-100.0f, -10.0f,-200.0f ) );
       node->attachObject( ent );
       node->setScale( 0.1f, 0.1f, 0.1f );

最后,我们要创建一个摄像机从适合的角度来观察它。我们来把摄像机移动到更多的位置。

       // Set the camera to look at our handywork
       mCamera->setPosition( 90.0f, 280.0f, 535.0f );
       mCamera->pitch( Degree(-30.0f) );
       mCamera->yaw( Degree(-15.0f) );

现在编译并运行代码。你应该能看到这个样子: [[1]]

在进入下一个部分之前,注意一下MoveDemoListener的构造器,它在MoveDemoApplication::createFrameListener方法里的第一行被调用。除了传入BaseFrameListener的标准参数,还有场景节点、实体、双端队列。

动画

现在我们来设置一些基本的动画。在Ogre里动画是非常简单的。要做的话,你需要从实体对象里获取AnimationState,设置它的选项,并激活它。这样就能使动画活动起来,但你还必须在每一帧后给它添加时间,才能让动画动起来。我们设置成每次移动一步。首先,找到MoveDemoListener的构造器,并添加以下代码:

      // Set idle animation
      mAnimationState = ent->getAnimationState("Idle");
      mAnimationState->setLoop(true);
      mAnimationState->setEnabled(true);

第二行从实体中获取到了AnimationState。第三行我们调用setLoop( true ),让动画不停地循环。而在一些动画里(比如死亡动画),我们可能要把这个设置为false。第四行才把这个动画真正激活。但等等...我们从哪里获取的“Idle”?这个魔术般的常量是怎么飞到这里来的?每个mesh都有它们自己定义的动画集。为了能够查看某个mesh的全部动画,你需要下载OgreMeshViewer才能看到。

现在,如果我们编译并运行这个demo,我们看见了...nothing! 这是因为我们还需要在每一帧里根据时间来更新这个动画的状态。找到MoveDemoListener::frameStarted方法,在方法的开头添加这一行:

       mAnimationState->addTime(evt.timeSinceLastFrame);

现在来编译并运行程序。你应该可以看了一个机器人正在原地踏步了。

移动角色

现在我们执行棘手的任务,开始让这个机器人从一点走到另一点。在我们开始之前,我想介绍一下保存在MoveDemoListener类里的成员变量。我们将使用4个变量来完成移动机器人的任务。首先,我们把机器人移动的方向保存到mDirection里面。我们再把当前机器人前往的目的地保存在mDestination里。然后在mDistance保存机器人离目的地的距离。最后,在mWalkSpeed里我们保存机器人的移动速度。

首先清空MoveDemoListener构造器,我们会用稍微不同的代码来替换。我们要做的第一件事是设置这个类的变量。我们将把行走速度设为每秒35个单位。有一个大问题要注意,我们故意把mDirection设成零向量,因为后面我们会用它来判断机器人是否正在行走。

       // Set default values for variables
       mWalkSpeed = 35.0f;
       mDirection = Vector3::ZERO;

好了,搞定了。我们要让机器人动起来。为了让机器人移动,我们只须告诉它改变动画。然而,我们只想要若存在另一个要移动到的地点,就让机器人开始移动。为了这个目的,我们调用nextLocation 函数。把代码加到MoveDemoListener::frameStarted方法的顶部,在调用AnimationState::addTime之前:

      if (mDirection == Vector3::ZERO) 
      {
          if (nextLocation()) 
          {
              // Set walking animation
              mAnimationState = mEntity->getAnimationState("Walk");
              mAnimationState->setLoop(true);
              mAnimationState->setEnabled(true);
          }
      }

如果你现在编译并运行,这个机器人将原地行走。这是由于机器人是以ZERO方向出发的,而我们的MoveDemoListener::nextLocation函数总是返回true。在后面的步骤中,我们将给MoveDemoListener::nextLocation函数添加更多的一点智能。

现在,我们准备要真正在场景里移动机器人了。为了这样做,我们需要在每一帧里让我移动一点点。找到MoveDemoListener::frameStarted方法,我们将在调用AnimationState::addTime之前,我们先前的if语句之后,添加以下代码。这段代码将处理当机器人实际移动的情况;mDirection != Vector3::ZERO。

       else
       {
           Real move = mWalkSpeed * evt.timeSinceLastFrame;
           mDistance -= move;

现在,我们要检测一下我们是否“走过”了目标地点。即,如果现在mDistance小于0,我们需要“跳”到这点上,并设置移动到下一个地点。注意,我们把mDirection设置成零向量。如果nextLocation方法不改变mDirection(即没有其它地方可去),我们就不再四处移动了。

           if (mDistance <= 0.0f)
           {
               mNode->setPosition(mDestination);
               mDirection = Vector3::ZERO;

现在我们移动到了这个点,我们需要设置运动到下一个点。只要我们知道有否需要移动到下一个地点,我们就能设置正确的动画;如果有其它地点要去,就行走。如果没有其它目的地,则停滞。

              // Set animation based on if the robot has another point to walk to. 
              if (! nextLocation())
              {
                  // Set Idle animation                     
                  mAnimationState = mEntity->getAnimationState("Idle");
                  mAnimationState->setLoop(true);
                  mAnimationState->setEnabled(true);
              } 
              else
              {
                  // Rotation Code will go here later
              }
          }

注意,如果queue里已经没有更多的地点要走的话,我们没有必要再次设置行走动画。既然机器人已经在行走了,没有必要再告诉他这么做。然而,如果机器人还要走向另一个地点,我们就要把它旋转以面对那个地点。现在,我们在else括号旁边留下注释占位符;记住这个地点,因为我们后面还要回来。

这里考虑的是当我们离目标地点很近的时候。现在我们需要处理一般情况,当我们正在到达而没有到达的时候。为此,我们在机器人的行走方向上对它进行平移,用move变量指定的值。通过添加以下代码来实现:

           else
           {
               mNode->translate(mDirection * move);
           } // else
       } // if

我们差不多做完了,除了还要设置运动需要的变量。如果我们正确地设置了运动变量,我们的机器人就会朝它该去的方向行走。看看MoveDemoListener::nextLocation方法,如果我们用完了所有的地点,它返回false。这是函数的第一行。(注意你要保留函数底部的return true语句)

       if (mWalkList.empty())
           return false;

现在我们来设置变量。首先我们从双端队列里取出一个向量。通过目标向量减去场景节点的当前向量,我们得取方向向量。然而我们仍有一个问题,还记得我们要在frameStarted方法里用mDirection乘以移动量吗?如果我们这么做,我们必须把方向向量转换成单位向量(即,它的长度等于一)。normalise函数为我们做了这些事,并返回向量的原始长度。唾手可得,我们需要设置到目的地的距离。

      mDestination = mWalkList.front();  // this gets the front of the deque
      mWalkList.pop_front();             // this removes the front of the deque
      mDirection = mDestination - mNode->getPosition();
      mDistance = mDirection.normalise();

编译并运行代码。搞定! 现在机器人行走到每一个地点,但它总是面朝着Vector3::UNIT_X方向(它的默认)。我们需要当它向地点移动时,改变它的朝向。

我们需要做的是获得机器人脸的方向,然后用旋转函数将它旋转到正确的位置。在我们上一次留下注释占位符的地方,插入如下代码。第一行获得了机器人脸的朝向。第二行建立了一个四元组,它表示从当前方向到目标方向的旋转。第三行才是真正旋转了这个机器人。

       Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
       Ogre::Quaternion quat = src.getRotationTo(mDirection);
       mNode->rotate(quat);

我们在基础教程4里已经对四元组进行过简单的介绍,但在这里才是我们对它的第一次使用。基本上说,四元组就是在三维空间里对旋转的表示。它们被用来跟踪物体是如何在空间里放置的,也可能被用来在Ogre里对物体进行旋转。我们在第一行里调用getOrientation方法,返回了一个表示机器人在空间里面向方向的四元组。因为Ogre不知道机器人的哪一面才是它的正面,所以我们必须用UNIT_X方向乘以这个朝向,以取得机器人当前的朝向。我们把这个方向保存在src变量里。在第二行,getRotationTo方法返回给我们一个四元组,它表示机器人从目前的朝向到我们想让它朝向方向的旋转。第三行,我们旋转节点,以让它面向一个新的方向。

我们创建的代码只剩下一个问题了。这里有一种特殊情况将会使SceneNode::rotate失败。如果我们正试图让机器人旋转180度,旋转代码会因为除以0的错误而崩掉。为了解决这个问题,我们需要测试我们是否执行180度的旋转。如果是,我们只要用yaw来将机器人旋转180度,而不是用rotate。为此,删除我们刚才放入的代码,并用这些代替:

      Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
      if ((1.0f + src.dotProduct(mDirection)) < 0.0001f) 
      {
          mNode->yaw(Degree(180));
      }
      else
      {
          Ogre::Quaternion quat = src.getRotationTo(mDirection);
          mNode->rotate(quat);
      } // else

这些代码的意思应该是比较清楚的,除了包在if语句里的内容。如果两个向量是互相反向的(即,它们之间的角度是180度),它们的点乘就将是-1。所以,如果我们把两个向量点乘而且结果等于-1.0f,则我们用yaw旋转180度,否则我们用rotate代替。为什么我加上1.0f,并检查它是否小于0.0001f? 不要忘了浮点舍入错误。你应该从来不直接比较两个浮点数的大小。最后,需要注意的是,这两个向量的点乘永远是落在[-1,1]这个区域之间的。如果还不太清楚的话,你应该先去学一学最基本的线性代数再来做图像编程! 至少你应该复习一下四元组与旋转基础,查阅关于一本关于基础的向量及矩阵运算的书籍。

好了,我们的代码完成了! 编译并运行这个Demo,你会看见一个机器人朝着指定的地点走动。

巩固练习

简单问题

1. 添加更多的点到路径中。同时在他点的位置放上Knonts来观察他想去哪里。

2. 机器人走完他的有效路程后就应该不存在了!当机器人完成行走,他就应该用执行死亡动画来代替待机动画。死亡的动画叫“Die”。

中级问题

1. 看完教程后,你注意到了mWalkSpeed有点问题吗?我们只是一次性设置了一个值,然后就再也没变过。就好像是一个类的不变的静态变量。试着改变一下这个变量。(提示:可以定义键盘的+和-分别表示加速和减速)

2. 代码中有些地方非常取巧,例如跟踪机器人是否正在走,用了mDirection向量跟Vector3::ZERO比较。如果我们换用一个bool型变量mWalking来跟踪机器人是否在移动也许会更好。实现这个改变。

困难问题

1. 这个类的一个局限是你不能在创建对象后再给机器人行走的路线增加新的目的地点。修补这个问题,实现一个带有一个Vector3参数的新方法,并且将它插入mWalkList队列。(提示:如果机器人还未完成行走过程,你就只需要将目的地点插入队列尾即可。如果机器人已经走完全程,你将需要让它再次开始行走,然后调用nextLocation开始再次行走。)

专家问题

1. 这个类的另一个主要局限是它只跟踪一个物体。重新实现这个类,使之可以彼此独立地移动任意数量的物体。(提示:你可以再创建一个类,这个类包含移动一个物体所需要知道的全部东西。把它存储在一个STL对象中,以便以后可以通过key获取数据。)如果可以不注册附加的framelistener,你会得到加分。

2. 做完上面的改变,你也许注意到了机器人可能会彼此发生碰撞。修复它,或者创建一个聪明的寻路函数,或者当机器人碰撞时检测,阻止它们彼此穿透而过。

中级教程2:射线场景查询及基础鼠标用法

有关这篇教程,无论遇到任何问题,都可以到论坛发帖寻求帮助。

目录

[隐藏]

介绍

本教程中,我们会初步创建一个基础场景编辑器。在过程之中,我们会涉及到:

  1. 如何使用RaySceneQueries阻止镜头穿透地面
  2. 如何使用MouseListener和MouseMotionListener接口
  3. 使用鼠标选取地面上的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]

进阶练习

简单练习

  1. 要阻止摄像机镜头看穿地形,我们选择地形上10个单位。这一选择是任意的。我们可以改进这一数值使之更接近地表而不穿过吗?如果可以,设定此变量为静态类成员并给它赋值。
  2. 有时我们确实想要穿越地形,特别是在场景编辑器中。创建一个标记控制碰撞检测开关,并绑定到一个键盘上的按键上。确保碰撞检测被关闭时,你不会在frameStarted中进行SceneQuery场景查询。

中级练习

  1. 当前我们每帧都要做场景查询,无论摄像机是否实际移动过。修补这个问题,如果摄像机移动了,只做一次场景查询。(提示:找到ExampleFrameListener中的移动向量,调用函数后,测试它是否为Vector3::ZERO。)

高级练习

  1. 注意到,每次我们执行一个场景查询调用时,有许多代码副本。将所有场景查询相关功能打包到一个受保护的函数中。确保处理地形一点不交叉的情况。

进阶练习

  1. 在这个教程中,我们使用了RaySceneQueries来放置地形上的对象。我们也许可以用它做些别的事情。拿来中级教程1的代码,完成困难问题1和专家问题1。然后将那个代码融合到这个代码中,使机器人行走在地面上,而不是虚空中。
  2. 增加代码,使每次你点击场景中的一点时,机器人移动到那个位置。

中级教程三

鼠标选取以及场景查询遮罩

目录

[隐藏]

[编辑] Introduction

本课紧接着上一课,我们将介绍如何在屏幕里选择任意的物体,以及如何限制哪些是可选的。

你能在这里找到本课的代码。在你浏览这个Demo的时候,最好是逐个地往你自己的工程里添加代码,并在编译后观察结果。

[编辑] 先决条件

本课程认为你已经学习了前面的课程。我们将会使用STL的iterator来遍历SceneQueries的多个结果,所以关于它们的基本知识是有帮助的。

[编辑] 从这开始

尽管我们是在上一次的代码上进行编辑,但为了后面的教程更加可读,做了一些修改。对于所有的鼠标事件,我创建了封闭函数来处理它们。当用户按下鼠标左键,"onLeftPressed"函数被调用,当按下右键时"onRightReleased"函数被调用,等等。在开始之前你应该花一些时间了解这些改变。

创建一个new.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)
       {
           // Setup default variables
           mCount = 0;
           mCurrentObject = NULL;
           mLMouseDown = false;
           mRMouseDown = false;
           mSceneMgr = sceneManager;
   
           // Reduce move speed
           mMoveSpeed = 50;
           mRotateSpeed /= 500;
   
           // Register this so that we get mouse events.
           mMouse->setEventCallback(this);
   
           // Create RaySceneQuery
           mRaySceneQuery = mSceneMgr->createRayQuery(Ray());
       } // MouseQueryListener
   
       ~MouseQueryListener()
       {
           mSceneMgr->destroyQuery(mRaySceneQuery);
       }
   
       bool frameStarted(const FrameEvent &evt)
       {
           // 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;
   
           // Setup the scene query
           Vector3 camPos = mCamera->getPosition();
           Ray cameraRay(Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y);
           mRaySceneQuery->setRay(cameraRay);
   
           // Perform the scene query
           RaySceneQueryResult &result = mRaySceneQuery->execute();
           RaySceneQueryResult::iterator itr = result.begin();
   
           // Get the results, set the camera height
           if (itr != result.end() && itr->worldFragment)
           {
               Real terrainHeight = itr->worldFragment->singleIntersection.y;
               if ((terrainHeight + 10.0f) > camPos.y)
                   mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
           }
   
           return true;
       }
   
       /* MouseListener callbacks. */
       bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
       {
           // Left mouse button up
           if (id == OIS::MB_Left)
           {
               onLeftReleased(arg);
               mLMouseDown = false;
           } // if
   
           // Right mouse button up
           else if (id == OIS::MB_Right)
           {
               onRightReleased(arg);
               mRMouseDown = false;
           } // else if
   
           return true;
       }
   
       bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
       {
           // Left mouse button down
           if (id == OIS::MB_Left)
           {
               onLeftPressed(arg);
               mLMouseDown = true;
           } // if
   
           // Right mouse button down
           else if (id == OIS::MB_Right)
           {
               onRightPressed(arg);
               mRMouseDown = true;
           } // else if
   
           return true;
       }
   
       bool mouseMoved(const OIS::MouseEvent &arg)
       {
           // Update CEGUI with the mouse motion
           CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
   
           // If we are dragging the left mouse button.
           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
   
           // 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
   
           return true;
       }
   
       // Specific handlers
       void onLeftPressed(const OIS::MouseEvent &arg)
       {
           // 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)
           {
               char name[16];
               sprintf(name, "Robot%d", mCount++);
   
               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
       }
   
       void onLeftReleased(const OIS::MouseEvent &arg)
       {
       }
   
       void onRightPressed(const OIS::MouseEvent &arg)
       {
           CEGUI::MouseCursor::getSingleton().hide();
       }
   
       virtual void onRightReleased(const OIS::MouseEvent &arg)
       {
           CEGUI::MouseCursor::getSingleton().show();
       }
   
   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)
       {
           // 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 setup
           mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
           mGUISystem = new CEGUI::System(mGUIRenderer);
   
           // Mouse
           CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
           CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
       }
   
       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
           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;
   }

在你继续之前,请保证能正常编译并运行这些代码。尽管有所改动,但它的效果与上一课相同。

[编辑] 标明选择的物体

在这一课里,我们能够做到当你放置物体后,能“拾取”并移动它。我们希望有一种途径,让用户知道她目前正在操纵哪一个物体。在游戏里,我们可能以某种特殊的方式来高亮这个物体。而在这里(也可以在你的程序没有发布之前),你可以用showBoundingBox方法来创建一个围绕该物体的方盒。

我们的基本思想是,当鼠标首次按下时,取消旧的选择物体上的包围盒,然后当选择了一新物体时,给新物体加上包围盒。为此,我们在onLeftPressed函数的开头添加如下代码:

      // 打开包围盒
     if (mCurrentObject)
         mCurrentObject->showBoundingBox(false);

然后在onLeftPressed函数的最末尾添加以下代码:

      // Show the bounding box to highlight the selected object
      if (mCurrentObject)
          mCurrentObject->showBoundingBox(true);

现在mCurrentObject总是在屏幕上高亮显示了。

[编辑] 添加忍者

我们现在想要修改代码,使得不只支持机器人,而且还能够放置和移动忍者。我们需要一个“机器人模式”和一个“忍者模式”,来决定在屏幕上放置的物体。我们把空格键设置成切换按钮,并且显示信息提示用户目前处于哪一种模式。

首先,我们把MouseQueryListener设置成机器人模式。我们添加一个变量来保存物体状态(即,我们放置的是机器人还是忍者)。找到protected区域中的MouseQueryListener,并添加这个变量:

   bool mRobotMode;                   // 当前状态

然后,在MouseQueryListener构造器的末尾加上这些代码:

       // 设置文本、缺省状态
       mRobotMode = true;
       mDebugText = "Robot Mode Enabled - Press Space to Toggle";

这样我们处于忍者模式了!真有这么简单就好了。。我们还要基于mRobotMode变量来创建一个机器人mesh,或者一个忍者mesh。在onLeftPressed里找到这段代码:

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

          Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
      

用下面代码替换。意思很明显,依赖mRobotMode状态,我们添加机器人还是忍者,并取相应名称:

          Entity *ent;
          char name[16];

          if (mRobotMode)
          {
              sprintf(name, "Robot%d", mCount++);
              ent = mSceneMgr->createEntity(name, "robot.mesh");
          } // if
          else
          {
              sprintf(name, "Ninja%d", mCount++);
              ent = mSceneMgr->createEntity(name, "ninja.mesh");
          } // else

现在我们差不多搞定了。剩下的唯一一件事就是绑定空格键,来改变状态。在frameStarted里找到如下代码:

        if (!ExampleFrameListener::frameStarted(evt))
           return false;

在它后面加上这些代码:

        // 切换模式
        if(mKeyboard->isKeyDown(OIS::KC_SPACE) && mTimeUntilNextToggle <= 0)
       {
               mRobotMode = !mRobotMode;
               mTimeUntilNextToggle = 1;
               mDebugText = (mRobotMode ? String("Robot") : String("Ninja")) + " Mode Enabled - Press Space to Toggle";
       }

好了完成了!编译并运行这个Demo,你现在可以通过空格键来选择你要放置的物体。

[编辑] 选择物体

现在我们进入本课最核心的部分:使用RaySceneQueries在屏幕上选取物体。在我们对代码进行修改之前,我想先详细介绍一下RaySceneQueryResultEntry。(请进入链接并浏览一下这个结构体)

RaySceneQueryResult返回一个RaySceneQueryResultEntry结构体的iterator。这个结构体包含三个变量。distance变量告诉你这个物体沿着射线有多远。另外两个变量的其中一个将是null。movable变量包含一个MovableObject对象,如果与射线相交的话。如果射线接触到一个地形片段,worldFragment将保存这个WorldFragment对象(比如地形)。

MovableObject基本上可以是任何你能绑在SceneNode上的对象(像实体、光源,等)。 在这里查看继承树,看看将会返回什么类型的对象。大多数RaySceneQueries的应用包括选取和操纵MovableObject对象,以及它们所绑定到的SceneNodes 。调用getName方法获取MovableObject的名称。调用getParentSceneNode(或getParentNode)获取它们所绑定到的SceneNode。如果RaySceneQueryResultEntry的结果不是一个MovableObject,movable变量则为null。

WorldFragment是完全另一种怪物。当RaySceneQueryResult中的worldFragment成员被设置时,就意味着返回结果是SceneManager创建的世界几何(world geometry)的一部分。返回的world fragment的类型是基于SceneManager的。它是这样实现的,WorldFragment结构体包含一个fragmentType变量,以指明world fragment的类型。基于这个fragmentType变量,设置其它成员变量(singleIntersection, planes, geometry, 或者renderOp)。一般来说,RaySceneQueries只返回WFT_SINGLE_INTERSECTION类型的WorldFragments。singleIntersection变量只是一个Vector3,用来报告交点的坐标。其它类型的world fragments超出了这课的范围。

下面我们来看一个例子,比如我们想要在RaySceneQuery之后打印一串返回结果。下面的代码做这些:(假设fout对象是ofstream类型的,并且已经用open方法创建出来)

// Do not add this code to the program, just read along:

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

   // 循环遍历所有结果
   for ( itr = result.begin( ); itr != result.end(); itr++ )
   {
      // Is this result a WorldFragment?
      if ( itr->worldFragment )
      {
         Vector3 location = itr->worldFragment->singleIntersection;
         fout << "WorldFragment: (" << location.x << ", " << location.y << ", " << location.z << ")" << endl;
      } //  if
      
      // Is this result a MovableObject?
      else if ( itr->movable )
      {
         fout << "MovableObject: " << itr->movable->getName() << endl;
      } // else if
   } // for

这样就能把射线遇到的所有MovableObjects的名称打印出来,而且能够打印与世界几何相交的坐标(如果碰到的话)。注意,这可能会出现一些奇怪的现象。比如,如果你正使用TerrainSceneManager,射线的发出点必须要在地型之上,否则相交查询不会把它注册为一个碰撞。不同的场景管理器对RaySceneQueries有不同的实现。所以当你使用一个新的场景管理器,最好先试验一下。

现在,如果我们再看看我们的RaySceneQuery代码,你会发现:我们并不需要遍历所有的结果!实际上我们只需要查看第一个结果,即世界几何(TerrainSceneManager的情况下)。但有点不妙,因为我们不能保证TerrainSceneManager总是最先返回世界几何。我们需要循环所有的结果,以确保我们能找到我们想要的。我们要做的另一件事情是“拾起”并拖拽已经被放置的物体。当前你若点击一个已经放置的物体,程序会忽略它,并在它后面放置另一个机器人。我们现在来修正它。

找到onLeftPressed函数。我们首先要保证当我们点击鼠标,我们能得到沿射线上的第一个东西。为此,我们需要设置RaySceneQuery按深度排序。找到onLeftPressed函数里的如下代码:

      // 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();

修改成这样:

      // Setup the ray scene query
      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);
      mRaySceneQuery->setSortByDistance(true);

      // Execute query
      RaySceneQueryResult &result = mRaySceneQuery->execute();
      RaySceneQueryResult::iterator itr;

现在我们能按顺序返回结果了,还要更新查询结果的代码。我们将重写这个部分,所以删除这段代码:

      // Get results, create a node/entity on the position
      if (itr != result.end() && itr->worldFragment)
      {
          Entity *ent;
          char name[16];

          if (mRobotMode)
          {
              sprintf(name, "Robot%d", mCount++);
              ent = mSceneMgr->createEntity(name, "robot.mesh");
          } // if
          else
          {
              sprintf(name, "Ninja%d", mCount++);
              ent = mSceneMgr->createEntity(name, "ninja.mesh");
          } // else

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

我们要实现能够选取已经放置在屏幕上的物体。我们的策略分为两步。首先,如果用户点击一个物体,则使mCurrentObject等于它的父节点。如果用户没有点击在物体上(而是点在地型上)时,就像以前一样放置一个新的机器人。第一个要做的修改就是,使用一个for循环来代替if语句:

      // Get results, create a node/entity on the position
      for ( itr = result.begin(); itr != result.end(); itr++ )
      {

首先我们要检查第一个交点的是不是一个MovableObject,如果是,我们把它的父节点赋给mCurrentObject。还要做另一个判断,TerrainSceneManager会为地型本身创建MovableObject,所以我们可能实际上会与他们相交。为了修正这个问题,我通过检查对象的名称来保证,它们的名称不类似于地型名称。一个典型的地形名称比如"tile[0][0,2]"。最后,注意这个break语句。我们只需要在第一个物体上做操作,一旦我们找到一个合法的,我们就应该跳出循环。

          if (itr->movable && itr->movable->getName().substr(0, 5) != "tile[")
          {
              mCurrentObject = itr->movable->getParentSceneNode();
              break;
          } // if

下面,我们要检查交点是否返回了WorldFragment。

          else if (itr->worldFragment)
          {
              Entity *ent;
              char name[16];

现在我们根据mRobotState来创建一个机器人实体或者一个忍者实体。创建实体之后,我们将创建场景节点,并跳出这个for循环。

              if (mRobotMode)
              {
                  sprintf(name, "Robot%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "robot.mesh");
              } // if
              else
              {
                  sprintf(name, "Ninja%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "ninja.mesh");
              } // else
              mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", itr->worldFragment->singleIntersection);
              mCurrentObject->attachObject(ent);
              mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
              break;
          } // else if
      } // for

不管你信不信,以上就是全部要做的! 编译并玩转这个代码。现在我们点击地形的时候会创建正确类型的物体,当我们点击一个物体的时候,会看到一个包围盒(别拖拽它,这还是下一步)。一个有意思的问题是,既然我们只需要第一个交点,而且我们已经按深度排序了,为什么不只用if语句呢?这主要是因为,如果第一个返回的物体是那些恼人的地砖,我们就会得到一个错误。我们必须循环直到发现不是地砖的东西,或者走到了列表末尾。

现在我们步入主题了,我们还需要在别的地方更新RaySceneQuery代码。在frameStarted和onLeftDragged函数里,我们只需要找到地形。因为地形总是在有序列表的最后面,所以没有必要对结果排序(所以我们想把排序关了)。但我们仍然想要循环遍历这些结果,只是为了防范TerrainSceneManager日后某天可能会改变,而不首先返回地形。首先,在frameStarted里找到这段代码:

      // 进行场景查询
      RaySceneQueryResult &result = mRaySceneQuery->execute();
      RaySceneQueryResult::iterator itr = result.begin();

      // 获得结果,设置摄像机高度
      if (itr != result.end() && itr->worldFragment)
      {
          Real terrainHeight = itr->worldFragment->singleIntersection.y;
          if ((terrainHeight + 10.0f) > camPos.y)
              mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
      }

然后用这些代码替换:

      // 进行场景查询
      mRaySceneQuery->setSortByDistance(false);
      RaySceneQueryResult &result = mRaySceneQuery->execute();
      RaySceneQueryResult::iterator itr;

      // 获得结果,设置摄像机高度
      for (itr = result.begin(); itr != result.end(); itr++)
      {
          if (itr->worldFragment)
          {
              Real terrainHeight = itr->worldFragment->singleIntersection.y;
              if ((terrainHeight + 10.0f) > camPos.y)
                  mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
              break;
          } // if
      } // for

不言而喻,我们添加了一行关闭了排序,然后把if语句转换成for循环,一旦我们找到我们想要的位置就跳出。在mouseMoved函数里,我们也是做同样的事情。在mouseMoved里找到这段代码:

          mRaySceneQuery->setRay(mouseRay);

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

          if (itr != result.end() && itr->worldFragment)
              mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
      

然后用这些代码替换:

          mRaySceneQuery->setRay(mouseRay);
          mRaySceneQuery->setSortByDistance(false);

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

          for (itr = result.begin(); itr != result.end(); itr++)
              if (itr->worldFragment)
              {
                  mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
                  break;
              } // if

编译并测试代码。这应该不会与我们上次运行代码时有太大出入,但我们更正确地处理了它。以后TerrainSceneManager的任何更新都不会影响到我们的代码。

[编辑] 查询遮罩

注意,不论我们处于何种模式,我们总能选取任意的物体。我们的RaySceneQuery将会返回机器人或者忍者,只要它在最前面。但并不一定总是要这样。所有的MovableObject允许你为它们设置一个遮罩,然后SceneQueries使你能够根据这个遮罩来过滤你的结果。所有的遮罩都是通过二进制“AND”操作来实现的,所以如果你对它不熟悉,应该补充这方面知识再继续下去。

我们要做的第一件事就是创建这个遮罩值。找到MouseQueryListener类的最开始,将这些添加到public声明后面:

  enum QueryFlags
  {
      NINJA_MASK = 1<<0,
      ROBOT_MASK = 1<<1
  };

这样就创建了一个含有两个值的枚举类型,它们的二进制表示是0001和0010。现在,每当我们创建一个机器人实体,我们调用它的"setMask"方法将这个查询标记设置为ROBOT_MASK。每当我们创建一个忍者实体时,我们调用它的"setMask"方法将这个查询标记设置为NINJA_MASK。现在,当我们在忍者模式下,我们将使RaySceneQuery仅考虑带有NINJA_MASK标记的物体。而在机器人模式里,我们将使它仅考虑ROBOT_MASK。

在onLeftPressed方法里找到这一段:

              if (mRobotMode)
              {
                  sprintf(name, "Robot%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "robot.mesh");
              } // if
              else
              {
                  sprintf(name, "Ninja%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "ninja.mesh");
              } // else

我们将添加两行来为它们设置遮罩:

              if (mRobotMode)
              {
                  sprintf(name, "Robot%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "robot.mesh");
                  ent->setQueryFlags(ROBOT_MASK);
              } // if
              else
              {
                  sprintf(name, "Ninja%d", mCount++);
                  ent = mSceneMgr->createEntity(name, "ninja.mesh");
                  ent->setQueryFlags(NINJA_MASK);
              } // else

我们还需要做到,当我们处于其之一种模式中时,我们只能点击和拖拽当前类型的物体。我们需要设置这个查询标记,使得只有正确的物体类型才被选择。为此,在机器人模式中,我们设置RaySceneQuery的查询标记为ROBOT_MASK,而在忍者模式里设置为NINJA_MASK。在onLeftPressed函数里,找到这个代码:

      mRaySceneQuery->setSortByDistance(true);
      

在这一行后面加上一行:

      mRaySceneQuery->setQueryMask(mRobotMode ? ROBOT_MASK : NINJA_MASK);

编译并运行这个教程。我们就只能选择我们所要的物体。所有的射线都穿过其它的物体,并碰撞到正确的物体。现在我们完成了代码,下一节将不会对它修改。

[编辑] 查询类型遮罩

当使用场景查询时还有一件事需要考虑。假设你在上面的场景里加入了一个公告板或者粒子系统,并且你想要移动它。我会发现查询不会返回你点击的公告板。这是因为SceneQuery还存在另外一个遮罩,查询类型遮罩(QueryTypeMask),它限制了你只能选择这个标记指定的类型。默认情况是,当你作一个查询时,它只返回实体类型的物体。

在你的代码里,如果想要查询返回公告板或者粒子系统,则你要在执行查询之前这么做:

mRaySceneQuery->setQueryTypeMask(SceneManager::FX_TYPE_MASK);

现在查询将只返回公告板或者粒子系统作为结果。

在SceneManager类里面,已经定义了6种类型的QueryTypeMask作为静态成员:

WORLD_GEOMETRY_TYPE_MASK // 返回世界几何
ENTITY_TYPE_MASK         // 返回实体
FX_TYPE_MASK             // 返回公告板/粒子系统
STATICGEOMETRY_TYPE_MASK // 返回静态几何
LIGHT_TYPE_MASK          // 返回光源
USER_TYPE_MASK_LIMIT     // 用户类型遮罩限制

没有手工设置这个属性时,QueryTypeMask的默认值是ENTITY_TYPE_MASK。

[编辑] 关于遮罩更多内容

我们的遮罩例子非常简单,所以我想介绍一些更复杂的例子。

[编辑] 设置MovableObject的遮罩

每当我们创建一个新的遮罩,它的二进制表示必须只包含一个“1”。即,这些是合法的遮罩:

00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000

等等。我们通过使用一个“1”并按位移动它,能够轻松地创建一个值。即:

00000001 = 1<<0
00000010 = 1<<1
00000100 = 1<<2
00001000 = 1<<3
00010000 = 1<<4
00100000 = 1<<5
01000000 = 1<<6
10000000 = 1<<7

直到1<<31。这样我们就有了32种不同的遮罩,可以用在MovableObject对象上。

[编辑] 有多个遮罩的查询

我们能使用“OR”操作符,来为多个遮罩进行查询。比如说,在游戏里我们有三个不同组的对象:

enum QueryFlags
{
    FRIENDLY_CHARACTERS = 1<<0,
    ENEMY_CHARACTERS = 1<<1,
    STATIONARY_OBJECTS = 1<<2
};

现在,如果我们只要查询friendly characters,可以这么做:

mRaySceneQuery->setQueryMask(FRIENDLY_CHARACTERS);

如果我们要同时查询enemy characters和stationary objects,可以这么做:

mRaySceneQuery->setQueryMask(ENEMY_CHARACTERS | STATIONARY_OBJECTS);

如果你有很多这一类的查询,你可以把它们定义到枚举类型里去:

OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS

然后只使用OBJECTS_ENEMIES作查询。

[编辑] 查询遮罩以外的所有东西

你还可以使用按位反转操作,来查询除了遮罩的所有事物,就像这样:

mRaySceneQuery->setQueryMask(~FRIENDLY_CHARACTERS);

这样返回所有事物,除了friendly characters。对于多个遮罩,你也能这么做:

mRaySceneQuery->setQueryMask(~(FRIENDLY_CHARACTERS | STATIONARY_OBJECTS));

这样返回所有物体,除了friendly characters和stationary objects。

[编辑] 选取所有物体或者不选取任何物体

你还以用遮罩干一些有趣的事情。请记住,如果你为场景查询把查询遮罩设置成QM,MovableObject的遮罩为OM,如果QM & OM 包含至少一个“1”,则表示符合。因此,为场景查询把查询遮罩设置成0,将会使它不返回任何MovableObject。把查询遮罩设置成1,则返回所有查询遮罩不为0的MovableObject。

使用为0的查询遮罩有时是非常有用的。比如,当只需要返回worldFragment时,TerrainSceneManager不必使用QueryMasks。可以这么做:

mRaySceneQuery->setQueryMask(0);

在你的场景管理器的RaySceneQueries里,你就只能得到worldFragment。如果你的屏幕里有很多物体,由于你只要地形的交点而不想浪费时间循环遍历所有的东西,这样做是很有用的。

中级教程四

成批选择和基本手动对象

目录

[隐藏]

介绍

在这一课里,我们将涉及如何进行成批选取。意思就是,当你在屏幕上点击并且拖拽鼠标时,一个白色矩形会追踪你正在选择的区域。当鼠标移动时,所有在选择区域里的物体都会被高亮。为了实现它,我们将学习两种对象: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查询(比如射线场景查询),然后再用物理引擎逐个检测查询返回的结果,检查网格的几何形状来看看是否真的击中了,还是仅仅非常的接近。

中级教程五

静态图元

目录

[隐藏]

内容

很多情况,你需要在场景中添加物体,但是却根本不需要移动它们.比如,除非你加入了物理因素,一块石头或者一棵树将永远不会被移动.Ogre为这种情况提供了StaticGeometry类,它允许你批渲染很多物体.这个通常要比手动在SceneNodes添加要快多了.在这个教程里我们要涵盖StaticGeometry的基本使用方法,另外还会再提及ManualObject的使用.请在前一个教程中获取ManualObject的使用方法.

在这个教程中,我们将手动创建一个草地mesh,然后在我们的场景中的StaticGeometry 实例中添加许多这样的草地.

本教程的代码在这里下载.


必要条件

创建一个cpp 文件,添加下面代码:

#include "ExampleApplication.h"
class TutorialApplication : public ExampleApplication
{
protected:
public:
    TutorialApplication()
    {
    }

    ~TutorialApplication() 
    {
    }
protected:
    MeshPtr mGrassMesh;

    void createGrassMesh()
    {
     }

    void createScene(void)
    {
        createGrassMesh();
        mSceneMgr->setAmbientLight(ColourValue(1, 1, 1));

        mCamera->setPosition(150, 50, 150);
        mCamera->lookAt(0, 0, 0);

        Entity *robot = mSceneMgr->createEntity("robot", "robot.mesh");
        mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(robot);

        Plane plane;
        plane.normal = Vector3::UNIT_Y;
        plane.d = 0;
        MeshManager::getSingleton().createPlane("floor",
            ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane,
            450,450,10,10,true,1,50,50,Vector3::UNIT_Z);
        Entity* pPlaneEnt = mSceneMgr->createEntity("plane", "floor");
        pPlaneEnt->setMaterialName("Examples/GrassFloor");
        pPlaneEnt->setCastShadows(false);
        mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(pPlaneEnt);
    }
};

#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
    TutorialApplication 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;
}
   在继续教程之前,确信你的代码可以编译。你可以看到一个机器人站在一个平面上。

创建场景

从ManualObject创建Mesh

我们要做的第一件事是创建要渲染的草地.创建3个交错的正方形,每个正方形都贴上草的材质,这样无

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

原文地址:https://www.cnblogs.com/skiwnchiwns/p/10342863.html