先决条件
本教程假定你已经拥有了c++程序设计的知识,并且已经安装和编译了一个Ogre的应用程序(如果你在设置你的应用程序中有困难,请参考this guide获得更详细的编译步骤)。这个教程同时也是建立在上一章基础上的,因此默认你已经了解了上个教程的内容。
[编辑]介绍
这一章我们将介绍Ogre中最有用的构造:帧监听(FrameListener)。在本指南最后你将了解帧监听,怎样运用帧监听去实现一些要求每一帧更新的东西,怎样去用Ogre的无缓冲输入系统。
代码你都可以在这篇指南中找到。当你看完这篇教程以后,你应该试着慢慢的添加一些代码到你自己的工程里,然后看看结果。
[编辑]从这开始
像上一个教程一样,我们将使用一个先前建立的代码作为我们出发的起点。在编译器中创建一个工程,添加如下的源代码:
#include "ExampleApplication.h" class TutorialFrameListener : public ExampleFrameListener { public: TutorialFrameListener(RenderWindow* win, Camera* cam, SceneManager *sceneMgr) : ExampleFrameListener(win, cam, false, false) { } bool frameStarted(const FrameEvent &evt) { return ExampleFrameListener::frameStarted(evt); } protected: bool mMouseDown; // Whether or not the left mouse button was down last frame Real mToggle; // The time left until next toggle Real mRotate; // The rotate constant Real mMove; // The movement constant SceneManager *mSceneMgr; // The current SceneManager SceneNode *mCamNode; // The SceneNode the camera is currently attached to }; class TutorialApplication : public ExampleApplication { public: TutorialApplication() { } ~TutorialApplication() { } protected: void createCamera(void) { } void createScene(void) { } void createFrameListener(void) { } }; #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 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; }
如果你是在Windows下使用OgreSDK的,请确定添加“"[OgreSDK_DIRECTORY]/samples/include”目录到这个工程(ExampleApplication.h文件所在的位置)除了标准包含以外。如果使用的是Ogre的源代码包,这个文件应该在“"[OgreSource_DIRECTORY]/Samples/Common/include”目录中。不要试着去运行程序,因为我们还没有定义键盘的行为。如果你有问题,请查看这个Wiki页获得设置你编译器的信息,如果仍然有问题请试着去看看帮助栏。
我们将在这章中定义程序控制器。
[编辑]帧监听
[编辑]介绍
在前面的指南中当我们添加代码到创建场景方法时候,我们仅仅考虑我们可以做什么。在Ogre中我们可以注册一个类去接收消息当一帧被渲染到屏幕之前和之后。FrameListener接口定义两个函数:
bool frameStarted(const FrameEvent& evt) bool frameEnded(const FrameEvent& evt)
Ogre的主循环(Root::startRendering)类似这样:
- Root object调用frameStarted方法在所有已经注册的FrameListeners中。
- Root object渲染一帧。
- Root object调用frameEnded方法在所有已经注册的FrameListeners中。
这个循环直到有任意一个FrameListener的frameStarted或者frameEnded函数返回false时终止。这些函数返回值的主要意思是“保持渲染”。如果你返回否,这个程序将退出。FrameEvent实体包含两个参数,但是只有timeSinceLastFrame在帧监听中是有用的。这个变量了解frameStarted或者frameEnded最近被激活时间。注意在frameStarted方法中,FrameEvent::timeSinceLastFrame将包含frameStarted时间最近被激活的时间(而不是最近被激活的frameEnded方法)。
一个重要的概念 你不能决定那个FrameListener被调用的次序。如果你需要确定FrameListeners被调用的准确顺序,你应该只注册一个FrameListener,然后用适当的顺序来调用所有的物体。
你应该也会注意到主循环事实上做了三件事,在frameEnded方法和frameStarted方法被调用中间什么都没有发生,你甚至可以换着用。你的代码也可以随你放哪。你可以放到一个大的frameStarted或者frameStarted方法中,也可以插到两者之间。
[编辑]注册一个帧监听
现在上面的代码是可以编译通过的,但是因为我们没有考虑在ExampleApplication和createCamera中创建帧监听,因此如果你运行这个程序你就无法结束她。在继续本指南其他内容之前我们将先解决这个问题。
找到TutorialApplication::createCamera方法添加如下代码:
// create camera, but leave at default position mCamera = mSceneMgr->createCamera("PlayerCam"); mCamera->setNearClipDistance(5);
除了最基本的之外我们什么都没有做。之所以我们要用ExampleApplication中的createCamera方法是因为createCamera方法可以用来移动相机和改变相机位置,这里我们详细讨论这个。
因为Root类是要每帧更新的,他也了解FrameListeners。我们首先要创建TutorialFrameListener的实例,然后把他注册到Root对象中。代码如下:
// Create the FrameListener mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr); mRoot->addFrameListener(mFrameListener);
mRoot和mFrameListener变量是在ExampleApplication类中定义的。addFrameListener方法添加一个帧监听者,removeFrameListener方法移除帧监听者(也就是说FrameListener就不用在更新了)。注意add|removeFrameListener方法仅仅是得到一个指向FrameListener的指针(也就是说FrameListener并没有一个名字你可以移除她)。这就意味着你需要对每一个FrameListener持有一个指针,让你随后可以移除他们。
这个ExampleFrameListener(我们的TutorialFrameListener是从它继承来的),还提供了一个showDebugOverlay(bool)方法,用来告诉ExampleApplication是否要在左下角显示帧率的提示框。我们会把它打开:
// Show the frame stats overlay mFrameListener->showDebugOverlay(true);
确定你可以编译和运行这个应用程序再继续。
[编辑]建立场景
[编辑]介绍
在我们对代码的深入研究之前,我将简单介绍一下我将做什么,这样你可以了解当我们创建和添加一些东西到场景中去的目的。
我们将把一个物体(一个忍者)放到屏幕中,并在场景中加上点光源。当你点击鼠标左键,灯光会打开或关闭。按住鼠标右键,则开启“鼠标观察”模式(也就是你用摄像机四处观望)。我们将在场景里放置场景节点,来让作用于不同视口的摄像机附在上面。按下1、2键,以选择从哪一个视口来观看场景。
[编辑]代码
找到TutorialApplication::createScene方法。首先我们需要把场景的环境光设置的很低。我们想要场景中物体在灯光关闭情况下仍然可见,而且我们仍然想做到在灯光开关时场景有所变化:
mSceneMgr->setAmbientLight(ColourValue(0.25, 0.25, 0.25));
现在把一个忍者加到屏幕中去:
Entity *ent = mSceneMgr->createEntity("Ninja", "ninja.mesh"); SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode"); node->attachObject(ent);
现在我们创建一个白色灯让后放到场景中,和忍者有一个相对小距离:
Light *light = mSceneMgr->createLight("Light1"); light->setType(Light::LT_POINT); light->setPosition(Vector3(250, 150, 250)); light->setDiffuseColour(ColourValue::White); light->setSpecularColour(ColourValue::White);
现在需要创建场景节点供摄像机绑定:
// Create the scene node node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode1", Vector3(-400, 200, 400)); node->yaw(Degree(-45)); node->attachObject(mCamera); // create the second camera node node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode2", Vector3(0, 200, 400));
现在我们在TutorialApplication类里做得差不多了。再到TutorialFrameListener里去...
[编辑]帧监听指南
[编辑]变量
在我们走得更远之前,还是先介绍一下TutorialFrameListener类里的一些变量:
bool mMouseDown; // 鼠标左键是否在上一帧被按下 Real mToggle; // 直到下一次触发的时间 Real mRotate; // 滚动常量 Real mMove; // 移动常量 SceneManager *mSceneMgr; // 当前场景管理器 SceneNode *mCamNode; // 当前摄像机所附在的场景节点
mSceneMgr拥有一个目前SceneManager的指针,mCamNode拥有目前SceneNode并且绑定着摄像机。mRotate和mMove是旋转和移动的变量。如果你想移动或旋转的更快或更慢,改变这两个变量的大小就可以了。 另外两个变量(mToggle和mMouseDown)控制我们的输入。我们将在本章中使用“非缓冲”(unbuffered)鼠标和键盘输入(“缓冲”(buffered)输入将是下一章的议题。这意味着我们将在帧监听器查询鼠标和键盘状态时调用方法。
当我们试图用键盘来改变屏幕中物体的状态时,会遇到一些有趣的问题。如果知道一个键正被按下,我们会去处理它,但到了下一帧怎么办?我们还会看见它被按下,然后再重复地做操作吗?在一些情况(比如用方向键来移动),我们是这样做的。然而,我们想通过“T”键来控制灯的开关时,在第一帧里按下T键,拨动了灯的开关,但在下一帧T键仍然被按着,所以灯又打开了...如此返复,直到按键被释放。我们必须在帧与帧之间追踪这些按键状态,才能避免这个问题。我将介绍两种不同的方法来解决它。
mMouseDown用来追踪鼠标在上一帧里是否也被按下(所以如果mMouseDown为真,在鼠标释放之前我们不会做同样的操作)。mToggle指定了直到我们可以执行下一个操作的时间。即,当一个按键被按下去了,在mToggle指明的这段时间里不允许有其它动作发生。
[编辑]构造函数
关于这个构造函数首先需要注意的是,我们要调用ExampleFrameListener的构造函数: 首先我们要继承ExampleFrameListener的构造函数。
: ExampleFrameListener(win, cam, false, false)
一个需要注意的是,第三和第四个参数要置为false。第三个参数指明是否使用带缓冲的键盘输入,第四个参数指明是否使用带缓冲的鼠标输入(我们在本课都不使用)。
在TutorialFrameListener的构造函数中, 我们把所有的变量设置为默认值:
// 键盘和鼠标状态追踪 mMouseDown = false; mToggle = 0.0; // Populate the camera and scene manager containers mCamNode = cam->getParentSceneNode(); mSceneMgr = sceneMgr; // 设置旋转和移动速度 mRotate = 0.13; mMove = 250;
好了。mCamNode变量被初始化成摄像机的任何当前父节点。
[编辑]帧启动方法
现在,我们到了教程最核心的部分:在每一帧里执行动作。目前我们的frameStarted方法里有如下代码:
return ExampleFrameListener::frameStarted(evt);
这块代码非常重要。ExampleFrameListener::frameStarted方法定义了许多行为(像所有的按键绑定、所有的摄像机移动等)。清空TutorialFrameListener::frameStarted方法。
开放输入系统(OIS)提供了三个主要的类来获得输入:Keyboard, Mouse, 和 Joystick 。在教程里,我们只有涉及如何使用Keyboard和Mouse对象。倘若你对在Ogre里使用摇杆感兴趣,请查阅Joystick类。
我们使用无缓冲输入时,要做的第一件事情就是获取当前鼠标键盘的状态。为了达到目的,我们调用Mouse和Keyboard对象的capture方法。示例框架已经帮我们创建了这些对象,分别在mMouse和mKeyboard变量里。将以下代码添加到目前还是空的TutorialFrameListener::frameStarted成员方法里。
mMouse->capture(); mKeyboard->capture();
接下来,我们要保证当Escape键被按下时程序退出。我们通过调用InputReader的isKeyDown方法并指定一个KeyCode,来检查一个按钮是否被按下。当Escape键被按下时,我们只要返回false到程序末尾。
if(mKeyboard->isKeyDown(OIS::KC_ESCAPE)) return false;
为了能继续渲染,frameStarted方法必须返回一个正的布尔值。我们将在方法最后加上这么一行。
return true;
所有我们将要讨论的代码都在最后的这一行"return true"之上。
我们使用FrameListener来做的第一个事情就是,让鼠标左键来控制灯的开关。通过调用InputReader的getMouseButton方法,并传入我们要查询的按钮,我们能够检查这个按钮是否被按下。通常0是鼠标左键,1是右键,2是中键。在某些系统里1是中键、2是右键。如果按键不能很好的工作,可尝试这样设置:
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
如果鼠标被按下,currMouse变量设为true。现在我们拨动灯开关,依赖于currMouse是否为true,同时在上一帧中鼠标没有被按下(因为我们只想每次按下鼠标时,只让灯开关被拨动一次)。同时注意Light类里的setVisible方法决定了这个对象实际上是否发光:
if (currMouse && ! mMouseDown) { Light *light = mSceneMgr->getLight("Light1"); light->setVisible(! light->isVisible()); } // if
现在,我们把mMouseDown的值设置成与currMouse变量相同。下一帧里,它会告诉我们上次是否按下了鼠标按键。
mMouseDown = currMouse;
编译运行程序。现在可以左击鼠标控制灯的开关!注意,因为我们不再调用 ExampleFrameListener的frameStarted方法,我们不能转动摄像头(目前)。
这种保存上一次鼠标状态的方法非常好用,因为我们知道我们已经对为鼠标状态做了动作。
这样做的缺点就是你要为每一个被绑定动作的按键设置一个布尔变量。一种能避免这种情况的方法是,跟踪上一次点击任何按键的时刻,然后只允许经过了一段时间之后才触发事件。我们用mToggle变量来跟踪。如果mToggle大于0,我们就不执行任何动作,如果mToggle小于0,我们才执行动作。我们将用这个方法来进行下面两个按键的绑定。
首先我们要做的是拿mToggle变量减去从上一帧到现在所经历的时间。
mToggle -= evt.timeSinceLastFrame;
现在我们更新了mToggle,我们能操作它了。接下来我们将“1”键绑定为摄像机附在第一个场景节点上。在这之前,我们检查mToggle变量以保证它是小于0的:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1)) {
现在我们需要设置mToggle变量,使得在下一个动作发生时至少相隔一秒钟:
mToggle = 0.5f;
接下来,我们将摄像机从当前附属的节点取下来,然后让mCamNode变量含有"CamNode1"节点,再将摄像机放上去。
mCamera->getParentSceneNode()->detachObject(mCamera); mCamNode = mSceneMgr->getSceneNode("CamNode1"); mCamNode->attachObject(mCamera); }
当按下2键时,对于CamNode2我们也是这么干的。除了把1换成2,if改成else if,其它代码都是相同的:
else if ((mToggle < 0.0f) && mKeyboard->isKeyDown(OIS::KC_2)) { mToggle = 0.5f; mCamera->getParentSceneNode()->detachObject(mCamera); mCamNode = mSceneMgr->getSceneNode("CamNode2"); mCamNode->attachObject(mCamera); }
编译并运行。我们通过按1、2键,能够转换摄像机的视口。
我们的下一个目标就是,当用户按下方向键或W、A、S、D键时,进行mCamNode的平移。
与上面的代码不同,我们不必追踪上一次移动摄像机的时刻,因为在每一帧按键被按下时我们都要进行平移。这样一来我们的代码会相对简单,首先我们来创建一个Vector3用于保存平移的方位:
Vector3 transVector = Vector3::ZERO;
好的,当按下W键或者上箭头时,我们希望前后移动(也就是Z轴,记住Z轴负的方向是指向屏幕里面的):
if (mKeyboard->isKeyDown(OIS::KC_UP) || mKeyboard->isKeyDown(OIS::KC_W)) transVector.z -= mMove;
对于S键和下箭头键,我们基本上也是这么做的,只不过方向是往正Z轴:
if (mKeyboard->isKeyDown(OIS::KC_DOWN) || mKeyboard->isKeyDown(OIS::KC_S)) transVector.z += mMove;
对于左右方向移动的,我们是在X的正负方向进行的:
if (mKeyboard->isKeyDown(OIS::KC_LEFT) || mKeyboard->isKeyDown(OIS::KC_A)) transVector.x -= mMove; if (mKeyboard->isKeyDown(OIS::KC_RIGHT) || mKeyboard->isKeyDown(OIS::KC_D)) transVector.x += mMove;
最后,我们还想沿着Y轴进行上下移动。我个人喜欢用E键或者PageDown键进行向下移动,用Q键或者PageUp键进行向上移动:
if (mKeyboard->isKeyDown(OIS::KC_PGUP) || mKeyboard->isKeyDown(OIS::KC_Q)) transVector.y += mMove; if (mKeyboard->isKeyDown(OIS::KC_PGDOWN) || mKeyboard->isKeyDown(OIS::KC_E)) transVector.y -= mMove;
好了,我们的transVector变量保存了作用于摄像机场景节点的平移。这样做的第一个缺点是如果你旋转了场景节点,再作平移的时候我们的x、y、z坐标就不对了。为了修正它,我们必须把作用在场景节点的旋转,也作用于我们的平移。这实际上比听起来更简单。
为了表示旋转,Ogre不是像一些图形引擎那样使用变换矩阵,而是使用四元组。四元组的数学意义是一个较难理解的四维线性代数。幸好,你不必去了解它的数字知识,了解如何使用就行了。非常简单,用四元组旋转一个向量只要将它们两个相乘即可。现在,我们希望将所有作用于场景节点的旋转,也作用在平移向量上。通过调用SceneNode::getOrientation(),我们能得到这些旋转的四元组表示,然后用乘法让它作用在平移节点。
还有一个缺陷要引起注意的是,我们必须根据从上一帧到现在的时间,来对平移的大小进行缩放。否则,你的移动速度取决于应用程序的帧率。这确实不是我们想要的。这里有一个函数供我们来调用,可以避免平移摄像机时带来的问题:
mCamNode->translate(transVector*evt.timeSinceLastFrame,Node::TS_LOCAL);
好了,我们来介绍一些新的东西。当你对一个节点进行平移,或者是绕某个轴进行旋转,你都能指定使用哪一个“变换空间”来移动它。一般你移动一个对象时,不需要指定这个参数。它默认是TS_PARENT,意思是这个对象使用的是父节点所在的变换空间。在这里,父节点是场景的根节点。当我们按下W键时(向前移动),我们走向Z轴负的方向。如果我们不在代码前面指明TS_LOCAL,摄像机都会向全局负Z轴移动。然而,既然我们希望按下W键时摄像机往前走,我们就需要它往节点所面朝的方向走。所以,我们使用“本地”变换空间。
还有另一种方式我们也能实现(尽管不是怎么直接)。我们获取节点的朝向,一个四元组,用方向向量乘以它,能得到相同的结果。这完全是合法的:
// Do not add this to the program mCamNode->translate(mCamNode->getOrientation()*transVector*evt.timeSinceLastFrame, Node::TS_WORLD);
这同样也在本地空间里进行移动。在这里,实际上没这样做的必要。Ogre定义了三种变换空间:TS_LOCAL, TS_PARENT, 和TS_WORLD。也许存在其它的场合,你需要使用另外的向量空间来进行平移或旋转。若真是这样,你可以像上面的代码那样做。找到一个表示向量空间的四元组(或者你要操作的物体的朝向),用平移向量去乘它,得到一个正确的平移向量,然后用它在TS_WORLD空间里移动。目前应该不会遇到这样的情况,我们后面的教程也不会涉及这些内容。
好了,我们有了键盘控制的移动。我们还想实现一个鼠标效果,来控制我们观察的方向,但这只有在鼠标右键被按住的情况下。为了达到目的,我们首先要检查右键是否被按下去:
if (mMouse->getMouseState().buttonDown(OIS::MB_Right)) {
如果是,我们根据从上一帧开始鼠标移动的距离来控制摄像机的俯仰偏斜。为了达到目的,我们取得X、Y的相对改变,传到pitch和yaw函数里去:
mCamNode->yaw(Degree(-mRotate * mMouse->getMouseState().X.rel), Node::TS_WORLD); mCamNode->pitch(Degree(-mRotate * mMouse->getMouseState().Y.rel), Node::TS_LOCAL); }
注意,我们使用TS_WORLD向量坐标来进行偏斜(如果不指明的话,旋转函数总是用TS_LOCAL作为默认值)。我们要保证物体随意的俯仰不影响它的偏斜,我们希望总是绕着固定的轴进行偏斜转动。如果我们把yaw设置成TS_LOCAL,我们就能得到这样的:
http://www.idleengineer.net/images/beginner04_rot.png
编译程序并运行。
本课不是关于旋转和四元组的完整教程(那样的话会构成另一系列的教程)。下一课里,我们将使用带缓冲的输入,而不用在每一帧里都检查按键是否被按下。