|
|
前言
0 x: y' x& y# j" t
7 X, ~/ P0 I8 l% X0 j3 L MikuMikuDance(简称MMD)是一款动画软件,早期视为Vocaload角色制作动画的软件,现在还经常能在B站等视频网站,或一些动画网站(某I站)看到MMD作品。& c1 B E0 I2 q* j' c8 i8 w
我在高中也简单学过操作这款软件以及PE、水杉等软件,学会了简单k帧、套动作、调渲染、加后期、压缩等技术,这与我学习计算机专业有很大的关系(虽然学校学的和这个八竿子打不着,或许我应该学美术去),现在已经分不清很多东西了,封面静画就是杂七杂八过气MME一锅扔的成果,得益于G渲的强大,还能看出一点效果。1 M, [- a' P* \
现在我想学一些3D的开发,包括用程序读取模型、动作等,很快我就想到之前用过的MMD。
4 p1 v5 l6 Z5 Z! I5 f' ^2 E 一些3D姿势估计(3D pose estimate)或许能得到骨骼位置以及PAF(骨骼间关系),但我需要知道3D动画是如何储存动作数据的,才能想到怎样将姿势估计得到的数据转化为动作数据。8 e6 a# M9 i/ r
因此我找了一些资料解析MMD的动作数据VMD(Vocaload Mation Data)文件,并写下这篇记录。0 c6 A) D" `1 ?( U& R, G- Y) Q
+ M" I- ~* [- q, D
根据MMD的规矩,上借物表:
3 _$ x( C3 K2 n; N3 c| 名称 | 来源 | | MikuMikuDanceE_v803 | 圝龙龍龖龘圝 | | 八重樱 | 神帝宇 | + E" L1 b& S; x
, g' G H$ x; o0 D7 Q* q
封面静画:# |. Z W, t; x9 c7 R- h
| 名称 | 类别\来源 | | LightBloom | 背光 | | AutoLuminousBasic | 自发光特效 | | HgSAO | 阴影 | | SoftLightSB | 柔化 | | SvSSAO | 阴影 | | XDOF | 景深 | | dGreenerShader | G渲 | | Tokyo Stage | 场景 |
: [# n6 e* A8 ]/ `9 C一、格式说明, L/ d/ O) i7 Y/ W
$ c7 P5 o( J& ?2 ` 首先,vmd文件本身是一个二进制文件,里面装着类型不同的数据:uint8、uint32_t、float,甚至还有不同编码的字符串,因此我们需要二进制流读入这个文件。3 d2 l0 J4 v& Y8 y0 A
vmd格式很像计算机网络的协议格式,某某位是什么含义,区别是,vmd文件的长度理论上是无限的,让我们来看看。
6 i' c- }5 ~- y# M" U vmd的大致格式如下:
5 M, p, {5 k3 f( g6 Y) z$ }) `9 S% [: ?. h- v, m8 ~ j1 t
头部+ c; v. m% w% ]# U% I$ y
关键帧数量
$ P* f! W3 B. S/ d# _% O 关键帧
: W8 N4 |" Y3 b5 H% Z& e, U0 V" I8 V! `, B. _
头部
. H; h- q8 y: q( B2 y" B/ N. C$ a; C2 S6 @& }
最开始的就是头部(header),看到这就有十分强烈的既视感:
! f0 Y4 L9 }# Z! { 类型
# \+ N% c5 y3 Y" P @4 x1 p; C | 长度 | 含义 | | byte | 30 | 版本信息 | | byte | 10 or 20 | 模型名称 |
% q4 g' B7 E9 p: X5 V8 A" c3 n; m! s4 E# G: T2 _% i; _' }6 A
其中,版本信息(VersionInformation)长度为30,是ascii编码的字符串,翻译过来有两种,一为“Vocaloid Motion Data file”,二为“Vocaloid Motion Data 0002”,长度不足30后用\0(或者说b'\x00')填充。这是由于vmd版本有两种,大概是为了解决模型名称长度不足,因此后续只影响模型名称的占用长度。
) M! @" J* z& `$ k 模型名称(ModelName),是动作数据保存时用的模型的模型名,通过这个我们可以获取到那个名称,我们知道,一个动作数据想要运作起来,只要套用模型的骨骼名称是标准的模板就可以,因此我想象不出这个名称有何用处,或许某些模型带有特殊骨骼,例如翅膀之类的,这样能方便回溯?模型名称的长度根据版本而决定,version1为10,version长度为20。编码原文写的是shift-JIS,是日语编码,这样想没错,然而我试验后发现并非如此,例如经常改模型的大神神帝宇的模型,他的模型名称用shift-JIS为乱码,用gb2312竟然能正常读出来;还有机动牛肉大神的模型,他的模型名称用gb2312无法解码,用shift-JIS解码竟然是正常的简体中文???怎么做到的?' {3 i6 F2 C% G! ?$ g
骨骼关键帧(BoneKeyFrame)
W+ [! n. B, ]( L% E) f8 C
$ I" x# N; m. S& a3 ^" P 骨骼关键帧,分为两部分:骨骼关键帧数、骨骼关键帧记录:
7 ^" y/ F m$ Y4 R| 类型 | 长度 | 含义 | | uint32_t | 4" u% |' c1 h8 b9 d# t5 ^
| 骨骼关键帧数量 BoneKeyFrameNumber | 9 B9 Y8 P% F( g* `; P* o
| 类型 | 长度 | 含义 | | byte | 15 | 骨骼名称 BoneName | | uint32_t | 4 | 关键帧时间 FrameTime | | float*3 | 12 | x,y,z空间坐标 Translation.xyz | | float*4 | 16 | 旋转四元数x,y,z,w Rotation.xyzw | | uint8_t * 16 or uint32 * 4 | 16 | 补间曲线x的坐标 XCurve ) m' Q' K" H, C
| | uint8_t * 16 or uint32 * 4 | 16 | 补间曲线y的坐标 YCurve | | uint8_t * 16 or uint32 * 4 | 16 | 补间曲线z的坐标 ZCurve | | uint8_t * 16 or uint32 * 4 | 16 | 补间曲线旋转的坐标 RCurve | | byte | 111 | 合计 | # ~, s4 U- _; Q3 m
% G7 [( |9 p0 v) l
为何要分开写呢?因为骨骼关键帧数量只需要一个就够了,而后面骨骼关键帧记录的数量会和前面的骨骼关键帧数量保持一致。/ v/ L, D! j$ N' `7 A9 l
. }1 S2 ~5 E8 D* W8 \1 p+ K" @
我们可以查一下,每个骨骼关键帧的数量为111字节。4 z7 x$ A5 N+ I# W6 y# H8 ?! N+ e- G
旋转坐标
+ Z. \8 V" v5 l9 R. H7 X1 v
" X1 `4 I: Y3 J+ Y6 D+ V" { 一开始还没发现,旋转坐标竟然有四个,分别为x, y, z, w,急的我去MMD里查看一下,发现和我印象中没有什么差别
% h3 L$ X! ?5 s- x. ^' g$ {/ Y都是[-180, 180]的角度值,我用程序跑的时候,这四个值完全看不懂;幸好在英文网站上找到这个表示方法:四元数。四元数是用四个值表示旋转的方法) u' R5 C( _' S
w+i·x+j·y+k·z
6 M0 X: C6 G* K,其中
1 ~ O3 y: ^3 B, v% @7 k2 ii、j、k
+ z3 f' @6 l g/ o都是虚数,我上网找了一堆资料,并且得到了四元数转化欧拉角的公式
! T: _ U* H4 z* W' l& G8 N\large X = \arcsin {(2wx-2yz)} \\ \large Y = \arctan2 {(2wy+2xz, 1-2x^2-2y^2)} \\ \large Z = \arctan2 {(2wz+2xy, 1-2x^2-2z^2)} \\+ ?+ \1 m' C0 k+ `; F
得到的是角度制,我们通过角度制转弧度制的公式即可算出和MMD中等同的角度表示。7 k) K6 O9 ~ \3 R9 ] @+ \/ M
- ~, D4 J3 y0 d. T( x3 c补间曲线$ k" B0 F" [( _4 e
! B" o1 B) I1 a4 w8 b$ a; ]; L9 r
为何补间曲线的类型不确定呢?上面csdn博客的教程说“uint8_t那里有冗余,每四个只读第一个就行”。说的没有问题,首先我们要清楚这个补间曲线坐标的含义。$ Q$ ?& S; p8 \+ ]: p
我们打开MMD,读入模型,随意改变一个骨骼点,记录帧,就会发现左下角会出现补间曲线。: L8 |0 D `! O, ~! K8 L
补间曲线的用处,就是自动补齐当前记录帧与上一个记录帧之间动作的变化顺序,曲线斜率越高,动作变化越快,具体教程可以参照贴吧中的教程,我们可以通过拖动红色的小x改变调节线,从而改变曲线
! b Z. S/ ]$ o4 r7 m每一组小红x的坐标,就可以唯一确定一条补间曲线,因此,上面的补间曲线存储的就是小红x的坐标4 U1 O6 o# r. k
(x_1, y_1, x_2, y_2)6 ?/ K, p$ I2 y3 ?1 f
,其中左下角调整线的小红x是看做点1,通过程序读取,我知道,小红x的坐标取值为[0~127]间的整数,因此用1字节完全可以存下,可能是当时的设计错误,用了32位整数存,高24位完全浪费了,完全可以不用读取,因此我们可以直接读取32位无符号整数或读取8位无符号整数,然后跳过24位。: s n. u, ?& W
如果曲线只有一个,那么为什么会有四个补间曲线呢?实际上不止一个,补间曲线框的右上角就有个下拉菜单可以选择,对于圆形骨骼,没有相对位置变化,x, y, z补间曲线没有用,只有旋转速率可以调节,而方框骨骼可以移动,因此x, y, z, 旋转补间曲线都有用处。
! e* ]5 [; U0 Y 0 W! p6 k# p, q' ~! ~) s/ g
回过头来,再说一下补间曲线的坐标,在这里,是以左下角为原点,横纵方向[0, 127]的坐标轴% r' ~; O/ h3 g- z2 T0 S; U9 A
n& i+ k C/ C1 ~1 ^ W
: T A8 [4 Q5 r- E/ X9 ] 后面的格式与这个格式大同小异。
. e# o+ k. X2 p( Q, f表情关键帧(MorphKeyFrame)6 W0 H: H3 Q2 B. P: z! v
' g& S8 }/ |& ?" O& P" ?. ~: O
表情关键帧分为:表情关键帧数、表情关键帧记录:
5 \" l, ~2 M6 T/ ~| 类型 | 长度 | 含义 | | uint32_t | 4
* c3 ~' D5 ~2 N% q | 表情关键帧数量 MorphKeyFrameNumber | 类型
, ]6 y7 [3 k8 F* B8 q | 长度
: L" I y2 Z K- v- Y* g2 Z+ R* B& T6 i | 含义8 K. @' X6 @# o1 S
| byte
i! i! K! X- F2 I' a5 _ | 152 f9 t! f4 }! x2 b
| 表情名称 MorphName
: C3 n, w% q0 S' F5 E | uint32_t4 S7 r* \- T; X( \( F7 Y
| 4
( J2 |9 b& ~5 g% r( O( o- l" a/ V | 关键帧时间 FrameTime
0 e0 D: A+ b3 C4 T0 n6 e | float: X. O0 d D1 n* V+ L
| 4 n- Y9 @8 U' B! H k
| 程度 Weight$ q6 d3 x+ ]$ u+ O2 k
| byte
1 ^4 t/ t P# }, }/ N5 R: b | 23
; ]4 ]! y0 B0 J- I | 合计
9 x) F8 c# c! g6 y | 表情关键帧每个记录长度为23字节,其中程度(Weight)是取值为[0, 1]之间的浮点数,在MMD中的表现如下:
8 g/ k7 M/ t T! L8 _+ N# G1 O镜头(CameraKeyFrame)( e- X+ t+ z, D
6 H2 Y4 N/ Y1 E
镜头关键帧分为:镜头关键帧数、镜头关键帧记录:
3 D x+ j, g: l' m4 _# j B3 ]. F| 类型 | 长度 | 含义 | | uint32_t | 4 | 镜头关键帧数量 CameraKeyFrameNumber | 5 g/ ~+ Z. V* K$ \. Y! \1 P
| 类型 | 长度 | 含义 | | uint32_t | 4 | 关键帧时间 FrameTime | | float | 4 | 距离 Distance | | float*3 | 12 | x,y,z空间坐标 Position.xyz | | float*3 | 12 | 旋转角度(弧度制) Rotation.xyz | | uint8_t*24 | 24 | 相机曲线 Curve | | uint32_t | 4 | 镜头FOV角度 ViewAngle | | uint8_t | 1 | Orthographic相机 | | byte | 61 | 合计 | 距离是我们镜头与中心红点的距离,在MMD中,我们可以通过滑轮改变
2 r( M- Q/ F1 c: ]. k2 ~* G) P$ M! H
3 ?1 `2 p" S, t" Y. q) G+ e 这有什么用呢?可以看下面的图:
# l( Z3 n, C; l" y当距离为0时,我们的镜头就在红点上,造成的效果是,当我们移动镜头的Y角度时,镜头就好像在我们眼睛上,视角是第一人称视角。可以看这里,是找镜头资料时偶然看到的。
3 i" g. f* L7 @ 旋转角度不再是四元数,而是普通的弧度制角度,我猜大概是镜头的万向锁情况没那么严重,因此用弧度制就能表示。
$ X3 t9 U' [1 \, ]* e0 b8 Q" ^" X Curve是曲线的意思,按照之前的的补间曲线,确实还有一个相机曲线,不过一个曲线=两个小红x=4个坐标点=四字节,因此24字节有20字节的冗余,它的前四个字节就已经表达了坐标,后面20个字节是将这4个字节重复了5次。
9 l% C; S& I! b; I/ F$ s: Z 镜头FOV角度和透视值有关,上面的博客写的是float,但实际上我试验是uint32_t,取值刚好就是MMD中的透视值。
. _ k5 ~9 K& | U" `% Y ; d U% C+ R( W+ Z6 e- P( Y* x: R
Orthographic似乎是一种特殊的相机,没有近大远小的透视关系(不确定),不过在我的实验中,它一直取值为0。和上面的已透视没有关系,当取消已透视时,透视值会强制为1。
2 H+ c2 s+ ^% o& [' O0 h' r* X 下面的骨骼追踪似乎没有记录,可能是强制转换成骨骼所在的坐标了。
2 s- U& Y1 A0 W8 X+ f7 l L/ a# W3 b8 H 后面的格式与这个格式大同小异。+ F# K" }. s. x- O0 ~
光线关键帧(LightKeyFrame)/ U) C$ U. m$ \
' r, i3 A' ^% @% p+ B$ G. E) S 表情关键帧分为:光线关键帧数、光线关键帧记录:
7 v% t6 \; J8 Y- M5 } Q7 j! I, ^| 类型 | 长度 | 含义 | | uint32_t | 4 | 光线关键帧数量 LightKeyFrameNumber | | 类型 | 长度 | 含义 | | uint32_t | 4 | 关键帧时间 FrameTime | | float*3 | 12 | RGB颜色空间 color.rgb | float*3
( W' z+ V9 `) }7 ]8 n) H | 12 | xyz投射方向 Direction.xyz | | byte | 28 | 合计 | rgb颜色空间之[0, 1]之间的数,类似html的RGB(50%, 20%, 30%)这种表示方法,转换方式就是把RGB值分别除以256。
. S7 Y$ g1 y @ B9 S 光线投射方向是[-1, 1]之间的小数。正所对的投射方向是坐标轴的负方向,例如将Y拉到1, 光线会从上向下投影。
o- [. }; E5 c; g8 Q二、代码读取/ W1 @9 c) [# E1 x
0 f, i4 o" W) H4 N5 b( n6 | \
我依旧会使用面向对象的方式构建VMD类,不过构造方法无力,属性太多,我选择用静态方法添加属性的方式构建对象
4 p- `9 ? j0 h5 i- class Vmd:
- def __init__(self):
- pass
- @staticmethod
- def from_file(filename, model_name_encode="shift-JIS"):
- with open(filename, "rb") as f:
- from functools import reduce
- array = bytes(reduce(lambda x, y: x+y, list(f)))
- vmd = Vmd()
- VersionInformation = array[:30].decode("ascii")
- if VersionInformation.startswith("Vocaloid Motion Data file"):
- vision = 1
- elif VersionInformation.startswith("Vocaloid Motion Data 0002"):
- vision = 2
- else:
- raise Exception("unknow vision")
- vmd.vision = vision
- vmd.model_name = array[30: 30+10*vision].split(bytes([0]))[0].decode(model_name_encode)
- vmd.bone_keyframe_number = int.from_bytes(array[30+10*vision: 30+10*vision+4], byteorder='little', signed=False)
- vmd.bone_keyframe_record = []
- vmd.morph_keyframe_record = []
- vmd.camera_keyframe_record = []
- vmd.light_keyframe_record = []
- current_index = 34+10 * vision
- import struct
- for i in range(vmd.bone_keyframe_number):
- vmd.bone_keyframe_record.append({
- "BoneName": array[current_index: current_index+15].split(bytes([0]))[0].decode("shift-JIS"),
- "FrameTime": struct.unpack("<I", array[current_index+15: current_index+19])[0],
- "
osition": {"x": struct.unpack("<f", array[current_index+19: current_index+23])[0], - "y": struct.unpack("<f", array[current_index+23: current_index+27])[0],
- "z": struct.unpack("<f", array[current_index+27: current_index+31])[0]
- },
- "Rotation":{"x": struct.unpack("<f", array[current_index+31: current_index+35])[0],
- "y": struct.unpack("<f", array[current_index+35: current_index+39])[0],
- "z": struct.unpack("<f", array[current_index+39: current_index+43])[0],
- "w": struct.unpack("<f", array[current_index+43: current_index+47])[0]
- },
- "Curve":{
- "x"
array[current_index+47], array[current_index+51], array[current_index+55], array[current_index+59]), - "y"
array[current_index+63], array[current_index+67], array[current_index+71], array[current_index+75]), - "z"
array[current_index+79], array[current_index+83], array[current_index+87], array[current_index+91]), - "r"
array[current_index+95], array[current_index+99], array[current_index+103], array[current_index+107]) - }
- })
- current_index += 111
- # vmd['MorphKeyFrameNumber'] = int.from_bytes(array[current_index: current_index+4], byteorder="little", signed=False)
- vmd.morph_keyframe_number = int.from_bytes(array[current_index: current_index+4], byteorder="little", signed=False)
- current_index += 4
- for i in range(vmd.morph_keyframe_number):
- vmd.morph_keyframe_record.append({
- 'MorphName': array[current_index: current_index+15].split(bytes([0]))[0].decode("shift-JIS"),
- 'FrameTime': struct.unpack("<I", array[current_index+15: current_index+19])[0],
- 'Weight': struct.unpack("<f", array[current_index+19: current_index+23])[0]
- })
- current_index += 23
- vmd.camera_keyframe_number = int.from_bytes(array[current_index: current_index+4], byteorder="little", signed=False)
- current_index += 4
- for i in range(vmd.camera_keyframe_number):
- vmd.camera_keyframe_record.append({
- 'FrameTime': struct.unpack("<I", array[current_index: current_index+4])[0],
- 'Distance': struct.unpack("<f", array[current_index+4: current_index+8])[0],
- "
osition": {"x": struct.unpack("<f", array[current_index+8: current_index+12])[0], - "y": struct.unpack("<f", array[current_index+12: current_index+16])[0],
- "z": struct.unpack("<f", array[current_index+16: current_index+20])[0]
- },
- "Rotation":{"x": struct.unpack("<f", array[current_index+20: current_index+24])[0],
- "y": struct.unpack("<f", array[current_index+24: current_index+28])[0],
- "z": struct.unpack("<f", array[current_index+28: current_index+32])[0]
- },
- "Curve": tuple(b for b in array[current_index+32: current_index+36]),
- "ViewAngle": struct.unpack("<I", array[current_index+56: current_index+60])[0],
- "Orthographic": array[60]
- })
- current_index += 61
- vmd.light_keyframe_number = int.from_bytes(array[current_index: current_index+4], byteorder="little", signed=False)
- current_index += 4
- for i in range(vmd.light_keyframe_number):
- vmd.light_keyframe_record.append({
- 'FrameTime': struct.unpack("<I", array[current_index: current_index+4])[0],
- 'Color': {
- 'r': struct.unpack("<f", array[current_index+4: current_index+8])[0],
- 'g': struct.unpack("<f", array[current_index+8: current_index+12])[0],
- 'b': struct.unpack("<f", array[current_index+12: current_index+16])[0]
- },
- 'Direction':{"x": struct.unpack("<f", array[current_index+16: current_index+20])[0],
- "y": struct.unpack("<f", array[current_index+20: current_index+24])[0],
- "z": struct.unpack("<f", array[current_index+24: current_index+28])[0]
- }
- })
- current_index += 28
- vmd_dict = {}
- vmd_dict['Vision'] = vision
- vmd_dict['ModelName'] = vmd.model_name
- vmd_dict['BoneKeyFrameNumber'] = vmd.bone_keyframe_number
- vmd_dict['BoneKeyFrameRecord'] = vmd.bone_keyframe_record
- vmd_dict['MorphKeyFrameNumber'] = vmd.morph_keyframe_number
- vmd_dict['MorphKeyFrameRecord'] = vmd.morph_keyframe_record
- vmd_dict['CameraKeyFrameNumber'] = vmd.camera_keyframe_number
- vmd_dict['CameraKeyFrameRecord'] = vmd.camera_keyframe_record
- vmd_dict['LightKeyFrameNumber'] = vmd.light_keyframe_number
- vmd_dict['LightKeyFrameRecord'] = vmd.light_keyframe_record
- vmd.dict = vmd_dict
- return vmd
9 Z0 U' ^1 g' K+ y/ Z3 b0 ~ 2 i3 H: |* ~# w. R
复制代码
* Z$ R' s* O9 Y( @9 u# ]: Q1 f7 a' X! d4 ^/ E3 a; R* ^
' l* Y4 U3 ?/ \
三、实验
2 w) q/ ?) R, L
7 i# P+ C" W6 U( s% L3 D. N. |" n* N 随意掰弯一些关节并注册、使用:- R# ~- P$ P! i$ ^% @" q
- if __name__ == '__main__':
- vmd = Vmd.from_file("test.vmd", model_name_encode="gb2312")
- from pprint import pprint
- pprint(vmd.dict)
1 `3 _3 ~9 l7 c( l4 f ; E: i) f% H; [( i' }
复制代码. c5 p1 D- `+ |5 l" T
output:; _ V1 `7 @. }7 F1 h0 @2 I$ _# r: k
- {'BoneKeyFrameNumber': 4,
- 'BoneKeyFrameRecord': [{'BoneName': '右腕',
- 'Curve': {'r': (20, 20, 107, 107),
- 'x': (20, 20, 107, 107),
- 'y': (20, 20, 107, 107),
- 'z': (20, 20, 107, 107)},
- 'FrameTime': 0,
- 'Position': {'x': 0.0, 'y': 0.0, 'z': 0.0},
- 'Rotation': {'w': 0.9358965158462524,
- 'x': 0.0,
- 'y': -0.3522740602493286,
- 'z': 0.0}},
- {'BoneName': '首',
- 'Curve': {'r': (127, 127, 127, 127),
- 'x': (0, 127, 0, 127),
- 'y': (0, 0, 0, 0),
- 'z': (127, 0, 127, 0)},
- 'FrameTime': 60,
- 'Position': {'x': 0.0, 'y': 0.0, 'z': 0.0},
- 'Rotation': {'w': 0.9191020727157593,
- 'x': 0.0,
- 'y': -0.3940184712409973,
- 'z': 0.0}},
- {'BoneName': '右ひじ',
- 'Curve': {'r': (127, 127, 127, 127),
- 'x': (0, 127, 0, 127),
- 'y': (0, 0, 0, 0),
- 'z': (127, 0, 127, 0)},
- 'FrameTime': 60,
- 'Position': {'x': 0.0, 'y': 0.0, 'z': 0.0},
- 'Rotation': {'w': 0.9568025469779968,
- 'x': 0.0,
- 'y': -0.290740042924881,
- 'z': 0.0}},
- {'BoneName': '右腕',
- 'Curve': {'r': (20, 20, 107, 107),
- 'x': (20, 20, 107, 107),
- 'y': (20, 20, 107, 107),
- 'z': (20, 20, 107, 107)},
- 'FrameTime': 60,
- 'Position': {'x': 0.0, 'y': 0.0, 'z': 0.0},
- 'Rotation': {'w': 0.593818187713623,
- 'x': 0.0,
- 'y': -0.8045986294746399,
- 'z': 0.0}}],
- 'CameraKeyFrameNumber': 0,
- 'CameraKeyFrameRecord': [],
- 'LightKeyFrameNumber': 0,
- 'LightKeyFrameRecord': [],
- 'ModelName': '八重樱',
- 'MorphKeyFrameNumber': 2,
- 'MorphKeyFrameRecord': [{'FrameTime': 60, 'MorphName': 'まばたき', 'Weight': 1.0},
- {'FrameTime': 60,
- 'MorphName': 'あ',
- 'Weight': 0.36000001430511475}],
- 'Vision': 2}
w3 @ U8 J3 D8 e0 B " \$ a: ]# W0 h/ z: M4 D }
复制代码3 ?. X5 ]. `- K) f7 X: \, _" E3 K
因为前面提到的编码模式,我选择用gb2312解码,在很多(也许是大部分)动作数据都会报错,可以去掉编码方式:
2 b3 ? `. r# Y- vmd = Vmd.from_file("test.vmd")
9 v! Y9 U* C* V7 `$ H , n5 k8 k8 I# f) F ^: o( V6 Y; s' U* K) a8 Q
复制代码( I- C J3 a. O, C( Z* X7 S, Z
我们没有移动方块骨骼,因此位置信息都是0。
' f8 y, G( G, O: p& X$ B 不喜欢看欧拉角的话,可以写一个转换方法:* ]- b [8 B- q6 `; H( ]
- @staticmethod
- def _quaternion_to_EulerAngles(x, y, z, w):
- import numpy as np
- X = np.arcsin(2*w*x-2*y*z) / np.pi * 180
- Y = -np.arctan2(2*w*y+2*x*z, 1-2*x**2-2*y**2) / np.pi * 180
- Z = -np.arctan2(2*w*z+2*x*y, 1-2*x**2-2*z**2) / np.pi * 180
- return X, Y, Z
- @property
- def euler_dict(self):
- from copy import deepcopy
- res_dict = deepcopy(self.dict)
- for index, d in enumerate(res_dict['BoneKeyFrameRecord']):
- x = d["Rotation"]["x"]
- y = d["Rotation"]["y"]
- z = d["Rotation"]["z"]
- w = d["Rotation"]["w"]
- X, Y, Z = Vmd._quaternion_to_EulerAngles(x, y, z, w)
- res_dict['BoneKeyFrameRecord'][index]["Rotation"] = {
- "X": X,
- "Y": Y,
- "Z": Z
- }
- return res_dict
1 o$ {; S4 v- F6 D# ]
% x. h0 ^" Y. z+ Y4 z复制代码
; z- \# R8 z. d3 T 这样只要调用:
. }6 F* _: N& v/ Q% M- vmd = Vmd.from_file("test.vmd")
- from pprint import pprint
- pprint(vmd.euler_dict)
6 a) ~) D6 v7 M! i& G0 H
, ^, T" ^* ]! @& }1 b复制代码5 v9 [6 h; T/ A* a
即可得到转换成欧拉角的结果,同样的方式还可以编写转换RGB、弧度、角度等0 @# I$ n, X, g. {9 T# K
python内置的json包可以很方便得将字典转换成json格式文档储存。- [5 `0 D# h' e
我们也可以试着写一些将VMD转换成vmd文件的方法。
* k0 w( O4 J/ U, W3 J* D# ^) h7 M( ^四、总结
+ G3 g# `8 ?) p$ j1 [4 w# l, v$ S9 m* @7 L2 a& r) W
通过学习VMD的文件结构,大致了解了储存动作数据的格式和一些方法,或许可以类比到一些主流的商业3D软件上。
5 S! U; V( ~+ ^! [9 N6 {+ \( C 读取程序并不难,我写程序的很多时间都是查二进制操作消耗的,通过这个程序,还巩固了二进制操作的知识。5 k3 j; i2 O {6 B( W; X/ ^0 Q7 D9 K% z
: b _# R* k7 h+ i% v
- x- b: y9 N9 r+ p( R1 S |
|