【Ogre编程入门与进阶】第九章 动画

12.1 动画基础

        动画是基于人的视觉原理创建的运动图像。人的眼睛通过视觉暂留效应,当对上一个画面的感知还未消失时,下一张画面又出现,这样就会有动的感觉。计算机图形学中的动画也同样遵循着这一原理。在三维软件中创建动画,需要为每个动画序列设置起始帧、结束帧和关键帧,中间帧由软件自身计算完成。

        Ogre本身不会对对象的速度、加速度进行跟踪,不能像人一样了解场景中动画角色动作的具体含义,例如它不知道什么是走路或者叉腰。Ogre能做的就是从数学的角度,逐帧的处理场景中对象的移动或者变形。每一帧,在ogre动画系统的控制下,根据任意变量的函数来定位与定量决定,同时进行场景的重绘。这样,在每秒24帧的动画中,同一个角色在一秒中画24个,每一个稍有不同,在一秒内连续播放,ogre就会通过其动画系统形成连续的动画场景。

12.2 Ogre所支持的动画类型

        一个成熟的绘制引擎少不了对动画的支持。从某种抽象层次上来讲,Ogre的动画可以理解为一系列运动轨迹的集合。其中的每一个动画轨迹都可以被用来存储一些动画数据,存储动画数据是利用了“时间函数”的方式,存储的数据包括输入时间参数等等。在动画中最重要的概念莫过于“关键帧”,关键帧的定义是:在某一个特定的时间点,对场景中的某一个物体位置、方向等状态的采样,对应到前面讲到的时间函数的概念,关键帧由所有轨迹数据的值组成。继续延伸下去,关键帧动画就是一种基于关键帧概念的动画类型,展开讲关键帧动画会将整个动画轨迹的时间轴划分为若干个不同的时间点,每个时间点就是一个对场景中的物体运动状态的采样,而且时间轴上的其他时间点(非关键帧点)可以利用特定的差值方法计算得到,从而使最终的动画效果比较流畅。

        关键帧可以有多种类型,比如骨骼、数字、节点、顶点等,基于这些不同的动画类型,Ogre系统中也支持多种的关键帧动画类型,它们分别是数字动画轨迹(NumericAnimationTrack)、节点动画轨迹(NodeAnimationTrack)和顶点动画轨迹等,下面我们就简单向读者介绍一下这几种类型:

 

12.2.1骨骼动画

        骨骼动画(SkeletalAnimation)又叫Bone Animation,最常用的动画类型。顾名思义,一个骨骼动画模型,其上面的顶点被绑定在有多块“骨头”组成的“骨骼”上,看上去像是在模拟人类的运动。当然,在动画中并不存在所谓的“骨头”或“骨骼”,它们不过是我们利用3Ds max或Maya等三位建模软件建立好的“实体”。数据与骨骼的绑定,这就好像身体上的肌肉附生在骨骼上一样关键帧动画需要存储每一帧的各个顶点的数据,而骨骼动画只需要存储每一帧的骨骼运动的数据,并根据网格数据中的骨骼信息来产生动画效果,骨骼要比顶点少得多,因此骨骼动画能用非常小的数据量实现类似关键帧动画的效果。

        与身体里的骨骼不同的是,在动画中并没有真实存在的骨头,它们被定义为移动、旋转和缩放这种“无实体”的空间变换。取而代之的是空间变换数据(正确的说,应该被称为“矩阵调色(Matrix Palette)”),骨骼动画就是通过这个表示骨头的矩阵来影响所绑定顶点的位置从而实现动画效果。在OGRE中.骨骼被定义为一个类似多叉树一样的父子结构。一个骨骼中所有的骨头都会有连接到上一级骨头,如同树枝一样,最终连接到根部,称之为根骨头。这种由骨头组成的层次结构称为骨架。每一块骨头都包含其自身的动画关键帧数据,根骨头没有父节点;骨头的运动会传递到所有下一级的骨头.然后依次传递,由此驱动骨架运动,。在空间变换的领域中,骨骼中的骨头和场景中的节点有着相同的情况:改变任何一个骨头都会影响所有子骨头的状态。

        与关键帧动画相比,使用骨髂动画有很多优点。首先,骨髂动画需要保存的动作数据量非常小。其次,通过指定不同的权重,可以很容易的将多个动作绑定在一起形成新的动作。还可以实现动作间的平滑过渡等等。

 

12.2.2顶点动画

        与通过计算动画骨骼,再根据骨骼位置计算顶点位置的动画方式不同。顶点动画是直接使用顶点让网格(mesh)产生动画的一种方式。每一组动作在顶点动画中对应一个顶点数据实体,对每个动画位置都要传送完整的顶点数据的拷贝。顶点动画被存储在.mesh文件中。有时这种方式是解决问题的唯一办法,例如让人脸模型产生面部表情及发音动画,实现逼真的面部表情动画。顶点动画分为两种子类型:变形动画和姿态动画。
   

12.2.2.1变形动画(MorphAnimation)

        变形动画是两种顶点动画技术中最简单最直观的一种。变形动画依靠在每个关键帧中及关键帧之间保存和插入表示顶点绝对位置的快照(snapshot)来生成动画。这种方法比较消耗资源,但是计算效率最高。因此,变形动画常常被用来制作场景中人物模型的肢体动作等,而人物模型表情的变化等较为细腻的动画类型应该交由下一小节“姿态”动画来实现。

        但是正是因为顶点绝对位置数据的使用,不可能在同样的顶点数据中混合多于一个的变形动画;如果想使用动画混合,则可以使用骨骼动画。当然,如果有两个动画,它们影响的顶点没有交集,那么也可以对这两个动画进行混合,不过最好避免这种情况出现。不过变形动画与骨骼动画能够组合应用。

 

12.2.2.2姿态动画(PoseAnimation)

        姿态动画可以用来模拟非常精细的动画,因此在3D游戏或动画的开发中,姿态动画常常被应用于人物模型的面部动画中,如:可以将姿态动画应用到人物角色的眼睛、眉毛、口型的变化等较为细腻的动画类型中来。

        为了产生姿态动画,需要引用预先包含在mesh中的一套动作集,而动作集中保存的顶点的位置是相对于复原位置的偏移量,是顶点的相对位置而不是绝对位置。而且也不要求每一个顶点都有偏移量,只有变化的顶点数据才被保存,没有偏移量的顶点在处理时会被自动忽略掉。因此,姿势动画不仅更灵活,而且对资源使用也更高效。

        一旦定义好了姿态,我们就可以在动画中引用他们。与变形动画不同的是,姿态动画允许将多个姿态混合成最终的顶点状态,模型的各种姿势被保存在动画轨迹中,这些姿势可以被混合来创建更复杂的动画。例如在制作面部表情动画时,姿势动画中每一个面部表情都可以被作为一个独立的动画,我们可以同时将一个表情混合在另外一个表情上,生成更为复杂的表情,如果每个姿态只影响面部的一部分,也可以组合所有的表情。当姿势动画的关键帧引用一个或多个姿势动画时,关键帧就会指定一个或多个姿态轨迹,每个姿态的引用都对其最终姿态有一个影响权重。权重决定了当姿势被混合时每个顶点偏移对最终的顶点位置产生多大的影响。不用每一帧都进行姿势混合,我们可以通过定义许多关键帧,使用多种姿态的混合,从而产生多个部位协调运动的动画。

 

笔者注:

        要十分小心多个姿态同时应用的状况,混合姿态数量的增加会消耗更多地执行时间来激活混合的姿态,因此在使用姿态动画时候有一些经验上的限制。例如在这个关键帧中混合了姿态1和姿态2,而在下一个关键帧中混合了姿态3和姿态4,那么在两个关键帧之间过渡的过程中就会执行4个活动轨迹,需要激活4个活动轨迹,而不是一般认为的2个。

 

 

12.2.4骨骼动画与顶点动画的混合

        在Ogre中,我们可以通过混合多种类型的动画来创建更复杂的动画效果。但是变形动画不能与姿态动画进行混合,也不能混合多个变形动画,这是由顶点动画的产生原因而决定的,这点要特别注意。

        当把顶点动画与骨骼动画混合的时候,Ogre会首先计算顶点动画的结果,然后在应用骨骼进行空间变换到最终位置,即那些在顶点上的效果会首先被应用到模型上,而后骨骼动画被应用到这个结果上。举例来说:可以对动画角色的复杂面部表情用顶点姿态动画进行控制,同时用骨骼动画控制整个角色身体动作的移动动画(包括头部模型)。

        通过把变形动画和骨骼动画混合,可以给骨骼动画角色增加一种变形网格的效果。例如,有时我们希望动画角色的衣服能够跟随实际骨骼动画的运动而发生变化,这时候就可以将衣服动画与角色的骨骼运动动画进行混合,通过骨骼动画控制角色本身的运动,而“外层”的衣服动画则可以交给变形动画进行处理,这样就可以很好的避免对衣服动画进行耗时的实时模拟计算。

        到目前为止,我们介绍的这些动画类型都可以在CPU中通过软件的方式执行,并且在默认的情况下就是这种执行方式。但是对于复杂的角色,通过硬件上面的顶点着色程序来实现硬件蒙皮是一个非常有用的想法。

 

笔者注:

        需要格外注意的一点是:硬件动画中的模型数据不能被CPU 使用。当我们需要对动画模型进行某些计算的时候,如果将顶点变换的计算工作交给GPU来执行的时候,你后面的代码就无法得到这些顶点的数据了。当我们在CPU中需要再次应用这些数据时,就需要在CPU中再次执行与GPU中相同的顶点变换计算。可以通过Entity::addSoftwareAnimationRequest()告知Ogre进行此操作。

 

        子网格模型的分离

        在开发3D游戏或动画的过程中,如果我们只需将混合顶点动画和骨骼动画应用到网格模型的一小部分上,例如:我们只需将混合顶点动画和骨骼动画应用到人物模型的口型中而不是全身上,这时候,我们只需要将整个网格分为两部分,其中的一个用于混合,而另一个暂时搁置,这样做的好处是可以减少计算的负荷,使程序的运行效率大大提升。

12.2.5场景节点动画

        从字面意思上不难理解,场景节点动画是基于“场景节点”的动画。场景结点动画是通过场景管理器创建的,在场景节点上也绑定着实体,上面的实体也会随着场景节点的移动而移动。它使得与之关联的实体对象自动的随着一起移动。你能在演示Demo_CameraTrack中看到照相机的轨迹行为,也可以在Demo_Fresnel的池塘中看鱼如何移动的。

        在具体的实现过程中,场景结点动画与骨骼动画是非常类似的。通常是使用SceneManager::createAnimation来创建一个场景节点动画,这个用SceneManager::createAnimation来创建的场景节点动画被称为“主动画”。在主动画创建完成之后,我们可以为你要使之“运动”的某一个或某几个场景节点创建一个NodeAnimationTrack,并为其创建关键帧,来控制场景节点的位置、方向、缩放等状态。

 

12.2.6数据动画

        数据动画是一种利用关键帧来保存相应动画数据信息的动画类型。每个关键帧中,都保存有一定量的动画数据信息。在Ogre中,通常是使用AnyNumeric这个数据类型来保存这些数据信息。另外,这里简单介绍一下,在Ogre中有一种被称为Any的特殊数据类型,它是一种可变类型,可以用来储存不同种类C++类型的数据。而我们在数据动画中要用到的AnyNumeric类实际上就是Any类的子类,AnyNumeric类专门用来负责存储各种诸如整形、浮点型等不同的动画数据类型。

8.3 动画实例

8.3.1 第一个动画实例

        首先,我们需要下面的模板框架,和我们以前生产的模板代码一样,只是添加了帧监听和用户输入的功能。这里笔者再次提醒,以便以后可以方便的重复使用这段代码模板,读者可以将其备份,需要的时候直接拿来用即可。

#include <windows.h>

#include "ExampleApplication.h"

 

class Example1 : public ExampleApplication,public Ogre::FrameListener,public OIS::KeyListener

{

public:

     Example1()

     {

         m_Node = NULL;

         mInputManager = NULL;

         mKeyboard = NULL;

         mMouse = NULL;

         mShutDown = false;

     }

     ~Example1()

     {

         if( mInputManager )

         {

              mInputManager->destroyInputObject( mKeyboard );

              mInputManager->destroyInputObject( mMouse );

              OIS::InputManager::destroyInputSystem(mInputManager);

              mInputManager = NULL;

         }

     }

     void createScene()

     {

         Ogre::Entity* ent = mSceneMgr->createEntity("Sinbad", "Sinbad.mesh");

         m_Node = mSceneMgr->getRootSceneNode()->createChildSceneNode();

         m_Node->setScale(10.0f,10.0f,10.0f);

         m_Node->attachObject(ent);

     }

     bool frameStarted(const FrameEvent& evt)

     {

         if(mShutDown)

         {

              return false;

         }

         mKeyboard->capture();

         mMouse->capture();

         return true;

     }

     bool frameRenderingQueued(const FrameEvent& evt)

     {

         return true;

     }

     bool frameEnded(const FrameEvent& evt)

     {

         return true;

     }

     bool keyPressed( const OIS::KeyEvent &arg )

     {

         if(arg.key == OIS::KC_ESCAPE)

         {

              mShutDown = true;

              return false;

         }

         return true;

     }

     bool keyReleased( const OIS::KeyEvent &arg )

     {

         return true;

     }

     void createFrameListener()

     {

         mRoot->addFrameListener(this);

 

         OIS::ParamList pl;

         size_t windowHnd = 0;

         std::ostringstream windowHndStr;

 

         mWindow->getCustomAttribute("WINDOW", &windowHnd);

         windowHndStr << windowHnd;

         std::string str = windowHndStr.str();

         pl.insert(std::make_pair(std::string("WINDOW"), windowHndStr.str()));

 

         mInputManager = OIS::InputManager::createInputSystem( pl );

         mKeyboard = static_cast<OIS::Keyboard*>(mInputManager->createInputObject( OIS::OISKeyboard, true ));

         mMouse = static_cast<OIS::Mouse*>(mInputManager->createInputObject( OIS::OISMouse, true ));

         mKeyboard->setEventCallback(this);

     }

protected:

private:

     Ogre::SceneNode* m_Node;

     OIS::InputManager* mInputManager;

     OIS::Keyboard* mKeyboard;

     OIS::Mouse*    mMouse;

     bool mShutDown;

};

 

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )

{

     Example1 app;

     app.go();

     return 0;

}

 

笔者注:

        如果读者对以上模板代码有任何疑问,请参阅前面章节的介绍。

 

第一步,我们需要给Example1类添加一个成员变量:

Ogre::AnimationState* mState;

第二步,在Example1类的构造函数中把mState初始化为NULL:

mState = NULL;

第三步,在createScene函数体中的代码后面继续加入如下代码:

mState = ent->getAnimationState("Dance");

if (mState)

{

     mState->setEnabled(true);

}

第四步:在frameRenderingQueued函数体中加入如下代码:

bool frameRenderingQueued(const FrameEvent& evt)

     {

         if (mState)

         {

              mState->addTime(evt.timeSinceLastFrame);

         }

         return true;

     }

编译并运行程序,你会发现一个正在跳舞的Sinbad:

【Ogre编程入门与进阶】第九章 动画_第1张图片

 

代码分析:

        通过上面的代码我们可以发现在Ogre中播放一个动画竟然如此简单,Ogre已经给我们封装好了大部分功能,让我们用起来十分方便。

        我们首先看一下第一步中的代码,我们增加了一个新的成员变量,当我们想要播放某个动画时,我们通常需要对每个动画创建一个叫做动画状态(AnimationState)的对象来控制动画的状态,通常我们可以通过Entity::getAnimationState (const String& name)函数来获得一个指向AnimationState的指针,正如同我们在第三步中做的那样;随后我们就可以在frameStarted或者frameRenderingQueued事件中,通过获取到的这个指针的方法来更新动画,正如同我们在第四步中做的那样;在操作动画状态之前,我们必须使用setEnabled方法使其生效,因此我们在第三步中才会加上mState->setEnabled(true);这一行代码,同样,除了这个函数之外,我们也可以通过其它的函数来设置动画的权重、时间位置以及是否无限循环等信息。动画状态(AnimationState)有一个addTime函数,可以持续的设置动画的位置信息,并且可以自动循环。addTime的参数可以是正数,也可以是负数(用来反转动画)。

8.3.2 同一时刻多个动画的融合

        在介绍多个动画的融合之前,我们先来讨论一个问题,我们现在把前面代码中的跳舞的动作改成其它的动画,找到前面mState = ent->getAnimationState("Dance");

这一行代码,把这里的"Dance"改为"RunBase"编译并运行,我们会看到人物正在做跑的动作,然后我们再改为"SliceVertical"编译并运行,我们会看到人物手部正在做切开的动作。这里我们的问题就来了,我们每次只能播放其中的一个动作,我们可不可以让这两个动作同时播放呢?答案是肯定了,Ogre中实现这一效果十分简单,下面我们在上节代码的基础上继续添加代码来完成这一功能:

第一步,把原来的"Dance"动作改为"RunBase",然后给Example1类添加一个新的成员变量:

Ogre::AnimationState* mState2;

第二步,在构造函数中初始化这个成员变量:

mState2 = NULL;

第三步,在createScene函数体中的代码后面继续加入如下代码:

mState2 = ent->getAnimationState("SliceVertical");

if (mState2)

{

     mState2->setEnabled(true);

}

第四步:在frameRenderingQueued函数体中继续加入如下代码(放在returntrue;之前):

if (mState2)

{

     mState2->addTime(evt.timeSinceLastFrame);

}

编译并运行,我们将会发现,两个动画已经融合到一起播放了:

【Ogre编程入门与进阶】第九章 动画_第2张图片

代码分析:

       这里新添加的代码其实和前面的基本一样,唯一的区别就是前面只播放一个动画,而这里我们同时播放了两个动画,当然,如果你愿意,你可以同时播放三个或者更多的动画,这样就解决了我们可以把不同的动作融合在一起的问题。

 




PS:很久以前就打算把学习Ogre中遇到的知识分享给大家(虽然只是一些皮毛),但是说来惭愧,一直很懒,直到最近才抽出点时间写一些自己的理解(Ogre入门级的东西),所以难免会有很多不足之处,分享是一种快乐,同时也希望和大家多多交流!

(由于在Word中写好的东西发布到CSDN页面需要重新排版(特别是有很多图片时),所以以后更新进度可能会比较慢,同时每章节发布的时间可能不一样(比如说我首选发布的是第二章,其实第一章就是介绍下Ogre的前世今生神马的,相信读者早就了解过~~~),但是我会尽量做到不影响大家阅读,还望大家谅解。)


 上述内容很多引用了网上现有的翻译或者内容,在此一并谢过(个人感觉自己有些地方写得或者翻译的不好),还望见谅,转载请注明此项!


你可能感兴趣的:(动画,OGRE)