前言

MikuMikuDance(简称MMD)是一款动画软件,早期视为Vocaload角色制作动画的软件,现在还经常能在B站等视频网站,或一些动画网站(某I站)看到MMD作品。

我在高中也简单学过操作这款软件以及PE、水杉等软件,学会了简单k帧、套动作、调渲染、加后期、压缩等技术,这与我学习计算机专业有很大的关系(虽然学校学的和这个八竿子打不着,或许我应该学美术去),现在已经分不清很多东西了,封面静画就是杂七杂八过气MME一锅扔的成果,得益于G渲的强大,还能看出一点效果。

现在我想学一些3D的开发,包括用程序读取模型、动作等,很快我就想到之前用过的MMD。

一些3D姿势估计(3D pose estimate)或许能得到骨骼位置以及PAF(骨骼间关系),但我需要知道3D动画是如何储存动作数据的,才能想到怎样将姿势估计得到的数据转化为动作数据。

因此我找了一些资料解析MMD的动作数据VMD(Vocaload Mation Data)文件,并写下这篇记录。

我的参考文献:

MMD中的VMD文件格式详解国内博客,解释VMD格式并用Java读取

VMD file formatMMD Wiki

本文会用python解析vmd文件,并纠正上述文章的一点错误。

根据MMD的规矩,上借物表:

名称

来源

MikuMikuDanceE_v803

圝龙龍龖龘圝

八重樱

神帝宇

封面静画:

名称

类别\来源

LightBloom

背光

AutoLuminousBasic

自发光特效

HgSAO

阴影

SoftLightSB

柔化

SvSSAO

阴影

XDOF

景深

dGreenerShader

G渲

Tokyo Stage

场景

一、格式说明

首先,vmd文件本身是一个二进制文件,里面装着类型不同的数据:uint8、uint32_t、float,甚至还有不同编码的字符串,因此我们需要二进制流读入这个文件。

vmd格式很像计算机网络的协议格式,某某位是什么含义,区别是,vmd文件的长度理论上是无限的,让我们来看看。

vmd的大致格式如下:

头部

关键帧数量

关键帧

头部

最开始的就是头部(header),看到这就有十分强烈的既视感:

类型

长度

含义

byte

30

版本信息

byte

10 or 20

模型名称

其中,版本信息(VersionInformation)长度为30,是ascii编码的字符串,翻译过来有两种,一为“Vocaloid Motion Data file”,二为“Vocaloid Motion Data 0002”,长度不足30后用\0(或者说b'\x00')填充。这是由于vmd版本有两种,大概是为了解决模型名称长度不足,因此后续只影响模型名称的占用长度。

模型名称(ModelName),是动作数据保存时用的模型的模型名,通过这个我们可以获取到那个名称,我们知道,一个动作数据想要运作起来,只要套用模型的骨骼名称是标准的模板就可以,因此我想象不出这个名称有何用处,或许某些模型带有特殊骨骼,例如翅膀之类的,这样能方便回溯?模型名称的长度根据版本而决定,version1为10,version长度为20。编码原文写的是shift-JIS,是日语编码,这样想没错,然而我试验后发现并非如此,例如经常改模型的大神神帝宇的模型,他的模型名称用shift-JIS为乱码,用gb2312竟然能正常读出来;还有机动牛肉大神的模型,他的模型名称用gb2312无法解码,用shift-JIS解码竟然是正常的简体中文???怎么做到的?

骨骼关键帧(BoneKeyFrame)

骨骼关键帧,分为两部分:骨骼关键帧数、骨骼关键帧记录:

类型

长度

含义

uint32_t

4

骨骼关键帧数量 BoneKeyFrameNumber

类型

长度

含义

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

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

合计

为何要分开写呢?因为骨骼关键帧数量只需要一个就够了,而后面骨骼关键帧记录的数量会和前面的骨骼关键帧数量保持一致,最后大概是这种效果:

我们可以查一下,每个骨骼关键帧的数量为111字节。

旋转坐标

一开始还没发现,旋转坐标竟然有四个,分别为x, y, z, w,急的我去MMD里查看一下,发现和我印象中没有什么差别

都是[-180, 180]的角度值,我用程序跑的时候,这四个值完全看不懂;幸好在英文网站上找到这个表示方法:四元数。四元数是用四个值表示旋转的方法

,其中

都是虚数,我上网找了一堆资料,并且得到了四元数转化欧拉角的公式

得到的是角度制,我们通过角度制转弧度制的公式即可算出和MMD中等同的角度表示。

补间曲线

为何补间曲线的类型不确定呢?上面csdn博客的教程说“uint8_t那里有冗余,每四个只读第一个就行”。说的没有问题,首先我们要清楚这个补间曲线坐标的含义。

我们打开MMD,读入模型,随意改变一个骨骼点,记录帧,就会发现左下角会出现补间曲线。

  补间曲线的用处,就是自动补齐当前记录帧与上一个记录帧之间动作的变化顺序,曲线斜率越高,动作变化越快,具体教程可以参照贴吧中的教程,我们可以通过拖动红色的小x改变调节线,从而改变曲线

每一组小红x的坐标,就可以唯一确定一条补间曲线,因此,上面的补间曲线存储的就是小红x的坐标

,其中左下角调整线的小红x是看做点1,通过程序读取,我知道,小红x的坐标取值为[0~127]间的整数,因此用1字节完全可以存下,可能是当时的设计错误,用了32位整数存,高24位完全浪费了,完全可以不用读取,因此我们可以直接读取32位无符号整数或读取8位无符号整数,然后跳过24位。

如果曲线只有一个,那么为什么会有四个补间曲线呢?实际上不止一个,补间曲线框的右上角就有个下拉菜单可以选择,对于圆形骨骼,没有相对位置变化,x, y, z补间曲线没有用,只有旋转速率可以调节,而方框骨骼可以移动,因此x, y, z, 旋转补间曲线都有用处。

回过头来,再说一下补间曲线的坐标,在这里,是以左下角为原点,横纵方向[0, 127]的坐标轴

1.png

后面的格式与这个格式大同小异。

表情关键帧(MorphKeyFrame)

表情关键帧分为:表情关键帧数、表情关键帧记录:

类型

长度

含义

uint32_t

4

表情关键帧数量 MorphKeyFrameNumber

类型

长度

含义

byte

15

表情名称 MorphName

uint32_t

4

关键帧时间 FrameTime

float

4

程度 Weight

byte

23

合计

表情关键帧每个记录长度为23字节,其中程度(Weight)是取值为[0, 1]之间的浮点数,在MMD中的表现如下:

镜头(CameraKeyFrame)

镜头关键帧分为:镜头关键帧数、镜头关键帧记录:

类型

长度

含义

uint32_t

4

镜头关键帧数量 CameraKeyFrameNumber

类型

长度

含义

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中,我们可以通过滑轮改变

这有什么用呢?可以看下面的图:

当距离为0时,我们的镜头就在红点上,造成的效果是,当我们移动镜头的Y角度时,镜头就好像在我们眼睛上,视角是第一人称视角。可以看这里,是找镜头资料时偶然看到的。

旋转角度不再是四元数,而是普通的弧度制角度,我猜大概是镜头的万向锁情况没那么严重,因此用弧度制就能表示。

Curve是曲线的意思,按照之前的的补间曲线,确实还有一个相机曲线,不过一个曲线=两个小红x=4个坐标点=四字节,因此24字节有20字节的冗余,它的前四个字节就已经表达了坐标,后面20个字节是将这4个字节重复了5次。

镜头FOV角度和透视值有关,上面的博客写的是float,但实际上我试验是uint32_t,取值刚好就是MMD中的透视值。

Orthographic似乎是一种特殊的相机,没有近大远小的透视关系(不确定),不过在我的实验中,它一直取值为0。和上面的已透视没有关系,当取消已透视时,透视值会强制为1。

下面的骨骼追踪似乎没有记录,可能是强制转换成骨骼所在的坐标了。

后面的格式与这个格式大同小异。

光线关键帧(LightKeyFrame)

表情关键帧分为:光线关键帧数、光线关键帧记录:

类型

长度

含义

uint32_t

4

光线关键帧数量 LightKeyFrameNumber

类型

长度

含义

uint32_t

4

关键帧时间 FrameTime

float*3

12

RGB颜色空间 color.rgb

float*3

12

xyz投射方向 Direction.xyz

byte

28

合计

rgb颜色空间之[0, 1]之间的数,类似html的RGB(50%, 20%, 30%)这种表示方法,转换方式就是把RGB值分别除以256。

光线投射方向是[-1, 1]之间的小数。正所对的投射方向是坐标轴的负方向,例如将Y拉到1, 光线会从上向下投影。

二、代码读取

我依旧会使用面向对象的方式构建VMD类,不过构造方法无力,属性太多,我选择用静态方法添加属性的方式构建对象

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("

"Position": {"x": struct.unpack("

"y": struct.unpack("

"z": struct.unpack("

},

"Rotation":{"x": struct.unpack("

"y": struct.unpack("

"z": struct.unpack("

"w": struct.unpack("

},

"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("

'Weight': struct.unpack("

})

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("

'Distance': struct.unpack("

"Position": {"x": struct.unpack("

"y": struct.unpack("

"z": struct.unpack("

},

"Rotation":{"x": struct.unpack("

"y": struct.unpack("

"z": struct.unpack("

},

"Curve": tuple(b for b in array[current_index+32: current_index+36]),

"ViewAngle": struct.unpack("

"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("

'Color': {

'r': struct.unpack("

'g': struct.unpack("

'b': struct.unpack("

},

'Direction':{"x": struct.unpack("

"y": struct.unpack("

"z": struct.unpack("

}

})

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

三、实验

随意掰弯一些关节并注册、使用:

if __name__ == '__main__':

vmd = Vmd.from_file("test.vmd", model_name_encode="gb2312")

from pprint import pprint

pprint(vmd.dict)

output:

{'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}

因为前面提到的编码模式,我选择用gb2312解码,在很多(也许是大部分)动作数据都会报错,可以去掉编码方式:

vmd = Vmd.from_file("test.vmd")

我们没有移动方块骨骼,因此位置信息都是0。

不喜欢看欧拉角的话,可以写一个转换方法:

@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

这样只要调用:

vmd = Vmd.from_file("test.vmd")

from pprint import pprint

pprint(vmd.euler_dict)

即可得到转换成欧拉角的结果,同样的方式还可以编写转换RGB、弧度、角度等

python内置的json包可以很方便得将字典转换成json格式文档储存。

我们也可以试着写一些将VMD转换成vmd文件的方法。

四、总结

通过学习VMD的文件结构,大致了解了储存动作数据的格式和一些方法,或许可以类比到一些主流的商业3D软件上。

读取程序并不难,我写程序的很多时间都是查二进制操作消耗的,通过这个程序,还巩固了二进制操作的知识。

我在google上找到了一个包saba,专门用于操控MMD的文件,包括模型、动作数据等

现在学一下图形学,等学有所得再做出更多东西。

vmd python 命令_【MMD】用python解析VMD格式读取相关推荐

  1. ubuntu中mysql怎么退出命令_如何使用Python进行MySQL数据库管理

    本节我们将学习使用Python进行MySQL数据库管理.Python有多种用于MySQL数据库管理的模块,这里使用MySQLdb模块.MySQLdb模块是MySQL数据库服务器的接口,用于向Pytho ...

  2. python升级版本命令_如何在python中安装和配置kivy库

    kivy是python的UI开发工具包,主要关注用户界面显示效果,可以在Android.IOS.Linux.OS X和Windows上运行.如果python开发中使用kivy,需要安装和配置相关文件和 ...

  3. python执行的命令_如何在Python中执行外部命令

    Python子进程模块允许生成新进程,从Python脚本执行外部命令. 您可以使用这些教程来安装最新版本的Python. 此外,还有许多可用于Python IDE. 就像在Ubuntu系统上安装PyC ...

  4. python调用adb shell命令_如何在python脚本里面连续执行adb shell后面的各种命令

    如何在python脚本里面连续执行adb shell后面的各种命令 adb shell "cd /data/local && mkdir tmp" adb shel ...

  5. python怎样执行curl命令_如何使用python执行curl命令

    如何使用python执行curl命令 我想在python中执行curl命令. 通常,我只需要在终端输入命令并按回车键. 但是,我不知道它在python中是如何工作的. 该命令如下所示: curl -d ...

  6. python股市_如何使用python和破折号创建仪表板来主导股市

    python股市 始终关注大局 (Keep Your Eyes on the Big Picture) I've been fascinated with the stock market since ...

  7. 学习python课程_想学习Python吗? 这是我们的免费4小时互动课程

    学习python课程 Python is a popular, versatile and easy-to-learn language. It's the go-to language for AI ...

  8. 易语言 python库_精易Python支持库 (1.1#1205版)发布啦!

    精易Python支持库 (1.1#1205版) 本支持库提供了 6 种库定义数据类型,提供了 87 种命令. 支持库说明 该支持库为易语言调用并执行Python代码.文件提供了支持. 使用本支持库,可 ...

  9. aws python库_如何使用Python,AWS和IEX Cloud创建自动更新股市数据的Excel电子表格

    aws python库 Many Python developers in the financial world are tasked with creating Excel documents f ...

  10. 少儿编程python教材_少儿编程|Python环境安装

    一.为什么要学Python? 小学 山东,浙江,北京地区小学已将Python内容纳入教材 高中 浙江省已将信息技术教材编程语言替换为Python 大学 计算机二级考试加入"Python 语言 ...

最新文章

  1. 封装了一套WeCenter的IOS SDK
  2. 这些编程语言程序员工资最高!Java才第四
  3. 学python有什么好处 学完可以做什么
  4. 如何构建可视化的营销数据大屏?
  5. Hacker's Browser
  6. SQL SERVER服务停止和启动命令行
  7. 硬件工程师前途到底怎样?看看大佬怎么说,看完跪了
  8. mysql排序两位数_MySQL_Mysql row number()排序函数的用法和注意,虽然使用不多,但是也有情况 - phpStudy...
  9. dubbo控制台安装
  10. vue lang_推荐一个基于Vue 的 H5 快速开发模板
  11. python基础(18)之 异常处理
  12. 吴恩达|机器学习作业6.0支持向量机(SVM)
  13. mysql特效_Cocos2d-x学习笔记(16)(常见22种特效)
  14. python种子数是什么意思_Python:随机种子问题
  15. 智能控制器在风机及水泵中的应用
  16. HC32F4 CRC32校验(附软件CRC32校验)
  17. 刽子手c语言,古代神秘职业:刽子手的祖师爷
  18. 计算机键盘都起什么作用,电脑键盘各个按键功能 电脑键盘各个按键有什么功能...
  19. js 绘画js 绘画路径_绘画是一种技能,而不是才能
  20. Cannot find name ‘console‘. Do you need to change your target library?ging the ‘lib‘ compiler option

热门文章

  1. SpringBoot+JMail
  2. Dynamics 365Online Server-Side OAuth身份认证二(S2S)
  3. 简单flash播放器代码
  4. 佳能最新版DPP免CD安装
  5. 利用tftpd32拷贝windows文件到虚拟机ubuntu中
  6. 三区三线划定-永久基本农田
  7. 2018年尚硅谷徐靖博老师的分布式电商项目视频
  8. linux开发板通过网线连接电脑
  9. EDEM2019 实例操作教程——输送带
  10. 在ppt中怎么加入倒计时 里面怎么加入倒计时【方法】