Skeletal Animation

Interpolation

为什么要讲插值呢?道理是简单的,艺术家们的精力是有限的,他们只能画出有限的关键帧,而关键帧之间的内容需要程序员发挥想象力填充 xD。

举个例子,假如我们需要创建一个一维动画,即从\(x=0\)的位置移动到\(x=1\),耗时为\(1\)。我们有一些插值选择,比如\(x=t\),这是一个普通匀速操作。我们也可以选择\(x=-2t^3+3t^2\),这个函数的好处在于在开始和结束处的导数都是\(0\),于是动画可能看上去更加平滑。当然还有各种其他插值函数。

Translation

假如前后的位置分别是\(p_0\)\(p_1\),那一种简单并合理的线形插值就是\((1-t)p_0+tp_1\)

Scaling

假如前后的缩放比例分别是\(s_0\)\(s_1\),那插值后的缩放比例就是\((1-t)s_0+ts_1\)

Rotation

旋转的插值就比前两个麻烦得多。旋转的定义是绕某个\(\mathbf{\hat v}\)旋转\(\theta\),其中\(\mathbf{\hat v}\)指转轴方向对应的单位向量,旋转的方向遵循右手定则。假设\(\mathbf{\hat v}\)的三个分量分别是\(x\)\(y\)\(z\),那么这个旋转对应的四元数\(p=\cos\frac{\theta}{2}+\sin\frac{\theta}{2}(x\mathbf{i}+y\mathbf{j}+z\mathbf{k})\)

设前后的旋转分别为\(q_0\)\(q_1\),像前面那样直接粗暴的插值是不行的,毕竟四元数包含的语义信息在这样的插值中全部丢失了,你甚至无法保证插值出来的四元数是单位四元数。于是有了一个玄妙的插值操作叫SLERP,意思是Spherical Linear Interpolation。

一言以蔽之,这个插值函数是\(q'=q_0(q_0^{-1}q_1)^t​\)。这里面令人迷惑的是这个指数操作,数学家们给出了如下的定义:

  1. \(q^t=e^{t\log(q)}\)
  2. \(e^q\)是一个针对\(q\)是纯四元数的操作,设\(q=\theta \mathbf{\hat v}\)\(e^q=\cos\theta+\sin\theta \mathbf{\hat v}\)
  3. \(\log(q)\)\(e^q\)的反函数

至于为什么这样的设计可以得到合理的结果,其实我也不知道。QAQ

这个插值有一个化简并且露面次数更多的版本,其中\(\theta=\frac{q_0\cdot q_1}{|q_0||q_1|}\)\[ q'=\frac{\sin(1-t)\theta}{\sin\theta}q_0+\frac{\sin t\theta}{\sin\theta}q_1 \]

Bone

什么是骨头呢?骨头是指可以对它附近(同一个Mesh内)的一些顶点造成影响的一个结构。

每一个pair<double, weak_ptr<Vertex>>都表示一个骨头对一个节点的影响,第一个参数是影响的权重。如果单独对一个顶点结算,那么建模软件保证对顶点产生影响的骨头权重之和是\(1\)

我们最后会在着色器里这样写:

这里的uBoneMatrices[MAX_BONES]不是指骨头的offset,而是沿着模型结构算出来的一个东西。事实上它是一个从模型空间到模型空间的矩阵,用来表示骨头在这一帧里的变化。待会儿会细讲。

至于为什么会有两个aBoneID和两个aBoneWeight,这仅仅是因为glsl不支持往vbo里面填一个数组一样的东西。如果一定要这样做的话,恐怕必须要用vec或者sampler去模拟数组。

骨骼

我们用assimp辅助完成fbx的解析。这中间涉及到了assimp中的一些结构比如aiNodeAnimaiBoneaiNode。由于官方文档在关键地方写的非常模糊(约等于什么都没说),所以我花了很多时间揣摩各个接口的意思。

首先要了解的是aiNodeAnim::mNodeNameaiBone::mNameaiNode::mName之间的关系。事实上,他们仨用的名字都是在同一套体系里的。其次他们的名字值域是有包含关系的:aiBone::mName\(\subseteq\)aiNodeAnim::mNodeName\(\subseteq\)aiNode::mName

在处理一个aiNode的转换矩阵时,先拿它的名字到aiNodeAnim的名字中找。如果有的话,就拿aiNodeAnim中关键帧信息对分别对translation,scaling,rotation分别插值,再乘起来作为当前aiNode的转换矩阵。如果没有的话就当转换矩阵时单位矩阵。把这个矩阵记作\(t(x)\)\(x\)是一个aiNode *类型。

不妨设从根到某个aiNode *x的路径为\(\{p_0,p_1,\cdots,p_{n-1}\}\)\(p_{n-1}=x\)。记\(t^*(x)=\Pi_{i=0}^{n-1}t(p_i)\)

扫一遍骨头,每一个骨头的最终bone_matrix\(=t^*(x)\times\)offset,其中\(x\)是和该骨头同名的aiNode *。这个bone_matrix也就是最后被送到着色器里的uBoneMatrices[MAX_BONES]

Screen shot

仓库地址:https://github.com/tigert1998/skeletal-animation.git

Reference

  1. https://www.3dgep.com/understanding-quaternions/