Ogre骨骼动画笔记
Ogre 骨骼动画 Skeleton Animation
第一是从Ogre骨骼动画的使用接口和内部实现两方面分析。
第二是从动态和静态两方面分析Ogre骨骼动画的内部实现。
动态分析主要分析Ogre一段骨骼动画Animation以及N段骨骼动画组成的某个Entity或者Model的动画集合AnimationSet。
静态分析主要分析在一帧中,Ogre是如何混合两个或两个以上的动画;更新这一帧动画时,对骨骼的操作,以及这些操作如何应用到顶点上,即蒙皮技术。
首先分析使用接口。
一个模型不止有一个动画,如一个人物模型,可能包括跑、跳、挥手、死亡等动作,这些动作的动画就构成了一个集合。
在Ogre中,一个动画称为AnimationState,故而一个动画集合称之为AnimationStateSet。
在AnimationStateSet内部管理了AnimationName到AnimationState的映射表,该成员类型为 map<String, AnimationState*>。
既然称之为AnimationState,那么就必须包含着一个动画的属性和所处的状态:动画名称,动画总长度,动画已经播放的时间,混播的权重比例,是否循环,是否起效,以及混播的一些参数。
归结一下,AnimationState包含的成员不外如下:
AnimationState:
BoneBlendMask* mBlendMask; // 每颗骨骼的权重
String mAnimationName;
AnimationStateSet* mParent; // 包含在哪个动画集合中。
Real mTimePos;
Real mLength;
Real mWeight;
bool mEnabled;
bool mLoop;
动画集合AnimationStateSet主要提供对AnimationState的管理,增删查改之类。并且记录当前起效的动画列表。
并且需要记录所管理的动画状态是否有更新,以供内部实现更新实际动画。
因此成为如下:
AnimationStateSet:
unsigned long mDirtyFrameNumber; // 记录最近一次动画有所改变的帧数
AnimationStateMap mAnimationStates;
EnabledAnimationStateList mEnabledAnimationStates;
第二,使用接口与内部实现的关联。
上层应用通过AnimationState改变了动画的状态,必然需要某种机制通知内部实现,或者内部实现需要查询AnimationState的状态,才能对内部状态做出更新。
这种关联就是通过AnimationStateSet中的DirtyFrameNumber来实现的,内部实现通过比较DirtyFrameNumber的差异做出判断,是否需要更新。
首先说更新动画的内部实现入口,这个内部入口很显眼,是Entity::updateAnimation()。
在Entity::updateAnimation()中判断动画是否需要更新的语句为:
bool animationDirty =
(mFrameAnimationLastUpdated != mAnimationState->getDirtyFrameNumber()) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty());
(后半个条件判断先不用管,它的大概意思就是说某人在一个正常人上面加了一根骨头,比如在贱客手上了加了一把剑的骨头,
而这个骨头是这个贱人自己手动更新的,这时也需要更新动画。)
为什么这么比较可行,因为离这条语句下面,不远的地方,有另一条语句:
mFrameAnimationLastUpdated = mAnimationState->getDirtyFrameNumber();
两条语句一结合,就很通俗易懂了对吧。敌不动我不动,敌动我骚动。
概貌如下:
void Entity::updateAnimation(void)
{
// ...
// ...
bool animationDirty =
(mFrameAnimationLastUpdated != mAnimationState->getDirtyFrameNumber()) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty());
//...
//...
mFrameAnimationLastUpdated = mAnimationState->getDirtyFrameNumber();
//...
}
第三分析内部实现。
先说静态的,也就是一帧内的情况。
首先需要列出几个跟内部实现有关的类。
第一个是Animation,这是动画的内部实现。第二个是Skeleton,这是动画将要应用到的对象。
第三个是Bone,一副骨架Skeleton包含N个骨头,动画应用到Skeleton,最终也就是落实到了Bone上面,他们是整体和部分的关系。
Animation和Skeleton的关系很简单,就是一个愿打一个愿挨。
Entity用Animation打Skeleton,被打的叫出了声,让Entity听到了,它就高兴了得意的笑着扭臀,于是动了,于是百媚生。
整个动的过程就是这样。
Skeleton还配备了各种蹂躏自己的打法,这些打法就构成了它的Animation集合,即typedef map<String, Animation*>::type AnimationList。
Entity更新Skeleton的方法是bool Entity::cacheBoneMatrices(void)。
此方法将动画状态应用到Skeleton上,更新Skeleton上的骨头们,骨头们一阵扭捏,于是从骨头们上拿到扭捏后的矩阵。
主要代码如下:
unsigned long currentFrameNumber = root.getNextFrameNumber();
if ((*mFrameBonesLastUpdated != currentFrameNumber) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty()))
{
if ((!mSkipAnimStateUpdates) && (*mFrameBonesLastUpdated != currentFrameNumber))
mSkeletonInstance->setAnimationState(*mAnimationState);
mSkeletonInstance->_getBoneMatrices(mBoneMatrices);
*mFrameBonesLastUpdated = currentFrameNumber;
return true;
}
和如上同,FrameNumber主要用来避免不要的扭捏,提高性能。
接下来细看mSkeletonInstance->setAnimationState(*mAnimationState);这行代码。
注意到没有,外部接口被内部实现征用了。
AnimationState只是存着Animation的一些状态,没有实质内容。实质内容在Animation里面。AnimationState和Animation通过名字相关联。
一般情况下,是通过AnimationName从另外一个映射表中拿到Animation实例。
当然Ogre更高级一点,考虑的情况更多:如果这个动画名字不在我Skeleton的十八摸打法AnimationList里面怎么办?
没关系,Skeleton还可以关联另外一个Skeleton,向好基友借一套专属风月宝鉴来,让Entity爽个够。
这也就是Skeleton::_getAnimationImpl(const String& name, const LinkedSkeletonAnimationSource** linker) 的实现。
这就减轻了美术做动作动画的劳动量。
通过AnimationName拿到了Animation后,就可以将其应用到Skeleton上了。
一个Entity可以同时做多个动作,比如一边点头一边哈腰一边挥手,一边点鼠标一边撸,所以要把这些同时起效的Animation一次应用到Skeleton上。
而且每个动作的权重也不一样,举个不好的例子,哈腰的时候,手臂会往下垂,挥手的时候,手臂也要动,但幅度和影响效果是不一样的,也就是权重不一样。
代码依然通俗易懂,犹如网络小说。具体代码如下:
ConstEnabledAnimationStateIterator stateIt = animSet.getEnabledAnimationStateIterator();
while (stateIt.hasMoreElements())
{
const AnimationState* animState = stateIt.getNext();
const LinkedSkeletonAnimationSource* linked = 0;
Animation* anim = _getAnimationImpl(animState->getAnimationName(), &linked);
// tolerate state entries for animations we're not aware of
//...
anim->apply(this, animState->getTimePosition(), animState->getWeight() * weightFactor,
animState->getBlendMask(), linked ? linked->scale : 1.0f);
// OR
anim->apply(this, animState->getTimePosition(),
animState->getWeight() * weightFactor, linked ? linked->scale : 1.0f);
//...
}
我们挑一个软的捏,比如void Animation::apply(Skeleton* skel, Real timePos, Real weight, Real scale);
先看这个apply的参数:
Skeleton——将要应用的框架;
timePos——这一帧内,动画进行到的时间;
weight——归一化的权重
scale——这是考虑到了同一套动作对大人和小孩两种不同体型,动作的幅度不太一样。
Animation::apply()方法中,第一行总是_applyBaseKeyFrame()。
这一句又反应了多个动作同时播的一种情况,简单的例子就是人物一边跑动,还一边挥刀砍人。
这两个动作并没有太大关联,没有权重比例分配,但有一先一后的关系。先的是基础动作,后的是派生动作。
用Ogre的原话说就是:
However, sometimes
it is useful for animators to create animations with a different starting
pose, because that's more convenient, and the animation is designed to
simply be added to the existing animation state and not globally averaged
with other animations。
这都是经验的体现啊。
接下来我们假设Animation已经apply到Skeleton了,Skeleton也因此发生了一些变化。
现在我们需要的就是拿出这些变化来,应用到Skeleton相关联的Mesh顶点上。
首先是从Skeleton中抽出变换Transform,包括旋转平移和缩放。
众所周知,Skeleton中的Bones是呈树形结构的(不知道现在就当作知道了),
因此每根骨头的Transform等于ParentBone的Transform乘上自己原本的Transform,这些Transform会逐一累积起来,于是形成了整个Skeleton的变换。
Entity把每根Bone的变换抽取出来,计算TransformMatrix,存在一个数组里,即Entity::mBoneMatrices,它将最终应用到蒙皮上。
Ogre中实现了软件蒙皮和硬件蒙皮,软件蒙皮在其代码中实现,主要应用于硬件不支持蒙皮、绘制阴影(stencilShadows)和强制使用软件蒙皮的场合。
硬件蒙皮在shader程序中实现,在GPU上运算实施。
硬件蒙皮参考http://bbs.iieeg.com/viewthread.php?tid=1811 和 http://www.verydemo.com/demo_c269_i66.html
软件蒙皮的代码位于
static void softwareVertexBlend(const VertexData* sourceVertexData,
const VertexData* targetVertexData,
const Matrix4* const* blendMatrices, size_t numMatrices,
bool blendNormals);
可以参考http://www.cppblog.com/flyindark/archive/2012/07/25/OgreSkeletonAnimation.html
主要思路是对每个顶点,取出它所关联的骨骼和对应的权重,加权计算每个顶点的新位置和法线:
首先对顶点进行计算
? 找到当前的混合索引值
? 用这个值索引出混合矩阵M4
? M4左乘以顶点V1(*)得到V2
? V2进行加权计算得到V3(=V2*weight)
? V3归一处理得到V4(=V3.normalized)
注意,顶点不需要归一化处理,但法线需要。
现在从动态角度分析,这个比较简单。
首先需要深入分析Animation的结构。Ogre中有三类动画:NodeAnimation,NumericAnimation,VertexAnimation。
与骨骼最动画相关的是NodeAnimation。Skeleton中的Bone就是继承一个Node实现的。这里只说NodeAnimation。
一个NodeAnimation中由若干个NodeAnimationTrack组合而成,Track意思为轨迹,即一个动作包含多条移动轨迹。
每个NodeAnimationTrack与一个Bone相对应,于是在Animation里有typedef map<unsigned short, NodeAnimationTrack*>::type NodeTrackList。
其中键值为Bone的索引。
每条移动轨迹Track由多个KeyFrame组成,每个KeyFrame记录状态(位置、缩放、朝向等))和时间点。每个Frame的状态就由这些KeyFrame插值而来。
回头看void Animation::apply(Skeleton* skel, Real timePos, Real weight, Real scale),
就是逐一的将每个NodeAnimationTrack应用到它对应的Node上,见代码Animation::apply():
NodeTrackList::iterator i;
for (i = mNodeTrackList.begin(); i != mNodeTrackList.end(); ++i)
{
// get bone to apply to
Bone* b = skel->getBone(i->first);
i->second->applyToNode(b, timeIndex, weight, scale);
}
在NodeAnimationTrack::applyToNode(Node* node, const TimeIndex& timeIndex, Real weight, Real scl)中,
就可以看到详细的关键帧插值过程了。
第一是从Ogre骨骼动画的使用接口和内部实现两方面分析。
第二是从动态和静态两方面分析Ogre骨骼动画的内部实现。
动态分析主要分析Ogre一段骨骼动画Animation以及N段骨骼动画组成的某个Entity或者Model的动画集合AnimationSet。
静态分析主要分析在一帧中,Ogre是如何混合两个或两个以上的动画;更新这一帧动画时,对骨骼的操作,以及这些操作如何应用到顶点上,即蒙皮技术。
首先分析使用接口。
一个模型不止有一个动画,如一个人物模型,可能包括跑、跳、挥手、死亡等动作,这些动作的动画就构成了一个集合。
在Ogre中,一个动画称为AnimationState,故而一个动画集合称之为AnimationStateSet。
在AnimationStateSet内部管理了AnimationName到AnimationState的映射表,该成员类型为 map<String, AnimationState*>。
既然称之为AnimationState,那么就必须包含着一个动画的属性和所处的状态:动画名称,动画总长度,动画已经播放的时间,混播的权重比例,是否循环,是否起效,以及混播的一些参数。
归结一下,AnimationState包含的成员不外如下:
AnimationState:
BoneBlendMask* mBlendMask; // 每颗骨骼的权重
String mAnimationName;
AnimationStateSet* mParent; // 包含在哪个动画集合中。
Real mTimePos;
Real mLength;
Real mWeight;
bool mEnabled;
bool mLoop;
动画集合AnimationStateSet主要提供对AnimationState的管理,增删查改之类。并且记录当前起效的动画列表。
并且需要记录所管理的动画状态是否有更新,以供内部实现更新实际动画。
因此成为如下:
AnimationStateSet:
unsigned long mDirtyFrameNumber; // 记录最近一次动画有所改变的帧数
AnimationStateMap mAnimationStates;
EnabledAnimationStateList mEnabledAnimationStates;
第二,使用接口与内部实现的关联。
上层应用通过AnimationState改变了动画的状态,必然需要某种机制通知内部实现,或者内部实现需要查询AnimationState的状态,才能对内部状态做出更新。
这种关联就是通过AnimationStateSet中的DirtyFrameNumber来实现的,内部实现通过比较DirtyFrameNumber的差异做出判断,是否需要更新。
首先说更新动画的内部实现入口,这个内部入口很显眼,是Entity::updateAnimation()。
在Entity::updateAnimation()中判断动画是否需要更新的语句为:
bool animationDirty =
(mFrameAnimationLastUpdated != mAnimationState->getDirtyFrameNumber()) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty());
(后半个条件判断先不用管,它的大概意思就是说某人在一个正常人上面加了一根骨头,比如在贱客手上了加了一把剑的骨头,
而这个骨头是这个贱人自己手动更新的,这时也需要更新动画。)
为什么这么比较可行,因为离这条语句下面,不远的地方,有另一条语句:
mFrameAnimationLastUpdated = mAnimationState->getDirtyFrameNumber();
两条语句一结合,就很通俗易懂了对吧。敌不动我不动,敌动我骚动。
概貌如下:
void Entity::updateAnimation(void)
{
// ...
// ...
bool animationDirty =
(mFrameAnimationLastUpdated != mAnimationState->getDirtyFrameNumber()) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty());
//...
//...
mFrameAnimationLastUpdated = mAnimationState->getDirtyFrameNumber();
//...
}
第三分析内部实现。
先说静态的,也就是一帧内的情况。
首先需要列出几个跟内部实现有关的类。
第一个是Animation,这是动画的内部实现。第二个是Skeleton,这是动画将要应用到的对象。
第三个是Bone,一副骨架Skeleton包含N个骨头,动画应用到Skeleton,最终也就是落实到了Bone上面,他们是整体和部分的关系。
Animation和Skeleton的关系很简单,就是一个愿打一个愿挨。
Entity用Animation打Skeleton,被打的叫出了声,让Entity听到了,它就高兴了得意的笑着扭臀,于是动了,于是百媚生。
整个动的过程就是这样。
Skeleton还配备了各种蹂躏自己的打法,这些打法就构成了它的Animation集合,即typedef map<String, Animation*>::type AnimationList。
Entity更新Skeleton的方法是bool Entity::cacheBoneMatrices(void)。
此方法将动画状态应用到Skeleton上,更新Skeleton上的骨头们,骨头们一阵扭捏,于是从骨头们上拿到扭捏后的矩阵。
主要代码如下:
unsigned long currentFrameNumber = root.getNextFrameNumber();
if ((*mFrameBonesLastUpdated != currentFrameNumber) ||
(hasSkeleton() && getSkeleton()->getManualBonesDirty()))
{
if ((!mSkipAnimStateUpdates) && (*mFrameBonesLastUpdated != currentFrameNumber))
mSkeletonInstance->setAnimationState(*mAnimationState);
mSkeletonInstance->_getBoneMatrices(mBoneMatrices);
*mFrameBonesLastUpdated = currentFrameNumber;
return true;
}
和如上同,FrameNumber主要用来避免不要的扭捏,提高性能。
接下来细看mSkeletonInstance->setAnimationState(*mAnimationState);这行代码。
注意到没有,外部接口被内部实现征用了。
AnimationState只是存着Animation的一些状态,没有实质内容。实质内容在Animation里面。AnimationState和Animation通过名字相关联。
一般情况下,是通过AnimationName从另外一个映射表中拿到Animation实例。
当然Ogre更高级一点,考虑的情况更多:如果这个动画名字不在我Skeleton的十八摸打法AnimationList里面怎么办?
没关系,Skeleton还可以关联另外一个Skeleton,向好基友借一套专属风月宝鉴来,让Entity爽个够。
这也就是Skeleton::_getAnimationImpl(const String& name, const LinkedSkeletonAnimationSource** linker) 的实现。
这就减轻了美术做动作动画的劳动量。
通过AnimationName拿到了Animation后,就可以将其应用到Skeleton上了。
一个Entity可以同时做多个动作,比如一边点头一边哈腰一边挥手,一边点鼠标一边撸,所以要把这些同时起效的Animation一次应用到Skeleton上。
而且每个动作的权重也不一样,举个不好的例子,哈腰的时候,手臂会往下垂,挥手的时候,手臂也要动,但幅度和影响效果是不一样的,也就是权重不一样。
代码依然通俗易懂,犹如网络小说。具体代码如下:
ConstEnabledAnimationStateIterator stateIt = animSet.getEnabledAnimationStateIterator();
while (stateIt.hasMoreElements())
{
const AnimationState* animState = stateIt.getNext();
const LinkedSkeletonAnimationSource* linked = 0;
Animation* anim = _getAnimationImpl(animState->getAnimationName(), &linked);
// tolerate state entries for animations we're not aware of
//...
anim->apply(this, animState->getTimePosition(), animState->getWeight() * weightFactor,
animState->getBlendMask(), linked ? linked->scale : 1.0f);
// OR
anim->apply(this, animState->getTimePosition(),
animState->getWeight() * weightFactor, linked ? linked->scale : 1.0f);
//...
}
我们挑一个软的捏,比如void Animation::apply(Skeleton* skel, Real timePos, Real weight, Real scale);
先看这个apply的参数:
Skeleton——将要应用的框架;
timePos——这一帧内,动画进行到的时间;
weight——归一化的权重
scale——这是考虑到了同一套动作对大人和小孩两种不同体型,动作的幅度不太一样。
Animation::apply()方法中,第一行总是_applyBaseKeyFrame()。
这一句又反应了多个动作同时播的一种情况,简单的例子就是人物一边跑动,还一边挥刀砍人。
这两个动作并没有太大关联,没有权重比例分配,但有一先一后的关系。先的是基础动作,后的是派生动作。
用Ogre的原话说就是:
However, sometimes
it is useful for animators to create animations with a different starting
pose, because that's more convenient, and the animation is designed to
simply be added to the existing animation state and not globally averaged
with other animations。
这都是经验的体现啊。
接下来我们假设Animation已经apply到Skeleton了,Skeleton也因此发生了一些变化。
现在我们需要的就是拿出这些变化来,应用到Skeleton相关联的Mesh顶点上。
首先是从Skeleton中抽出变换Transform,包括旋转平移和缩放。
众所周知,Skeleton中的Bones是呈树形结构的(不知道现在就当作知道了),
因此每根骨头的Transform等于ParentBone的Transform乘上自己原本的Transform,这些Transform会逐一累积起来,于是形成了整个Skeleton的变换。
Entity把每根Bone的变换抽取出来,计算TransformMatrix,存在一个数组里,即Entity::mBoneMatrices,它将最终应用到蒙皮上。
Ogre中实现了软件蒙皮和硬件蒙皮,软件蒙皮在其代码中实现,主要应用于硬件不支持蒙皮、绘制阴影(stencilShadows)和强制使用软件蒙皮的场合。
硬件蒙皮在shader程序中实现,在GPU上运算实施。
硬件蒙皮参考http://bbs.iieeg.com/viewthread.php?tid=1811 和 http://www.verydemo.com/demo_c269_i66.html
软件蒙皮的代码位于
static void softwareVertexBlend(const VertexData* sourceVertexData,
const VertexData* targetVertexData,
const Matrix4* const* blendMatrices, size_t numMatrices,
bool blendNormals);
可以参考http://www.cppblog.com/flyindark/archive/2012/07/25/OgreSkeletonAnimation.html
主要思路是对每个顶点,取出它所关联的骨骼和对应的权重,加权计算每个顶点的新位置和法线:
首先对顶点进行计算
? 找到当前的混合索引值
? 用这个值索引出混合矩阵M4
? M4左乘以顶点V1(*)得到V2
? V2进行加权计算得到V3(=V2*weight)
? V3归一处理得到V4(=V3.normalized)
注意,顶点不需要归一化处理,但法线需要。
现在从动态角度分析,这个比较简单。
首先需要深入分析Animation的结构。Ogre中有三类动画:NodeAnimation,NumericAnimation,VertexAnimation。
与骨骼最动画相关的是NodeAnimation。Skeleton中的Bone就是继承一个Node实现的。这里只说NodeAnimation。
一个NodeAnimation中由若干个NodeAnimationTrack组合而成,Track意思为轨迹,即一个动作包含多条移动轨迹。
每个NodeAnimationTrack与一个Bone相对应,于是在Animation里有typedef map<unsigned short, NodeAnimationTrack*>::type NodeTrackList。
其中键值为Bone的索引。
每条移动轨迹Track由多个KeyFrame组成,每个KeyFrame记录状态(位置、缩放、朝向等))和时间点。每个Frame的状态就由这些KeyFrame插值而来。
回头看void Animation::apply(Skeleton* skel, Real timePos, Real weight, Real scale),
就是逐一的将每个NodeAnimationTrack应用到它对应的Node上,见代码Animation::apply():
NodeTrackList::iterator i;
for (i = mNodeTrackList.begin(); i != mNodeTrackList.end(); ++i)
{
// get bone to apply to
Bone* b = skel->getBone(i->first);
i->second->applyToNode(b, timeIndex, weight, scale);
}
在NodeAnimationTrack::applyToNode(Node* node, const TimeIndex& timeIndex, Real weight, Real scl)中,
就可以看到详细的关键帧插值过程了。
还没人赞这篇日记