找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 159|回复: 0

【MMD】解析VMD格式读取

[复制链接]

983

主题

83

回帖

5万

积分

管理员

积分
53122
发表于 2022-6-30 08:28:08 | 显示全部楼层 |阅读模式
前言
6 a  z- X, N. T* P- E" W1 }# c' F2 y+ y! h3 x1 |7 o' m3 ~" `
  MikuMikuDance(简称MMD)是一款动画软件,早期视为Vocaload角色制作动画的软件,现在还经常能在B站等视频网站,或一些动画网站(某I站)看到MMD作品。& K1 S, ]4 \3 c0 E
  我在高中也简单学过操作这款软件以及PE、水杉等软件,学会了简单k帧、套动作、调渲染、加后期、压缩等技术,这与我学习计算机专业有很大的关系(虽然学校学的和这个八竿子打不着,或许我应该学美术去),现在已经分不清很多东西了,封面静画就是杂七杂八过气MME一锅扔的成果,得益于G渲的强大,还能看出一点效果。$ q- q2 f0 X5 R
  现在我想学一些3D的开发,包括用程序读取模型、动作等,很快我就想到之前用过的MMD。$ C# ^1 O# [1 h; ]2 R6 ~
  一些3D姿势估计(3D pose estimate)或许能得到骨骼位置以及PAF(骨骼间关系),但我需要知道3D动画是如何储存动作数据的,才能想到怎样将姿势估计得到的数据转化为动作数据。
  G+ \: p3 K7 S3 D  x! Z  因此我找了一些资料解析MMD的动作数据VMD(Vocaload Mation Data)文件,并写下这篇记录。
2 Q6 z' X) l( T, x1 t  l
. S# w' L7 g9 }; c7 p0 j7 ~  根据MMD的规矩,上借物表:
' B! K2 _  p( k& Q7 M( l  |
名称 来源
MikuMikuDanceE_v803 圝龙龍龖龘圝
八重樱 神帝宇

; J1 O; u3 A0 ?+ ^/ l7 B& u% m2 Y5 E7 Q! C* b5 _4 `
封面静画:
4 {0 ^5 ]/ G* l# W& ^, u3 N
名称 类别\来源
LightBloom 背光
AutoLuminousBasic 自发光特效
HgSAO 阴影
SoftLightSB 柔化
SvSSAO 阴影
XDOF 景深
dGreenerShader G渲
Tokyo Stage 场景
& m# T, I: E8 e( _- r4 V
一、格式说明! o# h& h1 E- x" N! f8 V
0 H3 \, s7 o, D( l6 L
  首先,vmd文件本身是一个二进制文件,里面装着类型不同的数据:uint8、uint32_t、float,甚至还有不同编码的字符串,因此我们需要二进制流读入这个文件。
5 h* y( F/ O9 `3 q* H9 Y  vmd格式很像计算机网络的协议格式,某某位是什么含义,区别是,vmd文件的长度理论上是无限的,让我们来看看。
; f* ^8 k# T) C- T6 Q  vmd的大致格式如下:
4 F- g" Z& A0 Q4 k# ^+ R7 i) z
3 N2 ~; k) b  G" I7 Y    头部/ A8 A7 {* c! U
    关键帧数量
; |1 _. A3 g) L8 A8 Y" ^; A# [3 _    关键帧  b# G8 |& ]: d' y) A/ S

$ r- N8 |% ]  A6 S+ n头部
3 P+ ~3 @4 {# M. L# Z0 h& G, ~
0 K! J) J  \1 R( c4 ^6 K% M  z9 [  最开始的就是头部(header),看到这就有十分强烈的既视感:9 ]- [- T: }( z) q/ p' D/ v
类型
7 F) I5 F  [2 Z+ n5 l
长度 含义
byte 30 版本信息
byte 10 or 20 模型名称

* m0 b) h! n0 P  R# p8 @: R3 S' o+ W1 q- F( F
  其中,版本信息(VersionInformation)长度为30,是ascii编码的字符串,翻译过来有两种,一为“Vocaloid Motion Data file”,二为“Vocaloid Motion Data 0002”,长度不足30后用\0(或者说b'\x00')填充。这是由于vmd版本有两种,大概是为了解决模型名称长度不足,因此后续只影响模型名称的占用长度。! t: k% O! |5 B: T* A( M
  模型名称(ModelName),是动作数据保存时用的模型的模型名,通过这个我们可以获取到那个名称,我们知道,一个动作数据想要运作起来,只要套用模型的骨骼名称是标准的模板就可以,因此我想象不出这个名称有何用处,或许某些模型带有特殊骨骼,例如翅膀之类的,这样能方便回溯?模型名称的长度根据版本而决定,version1为10,version长度为20。编码原文写的是shift-JIS,是日语编码,这样想没错,然而我试验后发现并非如此,例如经常改模型的大神神帝宇的模型,他的模型名称用shift-JIS为乱码,用gb2312竟然能正常读出来;还有机动牛肉大神的模型,他的模型名称用gb2312无法解码,用shift-JIS解码竟然是正常的简体中文???怎么做到的?
, k: D/ s" M$ u9 d  D骨骼关键帧(BoneKeyFrame)& q* J& I$ W3 u8 U8 r, {
8 T6 o( U1 L4 c2 D; G6 H
  骨骼关键帧,分为两部分:骨骼关键帧数、骨骼关键帧记录:7 H: n' K* x4 `8 E, S
类型 长度 含义
uint32_t4
& l, |. C9 }& Z8 r$ t# \$ w" r
骨骼关键帧数量 BoneKeyFrameNumber

# Z6 w( P' D( ^' h
类型 长度 含义
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   g1 s7 I+ n, m  r: e
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 合计
3 `* T. x9 |+ y* n4 {& J

' P2 r/ D2 q/ n$ n( U  i) a0 w  为何要分开写呢?因为骨骼关键帧数量只需要一个就够了,而后面骨骼关键帧记录的数量会和前面的骨骼关键帧数量保持一致。2 Q5 i; }7 H7 |- t  u
" I# A: c; b# Z; _6 ]" S- E- {
  我们可以查一下,每个骨骼关键帧的数量为111字节。( ?( T& w, k# I: [; t/ ~; X
旋转坐标) A* g( F, l4 o

4 N" u/ X7 D- L! p/ l5 X# x  k- O  一开始还没发现,旋转坐标竟然有四个,分别为x, y, z, w,急的我去MMD里查看一下,发现和我印象中没有什么差别9 a$ V8 H6 `- C* |3 M7 e
都是[-180, 180]的角度值,我用程序跑的时候,这四个值完全看不懂;幸好在英文网站上找到这个表示方法:四元数。四元数是用四个值表示旋转的方法! q" W7 j2 w  N0 P( ]/ X5 f# {
w+i·x+j·y+k·z, h) A, d3 b3 k+ K; ]
,其中
4 z4 r  i0 R8 E* B) Wi、j、k
5 L5 r" }9 v% y# O6 s2 j+ W都是虚数,我上网找了一堆资料,并且得到了四元数转化欧拉角的公式, L9 {/ o1 X* s1 }' W* U- W7 b# v& M
\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)} \\
: Z- I: k  q) S得到的是角度制,我们通过角度制转弧度制的公式即可算出和MMD中等同的角度表示。) k: p- @7 q" ]/ c1 N" s
' s& d9 `$ N; p# J8 C
补间曲线
  f8 q2 Y1 @% k; Z1 T4 t5 C3 o, T) @, Z& P) F& E& U* A
  为何补间曲线的类型不确定呢?上面csdn博客的教程说“uint8_t那里有冗余,每四个只读第一个就行”。说的没有问题,首先我们要清楚这个补间曲线坐标的含义。
) P& f) }( D! p) V$ ?  我们打开MMD,读入模型,随意改变一个骨骼点,记录帧,就会发现左下角会出现补间曲线。2 T6 Y$ Z) {# ?1 h6 R
  补间曲线的用处,就是自动补齐当前记录帧与上一个记录帧之间动作的变化顺序,曲线斜率越高,动作变化越快,具体教程可以参照贴吧中的教程,我们可以通过拖动红色的小x改变调节线,从而改变曲线
% N- x! Z& o- a2 b) E8 d
每一组小红x的坐标,就可以唯一确定一条补间曲线,因此,上面的补间曲线存储的就是小红x的坐标9 s) N% ]  c" y3 D' a' J+ p/ h
(x_1, y_1, x_2, y_2)( k4 e7 ]* ~9 k, O
,其中左下角调整线的小红x是看做点1,通过程序读取,我知道,小红x的坐标取值为[0~127]间的整数,因此用1字节完全可以存下,可能是当时的设计错误,用了32位整数存,高24位完全浪费了,完全可以不用读取,因此我们可以直接读取32位无符号整数或读取8位无符号整数,然后跳过24位。7 |  |6 e" S# ^' n
  如果曲线只有一个,那么为什么会有四个补间曲线呢?实际上不止一个,补间曲线框的右上角就有个下拉菜单可以选择,对于圆形骨骼,没有相对位置变化,x, y, z补间曲线没有用,只有旋转速率可以调节,而方框骨骼可以移动,因此x, y, z, 旋转补间曲线都有用处。( m( M; D0 `" u7 a! }4 Z' g

; Y. ^  b' R2 I
  回过头来,再说一下补间曲线的坐标,在这里,是以左下角为原点,横纵方向[0, 127]的坐标轴
8 o) z8 t" w+ N) E" I: n3 Q
  v- b0 B+ Y* F, G+ Y& H* f
4 ]. P( L9 G, u, x) b5 ?
  后面的格式与这个格式大同小异。
! W) I2 N# @% M8 A" F表情关键帧(MorphKeyFrame)
2 i% {  o' n+ l5 ?( y0 f; N3 f; ~. \# @
  表情关键帧分为:表情关键帧数、表情关键帧记录:/ ^" k: z8 X# ]7 G" z' m# ~' ?
类型 长度 含义
uint32_t4 ! P* G! N9 ]) v0 _* e& ]5 _
表情关键帧数量 MorphKeyFrameNumber
类型
' P4 Q( `: D  a( T/ c. |
长度' W4 e) b( U+ |: [3 b
含义
) f8 ]4 |  w5 U4 }3 T
byte
1 Q: g* ]" X% r* A8 q' g" U
155 @' ^/ R& k2 e1 F; v$ y" J
表情名称 MorphName3 F2 _+ o- w: z# z  \. n
uint32_t* N) y2 n5 ^; b3 X6 \
48 Y' d+ O# R  i! Z( l: `) q
关键帧时间 FrameTime, T5 n8 O, O$ q' z" r1 C/ g4 k& d
float
% \' d, }4 D& k8 T& q: z
4
- x& z  _) b( i% m0 F- A) _1 V! |
程度 Weight
( p3 o0 z; I- P% Y( K+ q1 o
byte+ B" x% E: ~& F6 m; L" M0 k2 Y
23+ {6 `0 T- t1 u/ I; y9 g. Y
合计
( K& x% f, t) B0 W2 B# Z6 h/ G! [5 ^& l
  表情关键帧每个记录长度为23字节,其中程度(Weight)是取值为[0, 1]之间的浮点数,在MMD中的表现如下:
. b0 q- o, ?- u) V
镜头(CameraKeyFrame). G1 `4 C& O1 F: _# W$ l
* ?" g( c# t8 S5 @) D0 u
  镜头关键帧分为:镜头关键帧数、镜头关键帧记录:
3 z2 j, \4 e; _+ [" S
类型 长度 含义
uint32_t 4 镜头关键帧数量 CameraKeyFrameNumber

; c- E. m0 B- I
类型 长度 含义
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中,我们可以通过滑轮改变
& Y+ I; ?" T1 |( G- Y% D
, b+ c  ]2 _2 j" C* A% z3 V
  这有什么用呢?可以看下面的图:* f. g6 W( t/ _6 U1 |3 a, U- ]
当距离为0时,我们的镜头就在红点上,造成的效果是,当我们移动镜头的Y角度时,镜头就好像在我们眼睛上,视角是第一人称视角。可以看这里,是找镜头资料时偶然看到的。5 e7 L7 |7 z% ]+ Q1 }+ ^- s
  旋转角度不再是四元数,而是普通的弧度制角度,我猜大概是镜头的万向锁情况没那么严重,因此用弧度制就能表示。
* B* X# X) U4 Z6 v  Curve是曲线的意思,按照之前的的补间曲线,确实还有一个相机曲线,不过一个曲线=两个小红x=4个坐标点=四字节,因此24字节有20字节的冗余,它的前四个字节就已经表达了坐标,后面20个字节是将这4个字节重复了5次。
! W5 a4 X  \6 R. l! b) s' W  镜头FOV角度和透视值有关,上面的博客写的是float,但实际上我试验是uint32_t,取值刚好就是MMD中的透视值。
0 ?/ x1 q! ^5 ~3 ~( U9 W6 J

' |* ^! k2 g- U' Z6 }
  Orthographic似乎是一种特殊的相机,没有近大远小的透视关系(不确定),不过在我的实验中,它一直取值为0。和上面的已透视没有关系,当取消已透视时,透视值会强制为1。1 Q' h# i- B9 T% |2 n0 m
  下面的骨骼追踪似乎没有记录,可能是强制转换成骨骼所在的坐标了。
4 n0 J$ x) n& r+ M% x4 G  后面的格式与这个格式大同小异。
5 y9 @. Z, s1 m2 Y' e# F光线关键帧(LightKeyFrame)9 k& J3 d; Y* T0 A
1 H% E5 |$ _  n3 D' e+ x4 C
  表情关键帧分为:光线关键帧数、光线关键帧记录:
0 A5 p; m* S6 q! E, g# j
类型 长度 含义
uint32_t 4 光线关键帧数量 LightKeyFrameNumber
类型 长度 含义
uint32_t 4 关键帧时间 FrameTime
float*3 12 RGB颜色空间 color.rgb
float*3 9 h2 N# y# p' u2 @: ]& v3 C
12 xyz投射方向 Direction.xyz
byte 28 合计
  
rgb颜色空间之[0, 1]之间的数,类似html的RGB(50%, 20%, 30%)这种表示方法,转换方式就是把RGB值分别除以256。$ m5 ?6 }% A' G7 r* [  U
  光线投射方向是[-1, 1]之间的小数。正所对的投射方向是坐标轴的负方向,例如将Y拉到1, 光线会从上向下投影。  z- o; O6 x; M: Z
二、代码读取
/ |; Q- }% u. X8 j# Q$ F; @, M6 a
# B# ~7 u8 w, X; }  我依旧会使用面向对象的方式构建VMD类,不过构造方法无力,属性太多,我选择用静态方法添加属性的方式构建对象
2 F) `& n6 h' O
  • 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 j# p& i8 b. j# f/ {
$ ?# _/ u- N" }5 s
复制代码( ?% S% f3 n0 D$ t" f6 n% F0 ^5 m8 T

& A1 x* b0 Z7 U" `4 F- {$ N2 r7 ^! ~/ b9 l
三、实验* j/ t$ f3 c, o9 K5 |
" }& V- h% O0 V- R1 t- g
  随意掰弯一些关节并注册、使用:% a+ f- p7 \0 _& D" C$ ?0 b
  • if __name__ == '__main__':
  •     vmd = Vmd.from_file("test.vmd", model_name_encode="gb2312")
  •     from pprint import pprint
  •     pprint(vmd.dict)

  • 8 M. z6 L3 B6 G9 ~: q* e
2 q+ V4 N' S5 r  e; W+ y0 }
复制代码4 C# o" _9 w) }
output:
, ~2 R% n! l! h
  • {'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}* \0 x# Q- }2 u9 P# n: G
/ o9 C/ N, ]+ N; _5 ^5 w8 K- ]# |: b
复制代码
! Q. p; {+ d+ Q* W! X2 j& x 因为前面提到的编码模式,我选择用gb2312解码,在很多(也许是大部分)动作数据都会报错,可以去掉编码方式:2 F% D+ t( E& _8 ~! U
  • vmd = Vmd.from_file("test.vmd")* X, D! n1 b/ Q
# i/ K. X1 F2 I6 D4 Q0 n
复制代码
' A" k, W2 m  U% s& V 我们没有移动方块骨骼,因此位置信息都是0。; i- j7 {  u0 W- l! O  l. r
  不喜欢看欧拉角的话,可以写一个转换方法:
, D9 x. b/ C/ t2 W5 m, v, X" `1 ^6 e
  •     @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# I  f$ c2 v5 m! j! D3 |% f8 f" E

- p8 _4 q! P) p' f复制代码* `8 ]  W0 h* \- y/ u. F
这样只要调用:
) O8 D1 l2 Z: w4 ~* b- ^+ c
  • vmd = Vmd.from_file("test.vmd")
  • from pprint import pprint
  • pprint(vmd.euler_dict)
    # l# W' `6 e0 ?! {
) D1 x# c5 z$ R6 b
复制代码! o" N4 T+ j: Z6 ^) X  E2 V
即可得到转换成欧拉角的结果,同样的方式还可以编写转换RGB、弧度、角度等
. }# u5 [6 p- ^2 y  python内置的json包可以很方便得将字典转换成json格式文档储存。
: K" s5 [+ m& n7 }  我们也可以试着写一些将VMD转换成vmd文件的方法。
7 Q' f$ a0 u3 w$ W- ~* x四、总结' q. X5 d8 }  G: o0 c

. |# [- m6 f% a& t  通过学习VMD的文件结构,大致了解了储存动作数据的格式和一些方法,或许可以类比到一些主流的商业3D软件上。+ k  w# ~, G* v
  读取程序并不难,我写程序的很多时间都是查二进制操作消耗的,通过这个程序,还巩固了二进制操作的知识。1 y9 l3 f( J# h. W

3 j% e! H, ~* c
1 F0 i* U6 ?9 K# d) r
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|返回首页

GMT+8, 2024-12-24 00:13 , Processed in 0.093634 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表