Ogre中级教程

中级教程一

来自 Ogre wiki

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

作者: Culver.

内容

目录

[隐藏]
  • 1 介绍
  • 2 前期准备
  • 3 准备开始
  • 4 设置场景
  • 5 动画
  • 6 移动角色
  • 7 巩固练习
    • 7.1 简单问题
    • 7.2 中级问题
    • 7.3 困难问题
    • 7.4 专家问题

介绍

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

本课的最终代码在这里。

前期准备

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

准备开始

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

#include "ExampleApplication.h"

#include 
using namespace std;

class MoveDemoListener : public ExampleFrameListener
{
public:

    MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,
        Entity *ent, deque &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 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 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 介绍
  • 2 前期准备
  • 3 开始
  • 4 创建场景
  • 5 帧监听器介绍
  • 6 创建帧监听器
  • 7 增加鼠标查看
  • 8 地形碰撞检测
  • 9 地形选择
  • 10 进阶练习
    • 10.1 简单练习
    • 10.2 中级练习
    • 10.3 高级练习
    • 10.4 进阶练习

介绍

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

  1. 如何使用RaySceneQueries阻止镜头穿透地面
  2. 如何使用MouseListener和MouseMotionListener接口
  3. 使用鼠标选取地面上的x和y坐标

你可以在这里找到完整代码。跟随着教程,你会慢慢地向你自己的工程项目中增加代码,并且随着编译看到结果。

前期准备

本教程假设你已经知道了如何创建Ogre工程,并且可以成功编译。假设你已经了解了基本的Ogre对象(场景节点,实体,等等)。你也应该熟悉STL迭代器基本的使用方法,因为本教程会用到。(Ogre也大量用到STL,如果你还不熟悉STL,那么你需要花些时间学习一下。)

开始

首先,你需要为此演示程序创建一个新工程。在创建工程时,选空工程、自己的框架,以及初始化进度条和CEGUI支持,不选编译后拷贝。向工程中,增加一个名叫“MouseQuery.cpp”的文件,并向其中添加如下代码:

#include 
#include 
#include 

#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. 增加代码,使每次你点击场景中的一点时,机器人移动到那个位置。

 

中级教程三

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

目录

[隐藏]
  • 1 Introduction
  • 2 先决条件
  • 3 从这开始
  • 4 标明选择的物体
  • 5 添加忍者
  • 6 选择物体
  • 7 查询遮罩
  • 8 查询类型遮罩
  • 9 关于遮罩更多内容
    • 9.1 设置MovableObject的遮罩
    • 9.2 有多个遮罩的查询
    • 9.3 查询遮罩以外的所有东西
    • 9.4 选取所有物体或者不选取任何物体

[编辑] Introduction

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

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

[编辑] 先决条件

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

 

[编辑] 从这开始

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

创建一个new.cpp文件并添加到你的工程里,并加入下面的代码:

   #include 
   #include 
   #include 
   
   #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。如果你的屏幕里有很多物体,由于你只要地形的交点而不想浪费时间循环遍历所有的东西,这样做是很有用的。

 

 

中级教程四

成批选择和基本手动对象

目录

[隐藏]
  • 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 
   #include 
   
   #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::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 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查询(比如射线场景查询),然后再用物理引擎逐个检测查询返回的结果,检查网格的几何形状来看看是否真的击中了,还是仅仅非常的接近。

 

中级教程五

静态图元

目录

[隐藏]
  • 1 内容
  • 2 必要条件
  • 3 创建场景
    • 3.1 从ManualObject创建Mesh
    • 3.2 添加Static Geometry
  • 4 结论
    • 4.1 Modifying the StaticGeometry Object
    • 4.2 Advanced Object Batching

内容

很多情况,你需要在场景中添加物体,但是却根本不需要移动它们.比如,除非你加入了物理因素,一块石头或者一棵树将永远不会被移动.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个交错的正方形,每个正方形都贴上草的材质,这样无论你从哪个角度看都是3D的效果.比较简单的创建方式是创建一个方型,第2个旋转60度,然后第3个再转60度. 和上一个教程一样,我们会用ManualObject建立我们的物体,但是不同的是我们要创建的是一个Mesh而不是线列表. 首先定义一些变量.我们要创建一个Vector3在其中定义X和Z的坐标,用它们来创建一个正方型,然后用Quaternion来转个方向.找到createGrassMesh函数,添加下面代码:

      const float width = 25;
      const float height = 30;
      ManualObject mo("GrassObject");

      Vector3 vec(width/2, 0, 0);
      Quaternion rot;
      rot.FromAngleAxis(Degree(60), Vector3::UNIT_Y);    //绕Y轴旋转60度

现在已经设定好变量了,接着需要定义ManualObject.RenderOperation需要设定为OT_TRIANGLE_LIST,意味着在定义好顶点之后,需要创建面.

      mo.begin("Examples/GrassBlades", RenderOperation::OT_TRIANGLE_LIST);
      for (int i = 0; i < 3; ++i)
      {

每个方形需要定义4个顶点.每个顶点要设定texture坐标,它告诉Ogre如何使用Examples/GrassBlades material中定义的材质.设定左上角点为(0,0),右下角点为(1,1).

          mo.position(-vec.x, height, -vec.z);   
          mo.textureCoord(0, 0);

          mo.position(vec.x, height, vec.z);
          mo.textureCoord(1, 0);

          mo.position(-vec.x, 0, -vec.z);
          mo.textureCoord(0, 1);

          mo.position(vec.x, 0, vec.z);
          mo.textureCoord(1, 1);

现在已经定义了方形的4个顶点.上一个教程简要提到,要定义面需要创建三角形,并且面对你按逆时针旋转.第1个方形要建2个三角形.第一个是(0th, 3rd, 1st)顶点(按照上面定义的顺序),第二个(0th, 2nd, 3rd).因为每次只创建4个顶点,所以需要用offset来取得正确的开始顶点数字.

          int offset = i * 4;
          mo.triangle(offset, offset+3, offset+1);
          mo.triangle(offset, offset+2, offset+3);

接着需要旋转.(旋转矩阵*矩阵)

          vec = rot * vec;
      }
      mo.end();

现在我们定义了一个manual object,最后要做的是将它转换为mesh.

      mo.convertToMesh("GrassBladesMesh");

注意如果你用这种方法创建了一个超级复杂的mesh.你需要将它存储为文件,然后在以后读回来,以便不用每次调用程序都重新创建一次.

      // 在该例子中不要添加下面代码
      MeshPtr ptr = mo.convertToMesh("GrassBladesMesh");
      MeshSerializer ser;
      ser.exportMesh(ptr.getPointer(), "grass.mesh");

现在让我们开始创建StaticGeomety吧.

添加Static Geometry

第一件事是为我们刚才建立的Mesh创建一个Entity,然后创建StaticGeometry实例.注意:我们只为StaticGeometry创建了一个Entity.找到createScene方法,在最后添加下面代码:

      Entity *grass = mSceneMgr->createEntity("grass", "GrassBladesMesh");
      StaticGeometry *sg = mSceneMgr->createStaticGeometry("GrassArea");  //相当于节点

      const int size = 375;
      const int amount = 20;

size变量定义了要在多大面积之内种草. Amount变量定义了在每一行要放多少个物体. 接着要定义size和origin.一旦创建了实例(通过调用StaticGeometry::build),我们就不能再修改origin和size了.如果你要在一个点周围放这些物体,需要将origin的x和z值设定为size x和z数值的一半.

      sg->setRegionDimensions(Vector3(size, size, size));
      sg->setOrigin(Vector3(-size/2, 0, -size/2));

上面将该物体放在点(0,0,0)周围.如果要在3D空间中其它点放置,用下面代码:

      //在该例子中不要添加下面代码
      sg->setOrigin(Vector3(-size/2, -size/2, -size/2) + Vector3(x, y, z));

注意:我们在创建mesh的时候定义过物体高度,在setRegionDimensions y数值要大于mesh的高度。下面要做的是将这个物体加入StaticGeomety.下面代码有点复杂,因为我们将上面的草变成草垛,并且随机赋予x和z的值/旋转角度和垂直放大倍数.

      for (int x = -size/2; x < size/2; x += (size/amount))
          for (int z = -size/2; z < size/2; z += (size/amount))
          {
              Real r = size / (float)amount / 2;
              Vector3 pos(x + Math::RangeRandom(-r, r), 0, z + Math::RangeRandom(-r, r));
              Vector3 scale(1, Math::RangeRandom(0.9, 1.1), 1);
              Quaternion orientation;
              orientation.FromAngleAxis(Degree(Math::RangeRandom(0, 359)), Vector3::UNIT_Y);

              sg->addEntity(grass, pos, orientation, scale);
          }

当你定义了StaticGeomety之后你需要调用addEntity或者addSceneNodt.addSceneNode会将Entity加到Static Geomety的所有子节点中,用每个子节点的位置,方向和缩放率.注意:要用addSceneNode的话,要将它从节点中挪走.否则,Ogre会渲染你创建的StaticGeomety以及原来你不想被渲染的节点. 最后,我们需要创建StaticGeomery令它被显示.

      sg->build();

现在运行你的程序,你可以看到一个站在草地上的机器人.

结论

Modifying the StaticGeometry Object

Once the StaticGeometry is created, you are not supposed to do too much with it, since that would mostly defeat the purpose. You can, however, do things like wave the grass with the wind. If you are interested in how to do this, take a look at the grass demo which comes with Ogre. The GrassListener::waveGrass function modifies the grass to perform a wave-like motion.

Advanced Object Batching

This is, of course, just the beginnings of object batching. You should use StaticGeometry any time you have objects that are grouped together and will not move. If you are trying to create something as intensive or as expansive as a forest or trying to add grass to a huge amount of terrain, you should take a look at one of the more advanced batching techniques, like the PagedGeometry Engine.

 

 

中级教程六

投影贴图

目录

[隐藏]
  • 1 介绍
  • 2 准备开始
  • 3 起始代码
  • 4 投影贴图
    • 4.1 平截头体(Frustums)
    • 4.2 修改材质
    • 4.3 调用函数
  • 5 消除反向投影
    • 5.1 介绍
    • 5.2 修改投影器
    • 5.3 修改材质
  • 6 炫耀一下投影
    • 6.1 简单的旋转
    • 6.2 修改视线范围
  • 7 最后要注意的

[编辑] 介绍

在这一课里,我们将介绍如何为场景中的一个物体添加投影贴图。投影纹理是非常有用的,比如你想要一个类似地面上的选择指示器,或者你正在瞄准的瞄准镜,或者投射在某个物体上的其它贴图。下面的截图是一个大家都喜爱的ogre头像,它被投影了一个瞄准器: [[1]]

你能在这里找到本课的代码。当你学习本课时,你应该逐个地往你的工程里添加代码,编译并观察结果。

[编辑] 准备开始

开始之前,我们需要两张新图。右击这两个链接,并保存到Ogre可以找到的地方:[decal.png] [decal_filter.png]

最好放在media/materials/textures目录下(对于大多数人,是在OgreSDK目录里)。

[编辑] 起始代码

用你喜爱的IDE创建一个cpp文件,并添加如下代码:

   #include "ExampleApplication.h"
   
   // A FrameListener that gets passed our projector node and decal frustum so they can be animated
   class ProjectiveDecalListener : public ExampleFrameListener
   {
   public:
       ProjectiveDecalListener(RenderWindow* win, Camera* cam, SceneNode *proj, Frustum *decal)
           : ExampleFrameListener(win, cam), mProjectorNode(proj), mDecalFrustum(decal), mAnim(0)
       {
       }
   
       bool frameStarted(const FrameEvent& evt)
       {
           return ExampleFrameListener::frameStarted(evt);
       }
   
   protected:
       SceneNode *mProjectorNode;
       Frustum *mDecalFrustum;
       float mAnim;
   };
   
   class ProjectiveDecalApplication : public ExampleApplication
   {
   protected:
       SceneNode *mProjectorNode;
       Frustum *mDecalFrustum;
       Frustum *mFilterFrustum;
   
       void createScene()
       {
           // Set ambient light
           mSceneMgr->setAmbientLight(ColourValue(0.2, 0.2, 0.2));
   
           // Create a light
           Light* l = mSceneMgr->createLight("MainLight");
           l->setPosition(20,80,50);
   
           // Position the camera
           mCamera->setPosition(60, 200, 70);
           mCamera->lookAt(0,0,0);
   
           // Make 6 ogre heads (named head0, head1, etc.) arranged in a circle
           Entity *ent;
           for (int i = 0; i < 6; i++)
           {
               SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
               ent = mSceneMgr->createEntity("head" + StringConverter::toString(i), "ogrehead.mesh");
               headNode->attachObject(ent);
               Radian angle(i * Math::TWO_PI / 6);
               headNode->setPosition(75 * Math::Cos(angle), 0, 75 * Math::Sin(angle));
           }
       }
   
       // The function to create our decal projector
       void createProjector()
       {
       }
   
       // A function to take an existing material and make it receive the projected decal
       void makeMaterialReceiveDecal(const String &matName)
       {
       }
   
       // Create new frame listener
       void createFrameListener(void)
       {
           mFrameListener= new ProjectiveDecalListener(mWindow, mCamera, mProjectorNode, mDecalFrustum);
           mRoot->addFrameListener(mFrameListener);
       }
   };
   
   #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
   #define WIN32_LEAN_AND_MEAN
   #include "windows.h"
   #endif
   
   #ifdef __cplusplus
   extern "C" {
   #endif
   
   #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
       INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)
   #else
       int main(int argc, char **argv)
   #endif
       {
           // Create application object
           ProjectiveDecalApplication 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
               std::cerr << "An exception has occurred: " << e.getFullDescription();
   #endif
           }
   
           return 0;
       }
   
   #ifdef __cplusplus
   }
   #endif

编者并运行这个程序,你应该可以看见六个Ogre头颅。

[编辑] 投影贴图

[编辑] 平截头体(Frustums)

一个平截头体(Frustums)表示一个头部被截取的棱椎,代表了一个可视区域或是一个投影。Ogre使用它来表示一个camera(Camera类直接继承了Frustum类)。在这一课里,我们将使用一个平截头体来把贴花投影到场景中的网格(mesh)上。

首先我们要创建一个投影器(projector),即创建代表它的平截头体,并绑定到场景节点上。找到createProjector方法并添加以下代码:

      mDecalFrustum = new Frustum();
      mProjectorNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("DecalProjectorNode");
      mProjectorNode->attachObject(mDecalFrustum);
      mProjectorNode->setPosition(0,5,0);

这样一个投影器就创建好了,当你离得越远时,这个贴图会变得更大,就像电影机一样。若你要创建一个投影器,使得不论多远它总是保持恒定的大小和形状,你就要添加如下代码(但现在不要这样做):

      // 别把这段加到工程里
      mDecalFrustum->setProjectionType(PT_ORTHOGRAPHIC);
      mDecalFrustum->setNearClipDistance(25);

通过设置正射投影的视线范围、横纵比、近截取距离,你决定了这个帖图的大小和形状,使得无论投影器有多远,它总是保持恒定。

继续之前,请先搞清楚我们的平截头体将要把贴图投影到的地点。在这个程序中,有一圈Ogre头颅,而这个平截头体处于它们的正中心(虽然微微向上抬起了5个单位),并指向-Z方向(由于我们没有改变朝向,是一个默认值)。这就意味着,最终我们运行程序时,贴图将投向后面的Ogre人头。

[编辑] 修改材质

为了让贴图最终显示在物体上,它使用的材质必须能够接收贴图。为此我们创建一个新的通路,在常规纹理上面渲染贴图。这个平截头体决定了这个投影贴图的位置、大小和形状。在这个Demo里面,我们将直接修改这个材质来接收贴图,但在实际的应用中,你应该创建这个材质的一个拷贝,再修改它,这样你就可以将材质切换回原来的。

首先获得这个材质,并为它创建一个新的通路。找到makeMaterialReceiveDecal并添加以下代码:

      MaterialPtr mat = (MaterialPtr)MaterialManager::getSingleton().getByName(matName);
      Pass *pass = mat->getTechnique(0)->createPass();

现在我们创建了通路,我们需要设置混合和光照。我们将添加新的纹理,它必须与当前的纹理正确地混合。为此我们将把场景混合(scene blending)设置成alpha透明,并把深度偏移(depth bias)设置成1(也就是贴图不透明)。最后我们把材质的光照关闭,这样不论场景使用何种光照,它总是显示的。如果你想在程序里让这个贴图受光照的影响,你就不必加上最后的函数调用:

      pass->setSceneBlending(SBT_TRANSPARENT_ALPHA);
      pass->setDepthBias(1);
      pass->setLightingEnabled(false);

接下来,新通路需要用我们的decal.png图像来创建一个新的纹理单元状态。第二个函数调用打开了投影纹理,并接收我们创建的平截头体。最后两个调用设置过滤和寻址模式:

      TextureUnitState *texState = pass->createTextureUnitState("decal.png");
      texState->setProjectiveTexturing(true, mDecalFrustum);
      texState->setTextureAddressingMode(TextureUnitState::TAM_CLAMP);
      texState->setTextureFiltering(FO_POINT, FO_LINEAR, FO_NONE);

我们已经把纹理的寻址模式设置成clamp,这样贴图就不会在物体上不停地循环了。对于过滤选项,当对象放大时,使用标准线性滤波,但对于缩小的情况,我们只是关闭过滤,并且完全关闭mip贴图。这样避免了当缩小时,贴图的边缘不能很好地融入纹理的其它部分。如果我们不这样做的话,贴图投影区域的边缘会非常的难看。

对于设置材质,以上是所有你要做的。

[编辑] 调用函数

我们已经建立好了函数,为了设置投影器和材质还必须调用它们。在createScene方法的最后面,加上以下代码:

      createProjector();
      for (unsigned int i = 0; i < ent->getNumSubEntities(); i++)
          makeMaterialReceiveDecal(ent->getSubEntity(i)->getMaterialName());

注意,在前面的循环里,ent变量已经保存有了Ogre头颅。由于所有的Ogre头颅使用相同的材质,我们只需要随机选择他们中的一个来获取材质名称。

[编辑] 消除反向投影

[编辑] 介绍

也许你已经注意到了,当运行程序里,存在两个投影贴图。一个是投射在-Z方向,也就是我们的平截头体的朝向,另一个投射在+Z方向,在我们创建的平截头体的后面的Ogre人头上。这个的原因是,当一个贴图从平截头体投影出去时,一个相应的(反向)贴图从它的背面投影出去。

这显然不是我们想要的。为了修正它,我们将引了一个过滤器来除去反向投影。

[编辑] 修改投影器

为了过滤掉反向投影,我们需要一个新的平截头体,让它指向我们想要过滤的方向。在createProjector方法中添加以下代码:

      mFilterFrustum = new Frustum();
      mFilterFrustum->setProjectionType(PT_ORTHOGRAPHIC);
      SceneNode *filterNode = mProjectorNode->createChildSceneNode("DecalFilterNode");
      filterNode->attachObject(mFilterFrustum);
      filterNode->setOrientation(Quaternion(Degree(90),Vector3::UNIT_Y));

这应该比较熟悉了。唯一的区别就是我们把这个节点旋转了90度以朝向背面。

[编辑] 修改材质

下面我们添加另一个纹理状态到材质的通路上。把以下代码加到makeMaterialReceiveDecal:

      texState = pass->createTextureUnitState("decal_filter.png");
      texState->setProjectiveTexturing(true, mFilterFrustum);
      texState->setTextureAddressingMode(TextureUnitState::TAM_CLAMP);
      texState->setTextureFiltering(TFO_NONE);

这都是非常熟悉的了。注意,我们正在使用过滤纹理,过滤平截头体,并关闭了过滤。编译并运行程序。你应该可以看到只有朝向前面的投影贴图。

[编辑] 炫耀一下投影

[编辑] 简单的旋转

为了炫耀一下这个投影,我们将旋转这个投影并更新它的视线范围(Field of View)。为了旋转这个投影器,只需要把下面几行代码添加到frameStarted方法里:

      mProjectorNode->rotate(Vector3::UNIT_Y, Degree(evt.timeSinceLastFrame * 10));

编译并运行程序。你将会看到贴图沿着一个圈被投影到Ogre人头。

[编辑] 修改视线范围

接下来,我们将修改这个投影器的视线范围(field of view)。由于我们不使用正射投影器,我们可以修改视力范围来增加或缩小投影的大小。作为演示,我们将把FOVy(field of view Y)设置成15到25度的夹角。下面的代码将增加或缩小贴图的大小(添加到frameStarted方法):

      mAnim += evt.timeSinceLastFrame / 2;
      if (mAnim >= 1)
          mAnim -= 1;

      mDecalFrustum->setFOVy(Degree(15 + Math::Sin(mAnim * Math::TWO_PI) * 10));

编译并运行程序。

[编辑] 最后要注意的

最后要注意到是,如果你在程序里使用贴图,必须保证贴图的边缘像素都是完全透明的(alpha为0)。否则,出于纹理clamping的工作机制,贴图会拖泥带水。

 

  • 中级教程七
资源与资源管理器

这篇文章的第一部分,将详细描述资源的载入、卸载和销毁的过程。第二部分中,我们将创建一个新的资源类型,以及一个相应的管理器。

目录

[隐藏]
  • 1 资源的生命周期
    • 1.1 术语
    • 1.2 从头开始创建资源
    • 1.3 资源卸载和销毁
    • 1.4 重新载入资源(reload)
  • 2 创建一个新的资源,并交付给资源管理器
    • 2.1 脚本装载器(ScriptLoader)
    • 2.2 手动资源装载器(ManualResourceLoader)
    • 2.3 使用

资源的生命周期

OGRE的API文档里介绍了一个资源的基本生命周期,但其中一些概念比较乏味。希望在这里,事情会更清楚。

术语

以下术语被用来区分一个资源载入时可能处于的不同阶段:

未知的(Unknown):OGRE不知道有这么一个资源。资源组里面保存有它的文件名,但OGRE还不知道它用来干什么的。

声明的(Declared):直接地,或其它动作引起的,资源已经被标记为可创建的。Ogre知道它是什么类型的资源,以及当创建它时,需要做些什么。

已创建(Created):OGRE已经为这个资源创建了一个空的实例,并添加到了相应的资源管理器。

已载入(Loaded):创建的实例被完全载入,资源的所有数据都驻留在内存里。这是资源文件能被实际访问的典型阶段。你不能在创建阶段访问这些文件。

从头开始创建资源

1. OGRE的原生资源管理器是在Root::Root里创建的。

2. 首先要做的是指定一个资源位置,它是通过调用ResourceGroupManager::addResourceLocation来完成的。这个方法做了一些事情:

    1.  创建指定的资源组(ResourceGroup),如果还没有创建的话。
    2.  创建新的指定类型的存档(Archive)实例。
    3.  创建新的资源位置(ResourceLocation),并把存档(Archive)添加给它,然后把这个资源位置(ResourceLocation)添加到资源管理组(ResourceGroup)。
    4.  最后一步,获取Archive里所有文件的列表,并把它们添加到ResourceGroup的列表中。这一步完成之后,资源就处于Unknown状态了。

3. 下一步是手动声明资源。虽然当ResourceManager开始解析脚本时,许多资源会被声明,但目前它们还没有被声明。如果你打算手动声明资源,调用ResourceGroupManager::declareResource方法。这样,所有手动声明了的资源,都处于Declared状态。其它仍然是Unkown。

4. 接下面初始化ResourceGroup,通过ResourceGroupManager::initialiseResourceGroup或者ResourceGroupManager::initialiseAllResourceGroups,后者只是对于所有的ResourceGroup来调用前者。它干了这么一些事情:

   1.  解析ResourceGroup里的所有脚本。脚本继承ScriptLoader,是由ResourceManager定义的。这可导致一些资源成为Declared。
   2.  创建所有的Declared资源。
   3.  相关的ResourceManager创建资源的一个新实例,并把它添加给自己。所有的资源都保存在ResourceManager里。
   4.  这个资源同样也被插入到“有序载入列表(ordered loading list)”里。这样使资源按照指定的顺序载入,如果你想要一次性载入整个资源组的话。一个资源的载入顺序在它的ResourceManager中指定。
   5.  目前,所有的Declared进入了Created阶段。

5. 我们完成初始化了。仍然没有资源被载入,但这是正常的,因为我们现在不使用。最后一步,从Created到Loaded,通过如下几种途径:

   1.  使用资源。比如,创建一个需要指定mesh的实体。显然,一旦一个资源以这种方式载入,如果另一个实体也需要它则不必再次载入。如果这个资源目前还是Unknown状态的,则它将被创建并且完全载入。
   2.  调用ResourceGroupManager::loadResourceGroup,所有Created资源将被载入。
   3.  相应的ResourceManager的load方法被调用。这可以用在载入那些还没有创建的资源,因为如果需要的话,它会自动地为你创建。
   4.  通过获取资源的指针,然后调用它的load方法,则资源将直接载入。当然,只有资源处于Created状态下,你才能这么做。
   5.  好了,被载入的资源可以直接使用了。

注意:如果你创建了自定义的资源管理器,你必须在手动声明资源之前初始化它们,否则可能会找不到它们的当中一些。

在你的程序里,这通常表现为如下的事件:

   1.  创建Root对象。
   2.  重复调用ResourceGroupManager::addResourceLocation,直到你已经添加了所有的资源位置。
   3.  创建所有自定义的ResourceManager对象,并通过调用ResourceGroupManager::_registerResourceManager注册它们。你可能还要注册ScriptLoader对象,通过调用ResourceGroupManager::_registerScriptLoader方法。
   4.  手动声明你要的任何资源,通过ResourceGroupManager::declareResource函数。
   5.  为你的资源组调用适当的初始化方法。对于单个组,调用ResourceGroupManager::initialiseResourceGroup,或者ResourceGroupManager::initialiseAllResourceGroups一口气初始化所有的。

资源卸载和销毁

ResourceManager::unload将一个资源从Loaded状态返回到Created。

想要完全移除资源,调用ResourceManager::remove。这样不论资源处于何种状态,都将打回Unknown状态。你可以通过ResourceManager::getByName来获取资源的指针,卸载或者移动它,如果你想这样做的话。

当一个资源管理器销毁时,所有存在的资源都会被移除。

重新载入资源(reload)

重新载入资源在编辑器里是非常有用的。本质上来说,资源先被卸载,然后再被载入。它从Loaded状态转到Created,然后再回到Loaded状态。资源必须处于Loaded状态,才能重新载入。

ResourceManager::reloadAll重新载入某一类型的所有资源。单个资源可以通过Resource::reload重新载入。

创建一个新的资源,并交付给资源管理器

现在我们知道了OGRE的资源系统的工作方式,创建一个新的资源类型实际上是非常简单的。你的程序几乎肯定要用到其它的资源,比如声音文件、XML或仅仅是普通文本。在这个例子里,我们将创建一个简单的文本装载器。这些代码都经过优美的划分,并且很容易扩展到其它的文件类型 -- 唯一需要改变的Resource::load方法以及TextFile资源的访问载入数据的公共接口。

有两种需要告诫的:脚本资源和手动资源装载器。这个例子也不会使用它们,但将作一些解释。

首先要创建的文件是TextFile.h。它声明了我们的资源,TextFile,并为它创建了一个共享指针的实现。如下:

   #ifndef __TEXTFILE_H__
   #define __TEXTFILE_H__
   
   #include 
   
   class TextFile : public Ogre::Resource
   {
       Ogre::String mString;
   
   protected:
   
       // must implement these from the Ogre::Resource interface
       void loadImpl();
       void unloadImpl();
       size_t calculateSize() const;
   
   public:
   
       TextFile(Ogre::ResourceManager *creator, const Ogre::String &name, 
           Ogre::ResourceHandle handle, const Ogre::String &group, bool isManual = false, 
           Ogre::ManualResourceLoader *loader = 0);
   
       virtual ~TextFile();
   
       void setString(const Ogre::String &str);
       const Ogre::String &getString() const;
   };
   
   class TextFilePtr : public Ogre::SharedPtr 
   {
   public:
       TextFilePtr() : Ogre::SharedPtr() {}
       explicit TextFilePtr(TextFile *rep) : Ogre::SharedPtr(rep) {}
       TextFilePtr(const TextFilePtr &r) : Ogre::SharedPtr(r) {} 
       TextFilePtr(const Ogre::ResourcePtr &r) : Ogre::SharedPtr()
       {
           // lock & copy other mutex pointer
           OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
               OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
               pRep = static_cast(r.getPointer());
           pUseCount = r.useCountPointer();
           if (pUseCount)
           {
               ++(*pUseCount);
           }
       }
   
       /// Operator used to convert a ResourcePtr to a TextFilePtr
       TextFilePtr& operator=(const Ogre::ResourcePtr& r)
       {
           if (pRep == static_cast(r.getPointer()))
               return *this;
           release();
           // lock & copy other mutex pointer
           OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
               OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
               pRep = static_cast(r.getPointer());
           pUseCount = r.useCountPointer();
           if (pUseCount)
           {
               ++(*pUseCount);
           }
           return *this;
       }
   };
   
   #endif

这有一个相应的cpp文件。我们使用一个简单string来保存数据,由此不需要特殊的初始化。如果你使用更复杂的对象,它们必须要正确地初始化。

   #include "TextFile.h"
   #include "TextFileSerializer.h"
   
   TextFile::TextFile(Ogre::ResourceManager* creator, const Ogre::String &name, 
                       Ogre::ResourceHandle handle, const Ogre::String &group, bool isManual, 
                       Ogre::ManualResourceLoader *loader) :
   Ogre::Resource(creator, name, handle, group, isManual, loader)
   {
       /* If you were storing a pointer to an object, then you would set that pointer to NULL here.
       */
   
       /* For consistency with StringInterface, but we don't add any parameters here
       That's because the Resource implementation of StringInterface is to
       list all the options that need to be set before loading, of which 
       we have none as such. Full details can be set through scripts.
       */ 
       createParamDictionary("TextFile");
   }
   
   TextFile::~TextFile()
   {
       unload();
   }
   
   // farm out to TextFileSerializer
   void TextFile::loadImpl()
   {
       TextFileSerializer serializer;
       Ogre::DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource(mName, mGroup, true, this);
       serializer.importTextFile(stream, this);
   }
   
   void TextFile::unloadImpl()
   {
       /* If you were storing a pointer to an object, then you would check the pointer here,
       and if it is not NULL, you would destruct the object and set its pointer to NULL again.
       */
   
       mString.clear();
   }
   
   size_t TextFile::calculateSize() const
   {
       return mString.length();
   }
   
   void TextFile::setString(const Ogre::String &str)
   {
       mString = str;
   }
   
   const Ogre::String &TextFile::getString() const
   {
       return mString;
   }

你可能注意到了在包含文件里有一个"TextFileSerializer"的引用。它是一个执行实际载入的工具类。它并不重要,尤其对于这种简单的资源,但它让我们序列化一个对象,而不用对Resource封装,尽管我们想这么做。基类Serializer包含了很多有用的工具函数。我们不使用它们,但会继承它们。

TextFileSerializer.h:

   #ifndef __TEXTSERIALIZER_H__
   #define __TEXTSERIALIZER_H__
   
   #include 
   
   class TextFile; // forward declaration
   
   class TextFileSerializer : public Ogre::Serializer
   {
   public:
       TextFileSerializer();
       virtual ~TextFileSerializer();
   
       void exportTextFile(const TextFile *pText, const Ogre::String &fileName);
       void importTextFile(Ogre::DataStreamPtr &stream, TextFile *pDest);
   };
   
   #endif

TextFileSerializer.cpp:

   #include "TextFileSerializer.h"
   #include "TextFile.h"
   
   TextFileSerializer::TextFileSerializer()
   {
   }
   
   TextFileSerializer::~TextFileSerializer()
   {
   }
   
   void TextFileSerializer::exportTextFile(const TextFile *pText, const Ogre::String &fileName)
   {
       std::ofstream outFile;
       outFile.open(fileName.c_str(), std::ios::out);
       outFile << pText->getString();
       outFile.close();
   }
   
   void TextFileSerializer::importTextFile(Ogre::DataStreamPtr &stream, TextFile *pDest)
   {
       pDest->setString(stream->getAsString());
   }

最后我们要写的类当然是TextFileManager。

TextFileManager.h:

   #ifndef __TEXTFILEMANAGER_H__
   #define __TEXTFILEMANAGER_H__
   
   #include 
   #include "TextFile.h"
   
   class TextFileManager : public Ogre::ResourceManager, public Ogre::Singleton
   {
   protected:
   
       // must implement this from ResourceManager's interface
       Ogre::Resource *createImpl(const Ogre::String &name, Ogre::ResourceHandle handle, 
           const Ogre::String &group, bool isManual, Ogre::ManualResourceLoader *loader, 
           const Ogre::NameValuePairList *createParams);
   
   public:
   
       TextFileManager();
       virtual ~TextFileManager();
   
       virtual TextFilePtr load(const Ogre::String &name, const Ogre::String &group);
   
       static TextFileManager &getSingleton();
       static TextFileManager *getSingletonPtr();
   };
   
   #endif

最后,TextFileManager.cpp

   #include "TextFileManager.h"
   
   template<> TextFileManager *Ogre::Singleton::ms_Singleton = 0;
   
   TextFileManager *TextFileManager::getSingletonPtr()
   {
       return ms_Singleton;
   }
   
   TextFileManager &TextFileManager::getSingleton()
   {  
       assert(ms_Singleton);  
       return(*ms_Singleton);
   }
   
   TextFileManager::TextFileManager()
   {
       mResourceType = "TextFile";
   
       // low, because it will likely reference other resources
       mLoadOrder = 30.0f;
   
       // this is how we register the ResourceManager with OGRE
       Ogre::ResourceGroupManager::getSingleton()._registerResourceManager(mResourceType, this);
   }
   
   TextFileManager::~TextFileManager()
   {
       // and this is how we unregister it
       Ogre::ResourceGroupManager::getSingleton()._unregisterResourceManager(mResourceType);
   }
   
   TextFilePtr TextFileManager::load(const Ogre::String &name, const Ogre::String &group)
   {
       TextFilePtr textf = getByName(name);
   
       if (textf.isNull())
           textf = create(name, group);
   
       textf->load();
       return textf;
   }
   
   Ogre::Resource *TextFileManager::createImpl(const Ogre::String &name, Ogre::ResourceHandle handle, 
                                               const Ogre::String &group, bool isManual, Ogre::ManualResourceLoader *loader, 
                                               const Ogre::NameValuePairList *createParams)
   {
       return new TextFile(this, name, handle, group, isManual, loader);
   }

脚本装载器(ScriptLoader)

有些资源比如材质,是从脚本载入的。在这种情况下,需要ResourceManager从ScriptLoader继承,然后在构造器里设置它的“脚本模式”-- 视为脚本的文件类型(*.material, *.compositor等)。再调用ResourceGroupManager::_registerScriptLoader来把自己注册成一个载入脚本的资源管理器。最后,调用ResourceGroupManager::initialiseResourceGroup时,所有注册了的脚本文件都将被解析。

如果你想要写一个载入脚本的资源管理器,就要从ScriptLoader继承,并实现parseScript方法。当执行ResourceGroupManager::initialiseResourceGroup时,要调用这个parseScript,在那里你应该声明所有你想创建的资源。

手动资源装载器(ManualResourceLoader)

当通过ResourceGroupManager::declareResource声明一个资源时,有一个可选的ManualResourceLoader。对于不是从文件载入的资源 -- 可能是程序创建的,可以使用ManualResourceLoader。这里有一个例子:

   // Do not add this to the project
   class ManualTextFileLoader : public Ogre::ManualResourceLoader
   {
   public:
   
      ManualTextFileLoader() {}
      virtual ~ManualTextFileLoader() {}
   
      void loadResource(Ogre::Resource *resource)
      {
          TextFile *tf = static_cast(resource);
          tf->setString("manually loaded");
      }
   };

如下为声明TextFile:

   // Do not add this to the project
   ManualTextFileLoader *mtfl = new ManualTextFileLoader;
   Ogre::ResourceGroupManager::getSingleton ().declareResource("hello.txt", "TextFile", "General", mtfl);

使用

为了使用我们的资源管理器,要在调用Root::initialise或任何ResourceGroup之前创建一个它的实例。

   TextFileManager *tfm = new TextFileManager();

我们关闭时要销毁它 -- 当然是在销毁Ogre::Root对象之前。OGRE不会自己销毁它,这是你的职责:

   delete Ogre::ResourceGroupManager::getSingleton()._getResourceManager("TextFile");

最后让我们来看一看例子。创建一个称为“hello.txt”的文件,放置到Ogre能找到的media文件夹。由于没有放置自定义脚本的目录,我建议把文件把在"media/materials/scripts"。text文件内容如下:

   Hello world!

创建一个main.cpp的文件,添加如下代码。请仔细看一下createScene方法,看看我们在里面做了些什么:

   #include 
   
   #include "TextFileManager.h"
   #include "TextFileSerializer.h"
   
   class TutorialApplication : public ExampleApplication
   {
   private:
       TextFileManager *mTFM;
   public:
       TutorialApplication()
           : mTFM(0)
       {
       }
   
       ~TutorialApplication()
       {
           delete mTFM;
       }
   
       void setupResources()
       {
           mTFM = new TextFileManager();
   
           // hello.txt will be created when initialiseResourceGroup is called
           ResourceGroupManager::getSingleton().declareResource("hello.txt", "TextFile");
           ExampleApplication::setupResources();
       }
   
       void createScene(void)
       {
           // Load the file, get the data
           TextFilePtr textfile = mTFM->load("hello.txt", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
           String str = textfile->getString();
   
           // Reload the file
           textfile->reload();
   
           // export the file
           TextFileSerializer serializer;
           serializer.exportTextFile(static_cast(textfile.getPointer()), "hello.out.txt");
   
           // unload/remove the file
           mTFM->unload("hello.txt");
           mTFM->remove("hello.txt");
       }
   };
   
   #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
       TutorialApplication app;
   
       try {
           app.go();
       } catch(Exception& e) {
   #if OGRE_PLATFORM == PLATFORM_WIN32 || 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;
   }

如果你用调试器进入到这个方法里,你将看到通过调用getString的确返回了hello.txt的内容。虽然目前这没什么意义,但你能轻松地扩展它,并创建你自己的资源和资源装载器

 

  • 中级教程八
示例框架揭秘

示例框架揭秘初学者教程0:场景背后的秘密——示例框架揭秘

目录

[隐藏]
  • 1 介绍
  • 2 示例框架
  • 3 示例程序
    • 3.1 基础骨架
    • 3.2 一步接一步
    • 3.3 让我们开始!
    • 3.4 初始化
    • 3.5 添加资源定位
    • 3.6 初始化Ogre核心
    • 3.7 创建场景管理器
    • 3.8 创建摄影机镜头
    • 3.9 创建视口
    • 3.10 创建资源监听器
    • 3.11 初始化资源
    • 3.12 创建场景
    • 3.13 销毁场景
  • 4 添加帧监听器
    • 4.1 什么是帧监听器
    • 4.2 它的工作方式
    • 4.3 增加代码
  • 5 增加Ogre基本输入
  • 6 移动摄影机镜头
    • 6.1 移动摄影机镜头
    • 6.2 OgreApplication additions
    • 6.3 帧开始函数
    • 6.4 修改createFrameListener
    • 6.5 处理非缓冲输入更新
  • 7 其它各种改进
  • 8 最终代码
    • 8.1 OgreApplication.h
    • 8.2 OgreApplication.cpp
  • 9 示例用法
    • 9.1 MyApplication.h
    • 9.2 MyApplication.cpp

介绍

如果你看看Ogre演示程序的代码,并读读Wiki教程,你可能注意到了ExampleApplication和ExampleFramelistener的使用。你可能会奇怪:示例框架在场景后面都做了写什么事情呢?对于Ogre编程来说,可以肯定的是要比

ExampleApp theApp;
theApp.go();

这点儿代码多得多,并且重载了createScene()函数。本教程将准确描述示例框架的工作方式。我们将通过框架实现我们的工作——并以包含所有示例框架代码的可以替代的Ogre应用程序类结束。注意:如果那你特别特别想拷贝粘贴代码,请选择正确的代码——在最最后的代码:-) 我们将逐步完成我们的工作,向“OgreStein类”中增加代码。注意2:如果你要使用Mac OS X,[这里有一些MacOSX命令行编译的提示 http://www.ogre3d.org/wiki/index.php/MacOsXCommandLineCompiling],可供参考。注意3:如果由于对OIS的改变而使你在Eihort(Ogre 1.4.X)下编译这个框架遇到任何麻烦,论坛上的[这个帖子 http://www.ogre3d.org/phpBB2/viewtopic.php?t=31659]可能会有所帮助。

示例框架

示例框架包括两个类:ExampleApplication(在ExampleApplication.h中)和ExampleFramelistener(在ExampleFramelistener.h中)。它们都位于Samples/Include目录下,用于Ogre演示程序之中。事实上,此框架被设计用来快速编写演示程序,不是一个大型设计项目。可能无法满足你想要的更大更严谨的设置使用要求。但是,它节省了大量的时间。而且它可以让你非常快地做一些Ogre测试。一个使用示例框架的应用程序以一个派生于ExampleApplication的类开始。

class GroovieDemo : public ExampleApplication
{
	// details ...
};

即意味着,所有ExampleApplication能做的事,GroovieDemo也都能做。 GroovieDemo需要定义ExampleApplication::createScene()函数,因为它是一个纯虚函数,所以GroovieDemo变成:

class GroovieDemo : public ExampleApplication
{
	virtual void createScene()
	{
		// details ...
	}
};

然后,你只要创建GroovieDemo实例,调用它的go()函数就行了。这样你就得到了最基本的Ogre演示程序。但是...ExampleApplication实际做了什么呢?

示例程序

这就是所发生的事:

go()
   setup()
      new Ogre::Root
      setupResources()
         // parses resources.cfg
      configure()
         // shows the Ogre config dialog which configures the render system.
         // constructs a render window.
      chooseSceneManager();
         // the scenemanager decides what to render
      createCamera();
         // we need a camera to render from
      createViewports();
         // a viewport to render to
      createResourceListener();
         // Create any resource listeners (for loading screens)
      loadResources();
         // Now we can load the resources: all systems are on-line.
      createScene();
         // Now that the system is up and running: create a scene to render.
   mRoot->startRendering();
   // Kick off Ogre

就像你看到的,它先创建一个Ogre::Root实例。然后解析resources.cfg,显示配置对话框,装载渲染系统,构造渲染窗口,选择场景管理器,创建摄影机镜头和视口,装载资源并开始渲染。

基础骨架

创建一个文件,取名OgreApplication.h并粘贴如下代码:

#ifndef __OgreApplication_h__
#define __OgreApplication_h__

#include 

using namespace Ogre;

class OgreApplication
{
public:
   OgreApplication();
   virtual ~OgreApplication();

   virtual void go();

protected:
   virtual bool initialise();
   virtual bool initOgreCore();

   virtual void createSceneManager();
   virtual void createCamera();
   virtual void createViewports();
   virtual void createResourceListener();

   virtual void addResourceLocations();
   virtual void initResources();

   virtual void createScene() = 0; // I am pure virtual, override me!
   virtual void destroyScene();

   Root *mRoot;
   Camera* mCamera;
   SceneManager* mSceneMgr;
   RenderWindow* mWindow;
};
 
#endif // __OgreApplication_h__

你会注意到一件事,就是某些函数命名不同于示例框架。那是因为新函数名更准确反映函数的功能。好了。这就是我们的OgreApplication所需要的东西,所以让我们开始吧!

一步接一步

好了。让我们创建一个新文件,取名为OgreApplication.cpp,并粘贴如下代码:

#include "OgreApplication.h"

//-------------------------------------------------------------------------------------
OgreApplication::OgreApplication()
 : mRoot(0)
{
}

//-------------------------------------------------------------------------------------
OgreApplication::~OgreApplication()
{
   delete mRoot;
}

如你所见,mRoot在构造函数中被赋为空值,并在析构函数中被删除。接下来,我们将向此文件增加代码。

让我们开始!

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::go()
{
   if (!initialise())
      return;

   mRoot->startRendering();

   // clean up
   destroyScene();
}

这段代码通过调用'initialise()'启动Ogre程序。如果initialise()失败,则程序返回,游戏结束。然后它调用mRoot上的startRendering()。最后,destroyScene()被调用做收尾工作,像清除我们在createScene()中创建的那一大堆东西。下面的是Root::startRendering()所做的工作:

void Root::startRendering(void) 
{ 
   assert(mActiveRenderer != 0); 

   mActiveRenderer->_initRenderTargets(); 

   // Clear event times 
   for(int i=0; i!=3; ++i) 
      mEventTimes[i].clear(); 

   // Infinite loop, until broken out of by frame listeners 
   // or break out by calling queueEndRendering() 
   mQueuedEnd = false; 

   while( !mQueuedEnd ) 
   { 
      //Allow platform to pump/create/etc messages/events once per frame 
      mPlatformManager->messagePump(mAutoWindow); 

      if (!renderOneFrame()) 
         break; 
   } 
}

它初始化渲染目标,清除事件时间,开始渲染循环,直至通过调用queueEndRendering()将mQueueEnd被置为false才停止。后文有关于Root::renderOneFrame()的更详细介绍。

初始化

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
bool OgreApplication::initialise()
{
   mRoot = new Root();

   // add resource locations
   addResourceLocations();

   // if we cannot initialise Ogre, just abandon the whole deal
   if ( !initOgreCore() ) return false;

   createSceneManager();
   createCamera();
   createViewports();

   // Set default mipmap level (NB some APIs ignore this)
   TextureManager::getSingleton().setDefaultNumMipmaps(5);

   // Create any resource listeners (for loading screens)
   createResourceListener();

   // Initialise resources
   initResources();

   // Create the scene
   createScene();

   return true;
};

这是示例程序的释义。创建Ogre::Root。解析并添加资源定位。显示配置屏(initOgreCore)。创建在前一步窗口中所选定的渲染系统。创建场景管理器。创建并设置摄影机镜头。创建视口并与摄影机镜头挂钩。解析并装载资源。创建场景。

添加资源定位

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::addResourceLocations()
{
   // Load resource paths from config file
   ConfigFile cf;
   cf.load("resources.cfg");

   // Go through all sections & settings in the file
   ConfigFile::SectionIterator seci = cf.getSectionIterator();

   String secName, typeName, archName;
   while (seci.hasMoreElements())
   {
      secName = seci.peekNextKey();
      ConfigFile::SettingsMultiMap *settings = seci.getNext();
      ConfigFile::SettingsMultiMap::iterator i;
      for (i = settings->begin(); i != settings->end(); ++i)
      {
         typeName = i->first;
         archName = i->second;
         ResourceGroupManager::getSingleton().addResourceLocation(archName, typeName, secName);
      }
   }
}

此函数简单地解析了resources.cfg并添加了资源定位。其中使用了Ogre::ConfigFile类。

初始化Ogre核心

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
bool OgreApplication::initOgreCore()
{
   // Show the configuration dialog and initialise the system
   // You can skip this and use root.restoreConfig() to load configuration
   // settings if you were sure there are valid ones saved in ogre.cfg
   if(mRoot->restoreConfig() || mRoot->showConfigDialog())
   {
      // If returned true, user clicked OK so initialise
      // Here we choose to let the system create a default rendering window by passing 'true'
      mWindow = mRoot->initialise(true);
      return true;
   }
   else
   {
      return false;
   }
}

此函数完成下面两件事之一:如果Root::restoreConfig()成功,则意味着当前有可用的配置,并将该信息应用于配置Ogre。如果没有可用配置,则显示配置对话框。你会问了,这个Ogre核心又是怎么初始化的呢?它所做的全部工作都显示在配置对话框中了,还是还有些东西发生在场景背后呢?是的,你说的对。配置对话框先调用Root::restoreConfig()。然后调用Root::getRenderSystem()。 Root::getAvailableRenderers()返回一个可用渲染器的列表(惊奇吧!) RenderSystem::getConfigOptions()返回一个带有可用渲染系统选项的ConfigOptionMap。 RenderSystem->setConfigOption(value, value)用来设置选项。例如:

selectedRenderSystem->setConfigOption("Full Screen","No");  
selectedRenderSystem->setConfigOption("Video Mode","800 x 600 @ 16-bit colour");
selectedRenderSystem->setConfigOption("Allow NVPerfHUD","No");
selectedRenderSystem->setConfigOption("Anti aliasing","None");
selectedRenderSystem->setConfigOption("Floating-point mode","Fastest");
selectedRenderSystem->setConfigOption("Rendering Device","RADEON 9200");
selectedRenderSystem->setConfigOption("VSync","No");

RenderSystem::validateConfigOptions()被用来在调用Root::setRenderSystem(selectedRenderSystem)之前审核配置选项。然后,Root保存配置:Root::saveConfig() 当这些完成时,就可以调用Root::initialise了。例如:

RenderWindow* Root::initialise(bool autoCreateWindow, const String& windowTitle)
{
   if (!mActiveRenderer)
     OGRE_EXCEPT(Exception::ERR_NO_RENDERSYSTEM_SELECTED,
     "Cannot initialise - no render "
     "system has been selected.", "Root::initialise");

   if (!mControllerManager)
      mControllerManager = new ControllerManager();

   mAutoWindow =  mActiveRenderer->initialise(autoCreateWindow, windowTitle);

   mResourceBackgroundQueue->initialise();

   if (autoCreateWindow && !mFirstTimePostWindowInit)
   {
      oneTimePostWindowInit();
      mAutoWindow->_setPrimary();
   }

   // Initialise timer
   mTimer->reset();

   // Init plugins
   initialisePlugins();

   mIsInitialised = true;

   return mAutoWindow;

}

Ogre初始化的例子,还可以参看本Ogre Wiki的其它部分。

创建场景管理器

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::createSceneManager()
{
   // Create the SceneManager, in this case a generic one
   mSceneMgr = mRoot->createSceneManager(ST_GENERIC);
}

要求Root创建一个通用场景管理器的实例。这里我们选择通过一个SceneType创建场景管理器实例,并且忽略表示实例名的第二个参数。在Ogre中,我们可以创建许多场景管理器的实例。每个摄影机属于一个场景管理器实例,所以在不同的场景管理器上创建多个摄影机令我们只要通过改变活动的摄影机就能够在它们之间转换。我们也可以选择像这样创建场景管理器:

mSceneMgr = mRoot->createSceneManager("DefaultSceneManager");

如果我们想要多个场景管理器,我们需要传递实例名:

mSceneMgr = mRoot->createSceneManager("DefaultSceneManager", "MainOgreApplicationInstance");

要获取一个场景管理器实例可以通过Ogre::Root或Ogre::Camera:

mCamera->getSceneManager("MainOgreApplicationInstance");

或者

mRoot->getSceneManager("MainOgreApplicationInstance");

创建摄影机镜头

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::createCamera()
{
   // Create the camera
   mCamera = mSceneMgr->createCamera("PlayerCam");

   // Position it at 500 in Z direction
   mCamera->setPosition(Vector3(0,0,500));
   // Look back along -Z
   mCamera->lookAt(Vector3(0,0,-300));
   mCamera->setNearClipDistance(5);
}

要求场景管理器创建一个名为“PlayerCam”的摄影机镜头。然后,用一些默认值配置摄影机镜头。这可能是一个你将要在自己的程序中重载的函数。

创建视口

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::createViewports()
{
   // Create one viewport, entire window
   Viewport* vp = mWindow->addViewport(mCamera);
   vp->setBackgroundColour(ColourValue(0,0,0));

   // Alter the camera aspect ratio to match the viewport
   mCamera->setAspectRatio(Real(vp->getActualWidth()) / Real(vp->getActualHeight()));
}

创建资源监听器

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::createResourceListener()
{

}

这里你可以创建自定义的资源组监听器,诸如:装载进度条之类的。看看Samples/Common下的ExampleLoadingBar,你会注意到它派生自ResourceGroupListener。 ResourceGroupListener在资源组装载过程中接受回调,详见API文档。

初始化资源

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::initResources()
{
   // Initialise, parse scripts etc
   ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
}

这里将所有资源组一步全都初始化了。如果你愿意,也可以手动初始化它们。 ExampleLoadingBar在Ogre完全配置完成之前初始化“Bootstrap”资源组,这是因为他需要OgreCore.zip中的媒体文件。

ResourceGroupManager::getSingleton().initialiseResourceGroup("Bootstrap");

创建场景

因为此函数为纯虚函数,所以我们再次不定义它。稍后,当我们从OgreApplication派生时,我们需要处理它。

销毁场景

向OgreApplication.cpp中增加如下代码:

//-------------------------------------------------------------------------------------
void OgreApplication::destroyScene()
{
}

此函数什么都不做,但是它必须定义。意思是,假如OgreApplication类的用户们不想在其后进行清除工作,它们可以选择不重载它。

添加帧监听器

至此我们的OgreApplication是相当有限的。它建立了Ogre系统并开始渲染,仅此而已。如果我们有能力每帧执行任务,像回应输入,更新逻辑等等,它其实还可以更棒。幸运的是,Ogre有一个帧监听器。

什么是帧监听器

如果你看过OgreFrameListener.h,你会发现下面这段:

struct FrameEvent
{
   Real timeSinceLastEvent;
   Real timeSinceLastFrame;
};


class _OgreExport FrameListener
{
   /*
   Note that this could have been an abstract class, but I made
   the explicit choice not to do this, because I wanted to give
   people the option of only implementing the methods they wanted,
   rather than having to create 'do nothing' implementations for
   those they weren't interested in. As such this class follows
   the 'Adapter' classes in Java rather than pure interfaces.
   */
public:
   virtual bool frameStarted(const FrameEvent& evt) { return true; }
   virtual bool frameEnded(const FrameEvent& evt) { return true; }

   virtual ~FrameListener() {}

};

意思就是,只要我们从FrameListener派生并重载两个函数:frameStarted和frameEnded,就可以给我们的OgreApplication增加帧监听支持。注意:如果我们不想重载,我们可以不重载那些函数。但是如果我们不重载其中一个,派生自FrameListener就毫无意义。

它的工作方式

还记得Root::startRendering()中的renderOneFrame函数吗?这就是:

bool Root::renderOneFrame(void)
{
   if(!_fireFrameStarted())
      return false;

   _updateAllRenderTargets();

   return _fireFrameEnded();
}

_fireFrameStarted()和_fireFrameEnded()是我们最感兴趣的。让我们来看看_fireFrameStarted:

bool Root::_fireFrameStarted(FrameEvent& evt)
{
   // Increment frame number
   ++mCurrentFrame;

   // Remove all marked listeners
   std::set::iterator i;
   for (i = mRemovedFrameListeners.begin();
      i != mRemovedFrameListeners.end(); i++)
   {
      mFrameListeners.erase(*i);
   }
   mRemovedFrameListeners.clear();

   // Tell all listeners
   for (i= mFrameListeners.begin(); i != mFrameListeners.end(); ++i)
   {
      if (!(*i)->frameStarted(evt))
         return false;
   }

   return true;

}

就在返回true前,它遍历所有注册的帧监听器,并调用它们的frameStarted(evt)。好了,现在我们知道他是如何工作的了。让我们增加一些代码。

增加代码

看看更新过的OgreApplication.h:

#ifndef __OgreApplication_h__
#define __OgreApplication_h__

#include 

using namespace Ogre;

class OgreApplication : public FrameListener
{
public:
   OgreApplication(void);
   virtual ~OgreApplication(void);

   virtual void go(void);

protected:
   virtual bool initialise();
   virtual bool initOgreCore();

   virtual void createSceneManager();
   virtual void createCamera();
   virtual void createViewports();
   virtual void createResourceListener();
   virtual void createFrameListener();

   virtual void addResourceLocations();
   virtual void initResources();

   virtual void createScene() = 0; // I am pure virtual, override me!
   virtual void destroyScene();

   // FrameListener overrides 
   virtual bool frameStarted(const FrameEvent& evt); 
   virtual bool frameEnded(const FrameEvent& evt); 

   Root *mRoot;
   Camera* mCamera;
   SceneManager* mSceneMgr;
   RenderWindow* mWindow;
};
 
#endif // __OgreApplication_h__

注意OgreApplication现在如何从FrameListener派生,又如何重载两个帧监听器函数。另外,还要注意新函数:createFrameListener()。现在我们需要在OgreApplication.cpp中增加函数定义:

//-------------------------------------------------------------------------------------
bool OgreApplication::frameStarted(const FrameEvent& evt)
{
   return true;
}

//-------------------------------------------------------------------------------------
bool OgreApplication::frameEnded(const FrameEvent& evt)
{
   return true;
}

//-------------------------------------------------------------------------------------
void OgreApplication::createFrameListener()
{
   mRoot->addFrameListener(this);
}

createFrameListener函数向Root注册了帧监听器,传递this是因为OgreApplication就是一个帧监听器。如果想让我们的frameStarted和frameEnded函数每帧调用,我们需要注册我们的帧监听器。现在,我们需要回到我们的OgreApplication::initialise()函数,增加对createFrameListener的调用。

   // Create the scene
   createScene();

   createFrameListener();

   return true;
};

我们还未向帧监听器函数中增加任何代码。我们先要向我们的OgreApplication增加输入!

增加Ogre基本输入

让我们增加一些基本输入。提示:输入分为非缓冲输入和缓冲输入。如果希望详细了解可以参考基础教程4:帧监听器和非缓冲输入,以及基础教程五:缓冲输入。

//-------------------------------------------------------------------------------------
bool frameStarted(const FrameEvent& evt)
{
	mKeyboard->capture();
	mMouse->capture();
	
	if(processUnbufferedKeyInput(evt)==false)
		return false;
	if(processUnbufferedMouseInput(evt)==false)
		return false;
}

移动摄影机镜头

至此输入系统准备完毕,可以让它发挥作用了:移动我们的摄影机镜头。

移动摄影机镜头

向OgreApplication.h中增加如下函数声明:

virtual void moveCamera();

向OgreApplication.cpp中增加如下函数定义:

//-------------------------------------------------------------------------------------
void OgreApplication::moveCamera()
{
   // Make all the changes to the camera
   // Note that YAW direction is around a fixed axis (freelook style) rather than a natural YAW (e.g. airplane)
   mCamera->yaw(mRotX);
   mCamera->pitch(mRotY);
   mCamera->moveRelative(mTranslateVector);
}

mRotX和mRotY是旋转摄影机镜头的,而mTranslateVector是移动摄影机镜头的。

OgreApplication additions

我们需要一些成员变量:

Real mMoveSpeed; 
Degree mRotateSpeed; 
float mMoveScale; 
Degree mRotScale; 

把它们添加在OgreApplication.h中的mTranslateVector和鼠标旋转变量附近。这些变量被用来计算移动速度:mMoveScale等于mMoveSpeed乘以自上一帧开始经过的时间,mRotScale等于mRotateSpeed乘以自上一帧开始经过的时间。

帧开始函数

让我们修改一下frameStarted:

// If this is the first frame, pick a speed
if (evt.timeSinceLastFrame == 0)
{
   mMoveScale = 1;
   mRotScale = 0.1;
}
// Otherwise scale movement units by time passed since last frame
else
{
   // Move about 100 units per second,
   mMoveScale = mMoveSpeed * evt.timeSinceLastFrame;
   // Take about 10 seconds for full rotation
   mRotScale = mRotateSpeed * evt.timeSinceLastFrame;
}

这应该被添加在frameStarted的开始处。如果这是第一帧,变量被设置为固定值。移动范围是移动速度乘以自上一帧开始经过的时间。现在我们需要在frameStarted函数中放置一个moveCamera()调用。将moveCamera()添加在frameStarted末尾,正好在return true;之前。

moveCamera();
return true;

修改createFrameListener

在OgreApplication.cpp中的OgreApplication::createFrameListener添加下面这些:

mRotateSpeed = 36;
mMoveSpeed = 100;
mMoveScale = 0.0f;
mRotScale = 0.0f;

是的,它们可能已经在OgreApplication构造函数中已经初始化过了,但是……(我还是要这么做,他是一个需要完成的部分。) mRotateSpeed和mMoveSpeed的值对于大多数场景都工作良好。

处理非缓冲输入更新

向OgreApplication.cpp中的OgreApplication::processUnbufferedInput添加如下代码:

if (mInputDevice->isKeyDown(KC_A))
{
   // Move camera left
   mTranslateVector.x = -mMoveScale;
}

if (mInputDevice->isKeyDown(KC_D))
{
   // Move camera RIGHT
   mTranslateVector.x = mMoveScale;
}

/* Move camera forward by keypress. */
if (mInputDevice->isKeyDown(KC_UP) || mInputDevice->isKeyDown(KC_W) )
{
   mTranslateVector.z = -mMoveScale;
}

/* Move camera backward by keypress. */
if (mInputDevice->isKeyDown(KC_DOWN) || mInputDevice->isKeyDown(KC_S) )
{
   mTranslateVector.z = mMoveScale;
}

if (mInputDevice->isKeyDown(KC_PGUP))
{
   // Move camera up
   mTranslateVector.y = mMoveScale;
}

if (mInputDevice->isKeyDown(KC_PGDOWN))
{
   // Move camera down
   mTranslateVector.y = -mMoveScale;
}

if (mInputDevice->isKeyDown(KC_RIGHT))
{
   mCamera->yaw(-mRotScale);
}

if (mInputDevice->isKeyDown(KC_LEFT))
{
   mCamera->yaw(mRotScale);
}

这是常规WASD移动,附带方向键移动和为了摄影机纵向移动的向上、向下翻页。改变大部分由mTranslateVector和mMoveScale完成。左右方向键会通过mRotScale摆动摄影机。

其它各种改进

待续

最终代码

以下是完整的基本程序框架:

OgreApplication.h

[OgreApplication.h http://www.ogre3d.org/wiki/index.php/OgreApplication_h]

OgreApplication.cpp

[OgreApplication.cpp http://www.ogre3d.org/wiki/index.php/OgreApplication_cpp] 使用提示:如果你不想使用OgreApplication的子类,并想在你的程序中直接使用它,你需要实现OgreApplication.cpp中的createScene,并将声明中的=0去掉,使之仅为虚函数而不是纯虚函数。

示例用法

别忘了你可以任意重载函数。注意你的程序也是一个帧监听器,所以你可以重载frameStarted而不必将什么东西传给什么,因为它全发生在同一个类中。你将你的类从OgreApplication派生出来,并重载了OgreApplication::createScene()。你也可以重载其它函数,那是可选的。

MyApplication.h

#ifndef __MyApplication_h__
#define __MyApplication_h__

#include "OgreApplication.h"

class MyApplication : public OgreApplication
{
public:
   MyApplication(void);
   virtual ~MyApplication();

protected:
   virtual void createScene();
   virtual bool frameStarted(const FrameEvent& evt);

   Entity* ogreHead;
   SceneNode* headNode;
};

#endif // #ifndef __MyApplication_h__

MyApplication.cpp

#include "MyApplication.h"

//-------------------------------------------------------------------------------------
MyApplication::MyApplication() : ogreHead(0), headNode(0)
{
}
//-------------------------------------------------------------------------------------
MyApplication::~MyApplication()
{
}
//-------------------------------------------------------------------------------------
void MyApplication::createScene()
{
	ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");

	headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
	headNode->attachObject(ogreHead);

	// Set ambient light
	mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));

	// Create a light
	Light* l = mSceneMgr->createLight("MainLight");
	l->setPosition(20,80,50);
}
//-------------------------------------------------------------------------------------
bool MyApplication::frameStarted(const FrameEvent& evt)
{
    // Just a silly example to demonstrate how much easier this is than passing objects to an external framelistener
    headNode->translate(0.0f, 0.005f, 0.0f);

    return OgreApplication::frameStarted(evt);
}
//-------------------------------------------------------------------------------------

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif

#ifdef __cplusplus
extern "C" {
#endif

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char *argv[])
#endif
{
   // Create application object
   MyApplication app;

   SET_TERM_HANDLER;

   try
   {
      app.go();
   }
   catch( Ogre::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
      std::cerr << "An exception has occured: " << e.getFullDescription().c_str() << std::endl;
#endif
   }

   return 0;
}

#ifdef __cplusplus
}
#endif

 

 

 

 

  • 中级教程九
深入CEGUI

 

出自Ogre3D开放资源地带

跳转到: 导航, 搜索

我想,迟到总比没到好。(译者:作者这句话意思大约是说,这篇文章虽然发布的晚了些,但是总比完全没有CEGUI这方面的教程好。)

目录

[隐藏]
  • 1 CEGUI
    • 1.1 UI的渲染
    • 1.2 输入系统和CEGUI
    • 1.3 CEGUI数据文件
  • 2 编码
    • 2.1 数据
    • 2.2 接下来,看编码
      • 2.2.1 初始化CEGUI
      • 2.2.2 CEGUI的输入支持
  • 3 结束语
  • 4 下载这些代码

CEGUI

在本文中我们将准备把CEGUI整合到实际开发项目中。

这个版本的代码将告别VC71项目,并采用VC8(Visual C++2005)项目。实际上,自我工作以来,一直到最近我都一直使用VC71。那些使用VC71的朋友应该有足够的经验把VC71的项目转移到VC8上----这是非常简单直接的事情。

这个例子的代码是基于Ogre1.4.x版本的----另外,如果你使用Ogre1.2.x版本,你可能会发现下面的例子是需要修改一些问题才能顺利运行的。这是因为这1.4.x版本的Ogre消息泵(message pump)有一些变动,另外OIS库本身也有些更变了。总之,下面的例子代码最好能够使用OgreSDK1.4.1进行编译测试。

CEGUI是一个2D的UI渲染库。它支持大部分渲染引擎,不仅支持Ogre,也同样的支持OpenGL和Irrlicht引擎.

它同时又是基于XML脚本文件格式应用的一个重量级库。当然,你可以在你的代码中定义所有的UI信息,但如果你在使用XML文件进行UI文件的定义也会非常简单,CEGUI也能够在运行时对这些XML文件进行读取和分析。CEGUI本身有一些例子和支持这些例子的资源。你可以看一看这些例子中是如何使用XML脚本文件,如何进行UI单元的定义的。也可以去看看例子中的 .layout 文件和 .xsd文件,这些会对你学习CEGUI有所帮助。当然,你可能需要在自己的程序中自定义一些UI组件。但是,很遗憾,自定义组件已经出了本文的讲解范围。请到CEGUI官方获得更多信息。

UI的渲染

CEGUI在一些四方形网格上渲染它的UI单元,并将它们渲染到一个“屏幕空间”中。那么,将意味着这些承载着UI单元的四方形网格的整个绘制过程是完全不接受任何视角变换和投影变换影响的。这一条是任何2D UI系统运行的基准方法。

CEGUI通过“OgreGUIRenderer”类与Ogre进行结合。这个类所在文件存放在Ogre的“Samples”文件夹下,然而却定义在“CEGUI”命名空间内。OgreGUIRenderer简单的取得了CEGUI中的一些绘制命令,并将这些命令转译成Ogre中的网格和材质数据,并将其放置到"层渲染队列(Overlay render queue)"中,通过这种手段Ogre就可以对其进行绘制了.同时,这样可以使UI在我们的渲染场景中永远处于近屏顶端。

在我们的程序代码中,我们除了提供一个OgreGUIRenderer类的实体对象给CEGUI,其他的渲染相关部分我们不用操心。

输入系统和CEGUI

CEGUI本身没有任何的输入处理,如果我们程序中需要对不同的输入数据进行处理,那么我们必须为它提供整套的输入事件处理系统。

这样做有一些好处。其中之一就是你不需要担心UI库会和你的应用程序争夺输入数据的处理权。此外,你可以自由的完全的把CEGUI从“输入循环”中移除出来,并不给它任何输入数据信息。你同样也不需要依赖于任何的第三方IO库和API,你可以自由的选择输入设备:OIS,SDL,DirectInput,Win32,XWindows作为输入库都可以,完全的取决于你。

在本文中我们将使用OIS做为CEGUI的输入处理系统。

CEGUI数据文件

说起来,你可能能够在代码中完美的进行你的UI层的定义。但是用以下几种XML文件做为CEGUI的配置方法将会更加简单和高效。这些文件包括”scheme”, ”looknfeel”, ”layout”, ”imagesets”和CEGUI显示使用的文字”font”。

Scheme 定义着在同一个配置下不同的有效UI元件。例如. 我们程序中需要使用的一些按钮Buttons,下拉选菜单ListBoxes等等UI元件都能够在 .scheme 格式的文件中找到。
Look-And-Feel 定义着每个需要显示的UI元件的各种属性,包括了它的纹理,颜色,状态属性等。我们可以查看 .looknfeel文件。
Layout 定义了UI元件的父层级关系,位置,大小等与其他UI元件可能联系到的属性。我们可以查看 .layout 文件。
Imageset 定义了一种配置下的纹理,UV值等信息,我们可以查看 .imageset 文件。
font 显而易见。CEGUI需要知道我们程序中使用的文字纹理类型,纹理清晰度等信息这些都记录在 . font 文件中。

在本文的附件中有个gui.zip文件,你能够在里面找到上述所有文件的一些例子。认真查看每个类型的文件,虽然它们都是XML格式的文件,注意它们各自的格式以及包含了UI的什么信息。如果你依旧看不明白,可以去CEGUI的官方站点看看对它们功能的描述。

本文将使用”TcharazLookSkin”这套配置。(Tcharaz是创建这个配置的一个人名,他创建了这个配置的.scheme, .look-and-feel, .texture和.imageset)

编码

好了,介绍的足够多了,现在我们看看实际编码。如果你坚持看了Ogre的这系列教程例子,你应该很熟悉如何创建一个按下Esc就退出的Ogre渲染窗口的框架了。在这个例子中,我们用一种主菜单的面版来替代以前的空窗口框架。这个主菜单包含三个按钮,但是我只打算让Quit这个按钮进行事件响应处理。我们除了使用ESC键之外,我们还可以通过Quit按钮退出本程序。不知道通过之前的讲解你是否能明白,CEGUI通过别的输入系统获得用户输入事件后能够将其事件都反映出来,就如本例,我们可以在自己的代码中对鼠标点击事件做出响应。一会你从代码中就能轻易的明白如何在自己的代码中Hook到CEGUI的事件行为。注意:我们下面的代码操作都是基于C++的,如果你对希望在脚本(例如Lua或其他脚本引擎)中Hook到CEGUI的事件行为,希望你能到CEGUI官方去看看如何做到。

数据

首先,我们来看一下CEGUI中至关重要的数据文件---Layout文件。你可以通过CEGUI的Layout编辑器来生成和编辑该文件,但是在本例中下面这个XML格式的 .layout 文件完全是我手动编写出来的,实际上很简单。在Ogre的Debug文件夹下resource目录下有个压缩包是gui.zip,你在里面会发现一个文件叫”Katana.layout”,下面是该文件的全部内容:

 
   
        
       
       
       
       
          
          
       
       
          
          
       
       
          
          
       
   

你在浏览这些代码的时候,首先可能发现里面那些神秘的数字。在CEGUI 0.5.x版本中,UI元件需要定义的是UI元件的绝对尺寸大小。而在这个layout文件中,我们使用的是统一的相对尺寸范围。Value =”” 中定义了元件的左,右,上,下点的位置。但这个位置我们既可以定义这些点的绝对位置,也可以是定义这些点针对其父容器UI元件的相对位置。例如,下面这个UI元件的区域定义:

{{0.4,0},{0.5,0},{0.6,0},{0.57,0}}

这些数值告诉了CEGUI,这个UI元件在其上一层元件容器中的相对位置和大小。

如本例,这个元件左上角将距离其上一层元件容器的左上角的右偏移距离为 (0.4 * 上一层元件容器宽度),这个元件左上角将距离其上一层元件容器的左上角的下偏移距离为 (0.5 * 上一层元件容器高度)。该UI元件宽度将为((0.6 – 0.4)* 上一层元件宽度 )大小,高度将为( (0.57 – 0.5) * 上一层元件告诉 )大小。

那么,那些参数中的”0”是什么意义呢?如果你想同时指定UI元件的绝对象素位置的话,可以把这些”0”替换为该元件的象素位置。所以这就意味着,你能够根据自己喜好,为UI元件设置相对坐标或者象素的绝对坐标。我想不起来指定象素绝对坐标时这些数值的意义,如果谁看到本例并且知道这些绝对坐标数值意义的话,请帮助补充到本文中。:)不过,如果谁是曾经做过网页的话,将会非常熟悉这些绝对坐标的意义和顺序,因为这俩完全是一个东西。

(补充:译者推荐在游戏制作中使用相对坐标,这样的话在对窗口缩放或对单一UI窗口缩放时会容易的避免一些问题。)

在上面的这个Layout文件中,第一行告诉CEGUI,我们想创建一个顶层的窗口”Main”,而且这个窗口大小是占满整个程序显示区----它的相对宽高是(1.0 - 0.0 )。我们不希望看到它的标题栏,也不希望看到这个顶层窗口右上边的关闭按钮和其边框。于是,下面我们设置他们的Value为false。(这三项是FrameWindow框架窗口默认存在的捆绑控件)。

接下来,我们在屏幕中放置三个按钮----这样的话,在我们运行程序的时候将会从上到下见到“Instant Action”“Options”“Quit”三个按钮。按钮控件的名称属性是非常重要的,这是我们在代码中获取该控件行为的唯一标识,这点我们一会就会见到。

接下来,看编码

我将要重点说明在代码中整合CEGUI时有什么更变。首先,在main.cpp中,需要额外加一些头文件。

// 需要能够创建CEGUI渲染接口

#include "OgreCEGUIRenderer.h"

// CEGUI 的头文件

#include "CEGUISystem.h"
#include "CEGUIInputEvent.h"
#include "CEGUIWindow.h"
#include "CEGUIWindowManager.h"
#include "CEGUISchemeManager.h"
#include "CEGUIFontManager.h"
#include "elements/CEGUIFrameWindow.h"

我们将创建GUI渲染器的一个实例,所以我们需要包含OgreCEGUIRenderer.h这个头文件。你可以在OgreSDK或Ogre源代码的Samples/include文件夹下找到该文件。

而上面代码中包含的CEGUI头文件都能够在OgreSDK的include/CEGUI文件夹下找到。(在安装OgreSDK时这些通常被自动设置为默认包含头文件目录了)。

如果你在编译时候这里出错,请保证同时添加了包含了这两个目录”include/CEGUI”和”include”。

你也可以不包含” elements/CEGUIFrameWindow.h”,它仅仅是老版本的CEGUI中残留的一个习惯,新版本的CEGUI中已经可以不包含它了。不过包含的话也是没什么坏处的。

初始化CEGUI

Main.cpp

// 在有一个有效的场景管理器和Windows窗口后,我们可以创建一个GUI渲染器
CEGUI::OgreCEGUIRenderer* pGUIRenderer = new CEGUI::OgreCEGUIRenderer
{
    Window,     // 这个渲染目标窗口应该在之前创建好,CEGUI将会在这个窗口上进行渲染。
     Ogre::RENDER_QUEUE_OVERLAY,      // CEGUI将会在这个渲染队列中进行渲染工作
     False;     // 渲染队列的排列方式。False代表将一切放置在渲染队列最前,而非最后。
     3000,      // 这个参数在现在的CEGUI中已经是不必须的了—--它指定UI的最大范围。
     sceneMgr,  // 用这个场景管理器对UI进行管理
};
// 创建一个根CEGUI类
CEGUI::System* pSystem = new CEGUI::System(pGUIRenderer);
// 设置Log层级,Log中能够提供我们一些Ogre运行的信息,CEGUI.log文件在工作目录下
CEGUI::Logger::getSingleton().setLoggingLevel(CEGUI::Informative);
// 使用指定的CEGUI配置(关于配置更多的信息可以查看CEGUI的文档)
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme", (CEGUI::utf8*)"GUI");
// 使用指定的CEGUI的鼠标图标 (鼠标信息被定义在look-n-feel中)
pSystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow");
// 指定在UI控件中使用的文字字体
CEGUI::FontManager::getSingleton().createFont("Tahoma-8.font", (CEGUI::utf8*)"GUI");
pSystem->setDefaultFont((CEGUI::utf8*)"Tahoma-8");
// 从 .layout脚本文件读取一个UI布局设计,并将其放置到GUI资源组中。(本例中的.layout文件您可以从 resource/gui.zip 中找到。)
CEGUI::Window* pLayout = CEGUI::WindowManager::getSingleton().loadWindowLayout("katana.layout", "", "GUI");
// 接下来我们告诉CEGUI显示哪份UI布局。当然我们可以随时更换显示的UI布局。
pSystem->setGUISheet(pLayout);

上面的代码中有大量的注释了,所以这里我不再重复太多。值得注意的是,我们在创建CEGUI渲染器时需要使用”Ogre渲染窗口对象” 和一个”场景管理器”,所以我们之前必须创建好这两个对象。

CEGUI的输入支持

我们若是需要修改InputHandle类中的一些input消息来源,则需要一些额外的参数:一个指向我们创建的CEGUI::System实例的一个指针。因为我们需要通过InputHandler将一些外界的消息压入CEGUI消息队列中。 Input.cpp

// 鼠标监听
bool InputHandler::mouseMoved(const OIS::MouseEvent &evt) 
{
     m_pSystem->injectMouseWheelChange(evt.state.Z.rel);
     return m_pSystem->injectMouseMove(evt.state.X.rel, evt.state.Y.rel);
}
bool InputHandler::mousePressed(const OIS::MouseEvent &evt, OIS::MouseButtonID btn) 
{
     CEGUI::MouseButton button = CEGUI::NoButton;
     if (btn == OIS::MB_Left)
           button = CEGUI::LeftButton;
     if (btn == OIS::MB_Middle)
           button = CEGUI::MiddleButton;
     if (btn == OIS::MB_Right)
           button = CEGUI::RightButton;
     return m_pSystem->injectMouseButtonDown(button);
}
bool InputHandler::mouseReleased(const OIS::MouseEvent &evt, OIS::MouseButtonID btn) 
{
     CEGUI::MouseButton button = CEGUI::NoButton;
     if (btn == OIS::MB_Left)
           button = CEGUI::LeftButton;
     if (btn == OIS::MB_Middle)
           button = CEGUI::MiddleButton;
     if (btn == OIS::MB_Right)
           button = CEGUI::RightButton;
     return m_pSystem->injectMouseButtonUp(button);
}
// 键盘监听
bool InputHandler::keyPressed(const OIS::KeyEvent &evt) 
{
     unsigned int ch = evt.text;
     m_pSystem->injectKeyDown(evt.key);
     return m_pSystem->injectChar(ch);
}
bool InputHandler::keyReleased(const OIS::KeyEvent &evt) 
{
     if (evt.key == OIS::KC_ESCAPE)
            m_simulation->requestStateChange(SHUTDOWN);
     return m_pSystem->injectKeyUp(evt.key);
}

注意:如果用户输入过快,那么injectChar或injectKeyUp时可能会出现字符消息丢失的可能,此时上列函数可能会返回false值。所以,若我们希望上面的函数长期返回true值,尽量使用按键消息缓冲。

代码中很清楚明确的说明了如何把input事件通知给CEGUI ---- 就是那几个”inject***”的函数调用而已。(这些函数定义才CEGUI::System中)。你同样可以在别的地方进行这几个函数调用。我把它们放在input handler类中是因为这样很方便,并且完全不用担心Input程序是否在休息,也无需去知道它低层实现的细节。---- 总之,它将数据通过某种处理后通知我们的UI使其处理某种行为。 Main.cpp

// InputHandler处理CEGUI的input事件后,我们需要获得它的一个指针,并将给CEGUI::System的一个实例使用。
InputHandler *handler = new InputHandler(pSystem, sim, hWnd);
// put us into our "main menu" state
sim->requestStateChange(GUI);

我们在上面代码中同时把”SIMULATION”状态替换成了”GUI”状态。一般情况下,我们程序中应当在直接进入游戏前提供一个主菜单,通过这个主菜单我们进行游戏状态的变换。所以,我们需要写一个主菜单类专门进行游戏状态的变换,它需要获取UI的信息并进行相应的处理。在本例中,我仅简单的提供一个框架说明这个流程,并为真正的对游戏状态进行变化,所以直接写在main.cpp中了。

// 创建一个主菜单实例
MainMenuDlg* pDlg = new MainMenuDlg(pSystem, pLayout, sim);

在本例中,这个MainMenuDlg的行为处理类在下面的MainMenuDlg.h / .cpp中有声明和定义。

MainMenuDlg.h

#pragma once
#include "CEGUIWindow.h"
namespace CEGUI
{
    class System;
    class Window;
}
class Simulation;
class MainMenuDlg
{
public:
      MainMenuDlg(CEGUI::System* pSystem, CEGUI::Window* pSheet, Simulation* pSimulation);
      ~MainMenuDlg();
      // CEGUI事件处理函数说明:函数的命名随便,但是你必须这样声明bool (const CEGUI::EventArgs &args)
      bool Quit_OnClick(const CEGUI::EventArgs &args);
      bool Options_OnClick(const CEGUI::EventArgs &args);
      bool Launch_OnClick(const CEGUI::EventArgs &args);
private:
      CEGUI::System* m_pSystem;    // 一个CEGUI::System实例的指针
       CEGUI::Window* m_pWindow;    // 指向一个版面的窗口指针
       Simulation* m_pSimulation;   // 指向一个仿真管理器的指针
};

MainMenuDlg.cpp

#include "MainMenuDlg.h"
#include "Simulation.h"
#include "CEGUISystem.h"
#include "CEGUIWindow.h"
#include "CEGUIWindowManager.h"
#include "elements/CEGUIPushButton.h"
MainMenuDlg::MainMenuDlg(CEGUI::System *pSystem, CEGUI::Window *pSheet, Simulation *pSimulation)
{
      m_pSystem = pSystem;
      m_pWindow = pSheet;
      m_pSimulation = pSimulation;
      // 钩住窗口元件的事件处理函数
       CEGUI::PushButton* pQuitButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdQuit");
       pQuitButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Quit_OnClick, this));
       CEGUI::PushButton* pOptionsButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdOptions");
       pOptionsButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Options_OnClick, this));
       CEGUI::PushButton* pLaunchButton = (CEGUI::PushButton *)CEGUI::WindowManager::getSingleton().getWindow("cmdInstantAction");
       pLaunchButton->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&MainMenuDlg::Launch_OnClick, this));
}
MainMenuDlg::~MainMenuDlg()
{
}
bool MainMenuDlg::Quit_OnClick(const CEGUI::EventArgs &args)
{
       m_pSimulation->requestStateChange(SHUTDOWN);
       return true;
}
bool MainMenuDlg::Launch_OnClick(const CEGUI::EventArgs &args)
{
       return true;
}
bool MainMenuDlg::Options_OnClick(const CEGUI::EventArgs &args)
{
       return true;
}

这里有两个重要的事情需要说明一下:(a)如何让事件处理函数钩取到CEGUI的事件。(b)在例子中,我们处理了”Quit”按钮按下的消息(象按下ESC键一样处理,让程序关闭)。如果你有兴趣,可以在上面的代码里对Launch_OnClick和Options_OnClick函数补充使其进行实际的消息处理,也可以为更多的GUI元件事件消息进行处理。

结束语

实际上,也没有更多的事情可做了。---- 何去创建你的UI事件处理,如何将Input事件压入CEGUI都已经告诉你了。你可以用这里的代码,也可以自己去写,但是基础的这些东西是没有变化的。关于CEGUI和Ogre确实也没有更多的可说的了 ---- 你只需要在自己的代码中对其他的一些UI元件事件进行hook后处理就可以了。

下载这些代码

我使用Ogre1.4.x和VC8把这些代码做了整理和测试。你可以在这里下载这些代码。请注意,我将编译后的可执行文件输出到 $(OGRE_HOME)/bin/Debug 文件夹下了,你可以自行修改其目录。同时,你要保证编译出的可执行文件目录下有个”resource”目录,你还需要将gui.zip拷贝到这个目录下。当然,在DEBUG模式下,你可以设置工作目录。另外注意的是,我仅对项目的Debug模式做了设置,所以,如果你想在Release模式下运行,你还需要自己配置一下编译环境。好运。

你可能感兴趣的:(OGRE)