原文地址:
http://www.cnblogs.com/oneDouble/archive/2012/07/11/2587193.html
原文:
代友转载,原译者:[email protected] 博客 www.singmelody.com
这章的重点会在模型动作和它们通常是怎样工作上,特别是在Ogre 3D 方面。没有运动,那么一个3D场景是没有生机的。运动是使场景现实和有趣的最重要的因素之一。
在这一章,我们将会:
※ 播放一个运动。
※把两个运动结合起来。
※在运动上关联实体。
一。【 添加运动 】
在之前的章节中,我们使用用户输入添加了程序的交互性。现在我们准备添加运动来增加另一种形式的交互性。运动对于每个场景是非常重要的因素。没有它们,每个场景看起来将会是静止的而且死气沉沉的,但是加入运动后,场景将会生动起来。所以让我们添加它们吧。
一如往常,我们将会使用之前章节的代码,但是这次我们什么也不删除。
1. 对于我们的运动,我们需要在帧监听中添加新的成员变量。添加一个指针来储存我们想要运动的实体,另一个指针保存运动状态:
Ogre::Entity* _ent; Ogre::AnimationState* _aniState;
2. 然后改变帧监听的构造函数把实体的指针作为一个新的参数:
Example34FrameListener(Ogre::SceneNode* node,Ogre::Entity* ent,RenderWindow* win,Ogre::Camera* cam)
3. 在构造函数的函数体中,把参数的实体指针赋值给新的成员变量:】
_ent = ent;
4. 在这之后,从实体中调用Dance动作并存储到专门为它而创建的成员变量中。最后,设置运动为enabled并循环运动。
_aniState = _ent->getAnimationState("Dance"); _aniState->setEnabled(true); _aniState->setLoop(true);
5. 然后,我们需要告诉运动自从上一次更新到现在已经过去多长时间。我们将会在frameStarted()函数中添加这件事;通过这个我们就可以。
_aniState->addTime(evt.timeSinceLastFrame);
6. 我们最后需要做的就是调整ExampleApplication 类让它可以与新的帧监听一起作用。添加一个新的成员变量到程序来保存一个实体指针。
Ogre::Entity* _SinbadEnt;
7. 把原来创建的局部指针分配给新创建的实体来储存。把
Ogre::Entity* Sinbad = mSceneMgr->createEntity("Sinbad", "Sinbad.mesh");
替换为
_SinbadEnt = mSceneMgr->createEntity("Sinbad", "Sinbad.mesh");
8. 当关联实体到结点也需要做同样的改变。
_SinbadNode->attachObject(_SinbadEnt);
9. 当然,当创建新的帧监听时,添加新的参数到调用的构造函数。
Ogre::FrameListener* FrameListener = new Example34FrameListener(_ SinbadNode,_SinbadEnt,mWindow,mCamera);
10. 编译运行程序。你将会看到Sinbad 在跳舞。
【 刚刚发生了什么 ? 】
用短短几行代码,我们实现了Sinbad的跳动。在第一步中,我们添加两个成员变量,用于运动模型。第一个指针变量用户存储模型。第二个Ogre::AnimationState的指针变量,被Ogre 3D用于描述单一运动和其相关联的信息。第二和第三步就比较容易理解了,第二步我们改变了构造函数来适应新指针,第三步保存了第一步中创建的成员变量。第四步就比较有趣了,我们请求实体返回给我们名为”Dance”的运动。每个实体变量保存了实体本身所有的运动,并且我们可以使用字符串变量和getAnimationState() 函数查询运动。这个函数返回运动的AnimationState指针,如果运动为空,它将会返回一个空指针。在获取完运动的状态,我们激活它。
这告诉Ogre 3D 播放这个动作。同样,我们设置循环属性为真,这样这个动作就可以循环播放,直到我们停止它为止。第五步是重要的一步,使用这行代码,使实体栩栩如生起来。每次场景被渲染时,然后添加运动时间,这样Ogre 3D 就可以播放了。确切点说,它相应自上一帧到现在的过去时间。这可能要由Ogre 3D 自己完成,但是这种方式更灵活。比如,我们可以添加第二个模型,但是我们想要它做个慢动作。如果Ogre 3D 自己更新动作,那么不论是以普通速度还是以慢速运动,模型的运动对我们来说将是很困难的。但是我们自己完成,我们可以把evt.timeSinceLastFrame *0.25便可以实现慢动作。在这步之后,便可以对构造函数进行小的修改,使之与帧监听的构造函数兼容。这样,我们需要保存想要运动的实体指针。
【 简单测试 —— 时间的重要性 】
为什么我们需要告诉Ogre 3D自从上次更新到现在时间过去了多少?这种方式的优点是什么?
二。【 同时控制两个动作 】
添加第二个模型,这个模型站在第一个模型旁边,但是以一个慢动作在运动。你应看到两个模型在显示运动的不同阶段,就好像下面图片显示的那样:
在添加我们第一个动作之后,我们将会研究为什么和怎样同时播放动作。
这里,我们将会使用与创建第一个例子同样的办法添加第二个动作:
1. 从Dance,改变运动到RunBase。
_aniState = _ent->getAnimationState("RunBase");
2. 编译运行程序。你可以看到Sinbad在跑动,但只是上半身的不动的原地跑动。
3. 对于我们第二个动作,我们需要一个保存运动状态的新指针:
Ogre::AnimationState* _aniStateTop;
4. 然后,我们需要获取运动的状态,激活并循环运动。我们需要调用的运动名为Runtop。
_aniStateTop = _ent->getAnimationState("RunTop"); _aniStateTop->setEnabled(true); _aniStateTop->setLoop(true);
5. 最后要做的一件事就是添加自上帧过去的时间,就好像我们第一次设置的那样:
_aniStateTop->addTime(evt.timeSinceLastFrame);
6. 然后编译运行程序。现在你可以看到Sinbad整个身子做跑的动作。
【 刚刚发生了什么 ?】
我们在同时使用了两个运动。在之前,如果你问自己为什么不调用像playAnimation(AnimationName)这样的函数,反而需要获取AnimationState来播放运动。现在你有答案了。Ogre 3D 支持在同一时间使用多种运动,但使用playAnimation(AnimationName)却实现不了。我们甚至可通过使用一个修改的变量和addTime()函数,以不同速度来控制模型。
三。【让我们的模型走两步 】
我们已经有个行走的运动了,但是模型并没有改变它的位置。我们将会添加基础的模型运动控制并使用我们所学来混合运动。
一如既往,我们将会使用之前的代码来作为起点:
1. 首先,我们需要帧监听中两个变量来控制移动和保存旋转。
float _WalkingSpeed; float _rotation;
2. 在构造函数中,我们初始化新的变量;我们每秒移动50个单位并以无旋转作为开始:
_WalkingSpeed =50.0f; _rotation =0.0f;
3. 然后我们需要改变运动状态并阻止它循环。这次,当新的运动开始时,我们需要控制模型而非交给Ogre 3D 控制。
_aniState = _ent->getAnimationState("RunBase"); _aniState->setLoop(false); _aniStateTop = _ent->getAnimationState("RunTop"); _aniStateTop->setLoop(false);
4. 在frameStarted() 方法中,我们需要两个局部变量,一个是为了显示这帧我们是否移动了模型,第二个变量是用于保存模型移动的方向。
bool walked = false; Ogre::Vector3 SinbadTranslate(0,0,0);
5. 同样在方法中,我们添加新的代码来控制模型的移动。我们将会使用方向键来移动。当一个键按下的时候,我们需要保存变换变量来存储模型移动的方向,并且需要以它移动的方向来设置旋转变量以旋转模型
if(_key->isKeyDown(OIS::KC_UP)) { SinbadTranslate += Ogre::Vector3(0,0,-1); _rotation =3.14f; walked = true; }
6. 我们需要以同样的方式添加另外三个方向键:
if(_key->isKeyDown(OIS::KC_DOWN)) { SinbadTranslate += Ogre::Vector3(0,0,1); _rotation =0.0f; walked = true; } if(_key->isKeyDown(OIS::KC_LEFT)) { SinbadTranslate += Ogre::Vector3(-1,0,0); _rotation =-1.57f; walked = true; } if(_key->isKeyDown(OIS::KC_RIGHT)) { SinbadTranslate += Ogre::Vector3(1,0,0); _rotation =1.57f; walked = true; }
7. 然后,在按键处理之后,我们需要检查一下这帧中 walked 是否为 true。如果是这种情况,我们需要检查运动是否停止。当为true时,我们重新开始运动:
if(walked) { _aniState->setEnabled(true); _aniStateTop->setEnabled(true); if(_aniState->hasEnded()) { _aniState->setTimePosition(0.0f); } if(_aniStateTop->hasEnded()) { _aniStateTop->setTimePosition(0.0f); } }
8. 如果我们这帧不动的话,需要设置两个运动的状态到0。否则,我们的模型将会看起来像在进行动作的一半时被冻住一般,而这看起来的效果不好。所以如果我们这帧不需要行走,我们需设置两个运动回到其起点位置。同样,当这帧不移动模型的时候,我们将会冻结两个运动,因为此时我们并不需要运动。
else { _aniState->setTimePosition(0.0f); _aniState->setEnabled(false); _aniStateTop->setTimePosition(0.0f); _aniStateTop->setEnabled(false); }
9. 最后我们需要做的就是应用变换和旋转到模型的场景结点:
_node->translate(SinbadTranslate * evt.timeSinceLastFrame * _ WalkingSpeed); _node->resetOrientation(); _node->yaw(Ogre::Radian(_rotation));
10 现在我们编译运行程序。在鼠标和WASD键的作用下,我们可以移动摄像机。用方向键,我们就可以移动Sinbad。每次移动他的时候,他便会做出正确的运动。
【刚刚发生了什么】
我们综合了用户输入和运动创建了第一个程序。这可以称得上我们目前为止第一个真正的交互程序。在第一步和第二步,我们创建和初始化了我们需要的变量。在第三步,我们改变了过去运动的方式;准确的说,我们之前总是直接激活运动并循环它。现在我们不想要直接激活它,因为我们只需要在移动的时候使用运动,除此之外的情况,看起来都很stupid。这也是我们冻结运动的循环的原因。我们只想要反应用户的输入,这样就没必要一直循环了。如果需要,我们将会自己启动运动。
我们基本上是在frameStarted() 方法中作出的改变。在第四步中,我们创建了一对稍后使用的局部变量。一个是来表示这帧模型是否移动bool类型的开关,另一个是表示移动方向的向量。第五步和第六步中查询方向键的状态。当一个键按下,我们改变方向向量,做相应的旋转并设置移动的开关为true。在第七步中我们使用这个开关,如果flag为true,意味着这帧模型会移动,我们激活运动并检查是否有运动达到它们的结尾。如果运动到达结尾,我们重置它们到起点这样,它们就可以再次继续播放了。因为当模型不移动的时候,我们不想要播放运动,所以在第八步我们设置它们运动为起点并冻结它们。在第九步中,我们应用了变换。然后重置旋转,在这之后,应用新的旋转。这一步很是必要,因为yaw函数添加旋转到现有的旋转,我们需要绝对的旋转而不是相对的旋转。因此,我们先重置旋转然后应用旋转到现在的归零旋转上。
四。【 添加双刀】
我们现在有一个可以通过用户输入控制行走和运动的模型。现在我们准备研究我们如何添加一个对象到运动模型上。
1. 在createScene函数之后,创建两把刀模型的实例并命名为Sword1 和 Sword2:
Ogre::Entity* sword1 = mSceneMgr->createEntity("Sword1", "Sword.mesh"); Ogre::Entity* sword2 = mSceneMgr->createEntity("Sword2", "Sword.mesh");
2. 现在使用一个名称来关联刀到模型:
_SinbadEnt->attachObjectToBone("Handle.L", sword1); _SinbadEnt->attachObjectToBone("Handle.R", sword2);
3.编译运行程序。你将会看到Sinbad手里有两把刀。
【 刚刚发生了什么?】
我们创建了两把刀的实例并关联它们到骨骼上。创建的实例并非难以理解。刚才的代码中比较难但比较有趣的部分就是调用了函数attachObjectToBone()。为理解这个函数的工作原理,我们需要讨论一下运动是如何保存和播放的。
×运动
对于运动来说,我们使用一种称为skeletons(骨架) 和 bones(骨骼) 的东西。这个系统的灵感是来自于自然;在自然界,基本上所有的生物都有一个骨架来支撑它。在骨架和一些肌肉的帮助之下,动物和人类可以朝一个确定的方向移动他们身体的一部分。比如,我们可以使用手指的关节来把手指握成一个拳头。在计算机图形学中运动也是以同样的方式来运行。美工定义3D模型而且为其创建骨架,这样模型就可以运动了。骨架是由骨骼和关节构成。关节是连接两块骨骼并且定义骨骼的运动方向。在这里是简化的骨架图;而通常,要比这里的多很多。
用关节,骨骼和骨骼的作用半径,一个美工就可以创建复杂的运动,比如我们正在使用的Sinbad运动。就好像运动,骨骼也是有名字的。Ogre 3D 让我们使用这些骨骼作为关联别的实体的点。这有着巨大的优势,当关联实体后,实体也会像骨骼一样得到旋转,这意味着如果我们在Sinbad的手中有一个关联点并关联刀之后,双刀就会一直在手中。因为当双手做动作时,刀也会得到同样的运动。如果这个函数不存在,那几乎不可能添加模型到手上或者关联东西到他们的后面,就如我们对刀做的操作一样。
五。【 打印输出模型的所有动作 】
我们已经知道美工定义运动的名字,但很多时候直接从Ogre 3D来获取运动的名字很重要。当我们没有制作这个模型的美工来询问里面有什么运动或当你想要检查一下导出的处理是否成功,获取名字这件事就显得十分重要了。现在我们将会研究如何在控制台中打印模型的所有运动。
我们将会使用之前的代码作为起点来打印我们实体所有的运动。
1. 在createScene() 函数最后,用set获取模型所有的运动:
Ogre::AnimationStateSet* set = _SinbadEnt- >getAllAnimationStates();
2. 然后定义一个迭代器并用一个set迭代器来初始化:
Ogre::AnimationStateIterator iter = set- >getAnimationStateIterator();
3. 并且,最后,遍历所有的动作并打印它们的名字:
while(iter.hasMoreElements()) { std::cout << iter.getNext()->getAnimationName() << std::endl; }
4. 编译运行程序。在开始程序并加载场景之后,你将会在控制台程序中看到下面的文字。
Dance
DrawSwords
HandsClosed
HandsRelaxed
IdleBase
IdleTop
JumpEnd
JumpLoop
JumpStart
RunBase
RunTop
SliceHorizontal
SliceVertical
【 刚刚发生了什么?】
我们请求实体返回给我们包含运动信息的数据集。我们然后遍历这个数据集并打印出运动的名称。我们看到有好多没有用过的和已经用过的运动。
【本章回顾】
在这章我们学习了很多有关运动和如何使用它来使我们的3D更有趣的方法。
具体的说,我们所学涵盖了以下内容:
※如何获取实体的运动并使用它。
※如何激活,冻结和循环程序和为什么需要告诉运动自上帧过去有多长时间。
※如何在同一时间使用两个运动。
※运动是如何使用骨架的?如何关联一个实体到一骨骼上?
※如何查询实体包含的所有运动。
在下一章,我们将会研究Ogre 3D的另一个新方面,主要是关于使用不同的场景管理器和为什么要这样做。