麻辣土豆模型和动作提取:二、身体动作

系列目录

* “土豆”指的是偶像大师百万现场剧场时光(ミリシタ/MLTD)

在这篇文章里,我讲一下土豆的身体动作数据。


这里先我简单说一下我用的工具。以前我为了读取土豆的谱面,写了个 MonoBehaviour(在 Unity 中使用时,实际上是继承自 ScriptableObject 的类)的反序列化工具,因此读取动作数据也是十分的简单。我这玩意儿比 Unity 自身的要方便一些。Unity 反序列化 ScriptableObject 用的是 AssetBundle.LoadAsset<T>() 方法,其泛型参数指定的就是对应 ScriptableObject 的真实(运行时)类型。(怎么看呢?去看 MonoScript,从那里面推断正确的类的完全限定名。)我的玩意儿比 Unity 好用一些。首先它不严格验证要反序列化的类型的全名,而如果这个名称与 asset bundle 中记录的名称不同,Unity 就会拒绝反序列化(返回 null)。其次 Unity 的反序列化只能作用于字段(带有 SerializableAttribute 修饰的那些),我这玩意儿可以用于字段或属性,只要是指定的名称。第三我这玩意儿还支持一些高级功能,比如重命名字段、自定义命名风格、自定义类型转换、自定义缺省值设置策略等等。它的速度自然是要比 Unity 的要慢一些,不过这没什么太大要求嘛。

总之,根据出现了的信息写出对应的实体类,加上合适的特性(attribute),马上就能反序列化出来。

接下来我们看看总体结构。首先,一个动作数据的 asset bundle 里有四个同样的实例,分别是 apa、apg、idl 和 dan,类型都是 Imas.CharacterImasMotionAsset。从名字上就可以看出,idl 是 idle,dan 是 dancing,作用也就如字面意思。不过我还没理解 apa 和 apg 是什么。

下面我们看实体类的信息。就从最常用的,也是我们首要目标的 dan 下手。可以看到,动作数据的基本单元是 Curve,它有 pathattribsvalues 三个字段。pathvalues 的意义都非常明显;不理解的话,去看它们的值。稍微非常规一点的就是 attribs,明显指定了这个曲线的属性。不过它的设计也很蛋疼,用了一个字符串数组去存储各个属性的键值对。先不这么吹毛求疵,看看它的值。在动作数据中,键值对有两个,一个是 property_type,一个是 key_type。前者很好理解。后者看看取值范围,它是一个枚举,在文件中出现的有 ConstDiscreate(笑)和 FullFrame。这三种其实都不麻烦,非常直白,一看就懂。稍微有点额外信息的是 FullFrame,从歌曲的总时间和其元素数可以推断出,土豆的动画帧率标准是 60 fps。

如果你仔细看的话,会发现 path 是形如“MODEL_00/BODY_SCALE/BASE/MUNE1/MUNE2/SAKOTSU_L/KATA_L/UDE_L/TE_L/OYA3_L”的字符串。其实我在研究土豆动画(非彼“动画”;所以说百万什么时候动画化呢?)的时候我是先从动作数据入手的,所以这样的字符串让我有了对土豆骨骼层级的第一印象,而不像 VMD 那样根本看不出。你可能要问了,为什么是这种格式呢?我一开始也以为只是万代的开发人员自己玩耍随便定的,但当我去用代码控制 Unity 的动画系统(Mecanim)之后,发现它原来就是 Mecanim 的记法。

在新版的 Mecanim 中,动画是预制的,用 Animator 控制。(从土豆的模型 bundle 来看,有 Animator,用的应该是新版。)我想播放自定义动画,然而并不知道 Animator 怎么去用(对不起,没经验);不过传统的创建 AnimationClip 的方法还是能查到的。其中就有添加动画曲线的方法:AnimationClip.SetCurve(string path, Type animatedType, string property, AnimationCurve curve)。可以见到,第一个参数就是 path,它表示的是以模型为原点(""),要控制的 GameObject 的相对路径。结合模型的层次关系,很容易就将它们对上号。所以从 CharacterImasMotionAssetAnimationClip 的转换非常容易。

但是如果是自己的动画系统呢?我想了一个偷懒的办法,就是将所有的动画曲线展开,计算其在每一帧的值。不过这可能也不算偷懒。VMD 和 Mecanim 的曲线最大的不同在于,对于一个组合变量(比如四元数表示的旋转,由 X、Y、Z 和 W 组成),VMD 在设置关键帧时只能同时设置所有分量,而 Mecanim 则可以对每个分量单独设置。这就决定了,如果要求结果100%相同,必须要计算各个分量在所有帧的值,用离散模拟连续。毕竟视频是由离散的帧组成的,物理效果也是逐帧计算的,所以如果生成的动画序列将时间精确地分割到每个单元(也就是帧),就能获得和自动补间一样的结果。这个方法的最大问题在于,它生成的 VMD 关键帧非常多,一首2:20的歌在300万左右,MMD 根本无法处理(32位只能处理30万,64位60万),只能用 MMM。不过 MMM 倒是一点都不卡。之后还会碰到两个系统关键帧设定粒度不同带来的问题,那个以后再说。

8月27日追记:前些日子我改进了代码,大大减少了生成的关键帧数量(也减少了生成的文件大小),所以 MMD 理论上应该是可以处理的。不过由于 MMM 和 MMD 的坐标系不同,所以实际上 MMD 仍然无法使用生成的 VMD——导入之后整个人都变形了。可以通过再写代码修正,不过我懒得做了。

分享到 评论