🧻

Animation Compression

Animation Clip Storage

notion image
💡
动画数据存储的数据量实际上不像大家想象的那么小,因此需要动画压缩。

Animation Data Size

notion image
以 LOL 为例,100 多个英雄,每个英雄大概有 5 到 10 秒的动画。每一个动画一秒钟要存 30 帧,30 帧里面要存平移、放缩和旋转的数据。加在一起,动画数据量就将近两到三个 G 了,这个数据量其实不能忽视的。

Distinction among Animation Tracks

notion image
当我们去看同一个 Joint,其实它很多数据是不变的。比如说大部分的骨骼,它的放缩都是一个常数 1。大部分骨骼在 local space 的位移其实也是不变的,因为连着的两个 Joint 之间的位移,只要骨头不打断,它们一直都是连在一起的。
rotation 的数据量大。但当我们表达很多角色动作的时候,比如说像手指,当角色走的时候,他的手指都不会一边走还一边捏来捏去,很多时候他的手指头有十几根骨骼,但也几乎没有 rotation 的动画。上臂虽然有一些动画,但是它的幅度也很小。大腿会一块抬起来一块放下去,所以这数据它又会不一样。所以当我们观察到动画数据这个性质,实际上就能知道怎么去降低它的数据量了。

Distinction among Joints

notion image

Simplest Compression - DoF Reduction

  • Scale
    • Discard scale track (Usually not changed in humanoid skeleton except facial joints)
  • Translate
    • Discard translate track (Usually not changed in humanoid skeleton except the pelvis, facial joint and other special joints)
第一种方法就是对于不变的 track 直接干掉,比如说 scale。 translation 不存沿时间轴的一串数据,只存一个值就完事了。我们能很快地定义哪些骨骼完全没有位移变化。

KeyFrame

notion image
对于 rotation 的话,我们对压缩的方法引入 Key frame (关键帧)的概念。关键帧是指:整个运动信号其实可以用一些关键帧,再在关键帧之间插值表达出来。

Keyframe Extraction - Linear Keys Reduction

notion image
Key Frame 之间的插值其实和前面讲的 matching 的算法很像。
比如先选取时间 0 作为第一个关键帧,依次往前走,走到下个点之间,进行线性插值。当原始数值和插值出来的值之间的误差超过了一定阈值的时候,就把那个点退回来,把下一个点就作为它的关键帧。然后再以它为起点,再往前插值。永远保证 error 小于阈值。这就可以把原来几百帧的动画变成几十个关键帧连接在一起的动画。
要注意的是,关键帧和关键帧之间的时间间距是不均等的,取决于信号在这个时间轴上的变化的频率。
但这种插值对于旋转来讲,还是不太符合它的真实的数据。其实在关节的旋转数据,即使表达成四元数,它实际上很多时候是一个光滑的弧线。那么如果用线性差值的话,很多时候线性插值表达的误差会很大,就得加很多的关键帧。这时候就会引入大名鼎鼎的 Catmull-Rom 曲线。
💡
线性插值的好处就是在 runtime 的时候计算很简单。

Catmull-Rom Spline

notion image
Catmull-Rom 曲线是一个非常经典的多项式的一个曲线,它基本上就是三阶。其中有个参数是 指锐度,一般取 0.5。
给定任何两个点,,在 往外延到 那个点, 往外延到 那个点,那么就可以定义一个连续的多项式曲线。这个曲线可以用上面的方程去表达。
这个曲线非常光滑。这里就不继续展开讲,这个曲线基本做任何东西都会用到。
💡
有了 Catmull-Rom 曲线之后,会发现我们去逼近一个真实的 rotation 信号的时候,用的 Key frame 的数量就会少很多。插值的精度也会更好控制。
 
notion image
notion image
这个过程都是离线计算的,所以不用担心这个计算很复杂,因为到实时的时候,我们就只能看到控制点,最多只需要拿 4 个点的数据,用一个多项式计算,就能插出它的值。

Float Quantization

notion image
notion image
对于它数值表达,对于位移、旋转,我们都不会用浮点数。浮点数 32 位,存储量会非常大。
有个很简单的解决办法就是,如果这个数值在一个空间的话,可以用一个定点数来模拟。
比如这个数值的范围是负的,比如说大概是 -50~120。那可以把这个区间归一化到 0 到 1 上面。这样对于任何一个数值,可以用 16 位的整数来表达,这样就可以表达到 0.01 甚至更高一点的精度。而这对于很多的数值表达来讲其实就已经够了
💡
那我们怎么对四元数进行定点化的这个数值压缩呢?这就要利用到四元数的一个很有意思的数学特性。

Quaternion Quantization

notion image
notion image
当我们对四元数进行归一化压缩的时候,我们会发现虽然每一个数值都有可能在正负 1 之间波动,但是把最大的数扔掉的话,其他三个数值一定会小于正负二分之根号 2,也就意味着我只需要拿两个 bit 存哪一位是最大值,把剩下的三个值存下来,就可以通过归一化反向算法,还原整个四元数。
知道了四元数中其他的三个数值,就能反着算出剩下的那个数:
 
工业上发现,把虚部的三个量用大概是 15 个比特来表达,就可以把它表达得非常的准确。所以在工业上去压缩一个四元数的时候,并不是直接存了四个 float。
本来四个 float 要 4*4 bytes = 16 bytes,但压缩之后只要 48 bits,也就是 6 bytes,这样就压缩掉了将近 60% 以上额外的空间。

Size Reduction from Quantization

notion image

Error Propagation

notion image
⚠️
但压缩的时候也会带来很多问题。
虽然每个关节的动画压缩的时候,我们都把它的 error 控制住了,但是因为我们的动画是从一个关节传递到下一个关节,这样 error 会一直传递下去。
也就是说当在前面几根骨骼看的时候,error 还可以。但当从 Pelvis 一路传递到手指尖的时候,这个 error 就会大得离谱了。而我们的武器就挂在手上,所以如果动画压缩没有做好的话,就会发现压缩完的动画,手上拿的这个武器会一直在那抖。

Joint Sensitivity to Error

notion image
动画压缩如果做得不好,就会发现,当拿着长柄武器的角色做一个动作的时候,跳起来往地上一打的时候,就会发现那个动画是砸到地上时,就开始噔噔噔在那抖,就跟那个触电了一样特别明显。而且如上图那样,柄和手一直是错开来的。
💡
不同的骨骼对于 error 的敏感程度是不一样的。
💡
因此我们开始定义我们的 error。

Measuring Accuracy - Data Error

notion image
我们用一些很简单的方法去定义这个 error。比如说我们去测量四元数、平移、旋转、缩放的 error。这个 error 的定义是最简单的,但不符合人的直觉。因为动画压缩本质上是希望在人的认知上不会产生差距。

Measuring Accuracy - Visual Error

notion image
💡
在行业里面,我们一般最关注的是 visual error(视觉 error)。
最粗暴的做法是:把模型上的所有的点,用动画压缩完和没有压缩前的数据进行比较,来计算像素级的差距。但这么多动画,每一帧都要算几万个顶点,这样算会死人的!
notion image
 
在行业里面,我们一般定义 error 的方法是:在每一个 Joint 定义两个垂直的点,给出一定的 offset。如果这个骨骼特别敏感,offset 就给大一点。如果是一个小骨骼或者不重要的骨骼,offset 就给小一点。这样就可以定量地去估计,压缩前和压缩后对于整个这个 skeleton 它的 error 到底产生了多少。
💡
当我们比较一个两个压缩算法的好坏的时候,实际上也是用这个方法。但是其实目前讲的是最简单的压缩算法,动画压缩在现代游戏引擎里面是一个非常重要的研究领域,其实到现在也没有解决得非常的好。

Error Compensation - Adaptive Error Margins

notion image
在骨架的不同位置,对 error 的敏感度是不一样的。

Error Compensation - In Place Correction

notion image
一个简单的想法:既然 error 会沿着 Joint 一直在往上传递,假设上一个 Joint 产生了一个 error,下一个 Joint 能不能反向地对它进行补偿?
其实在系统控制理论里面,经常有种叫误差补偿的原理。
我们会发现在进行这种误差补偿的时候,实际上那个本来在末端的这些骨骼,它以前的动画数据是一个非常平滑的低频数据。但是它为了要补偿上面几根骨骼传递过来的 error 的时候,因为每一个骨骼的频率不一样,这些频率压缩完产生的 error 会依次叠加,结果在这个终端骨骼上会产生非常高频的数据,就导致了末端骨骼的动画压缩效果很差,还会产生很多很奇怪的很高频的抖动。虽然看到的抖动动作不大,但是会觉得有点怪。
现在有些很前沿的方法,会把一些末端骨骼存成一个单独的在模型坐标系的 track(??),这方法叫 forward and inverse connect animation,这个方法不展开了,因为太复杂了。