英语水平有限,欢迎大家批评指正
本文并没有将原文全部翻译,只是将其中的一些知识点翻译总结了一下,想要查看详细讲解的话,可以到原文处看一下,附上英文原文地址:http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Basic+Tutorial+4&structure=Tutorials
Terrain
Introduction
在OGRE中,我们可以注册一个类在一帧被渲染到屏幕前后来接收消息。该类就是FrameListener。FrameListener接口声明了三个函数用来接收帧事件:
virtual bool frameStarted(const FrameEvent& evt);//帧被渲染前调用
virtual bool frameRenderingQueued(const FrameEvent& evt);//在所有渲染目标的渲染命令放出之后,渲染窗口把他们的缓存翻过来之前调用。
virtual bool frameEnded(const FrameEvent& evt);//帧被渲染后调用
它一直循环直到有一个FrameListener的frameStarted或frameRenderingQueued或frameEnded函数返回false。这些函数的返回值基本上都意味着“继续渲染”。如果返回false,程序将退出。
FrameEvent对象包含两个变量,但只有timeSinceLastFrame对FrameListener有用。它记录了上一次frameStarted或frameEnded开始执行到现在的时间。注意在frameStarted方法中,FrameEvent::timeSinceLastFrame 包含的是上一次frameStarted方法开始执行到现在的时间(而不是frameEnded方法开始执行到现在的时间)。
理解OGRE FrameListeners的一个重要概念就是他们被调用的顺序完全取决于Ogre。你不能决定FrameListener被调用的顺序。如果你想确保FrameListeners按一定的顺序被调用,就需要只注册一个FrameListener并按照合适的顺序调用所有对象。
调用FrameListener的三个方法中的哪一个取决于你的需要,如果你只是想要每一帧更新一下你的stuff,那么把他放到frameRenderingQueued事件中,因为它会在GPU因翻转渲染缓存而变忙之前调用。
如果你想要提高性能,那么就使用FrameListener的frameRenderingQueued函数来更新每一帧。
Registering a FrameListener
为了让我们的类成为一个全功能FrameListener,你需要使用Ogre::Root来注册他。因为Ogre::Root需要知道当一个帧事件发生时应该调用什么FrameListeners。添加删除FrameListener可以用两个函数:Ogre::Root::addFrameListener 和Ogre::Root::removeFrameListener。addFrameListener 方法添加一个FrameListener ,removeFrameListener方法删除一个FrameListener。这两个方法只接受一个FrameListener的指针,这意味着他们没有名字。
一个类把自己注册为一个FrameListener的代码如下: mRoot->addFrameListener(this);
这之后他就可以通过FrameListener的函数frameStarted、frameRenderingQueued、frameEnded从Ogre::Root接收帧事件。
Setting up the Scene
Introduction
我们将添加一个对象(一个忍者)和一个点光源到场景中,如果你点击鼠标左键,光源将打开或关闭。你可以使用IKJL键来移动绑定忍者的节点:I向前,K向后,J向左,左shift加J绕y轴向左旋转,L向右,左shift加L绕y轴向右旋转,U和O键向上和向下。
The Code
首先设置环境光:
mSceneMgr->setAmbientLight(Ogre::ColourValue(0.25, 0.25, 0.25));
现在添加忍者实体到场景原点:
Ogre::Entity* ninjaEntity = mSceneMgr->createEntity("Ninja", "ninja.mesh");
Ogre::SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode");
node->attachObject(ninjaEntity);
现在我们创建一个白色的点光源并把他放到场景中,离忍者一定的距离:
Ogre::Light* pointLight = mSceneMgr->createLight("pointLight");
pointLight->setType(Ogre::Light::LT_POINT);
pointLight->setPosition(Ogre::Vector3(250, 150, 250));
pointLight->setDiffuseColour(Ogre::ColourValue::White);
pointLight->setSpecularColour(Ogre::ColourValue::White);
The FrameListener
在frameRenderingQueued函数中添加代码:
bool BasicTutorial4::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
bool ret = BaseApplication::frameRenderingQueued(evt);
if(!processUnbufferedInput(evt)) return false;
return ret;
}
他只是调用了BaseApplication::frameRenderingQueued和processUnbufferedInput函数,然后回到了循环。为了继续渲染,frameStarted方法必须返回一个有效的bool值。如果有任何返回值为false,Ogre就会中断渲染循环并推出程序。
下面给processUnbufferedInput函数添加代码。
Processing Input
Variables
我们需要在processUnbufferedInput函数中定义一些静态变量:
bool BasicTutorial4::processUnbufferedInput(const Ogre::FrameEvent& evt)
{
static bool mMouseDown = false; // If a mouse button is depressed
static Ogre::Real mToggle = 0.0; // The time left until next toggle
static Ogre::Real mRotate = 0.13; // The rotate constant
static Ogre::Real mMove = 250; // The movement constant
mRotate 和mMove 是我们的旋转和移动的常量。如果想要旋转或移动更快或更慢,就把这两个变量修改的更大或更小。
另外两个变量(mToggle 和mMouseDown )控制我们的输入。本教程中我们将使用“非缓存的”鼠标和键盘输入。这意味着我们将在帧监听期间调用方法来查询键盘和鼠标的状态。
当我们使用键盘改变屏幕上某些对象的状态时会出现一个有趣的问题,当我们按下一个键,我们根据该信息产生相应的动作。但是下一帧呢?我们还要相同的结果吗?
在某些情况(比如按键移动),这是我们想要的。但是,假如我们想要T键来控制一个光源的开关。第一帧T键被按下,光源打开,下一帧T键仍被按着,所以光源被关闭……这样循环直到T键被放开。
我们要知道键在各帧之间的状态来避免这样的问题,我们将用两个不同的方法来解决。
mMouseDown变量记录鼠标前一帧是否也被按下(如果为true,直到鼠标被放开我们都不会再执行相同的动作),mToggle变量记录我们可以再执行动作的时间,当一个按钮被按下,mToggle被赋一个一定长度的时间,这期间不能有其他动作。
这些都是本地静态变量,主要是为了方便。OIS(Object Oriented Input)系统提供了三个主要的类来接收输入:Keyboard, Mouse, 和Joystick。每帧中键盘和鼠标的当前状态都必须通过鼠标键盘对象的capture方法来捕获(captured)。在BaseApplication::frameRenderingQueued中添加代码:
mMouse->capture();
mKeyboard->capture();
首先我们要让鼠标左键可以开关光源。我们可以通过getMouseButton方法查看鼠标是否被按下。通常0代表鼠标左键,1代表鼠标右键,2代表鼠标中键。有的系统中1代表中键,2代表右键。在BasicTutorial4::processUnbufferedInput函数中添加代码:
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
鼠标如果被按下currMouse将为true。现在我们根据currMouse 是否为true以及前一帧鼠标是否被按下来开关光源。注意Light类的setVisible方法决定了光源是否发光。
if (currMouse && ! mMouseDown)
{
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}
现在用mMouseDown变量保存currMouse 的值,下一帧mMouseDown将告诉我们上一帧鼠标是否被按下。
mMouseDown = currMouse;
编译运行程序。这种方法很好的运行了,但他的缺点是我们绑定了动作的键都要有一个布尔值变量。解决这个问题的一种方法就是我们记录上一次如果有键被按下,那么在一定的时间内不能再有动作发生。我们把这个状态记录在mToggle变量中。如果mToggle大于0,我们不做任何动作。mToggle小于0,就做出相应的动作。
首先就是减去mToggle变量自上一帧开始消逝的时间:
mToggle -= evt.timeSinceLastFrame;
在其他改变发生前mToggle都延迟0.5秒,让我们添加另一种开关光源的方法:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
mToggle = 0.5;
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}
下面就是实现按IJKL键时移动忍者节点,首先创建一个Vector3来记录我们想要移动到的位置:
Ogre::Vector3 transVector = Ogre::Vector3::ZERO;
下面按下IJKL各键,进行移动:
if (mKeyboard->isKeyDown(OIS::KC_I)) // Forward
{
transVector.z -= mMove;
}
if (mKeyboard->isKeyDown(OIS::KC_K)) // Backward
{
transVector.z += mMove;
}
if (mKeyboard->isKeyDown(OIS::KC_J)) // Left - yaw or strafe
{
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// Yaw left
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(mRotate * 5));
} else {
transVector.x -= mMove; // Strafe left
}
}
if (mKeyboard->isKeyDown(OIS::KC_L)) // Right - yaw or strafe
{
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// Yaw right
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(-mRotate * 5));
} else {
transVector.x += mMove; // Strafe right
}
}
最后是按下U和O键,向上和向下移动:
if (mKeyboard->isKeyDown(OIS::KC_U)) // Up
{
transVector.y += mMove;
}
if (mKeyboard->isKeyDown(OIS::KC_O)) // Down
{
transVector.y -= mMove;
}
Ogre使用四元数(Quaternions)来进行所有的旋转操作。使用四元数来旋转一个向量(vector),你所有做的就是让两者相乘。在本例中,我们把对场景节点的旋转操作看做变换向量。通过调用SceneNode::getOrientation()来获取一个四元数,然后和要变换的节点进行乘法。
我们必须注意的第二个陷阱是我们得根据自上一帧到现在的时间量来缩放我们变换的幅度。否则,你移动的快慢将依据你的程序的帧率。这肯定不是我们所要的。下面就是没有以上问题的移动摄像机的函数的调用:
mSceneMgr->getSceneNode("NinjaNode")->translate(transVector * evt.timeSinceLastFrame, Ogre::Node::TS_LOCAL);
无论何时你移动或旋转,你可以指定你想使用哪个变换空间(Transformation Space)来移动对象。通常你移动对象时,不需要设置该参数。他的默认值为TS_PARENT,表示无论母节点在哪个变换空间(Transformation Space)对象都被移动。这种情况中,母节点就是根场景节点。
当我们按I键时(向前),我们在Z方向上减小,表示我们向Z轴负方向移动。如果我们不在前一行代码指定为TS_LOCAL,我们将眼Z轴负方向移动忍者。但是,有用我们是要按I键让忍者向前走,我们需要让他朝着节点面向的方向走,所以我们使用了本地(local)变换空间。
还有另一种方法可以让我们做到这些(虽然这种方法不太直接)。我们可以获取节点的方向,一个四元数,然后和方向向量相乘得到相同的结果。这很有效果:
// Do not add this to the program
mSceneMgr->getSceneNode("NinjaNode")->translate(mSceneMgr->getSceneNode("NinjaNode")->getOrientation()*transVector*evt.timeSinceLastFrame, Ogre::Node::TS_WORLD);
这个也是在本地(local)空间上移动忍者节点。OGRE定义了三种变换空间:TS_LOCAL、TS_PARENT和TS_WORLD。
也可能你会需要在一个不是这三种变换空间的空间中进行移动或旋转操作,如果是这样,你应该像前一行代码那样做。用一个四元数代表向量空间(或你要操作的对象的方向),和变换向量相乘得到正确的变换向量,然后在TS_WORLD空间中移动他。
编译运行你程序!