Mage小组 著
Email: norman_chen@163.com
renwind@163.com
QQ: 18725262
http://www.173d8.com
http://blog.csdn.net/pizi0475
动画基础
实现动画的基本原理:多个关键帧组成一个动画轨迹,多个动画轨迹组成一个动画。在各个关键帧之间通过插值算法(Spline插值或线性插值)进行插值来生成最终的动画。一个动画不只包括关键帧,还有其它的一些属性(动画名、动画长度、动画的权重、是否被启用等)。一个动画轨迹可以指定其控制的节点。
OGRE中与基本动画相关的类
根据动画原理,我们抽象出以下几个类:
SimpleSpline与RotationSpline类
实现了样条的插值算法。SimpleSpline用来对Translate和Scale进行插值,RotationSpline用来对Rotation进行插值。
关键帧类(KeyFrame)
组成动画最基本的元素。一个动画轨迹里有多个关键帧,每个关键帧具有自己的位置、缩放比例和旋转角,同时每个关键帧还保存有自己在整个动画轨迹里所处的时间点。在实际运行时,根据当前时间,通过对两个关键帧的插值可以得到当前帧(当前位置、缩放比例和旋转角)。随着时间的变化,插值得到的当前帧也是变化的,动画就产生了。由于关键帧包括位置信息、缩放比例信息和旋转信息,所以可以实现运动动画、缩放动画和旋转动画以及混合动画。
动画轨迹类(AnimationTrack)
动画轨迹由多个关健帧组成,负责对相邻的两个关键帧之间插值。
每个动画轨迹保存关键帧列表、本动画轨迹所属的动画(Animation)、本动画轨迹所控制的Node,又由于动画轨迹负责关键帧的插值,所以它保存SimpleSpline(简单样条插值计算器)和RotationSpline(旋转样条插值计算器)对象以实现插值功能。
一个动画轨迹可以对一个物体产生作用,使物体按照动画轨迹运动。这个物体可以是一个Node(一块骨头或场景中的一个结点)。如果骨头受动画轨迹控制,可以实现骨骼动画,如果场景节点受动画轨迹控制,可以直接实现场景节点的动画运动。
重要函数
创建一个关键帧,传入这一帧的所在的时间点。
KeyFrame* createKeyFrame(Real timePos);
根据当前时间,得到插值计算出来的的当前帧。
KeyFrame getInterpolatedKeyFrame(Real timeIndex) const;
使当前动画轨迹对其控制的节点产生作用,参数是当前时间点、权重和是否累计权重。
void apply(Real timePos, Real weight = 1.0, bool accumulate = false);
动画类(Animation)
一个动画由多个动画轨迹(AnimationTrack)组成。而一个动画轨迹可以控制一个节点,这样一个动画可以使多个节点沿着自己的轨迹运动。
设想一下一个人的行走动画,人身上的关节点都沿着自己的轨迹运动,如果把两个关节点连接起来,就形成骨头,再让表面的网格受骨头影响而运动,骨骼动画的基础有了!
每个动画保存自己的AnimationTrack列表,保存动画名称和长度(时间)。
重要函数
设置关键帧间的插值方式。参数IM_LINEAR 代表线性插值、IM_SPLINE代表样条插值。
void setInterpolationMode(InterpolationMode im);
创建一个动画轨迹。第一个参数是这个动画轨迹的唯一标识,第二个参数指定应用这个动画轨迹的节点。
AnimationTrack* createTrack(unsigned short handle, Node* node);
使当前动画对其控制的节点产生作用(委托AnimationTrack进行),参数是当前时间点、权重和是否累计权重。
void apply(Real timePos, Real weight = 1.0, bool accumulate = false);
动画状态类(AnimationState)
一个动画状态类的对象对应一个动画类的对象,看上面的类图,AnimationState的数据成员mAnimationName与Animation类的数据成员mName是相对应的。它保存相应动画的状态。
动画状态包括动画名、当前时间点,动画长度(总时间)、动画权重和动画的enable开关。
重要函数
设置动画是否启用。
void setEnabled(bool enabled);
移动当前时间点,让动画状态在动画时间线上向前移动。参数为移动量。
void addTime(Real offset);
场景管理器类(SceneManager)
场景管理器负责动画和动画状态的创建与维护。在场景管理器里保存有mAnimationsList(动画列表)和mAnimationStates(动画状态列表),其中每个动画和动画状态通过名字一一对应。
场景管理器中有一个很重要的方法_applySceneAnimations ,它在每次渲染时(_renderScene函数里)都会被调用(自动调用不需要程序员控制),它的任务是在每一帧渲染前根据动画状态更新动画,完成动作。_applySceneAnimations方法遍历全部的动画状态,并根据这些状态找出与之一一对应的动画,再找出每个动画中的全部动画轨迹,在每个轨迹里都保存有该轨迹控制的节点。_applySceneAnimations方法先将这些被动画控制的节点都还原为初始状态,而后再调用动画的apply方法将全部节点更新到新的位置、大小或方向,从而使动画向前进行。
通过FrameLisener调用动画状态的addTime方法,可以完成动画状态的更新。场景管理器的_applySceneAnimations方法会根据新的动画状态将对应动画的相关节点的位置、大小和方向也更新,这就是OGRE中实现动画的基本原理和方法。
重要函数
创建动画Animation,参数为动画名和长度(时间)
virtual Animation* createAnimation(const String& name, Real length);
创建动画状态AnimationState,参数是与动画对应的名称。
virtual AnimationState* createAnimationState(const String& animName);
基本动画实例:
定义一个10秒种的动画,这个动画包含一个动画轨迹(上下翻转)。让这个动画应用到当前摄像机上去,程序运行时,我(第一人称摄像机)应该在上下翻转。
在createScene函数里做初始化工作。
首先我们考虑怎样可以把动画应用到当前摄像机上。因为一个动画可以应用到一个节点上,所以可以创建一个节点并将当前摄像机attach到这个节点上去,代码如下:
SceneNode* camNode = mSceneMgr->getRootSceneNode()->createChild();
camNode->attachObject(mCamera);
下面定义动画、动画轨迹以及关键帧:
// 定义动画,指定动画的名称及长度(这里为10秒)
Animation* anim = mSceneMgr->createAnimation("CameraTrack", 10);
// 指定动画关键帧之间的插值方式(包括线性插值和样条插值)
anim->setInterpolationMode(Animation::IM_SPLINE);
// 定义动画的一个动画轨迹,并指定这个轨迹是作用到camNode节点上的
AnimationTrack* track = anim->createTrack(0, camNode);
// 定义动画轨迹包含的关键帧,下面定义了四个关键帧,加上起始帧
// 五个关健帧形成了一个翻转的动画。
KeyFrame* key = track->createKeyFrame(0); // startposition
key = track->createKeyFrame(2.5);
key->setTranslate(Vector3(500,500,-1000));
key = track->createKeyFrame(5);
key->setTranslate(Vector3(-1500,1000,-600));
key = track->createKeyFrame(7.5);
key->setTranslate(Vector3(0,-100,0));
key = track->createKeyFrame(10);
key->setTranslate(Vector3(0,0,0));
然后定义AnimationState类的对象,它和刚才定义的动画类相对应。设置动画的状态为启用:
mAnimState = mSceneMgr->createAnimationState("CameraTrack");
mAnimState->setEnabled(true); // 启用该动画
到此,初始化工作就做完了。
最后,要想使动画动起来,我们需要重载ExampleFrameLisener类的frameStarted函数,并调用下面的函数,根据传入的时间来设置动画的状态: mAnimState->addTime(evt.timeSinceLastFrame);
骨骼动画
基本概念
什么是骨骼动画
骨骼动画用骨架(由一系列骨头构成的继承体系)来单独保存动作信息,这样就将动作信息与网络、皮肤信息分割成两种数据结构分别进行处理,从而比以住的动画技术效率更高。
每块骨头都有自己的位置和旋转方向,这些骨头以树的形式组织起来构成骨骼。例如:腕关节是肘关节的子节点,肘关节又是肩关节的子节点。肩关节旋转会自动带动肘关节运动,腕关节同样也会运动。
那么怎么使一个网格产生动作效果呢?我们可以使网格上的每个顶点都对应于一块或多块骨头,当这些骨头移动时便会影响网格的位置。如果一个点与多块骨关联,则必须通过指定权重来决定每块骨头对此顶点的影响程度(一个顶点对应一块骨头时,该点的权重为1.0 )。
与关键帧动画相比,使用骨骼动画有很多优点。首先,骨骼动画需要保存的动作数据量非常小。其次,通过指定不同的权重,可以很容易的将多个动作绑定在一起形成新的动作。还可以实现动作间的平滑过渡等等。
当然骨骼动画的实现中,骨骼本身的运动还是需要关键帧来完成,但信息量已经很小了。
Ogre的骨骼动画
Ogre骨骼信息和动画信息保存到后缀名为.skeleton的文件中,你可以通过OGRE提供的导出插件工具(Milkshape和3Dmax的exporter)来导出该类型的文件。当你创建基于.Mesh文件的Entity时,.Skeleton文件将会自动被系统加载进来。为了操作方便,Entity自动给每一个动作指定一个AnimationState类(请参考基本动画)的对象,你可以通过Entity::getAnimationState函数来得到具体的动作。
示例一:行走的机器人
该实例创建基于robot.mesh文件的实体(Entity)对象,指定其“走”动作(动作信息都在.skeleton文件里),并将其显示到屏幕上。robot.mesh文件中保存机器人的网格信息,Entity会自动加载保存有机器人骨骼信息的robot.skeleton文件。我们重载ExampleApplication类的createScene函数,其部分代码如下:
void createScene(void)
{
// ….
// 设置关键帧之间的插值方法为样条插值
Animation::setDefaultInterpolationMode(Animation::IM_SPLINE);
// 创建基于网格文件robot.mesh的Entity
Entity *ent = mSceneMgr->createEntity("robot", "robot.mesh");
// 将实体附属到场景根结点上
mSceneMgr->getRootSceneNode()->createChild()->attachObject(ent);
// 得到实体“走”动作的AnimationState类对象
mAnimState = ent->getAnimationState("Walk");
// “Enable”(起始)该动作
mAnimState->setEnabled(true);
}
在createScene函数里成功的指定了机器人的当前动作为“走”,下一步要让动画“动”起来,还需要根据时间跨度计算当前帧的骨骼位置。重载ExampleFrameListener类的frameStarted函数:
bool frameStarted(const FrameEvent& evt)
{
// 将两帧之间的时间差传入AnimationState::addTime函数,该函数内部会计算出动// 画的当前时间点。
mAnimState->addTime(evt.timeSinceLastFrame);
// 调用父类的frameStarted函数
return ExampleFrameListener::frameStarted(evt);
}