blog_name
2014-06-02 22:50 | Lable: Programming |
角色动画小记

角色动画小记



[t1] @= <span style="font-weight: bold; color: white; background-color: #959595; padding: 2px 2px 2px 10px; margin: 2px; line-height: 180%; display: block;">
[/t1] @= </span>
[t2] @= <span style="font-weight: bold; margin: 2px; line-height: 150%; border: 1px solid black; width: 120px; display: inline-block; text-align: center;">
[/t2] @= </span>
[t3] @= <span style="font-weight: bold; color: #0000FF;">
[/t3] @= </span>
[eq] @= <span style="width: 400px; display: inline-block; text-align: center;">
[/eq] @= </span>
the demo is from the book: 3D Game Programming with DirectX 11, Chapter 25

龙书上演示的骨骼蒙皮动画,Soldier的模型是光头和战斗服,
DX9的动画实现是用D3DX 9.0c Animation API,这些API在DX11中已经不在了,于是作者自己封装。
这篇笔记是书上内容的摘抄,方便平时参考,翻译很烂,需要了解完整请看原版。

FRAME HIERARCHIES
人体树结构粗略模型:

frame_hierarchies1

数学公式:
转换Bone的局部坐标系到父坐标系,以一只手臂举例:

frame_hierarchies2

F0、F1、F2(插图略)分别是Bone 0、Bone 1、Bone 2的所在框架。
Ai是to-parent矩阵,A0是转换框架F0到世界空间。
在手臂结构中,转换第i个对象到世界空间的矩阵Mi定义为:

Mi = AiAi-1A1A0公式25.1

这样依次从子坐标系转换到父坐标系,最终会达到祖先坐标系,于是转换到了世界坐标系。
实际上的转换要比这个例子复杂些,因为人体是树结构而非简单一个线性的手臂结构。
这个Demo的Soldier模型总共用了56块骨头。

SKINNED MESHES
定义:
骨骼提供了自然的框架结构来驱动一个角色动画系统。
骨骼被一层外部皮肤包围,皮肤即是我们做的3D几何模型。
皮肤的顶点是与绑定空间(bind space1)相关联的,
绑定空间也就是整个皮肤相对的局部坐标系(通常是root坐标系)。
每个在骨架中的骨头(bone)影响到子集皮肤的的形状和位置(比如顶点影响)。
因此,我们运动骨骼,其附属的皮肤也相对运动,对应其骨架的当前姿势。

重建一块骨头的To-Root转换:
与to-parent转换不同的是,我们转换root坐标系到世界坐标系使用一个单独的步骤,
而不是去找每块骨头的to-world矩阵。
另一点不同是,to-parent转换走过一个节点的祖先采用自底向上的方式,从一块骨头开始,移动到它的祖先。
但是,实际上是自上而下的的方法更有效率。
我们从root开始沿着树向下移动。标记第n块骨头以整数0,1,...,n-1,
以下公式便是表示第i块骨头的to-root转换:

toRooti = toParenti·toRootp公式25.2

p是骨头标记表示骨头i的父骨头。
按照这个公式的顺序,可以知道每块父骨头的toRootp会在计算骨头i的toRooti之前正好得到。
剩下的toParenti每次只需计算一次,避免了很多重复的点乘。

偏移转换:
在使用公式25.2之前有一个问题,骨头所影响的顶点不是与骨头的坐标系相关联的,
而是与绑定空间关联(建模mesh时的坐标系)。
因此先要转换顶点从绑定空间到骨头空间,从而控制这些顶点。
这个叫offset transformation。

用offset矩阵转换任意一块骨头B的顶点,我们移动顶点从绑定空间到骨头B的空间。
然后,一旦有了骨头B空间的顶点,我们可以用B的to-root转换,把顶点放置回当前动画姿势的角色空间。

组合offset转换与to-root转换,成为一个新的转换叫final transform,第i块骨头的Fi最终转换矩阵为:

Fi = offseti·toRooti公式25.3

动画骨架:
在上一章Demo中演示了如何动画一个单独物体,
定义关键帧指定一个特定时间物体的位置、方向和大小,然后按照时间排列一序列的关键帧,
这就是一个整体动画的大致定义。
关键帧与关键帧之间的动画需要插值来计算。
我们把这个动画系统扩展到动画骨架。

动画一个骨架并不比动画一个单独物体难太多。
我们可以认为一块骨头是一个单独的物体,一个骨架只是一个连接在一起的骨头的集合。
假定每块骨头可以单独的移动。
因此,要动画一个骨架,只需要局部地动画每块骨头。
然后当每块骨头完成局部动画后,我们把它的祖先们的运动考虑进去,转换骨头到root空间。

我们把一个序列的骨架动画定义为一个动画片段,让特定的骨架动画工作在一起。
比如说:“走路”、“跑步”、“格斗”、“闪避”、“跳跃”都是动画片段的列子。

一个角色在程序中通常会有好几个动画片段,为了表现所有需要的动画。
所有动画片段工作在相同的骨架上,使用相同数量的骨头(尽管一些相同的骨头在特别的动画中会被固定)。
我们使用一个map数据结构储存所有的动画片段,用一个名字命名一个动画片段:
std::map<std::string, AnimationClip> mAnimations;
AnimationClip& clip = mAnimations["attack"];

最后,前面提到过,每块骨头需要offset转换,转换bind空间的顶点到bone空间;
此外,我们需要一个方法来展现骨架层次结构(我们用一个array,详见下一部分)。
这样给我们最终的数据结构来储存骨架动画数据。

SkinnedDate的数据结构代码略。

计算Final转换:
角色的框架结构一般来说是一个树,类似于前面提到的人体树结构粗略模型。
我们用一个整数array来建立结构层次,ith array entry提供ith bone的parent index。
此外,ith entry在动画片段中对应ith BoneAnimation,以及ith entry对应ith offset transform。
root骨头总是在element 0,以及它没有父母。

查找grandparent offset transform的代码略。

final transform的代码略。

角色模型层次结构array前10个骨头的数据示例:
ParentIndexOfBone0: -1
ParentIndexOfBone1: 0
ParentIndexOfBone2: 0
ParentIndexOfBone3: 2
ParentIndexOfBone4: 3
ParentIndexOfBone5: 4
ParentIndexOfBone6: 5
ParentIndexOfBone7: 6
ParentIndexOfBone8: 5
ParentIndexOfBone9: 8
这样Bone9,他的parent是Bone8,Bone8的parent是Bone5。。。
Bone2的parent是root node Bone0。
注意在有序数组中一个child bone永远不会在它的parent bone之前出现。

VERTEX BLENDING
上一部分展示了如何动画骨架,在这一部分我们将关注动画骨架上皮肤的顶点。
这个算法叫vertex blending。

顶点混合的策略如下。
我们有个一个潜在的骨头层次结构,但皮肤本身是一个连续的网格(我们不能分开网格,让部分单独运动)。
此外,一个或多个骨头会影响皮肤顶点;最终结果由这些骨头的final transform加权平均所决定。
(权重在建模时被艺术家指定,会被保存进文件)
随着这一步,可以在关节上实现平滑过度的blend,从而让皮肤有弹性的感觉。

在实践中,[Moller08]指出我们通常不会需要超过4块骨头来影响每个顶点。
因此,在我们的设计中会考虑一个顶点被影响的骨头数量最大值是4。
为了实现顶点混合,我们为一个角色建立连续的网格皮肤。
每个顶点包含4个索引,索引骨矩阵调色板(bone matrix palette),指示final转换矩阵的数组。
(final转换矩阵是一个在骨架中的骨头的entry)
此外,每个顶点还包含4个权重,指示顶点被各个骨头影响的量。
这样我们有了下面的顶点数据结构:
struct PosNormalTexTanSkinned
{
	XMFLOAT3 Pos;
	XMFLOAT3 Normal;
	XMFLOAT2 Tex;
	XMFLOAT4 TangentU;
	XMFLOAT3 Weights;
	BYTE BoneIndices[4];
};
BoneIndices指向骨头的final转换矩阵。

一个连续的网格的顶点有这样的格式,为顶点混合而准备,我们称它为一个skinned mesh。

任意顶点v的顶点混合位置v',与root frame关联,
(记住一旦我们在root坐标系有了一切,就执行world转换)
这个位置的计算表示为如下的加权平均公式:

v' = w0vF0+w1vF1+w2vF2+ w3vF3

其中w0+w1+w2+w3 = 1;也就是总的权重加起来是1。

注意这个等式,我们以final bone转换单独地控制所给顶点v(比如,矩阵F0F1F2F3)。
然后用这些单独转换的点进行加权平均,来计算最终的顶点混合位置v'。

转换法线和切线也是类似的:

n' = normalize(w0nF0+w1nF1+w2nF2+ w3nF3)

t' = normalize(w0tF0+w1tF1+w2tF2+ w3tF3)

这里我们假定转换矩阵Fi不包含任何不一致的缩放。
否则,我们在转换法线时,需要用inverse-transpose (Fi-1)T(见§7.2.2)。

顶点着色代码略。

余下部分是如何载入动画数据的实例,代码略。

其他文章
d3dx_skinnedmesh.pdf
Skinned Mesh Character Animation with Direct3D 9.0c
一篇DX9的文章,原链接已经失效,但依然可以在网上找到

Introduction to Game Development
可以在线阅读一部分,Character Animation章节介绍理论

Chapter 4. Animation in the Dawn Demo
宝石书

Skeletal Animation
OpenGL Wiki

Dual Quaternion Skinning
一种与本文所用Linear Blend Skinning 不同的骨骼动画算法介绍

[blender to UE4]Problem with animations
Blender中的Dual Quaternion,叫Preserve Volume

标记
1. bind space的定义,在d3dx_skinnedmesh.pdf中提到:
An offset matrix transforms vertices, in the bind pose, from bind space to the space of the respective bone.
The bind pose is the default layout of the character mesh geometry before applying any bone
transformations (i.e., the bone transforms are identity matrices).
What Is a Binding Pose in Character Animation?

Last modified: 2016-02-14 16:33:54
Comments: 0

-
.

*Name:
Email: (Won't be published.)
Website:
*Content:
*Type the word:


Avatar support: gravatar.com
Comment needs to be plain text.
*: Required

Previous | Next page | 0-0
0