麻辣土豆新动作的骨骼曲线表

这次讲讲我是怎么得到土豆新动作的骨骼曲线表的。


我在土豆二周年之后就没玩土豆了。但是从2019年8月开始,陆续接到一些土豆文件变化的 issue。Unity 版本更新引起的问题这里不讨论。一个大的变化是土豆的动作文件不再公布源文件,只有“运行时”的文件。大多数动作还是有源文件的,仅有“运行时”文件的只有少数,估计是复杂舞蹈的技术限制(运行时生成内存爆炸?)或者是资源发布错误。总之两种情况都必须处理。

在老的资源列表中,这两个文件都会给出。比如,dan_shtstr_01.unity3dShooting Stars 的动作)就会附带一个 dan_shtstr_01.imo.unity3d。在这个 .imo.unity3d 中就可以看到序列化了的源文件。将它转换成完整的帧序列还是很简单的。

但是新列表就不再带这些 .imo.unity3d 了。在 issue 中提到了 Rebelliondan_rebell_01.unity3d)。于是我只能去看这个文件的内容是什么。原来它是几个 AnimationClip,而且从结构和命名上分析,应该就是源文件“编译”之后的。也就是说,之前那些 .imo.unity3d 可能是不小心放进去的(自动构建时忘记排除)。也有可能是选择性地下载,如果网络差就下载源文件并在第一次播放时本地“编译”,这样可以减少数据传输量;如果网络好则直接下载成品。不管是因为什么,现在只有成品了。

那么第一步就很明显了:读取需要的 AnimationClip。这倒不难。而且由于土豆一直用的是 Generic 而不是 Humanoid,所以动作数据保存的是整个时间轴上的完整骨骼变换。也就是说,每一帧的所有变换的值都在。

接下来就有点麻烦了。可以看到骨骼动画中有180条曲线(curve);那些源文件的曲线数量也是180,所以很大概率是一一对应的。但是这些曲线只有一个简单的索引值,并不像源文件那样写明对应的是哪个关节。这个索引肯定是在制作骨骼动画的时候自动按照某种规则指定的,而且应该是各文件统一的。所以,我们应该能找到一个静态的映射方式(映射表),得知某个索引对应的是哪个关节的什么变换。

我们先来看一下原来的顺序:

(POSITION, AngleX)
(POSITION, AngleY)
(POSITION, AngleZ)
(MODEL_00, AngleX)
(MODEL_00, AngleY)
(MODEL_00, AngleZ)
(MODEL_00/BODY_SCALE/BASE, AngleX)
(MODEL_00/BODY_SCALE/BASE, AngleY)
(MODEL_00/BODY_SCALE/BASE, AngleZ)
...
(MODEL_00, PositionX)
(MODEL_00, PositionY)
(MODEL_00, PositionZ)
(MODEL_00/BODY_SCALE/BASE, PositionX)
(MODEL_00/BODY_SCALE/BASE, PositionY)
(MODEL_00/BODY_SCALE/BASE, PositionZ)

先试试这个顺序吧。但是结果很不妙。为了避免精神污染,我就不放图了。

嗯……怎么找正确的顺序呢?又到了找弱点的时间了。我认为,在上文静态表的基础上,还应该有附加条件:

  1. 这个表很可能不是随机的。如果是的话,美术方面的维护和调试会很麻烦。当然也不排除万代丧心病狂,写一个内部插件自动完成这件事——在国内还真的有可能,但是日本……不太可能。
  2. 项之间应该会呈现明显的逻辑相关性。
    1. 同一个关节的某种分量会分组。比如,MODEL_00 的三个旋转分量会成一组,三个位移分量会成一组。
    2. 有固定的层次上的先后顺序。比如,考虑 MODEL_00/BODY_SCALE/BASE/MUNE1/MUNE2MODEL_00/BODY_SCALE/BASE/MUNE1/MUNE2/SAKOTSU_L,前者是后者的父关节。如果前者在后者之前,那么所有的父节点应该在其子节点之前,反之则是之后。但是不确定对称关节的顺序,可能是左前右后,也可能是左后右前。根据老的数据推测,是左前右后。
  3. 有可能遵循其他的简单先后逻辑或者它们的组合(可能性未知):
    1. 关节名称的字符串顺序;
    2. 所有旋转在位移之后或之前;
    3. 同一个关节的旋转组和位移组相邻,组成大组。
  4. 考虑到骨骼层级,新顺序可能很大一部分的内部相对顺序都和原顺序中的一致。

我试了几个写起来简单的顺序,但是都不对。这下我就有点头疼了,毕竟可是有180条曲线啊。

突然,在调试的时候,我发现动画帧的数据好像有着很明显的模式。一个帧存着这帧对应时间点的所有关节的变换数据,所以看这个帧里的数组的时候相当于在对各个变换进行横向的比较。于是我就想到了一种方法:能不能通过分析这种模式来找出新顺序呢?

说干就干。我写了两个小方法,将动画的帧输出到 CSV 文件中。用的分别是 dan_hmt001_01.imo.unity3dBlooming Star)和 dan_rebell_01.unity3d。在 Excel 中打开:

老文件:hmt001

新文件:rebell

可以看到,列之间显示出了很强的分组倾向。然后我们再将这些列的模式写出来(这一切发生在我的脑中,这里写出来更容易理解):

[rebell]

#1: (~-0.00x approx, 0 const, ~-0.07 approx)
#2: (0 const, 0 const, 0 const)
#3: (~-0.00x approx, 0 const, ~-0.07 approx)
#4: (0 const, ~+0.87 approx, 0 const)
#5: (0 const, 0 const, 0 const)
...
#60: (? var, ? var, ? var)

[hmt001]

MODEL_00#rotation: (0 const, 0 const, 0 const)
MODEL_00#translation: (~-0.005 const, 0 const, ~-0.03x approx)
BASE#rotation: (~+0.754 const, ~-78.6 const, ~-3.35 const)
BASE#translation: (0 const, ~+0.87 approx, 0 const)
...
SCALE_POINT#translation: (0 const, 0 const, 0 const)

有些分量是常数,有些是变化较小的(因舞蹈不同可能会有不同值,变化范围和速度作为分析依据不是很有用),有些是自由变化的(无法用作分析依据)。源文件中指明了一些曲线是常数,而且它们是0(比如 POSITION 的旋转分量);这些是确定不会变化的,可以作为映射表结构的“基准点”使用。

考虑“是否为常量0”这点和基准值、变化速度,观察可以发现,中间一大块(人体关节)是有着完全相同的模式的。人体关节只有旋转。因此先假设这些关节顺序都不变。需要重点关注的是这几个关节:POSITIONSCALE_POINTMODEL_00BASE,它们一共有7个组(4个位移3个旋转)。只要搞清楚了这7个组的顺序,应该就能得到正确的结果。

接近了。

rebell 的最后分量都不是常量组,所以仅有的会出现常量的组(就是上面这几个)一定是排在最前面的。再考虑到组之间的逻辑关系,开头的应该是 POSITION(模型子空间)而不是 MODEL_00(实际的模型根关节),否者就会将 MODEL_00 的层次给破坏掉(因为必定造成 POSITION 或者其子关节 SCALE_POINT 和后面的身体关节相邻)。

那么第1组是 POSITION 的位移还是旋转呢?观察数据的模式((?, 0, ?)),是位移。

第2组是什么呢?根据上面的大前提,有两种可能:POSITION 的旋转,或者是 SCALE_POINT 的位移。不知道,先放着。

这时候可以看到第4组的模式((0, ?, 0))——而这个模式,在7个组中只有一种可能:BASE 的位移。

于是我们现在可以得到这样的一个顺序:

#1: POSITION#translation
#2: ?
#3: ?
#4: MODEL_00/BASE#translation
#5: ?
#6: ?
#7: ?
#8: MODEL_00/BODY_SCALE/BASE/KOSHI#rotation(各个身体关节开始)
...

到这里应该很明显了。想想上面的大前提。如果在第1组和第4组之间插入任何的旋转,就会破坏整个逻辑。因此只有一种可能:

#1: POSITION#translation
#2: POSITION/SCALE_POINT#translation
#3: MODEL_00#translation
#4: MODEL_00/BODY_SCALE/BASE#translation
#5: POSITION#rotation
#6: MODEL_00#rotation
#7: MODEL_00/BODY_SCALE/BASE#rotation

将这个顺序代入,就可以发现我们得到了正确的结果:

Rebellion

代码在这里

分享到 评论