一个奇怪的骨骼动画系统

语死早禁止打人。以下不谈蒙皮只谈骨骼。

首先,通常的骨骼动画是什么样的,先从fbx sdk(2015)的例子里的骨骼动画说起。
fbx内置的矩阵风格是变换矩阵*列向量。觉得别扭的请自行脑内transpose并自行脑内调换矩阵顺序。这点必须要说清楚,统计表明某些惯用列向量的程序员一辈子不知道有行向量,统计表明某些windows游戏程序员一辈子不知道矩阵能转置之后反过来乘。
它里面设计最常用的线性插值法的蒙皮动画,其主要的变换和矩阵计算集中在以下代码里。

_(:3 ⌒゙)_我已经很努力地用了code标签了所以别打我
论删除线之美←你快够了

(。・ω・)ノ゙看上去有点费解,来画个图♪

它所求的其实就是从绑定时的模型空间的顶点变换到当前关键帧的顶点的矩阵。

写到这里的时候其实我心里突然升起一片疑云。因为将顶点原样输出跟加了骨骼变换输出的结果,大小不一样。这是件奇怪的事,我能想到的,就是动画过程中某一级父节点设了不同的scaling。但是原则上来讲骨骼是没有scaling信息的,所以到底事哪一级变换被改变了,我很疑惑。

经过调试,我发现lClusterGlobalCurrentPosition这个矩阵特别小。其他涉及变换的矩阵无一例外会让模型以不同的方式拉伸两倍左右。但是只有这一个矩阵,本身没有scale。这是为什么,估计要去max里找答案了。

言归正传。

fbx这个变换方式思路上是没有问题的,原则上也是没有问题的。但它为了演示蒙皮的过程,可能把计算给搞复杂了。比如offset矩阵,完全不用每帧都算一次好伐。

这里谈谈offset矩阵。对美术来说,蒙皮就是对着一个binding模型刷顶点权重。顶点权重意味着这个顶点受哪根骨骼影响多少。这个binding模型通常就是个伸得特别开的人物模型。而动作美术的工作就是摆弄骨骼的位置了。想像一下吧,在世界参考系下,你的手指的位置会随着手腕移动,但你动手指的时候,手腕的位置不受影响。而这就是树形结构的便利之处,子节点在父节点空间移动,父节点的坐标不受影响。而为了得到这种效果,必须将所有的顶点转换到对应骨骼节点的空间中。这就是offset矩阵的存在意义了。它其实就是当前节点变换到绑定空间的逆变换,就是绑定空间中,对应骨骼在绑定空间的变换矩阵的逆矩阵。

得到offset矩阵之后,一切骨骼都变得好玩多了。你让一根骨骼转多少度,它就可以带着子节点转多少度。而大部分引擎从fbx或建模软件中导出来保存的,就是在动画的某一时刻,或者某一个关键帧处,类似于“让骨骼转多少度”的矩阵。它们都是相对于单个骨骼节点空间的矩阵。

fbx那货为了方便抬手就是各种矩阵各种逆矩阵,它甚至有接口直接拿到从骨骼变换到世界的矩阵-_-#。都不知道它内部是什么时候把这个矩阵给算出来的,更不知道里面是否有冗余计算。它这个播放动画的方法比较非主流也比较暴力。其实offset矩阵一来在模型读取时就能拿到,二来甚至可以预先乘到顶点里去。当然fbx本身可以对同一个模型保存多套蒙皮信息,乘进去就没法玩儿了。再者,这个例子里面完全就是拿矩阵变换矩阵,对于关键帧之间的插值由库内部实现了,没有暴露出去。

但是骨骼系统并不是只有对美术流程才有意义的东西。问题在于无论是在硬盘上还是在内存里,对一个模型来说每秒六十帧的数据量都太多了。于是给一定的帧,实际当前时间的关键帧由相邻两帧的数据插值出来成了常用方法。据说对这种东西有个对应的词叫补间动画。。。我忘了ˊ_>ˋ。

一般的骨骼的本地变换只有rotation和translation。

为什么有这种约定,从美术意义上来说一般没有让大腿变粗变细的需求,从程序实现的角度来说。。。我猜是因为如果加上scaling,两个矩阵相乘的结果可能会导致切变,scaling和rotation到时候就粘在一起想拿拿不出来了。。。ˊ_>ˋ嘛理由如何都不重要。

rotation和translation的线性插值都很容易,一个是Quaternion插值,一个是Vector3D的插值。具体怎么干这里不是讨论的重点,重点在于正确的插值姿势是对着每根骨骼本地的变换插值,然后再将每根骨骼插值的结果变换到模型空间。

为什么要对本地空间插值,这里脑补一下吧,如果对模型空间的骨骼插值的情况。假设现在有一根父bone,它有个子bone。一个变换是子bone没动,父骨骼带着子骨骼转了150度。这个情况,如果从模型空间插值过去的话,子骨骼的轨迹压根不是跟着父骨骼转过去的弧形,而是直着过去的。然后考虑另一种情况,父骨骼转了150度,子骨骼自身转了60度。对世界空间来说,子骨骼的角度改变了180度以上,你觉得它的四元数插值结果会让它以什么方式旋转?

当然,如果你相邻两个关键帧的动作幅度小的话,用户未必看出问题来。

但当这个“如果”成立的话,那你就会得到一个奇葩但是效率极高的动画系统。

回头看正常的动画系统。每帧每根骨骼都要有一次线性插值,插值的结果要跟上一级的模型空间矩阵相乘。意味着除了插值运算,还有n个矩阵乘法。优化方法呢?
1.把递归改成循环。。。
2.SSE大法好,看16个float的矩阵,多美丽的内存布局!
3.多开几个线程ˊ_>ˋ
4.剃,剔除。。。-_-#糟了感觉要被打。。。

但如果插值时根本不需要在bone的本地空间进行的话,那n个矩阵乘法都能完全舍去。

回头看fbx的代码,从最后一行看,它是将顶点从binding space直接变换到当前时刻的local space的。而从binding时每根骨骼的位置,到local space,它用了lClusterRelativeCurrentPositionInverse这个矩阵。而不管是顶点,还是offset矩阵,其实都是binding
时已经确定的信息。唯一需要计算的是在于运行时相邻两个关键帧之间lClusterRelativeCurrentPositionInverse的插值。

那么,如果相邻两个关键帧之间的动作幅度不是特别大的话,即使不在bone space插值,在local space插值的话,用户应该看不太出来才对。

那么,需要准备好的数据,就是每个关键帧中,每一根骨头的local space变换了。因为所有的骨骼都只有translation和Rotation,所以这些矩阵的合并结果也能用一个translation和Rotation来表示,所以插值的时候,从合并后的矩阵中拿出四元数和平移量用来插值也非难事。

这就是我们之前那个奇葩的骨骼动画系统了。。。= =||。问题一眼就能看出来:如果两个关键帧差别稍大一点,那插值的结果就非常不协调了。这个问题看似可以通过在导入骨骼动画时让关键帧的采样间隔变小,牺牲数据量来解决,但是,碰到两段动画,其动作需要过渡的时候,弊端就完全显露出来了。

《一个奇怪的骨骼动画系统》有7个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>