您的位置:首页 > 其它

(6) 帧监听与无缓冲输入

2010-10-27 23:03 302 查看
从这里开始

添加如下初始代码:

BasicTutorial5.h

BasicTutorial5.cpp

代码中,我们重载了基类的frameRenderingQueued方法,并定义了一个私有方法processUnbufferedInput。后续通过完善其代码以实现一些有趣的东西。

帧监听

介绍

OGRE在渲染每一帧时,会产生两个通知:帧开始渲染、帧结束渲染,我们可以通过注册回调去捕获这通知,并做些事情。
通过类FrameListener可以简单的做到这些。类FrameListener定义了如下3个方法,通过这3个方法可以捕获帧渲染的通知:

// 当一帧开始要渲染时,会先调用本函数
virtual bool frameStarted(const FrameEvent& evt);

// 当所有的渲染对象已经准备就绪,但还没有输入到终端时会调用本函数
virtual bool frameRenderingQueued(const FrameEvent& evt);

// 当一帧渲染完毕后,会调用本函数
virtual bool frameEnded(const FrameEvent& evt);


帧渲染会永不停息的工作,直到类FrameListener的如上3个方法返回false时停止。

对象FrameEvent包含有两个变量,但只有变量timeSinceLastFrame对于帧监听是有用的。这个变量记录frameStarted或者frameEnded最近被激活时间。注意在frameStarted方法中,FrameEvent::timeSinceLastFrame将包含frameStarted最近被激活的时间(而不是最近被激活的 frameEnded方法)。
一个重要的概念你不能决定FrameListener被调用的次序(当注册了多个帧监听)。如果你需要确定FrameListeners被调用的准确顺序,你应该只注册一个FrameListener,然后用适当的顺序来调用所有的物体。

注册帧监听
其实上面我们所给出的初始代码,已经是一个帧监听了。本类继承了BaseApplication,而类BaseApplication继承了类FrameListener:

class BaseApplication : public Ogre::FrameListener


查看BaseApplication的代码,他引入的frameRenderingQueued方法,并重载了createFrameListener方法。我们现在解释下这些方法实际干了些啥。
为了让底层知道我们注册的帧监听,需要使用Ogre::Root类来进行注册,有两个方法:

// 添加一个帧监听
Ogre::Root::addFrameListener

// 移除一个帧监听
Ogre::Root::removeFrameListener


通过查看BaseApplication::createFrameListener方法,你可以看到:

mRoot->addFrameListener(this);


有了此代码,BaseApplication类就可以接收到帧渲染事件了。

如何工作?

让我们看看Ogre::Root::renderOneFrame的代码:

// 渲染一帧
bool Root::renderOneFrame(void)
{
// 产生帧开始时间,调用所有注册上来的帧回调
if(!_fireFrameStarted())
return false;

if (!_updateAllRenderTargets())
return false;

// 产生帧结束时间,调用所有注册上来的帧回调
return _fireFrameEnded();
}


为了了解frameRenderingQueued的事件是在哪里产生,让我们看看_updateAllRenderTargets的实现:

bool Root::_updateAllRenderTargets(void)
{
// update all targets but don't swap buffers
mActiveRenderer->_updateAllRenderTargets(false);

// give client app opportunity to use queued GPU time
bool ret = _fireFrameRenderingQueued();

// block for final swap
mActiveRenderer->_swapAllRenderTargetBuffers(mActiveRenderer->getWaitForVerticalBlank());

// more code follows ...


可以看到,当所有渲染对象交换到GPU前,就产生了此事件。
到目前为止,这些就是你所需要了解帧渲染的全部。

配置场景

简介

在我们对代码的深入研究之前,我将简单介绍一下我将做什么,这样你可以了解当我们创建和添加一些东西到场景中去的目的。
我们将把一个物体(一个忍者)放到屏幕中,并在场景中加上点光源。当你点击鼠标左键,灯光会打开或关闭。按住鼠标右键,则开启“鼠标观察”模式(也就是你用摄像机四处观望)。我们将在场景里放置绑定忍者的场景节点,通过IKJL键来控制他的移动,UO控制其跳跃或下蹲。

代码

找到createScene方法。首先我们需要把场景的环境光设置的很淡。我们想要场景中物体在灯光关闭情况下仍然可见,而且我们仍然想做到在灯光开关时场景有所变化:

// 设置一个颜色比较淡的环境光
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);


这些就是配置场景所需要的(编译下看看效果),然后我们转入帧监听……

帧监听

我们需要放一些代码到frameRenderingQueued方法中:

bool BasicTutorial5::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
// 首先交由基类处理
bool ret = BaseApplication::frameRenderingQueued(evt);

// 处理非缓冲输入
if(! processUnbufferedInput(evt))
// 结束渲染
return false;

return ret;
}


这样每一帧渲染时,我们将都会去调用processUnbufferedInput函数,以处理输入。

处理输入

变量

bool BasicTutorial5::processUnbufferedInput(const Ogre::FrameEvent& evt)
{
// 鼠标左键是否在上一帧被按下
static bool mMouseDown = false;

// 直到下一次触发的时间
static Ogre::Real mToggle = 0.0;

// 滚动常量,可以控制此数值大小以改变鼠标的滚动速度
static Ogre::Real mRotate = 0.13;

// 移动常量,决定了鼠标的移动速度
static Ogre::Real mMove = 250;


变量mMouseDown和mToggle控制我们的输入。我们将使用“非缓冲”鼠标和按键输入,“缓冲”输入后续会继续讨论,这意味着我们将在帧监听器查询鼠标和键盘状态时调用方法。
当我们试图用键盘来改变屏幕中物体的状态时,会遇到一些有趣的问题。如果知道一个键正被按下,我们会去处理它,但到了下一帧怎么办?我们还会看见它被按下,然后再重复地做操作吗?在一些情况(比如用方向键来移动),我们是这样做的。然而,我们想通过“T”键来控制灯的开关时,在第一帧里按下T键,拨动了灯的开关,但在下一帧T键仍然被按着,所以灯又打开了...如此返复,直到按键被释放。我们必须在帧与帧之间追踪这些按键状态,才能避免这个问题。我将介绍两种不同的方法来解决它。
mMouseDown用来追踪鼠标在上一帧里是否也被按下(所以如果mMouseDown为真,在鼠标释放之前我们不会做同样的操作)。mToggle指定了直到我们可以执行下一个操作的时间。即,当一个按键被按下去了,在mToggle指明的这段时间里不允许有其它动作发生。
这些变量是static变量,就是为了实现方便。
OIS(Open Input System)提供了3个类来接受输入:Keyboard,Mouse,Joystick。本教程我们只覆盖了如何使用键盘和鼠标输入。在继续后续内容之前,让我们先简单看看BaseApplication::frameRenderingQueued的实现。
在每一帧中,需要捕获键盘和鼠标的当前状态,这是通过调用capture方法来实现的:

mMouse->capture();
mKeyboard->capture();


哦,这代码别加到我们自己的方法中。

第一件事我们要做的,是通过鼠标左键的点击来控制点光源的开关。
通过getMouseButton方法,我们可以知道某个鼠标按键是否被按下,将如下代码加入到processUnbufferedInput中:

// 判断按钮是否被按下了
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);


然后,通过currMouse的状态决定点光源的开关状态:

if (currMouse && ! mMouseDown)
{
// 如果前一帧鼠标没有按下,而本帧鼠标按下了,需要切换光源的开关状态
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}

然后,更新本帧鼠标的状态:

mMouseDown = currMouse;

编译运行,看看效果怎么样。使用各按键看看效果,因为我们继承了类BaseApplication的frameRenderingQueued方法,所以也可以使用WASD进行相机的移动。
这种保存上一次鼠标状态的方法非常好用,因为我们知道我们已经对为鼠标状态做了动作。
这样做的缺点就是你要为每一个被绑定动作的按键设置一个布尔变量。一种能避免这种情况的方法是,跟踪上一次点击任何按键的时刻,然后只允许经过了一段时间之后才触发事件。我们用mToggle变量来跟踪。如果mToggle大于0,我们就不执行任何动作,如果mToggle小于0,我们才执行动作。我们将用这个方法来进行下面两个按键的绑定。
首先我们要做的是拿mToggle变量减去从上一帧到现在所经历的时间。

mToggle -= evt.timeSinceLastFrame;

现在我们更新了mToggle,我们能操作它了。让我们添加如下额外的代码,用另一种方式控制灯光的开关。

if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
// 设置mToggle变量,使得在下一个动作发生时至少相隔一秒钟
mToggle  = 0.5;
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}

编译运行看看,使用按键“1”也可以控制其开关状态吧?

下一件事情,当我们使用按键IJKL时需要变换忍者的节点。
首先,创建一个Vector3以保存变换的目标值:

Ogre::Vector3 transVector = Ogre::Vector3::ZERO;

当“I”键按下时,向-Z轴方向移动(即向屏幕里面移动):
if (mKeyboard->isKeyDown(OIS::KC_I)) // Forward
{
transVector.z -= mMove;
}


其他情况类似类似:

// K按下了
if (mKeyboard->isKeyDown(OIS::KC_K)) // Backward
{
transVector.z += mMove;
}

// J按下了
if (mKeyboard->isKeyDown(OIS::KC_J)) // Left - yaw or strafe
{
// 同时SHIFT键也按下了
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// Yaw left
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(mRotate * 5));
} else {
transVector.x -= mMove; // Strafe left
}
}

// L按下了
if (mKeyboard->isKeyDown(OIS::KC_L)) // Right - yaw or strafe
{
// 同时SHIFT键也按下了
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// Yaw right
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(-mRotate * 5));
} else {
transVector.x += mMove; // Strafe right
}
}

// 向上移动
if (mKeyboard->isKeyDown(OIS::KC_U)) // Up
{
transVector.y += mMove;
}

// 向下移动
if (mKeyboard->isKeyDown(OIS::KC_O)) // Down
{
transVector.y -= mMove;
}

好了,我们的transVector变量保存了作用于摄像机场景节点的平移。这样做的第一个缺点是如果你旋转了场景节点,再作平移的时候我们的x、y、z坐标就不对了。为了修正它,我们必须把作用在场景节点的旋转,也作用于我们的平移。这实际上比听起来更简单。
为了表示旋转,Ogre不是像一些图形引擎那样使用变换矩阵,而是使用四元组。四元组的数学意义是一个较难理解的四维线性代数。幸好,你不必去了解它的数字知识,了解如何使用就行了。非常简单,用四元组旋转一个向量只要将它们两个相乘即可。现在,我们希望将所有作用于场景节点的旋转,也作用在平移向量上。通过调用SceneNode::getOrientation(),我们能得到这些旋转的四元组表示,然后用乘法让它作用在平移节点。
还有一个缺陷要引起注意的是,我们必须根据从上一帧到现在的时间,来对平移的大小进行缩放。否则,你的移动速度取决于应用程序的帧率。这确实不是我们想要的。这里有一个函数供我们来调用,可以避免平移摄像机时带来的问题:
mSceneMgr->getSceneNode("NinjaNode")->translate(transVector * evt.timeSinceLastFrame, Ogre::Node::TS_LOCAL);


好了,我们来介绍一些新的东西。当你对一个节点进行平移,或者是绕某个轴进行旋转,你都能指定使用哪一个“变换空间”来移动它。一般你移动一个对象时,不需要指定这个参数。它默认是TS_PARENT,意思是这个对象使用的是父节点所在的变换空间。在这里,父节点是场景的根节点。当我们按下W键时(向前移动),我们走向Z轴负的方向。如果我们不在代码前面指明TS_LOCAL,摄像机都会向全局负Z轴移动。然而,既然我们希望按下W键时摄像机往前走,我们就需要它往节点所面朝的方向走。所以,我们使用“本地”变换空间。
还有另一种方式我们也能实现(尽管不是怎么直接)。我们获取节点的朝向,一个四元组,用方向向量乘以它,能得到相同的结果。这完全是合法的:
// 这代码别加到你的程序中哦
mSceneMgr->getSceneNode("NinjaNode")->translate(mSceneMgr->getSceneNode("NinjaNode")->getOrientation() * transVector * evt.timeSinceLastFrame, Ogre::Node::TS_WORLD);


这同样也在本地空间里进行移动。在这里,实际上没这样做的必要。Ogre定义了三种变换空间:TS_LOCAL, TS_PARENT, 和TS_WORLD。也许存在其它的场合,你需要使用另外的向量空间来进行平移或旋转。若真是这样,你可以像上面的代码那样做。找到一个表示向量空间的四元组(或者你要操作的物体的朝向),用平移向量去乘它,得到一个正确的平移向量,然后用它在TS_WORLD空间里移动。目前应该不会遇到这样的情况,我们后面的教程也不会涉及这些内容。
编译执行,好好体验下效果吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: