SYQY网游福利站-新活动速递_热门攻略_礼包中心

一、前言

PMX是Polygon Model eXtended的简写,是动画软件MikuMikuDance中的模型文件,是.PMD格式(Polygon Model Data)的继承者。

上次解析的是MMD中的动作文件VMD,相比于VMD,模型文件需要了解的东西更多,为此我还简单学了下建模工具Blender。

同样的,这次也有参考的网站。

国外Github上,看样子是老外翻译日文到英语的文档

https://gist.github.com/felixjones/f8a06bd48f9da9a4539f

日文Wiki

https://www6.atwiki.jp/vpvpwiki/pages/284.html

参考PmxEditor说明

【PE教学】超超超初心者向PE入门Part1 - 基本原理

古剑二MMD&绑骨教程

mmd教程/pe教程-oeasy

VPVP:https://www6.atwiki.jp/vpvpwiki/pages/118.html

借物表:

名称

MikuMikuDance十周年汉化版

PmxEditor_0254e

八重樱、q版樱火轮舞、女仆丽塔-洛丝薇瑟2.0-神帝宇改模

MMD-ray渲染

使用工具

相较之前用MMD演示动作数据构成,PMX文件的构成用PmxEditor研究更好,Pmx编辑窗口很好的把文件结构展示出来。

二、PMX文件解析

1.类型说明

相对于上次VMD的解析,PMX的类型更为复杂,用简单的C数据类型解释很麻烦,因此向国外的文档学习,将其中某些元素定义为一些类型。可以先大致看一眼。

(1)C类型

C语言风格基础类型,复杂类型也是由许多基础类型定义。

类型名称

C类型

标准类型

大小(字节)

范围

byte

char

uint8_t

1

ubyte

unsigned char

uint8_t

1

short

short

int16_t

2

ushort

unsigned short

uint16_t

2

int

int

int32_t

4

uint

unsigned int

uint32_t

4

float

float

float

4

IEEE754标准定义float范围

(2)PMX类型

PMX格式所定义类型,不过这些类型在大多数图形库中都有定义

类型名称

类型说明

类型结构

大小(字节)

vec2

XY 向量

float, float

8

vec3

XYZ 向量

float, float, float

12

vec4

XYZW 向量

float, float, float, float

16

text

前四个字节说明字符串长度,编码定义在后面的Globas全局定义中

int, byte[]

4+字符长度

flag

标志类型,每个Byte可含有8个标志0为false,1为true

byte

1

index

索引类型,每个要素的索引不同,后面详细说明

byte/ubyte/short/ushort/int

1/2/4

(3)索引类型

有时候我们需要记住元素的索引,例如某个面是由哪几个点组成的,而表示一个点,就需要索引。

索引的大小不是确定的,有些元素需要很多索引,因此索引类型也很大;而有些元素很少,索引节点的大小很小。

同样的元素,例如顶点,有时一个模型只有几个顶点,有的有几万个顶点,因此同样是节点的索引,大小也可能不一样。PMX将索引类型分为3种:Type1、Type2、Type4,分别占有1、2、4个字节,模型的索引的大小储存在Globas全局定义中,后面会提到。

类型名称

说明

类型1

类型2

类型4

类型1最大值

类型2最大值

类型4最大值

空值

Vertex

顶点

ubyte

ushort

int

255

65535

2147483647

N/A

Bone

骨骼

byte

short

int

127

32767

2147483647

-1

Texture

纹理

byte

short

int

127

32767

2147483647

-1

Material

材质

byte

short

int

127

32767

2147483647

-1

Morph

变形(表情)

byte

short

int

127

32767

2147483647

-1

Rigidbody

刚体

byte

short

int

127

32767

2147483647

-1

2.文件总体结构概览

和VMD文件类似,都是全局信息-[元素总数-元素信息]。

Header

版本

说明

Model information

Globals

2.0

模型全局信息

Vertex count

Vertices

2.0

顶点

Surface count

Surfaces

2.0

Texture count

Textures

2.0

纹理

Material count

Materials

2.0

材质

Bone count

Bones

2.0

骨骼

Morph count

Morphs

2.0

变形(表情)

Displayframe count

Displayframes

2.0

表示枠(huà)

Rigidbody count

Rigidbodies

2.0

刚体

Joint count

Joints

2.0

关节点

SoftBody count

SoftBodies

2.0

软体

软体应该是作为Pmx独有的,很明显的,PmdEditor没有软体按钮。

下面是真正开始解析文件每一部分格式。

3.头部、全局信息

Header

模型头部,描述模型的基本信息

数据含义

结构

说明

签名

byte[4]

"PMX " [0x50, 0x4D, 0x58, 0x20]

值为"PMX空格"

版本号

float

2.0、2.1

全局信息数量

byte

8(PMX2.0)

PMX2.0有8条全局信息

全局信息

byte[全局信息数量]

用于描述各索引的数量

见后面的全局信息说明表

本地模型名

text

本地语言模型名,例如中文、日文

通用模型名

text

英文名

本地语言模型描述

text

打开MMD弹出的注意事项,本地语言

通用语言模型描述

text

英文注意事项

全局信息

用于描述索引类型的类型,就是上面索引类型的[1, 2, 4]

位置

含义

说明

0

字符串编码

{0, 1}

0为UTF16LE编码, 1为UTF8编码

1

额外的vec4数量

{0, 1, 2, 3, 4}

在这里可以定义为每个顶点增加vec4的数量,详见后面的顶点说明

2

顶点索引大小

{1, 2, 4}

这个大小用于表示索引类型的类型

3

纹理索引大小

{1, 2, 4}

也可以理解为所占的字节有多少

4

材质索引大小

{1, 2, 4}

间接能知道模型的该元素数量大致为多少

5

骨骼索引大小

{1, 2, 4}

6

变形索引大小

{1, 2, 4}

7

刚体索引大小

{1, 2, 4}

4.顶点

首先我们打开PmxEditor并读入一个模型,然后进入编辑器的顶点页面。因为顶点有复杂的数据,这次同样先说明一些其他的东西。如果感觉迷惑了不要紧,对照着顶点页面研究就能很好的明白各处的含义。

额外的vec4

方才在全局定义中,有一个额外的vec4选项,定义为每个顶点多出几个vec4,每个顶点可以最多获得4个额外的vec4值,文件文身也不知道这是什么含义,多数情况会直接传递给vertex shader决定使用情况。例如,vec4用于以下方面:

额外的纹理UV

高光贴图UV

详细参考这个教程

右侧的贴图就是左侧的高光贴图,因为黑色部分的RGB值为0,不会参与高光的计算(参与了但值为0),而金属边框会参与计算,做出的结果是:木头部分不会反射高光,而金属部分会出现高光

法线贴图UV

通过改变法向量,使得平面图计算出阴暗交替的感觉。

顶点颜色(PMX2.1拓展)

忽略这些额外的vec4是最安全的,但同时也会导致很多东西无法显示。

这里就是追加UV选项的地方

顶点绑骨

顶点要随着骨骼运动、变形,每个骨骼对顶点有相应的权重,在MMD中,一个顶点最多和4个骨骼相关联;随着绑定骨骼数量的不同,顶点绑骨的类型也不同。以下是绑骨类型:

无绑骨

骨骼索引-1是零值,骨骼应该被忽略。

BDEF 1(Bone Deform)(2.0)

类型

名称

说明

索引-骨骼

骨骼1

权重==1

注意:索引类型(index)对每个元素不同,骨骼索引的定义看上面的索引类型说明。

BDEF 2(2.0)

类型

名称

说明

索引-骨骼

骨骼1

索引-骨骼

骨骼2

foat

骨骼1权重

骨骼2权重 = 1-骨骼1权重

BDEF 4(2.0)

类型

名称

说明

索引-骨骼

骨骼1

索引-骨骼

骨骼2

索引-骨骼

骨骼3

索引-骨骼

骨骼4

foat

骨骼1权重

foat

骨骼2权重

foat

骨骼3权重

foat

骨骼4权重

不保证=1

SDEF(2.0)

一种SBS(Spherical Blend Skinning)球面混合蒙皮方式,能使皮肤的扭动更自然。

类型

名称

说明

索引-骨骼

骨骼1

索引-骨骼

骨骼2

foat

骨骼1权重

骨骼2权重 = 1-骨骼1权重

vec3

vec3

vec3

QDEF(2.1)

双四元数变型混合

类型

名称

说明

索引-骨骼

骨骼1

索引-骨骼

骨骼2

索引-骨骼

骨骼3

索引-骨骼

骨骼4

foat

骨骼1权重

foat

骨骼2权重

foat

骨骼3权重

foat

骨骼4权重

不保证=1

顶点数据

顶点部分以一个int开始,定义了有多少个顶点(注意它是一个带符号的int),后面跟着每个顶点的定义

每个顶点的格式如下:

含义

类型

说明

位置

vec3

XYZ

法向量

vec3

XYZ

UV坐标

vec2

XY

额外的vec4

vec4[N]

N的大小见上面的全局定义

变型权重类型

byte

0=BDEF1, 1=BDEF2, 2=BDEF4, 3=SDEF, 4=QDEF

变型权重

顶点绑骨类型

见上面的顶点绑骨结构

边缘放大率

float

[0, 1]

5.面

面以三个顶点为一组,定义单个三角形面;三角形面向按顺时针缠绕顺序定义,顺时针为正面,逆时针为背面(与DirectX一样)。

同样别忘了先读取一个int来确定有多少个面,不过要注意,面的定义是按照索引数量定义的,因此要将这个数字整除3。

含义

类型

说明

索引-顶点数组

index-vertex[3]

参考上面定义的索引类型

附加面类型(PMX2.1)

材质数据在2.1版本有扩充,能包含点和线的绘制,详细需要查阅资料。

6.纹理

此处是纹理路径表,路径本身是字符串,它的格式和操作系统相关,可能是绝对路径也可能是相对路径。

某些实现程序会基于文件名修改纹理,例如使用"_s"作为球体贴图或使用"_n"作为法线贴图。

纹理贴图没有首选格式,最常见个纹理格式可能是bmp、png、tga,有时候也可能是jpg或dds格式。

保留的纹理名称

纹理名称toon01.bmp"到"toon10.bmp"是保留的,模型不应该通过纹理数据引用这些纹理。如果不巧使用了,会发生什么取决于实现程序,某些实现将忽略这些,某些实现会使用内部的纹理。

同样,纹理数据以一个int开头,定义总共有多少纹理。

含义

类型

说明

路径

text

纹理路径通常是相对的

PmxEditor编辑没有独立的纹理页面,但在材质页面能看到材质引用的纹理。

7.材质

PMX用基于材质的渲染,而不是基于物理的渲染,

鉴于后面的材质数据会使用标志位,先放出材质的标志类型定义(1byte=8flag):

位置

含义

效果

版本

0

no-cull 禁用背面剔除

双面描绘

2.0

1

Ground shadow 地面阴影

将阴影投射到几何体上

2.0

2

Draw shadow 描绘阴影

渲染到阴影贴图(本影标示)

2.0

3

Receive shadow 受到阴影渲染

从阴影贴图接受阴影(本影)

2.0

4

Has edge 有边缘

有边缘描绘(轮廓线有效)

2.0

5

Vertex colour 顶点颜色

使用额外的vec4作为顶点的颜色

2.1

6

Point drawing 画点

三个顶点都被画出

2.1

7

Line drawing 画线

三个边被画出

2.1

点线绘图(PMX2.1)

如果绘点和绘线标志同时存在,绘点标志将覆盖绘线标志。

普通面通过顶点 [A -> B -> C]渲染

通过点绘制,每个顶点被渲染为单独的点[A,B,C],从而产生3个点。

线条图将呈现3条线[A-> B,B-> C,C-> A],产生3条线。

启用点渲染时,材质(不是顶点)的边尺度值将控制点的大小。

如果设置了边缘标志,则点或线将具有边缘,这是未定义的行为。

材质数据

与之前相同,材质也有一个int记录材质的数量,材质的数据结构定义如下:

含义

类型

说明

本地材质名称

text

本地的材质名称,日语、中文等等

通用材质名称

text

通用的材质名称,一般是英文

漫反射颜色Diffuse

vec4

RGBA

镜面光(高光)颜色Specular

vec3

RGB

镜面光强度Specular strength

float

镜面高光的大小

环境色ambient

vec3

当光线不足时的阴影色(既基础色,让阴影不那么黑)

绘制标记

flag

见上面的材质标志

边缘颜色

vec4

RGBA

边缘比例

float

[0, 1]

索引-纹理

index-texture

参考索引类型

环境(高光贴图)索引

index-texture

与纹理索引相同,但用于环境映射

环境(高光贴图)混合模式

byte

0=禁用, 1=乘, 2=加, 3=额外的vec4[注1]

贴图引用

byte

0=引用纹理, 1=内部引用

贴图值

索引-纹理/byte

取决于贴图引用[注2]

元数据

text

用于脚本或其他数据

面数量

int

表示当前材质影响了多少个面[注3]

注1:环境混合模式3将使用第一个额外的vec4来映射环境纹理,仅使用X和Y值作为纹理UV。它被映射为附加纹理图层。这可能与第一个额外vec4的其他用途冲突。

注2:Toon值将是一个非常类似于标准纹理和环境纹理索引的纹理索引,除非Toon引用字节等于1,在这种情况下,Toon值将是一个引用一组10个内部toon纹理的字节(大多数实现将使用“toon01.bmp”到“toon10.bmp”作为内部纹理,请参阅上面纹理的保留名称。

注3:表面计数总是3的倍数。它基于先前材质的偏移量与当前材质的尺寸。如果将所有材质的所有表面计数相加,则应最终得到表面总数。

人话3:图上的“脸红”为第一个材质,有81个面,[0, 81)面都属于本材质,而“首”由38个面组成,索引[81, 81+38]都属于“首”材质,以此类推,这样将材质、纹理、面组成的网格,组成面的顶点都结合起来了。

环境色中,扩散色其实是diffuse漫反射,而反射色是specular高光反射色,反射强度是指光泽(Shininess),反射光强度是根据出射光与人眼矢量夹角的cos值确定的(处于0到1之间),shininess会将得到的数字再进行平方选择,得到的值为specular^{(2^{shininess})},比如shininess为5,则要进行32次的pow。

因此反射强度越小,光泽就越大。不过在MME自发光特效中,反射强度大概是不光代表光泽,同时代表光强,往往模型的反射强度值会设为120左右,这样物体的光泽很小,但光强特别大,看起来很闪耀。

8.骨骼

用于骨骼动画。

骨骼标志

位置

含义

效果

版本

0

骨骼尾部(尖端)位置

0为连接相对位置,1为连接子骨骼

2.0

1

可旋转

启用旋转

2.0

2

可移动

启用移动

2.0

3

可见

显示

2.0

4

启用

操作

2.0

5

IK

是IK骨

2.0

8

付予旋转

继承亲骨的旋转

2.0

9

付予移动

继承亲骨的移动

2.0

10

固定轴

轴限制

2.0

11

本地轴

Local轴

2.0

12

后算物理

先变形

2.0

13

外部亲骨骼变形

外部亲

2.0

变形阶层在这里有讨论,大致意思是:变形阶层的作用是物理的计算顺序,值越大越靠后计算,一般IK骨的值才是1,其他是0,而原文提到TDA式MIKU的眼睛是2,用于特殊的需求,具体可参照原文。

(继承)付予旋转和移动的原意好像不单单是继承旋转和移动的作用,毕竟没有IK的骨骼都会跟着亲骨走,我发现有付予旋转的骨骼多是隐藏骨骼,而作用大概是可以增加或减少亲骨对当前骨骼的影响,应用可以看这里: 【MMD教程相关】不追加捩骨的情况下解决模型手肘旋转时的扭曲。

骨骼继承

名称

类型

作用

亲骨索引

索引-骨骼

参阅骨骼索引类型

影响权重

float

亲骨的影响力

假如上图的付予栏位中的旋转、移动有一个被选中,这个类型有效

骨骼固定轴

名称

类型

作用

轴方向

vec3

骨骼指向的方向

这个用在手臂中那个圆形打着叉的捩(liè)骨中:

骨骼Local坐标

名称

类型

作用

X矢量

vec3

Z矢量

vec3

PmxEditor最下面的local轴选项,这个见于左腕、右腕(左右胳膊)、以及往下延伸的子骨骼中,其他用法可以看看这个。

MMD左下角有操作手柄:

GLOBAL是可以点击的,会变成LOCAL。

默认情况(模型选项Local轴无效),LOCAL轴依旧可以使用,不旋转模型的情况下,两个坐标没有区别,如果将模型旋转,GLOBAL轴会根据世界坐标旋转,而LOCAL轴会根据模型坐标旋转。

当Local轴选项有效时,GLOBAL模式没有改变,而LOCAL的坐标会根据骨骼自身坐标系进行旋转:

Global,Local无效,Local有效

这个骨骼坐标系的定义就是由三个基向量(上图左3)确定。

这个设定不会改变骨骼动画的行为;解释一下,我们现在已知坐标系有三种:世界坐标系、模型坐标系、骨骼坐标系,为了区分,如上图绕世界坐标系Y轴旋转一个角度做对比。

如果我们用Global手柄轴做旋转,永远是在世界坐标系下旋转,此时你拖动X轴手柄,会发现XYZ轴一起被改变。

如果用Local轴旋转,需要看骨骼本身是否有骨骼坐标系(既上边的Local轴选项是否有效),如果像手臂等有效骨骼,旋转Local一样会发生XYZ轴同时改变,但如果无效,就不会发生,可以确定,这个轴旋转是依照模型坐标系来确定的。

Local轴只会影响用户对模型的部分旋转操作,而动作数据记录的是模型坐标系,因此这里只会影响用户的操作,即使这里随意设定,也不会影响动画的展示。

通常情况下,LocalX轴会设定为normalize(childBone-parentBone)(和上图不同),LocalZ=cross(LocalX, [0, 0, 1]),LocalY=cross(LocalZ, LocalX),这个可以去试试,和相机旋转的计算方式很像。

外亲骨

名称

类型

作用

骨骼索引

索引-骨骼

MMD即时绑定两个模型,为什么会出现在模型文件中,真是个迷。

IK 角度限制

名称

类型

作用

下限

vec3

最小角度(弧度制)

上限

vec3

最大角度(弧度制)

IK链

名称

类型

作用

骨骼索引

索引-骨骼

参阅索引类型

角度限制

byte

值为1时,使用角度限制

IK 角度限制

~

如果角度限制值为1,那么此处参照上面的IK 角度限制

~是参考其他类型,也可能没有的意思。

IK骨

名称

类型

作用

目标骨骼索引

索引-骨骼

参阅索引类型

循环计数

int

IK解算 CCD(循环坐标下降法)循环次数

限制角度

float

IK骨旋转角度限制

IK链计数

int

IK链接的骨骼数量

IK链接

IK链[N]

N是链接数,可以为0,类型参阅上面的IK链类型

这里IK骨的数据看起来很迷

IK是反向动力学骨骼,可以根据骨骼自身状态,反向影响亲骨,而非其他骨骼那样由亲骨带动子骨。

图中可见到足IK有三处连接,分别非亲骨+IK链的上方,指向相对位置的左方,以及作为IK目标且不可见的右足先IK连接在右下方。

原文中没有给出单位角和loop的作用,我在谷歌查了下:这里,文章表示,Loop和模型本身无关,而是MMD内部计算某个最佳方案的算法有关,如果为0可能导致IK不会工作,因此很多模型都直接给个40作为值,角度也是这样,并且如果不给单位角值,它会自动生成一个能适应性良好的值。

(更新:IK解算用了循环坐标下降法,Loop是指循环次数,而角度限制是应对特殊的关节,如膝盖只让模型坐标系下的X轴旋转,并且关节角度不得大于180度,相对来说头发如果有IK,限制会小很多)

上图的那个单位角114.5916,在文件中存储是2.0,也就是弧度制:。

骨骼数据

骨骼数据以有符号int开头,定义了有多少个骨骼

名称

类型

说明

骨骼本地名称

text

通常是日语

骨骼通用名称

text

一般是英文

位置

vec3

亲骨索引

索引-骨骼

特殊的:操作中心的亲骨为-1

变形阶层

int

计算物理的顺序

标志

flag[2]

见Bone标志

尾部位置

vec3/索引-骨骼

见上面的材质标志

骨骼继承

~

如果Flag中此处为True,就参阅上面的骨骼继承

固定轴

~

同上

Local轴

~

同上

外部亲

~

同上

IK

~

同上

9.变形

变形就是我们常说的表情

变型类型

标识变形的类型,长度只有1byte。

对于使用者,常认为变型有三种或五种等等,从文件存储来说似乎不止这些:

变型模式

说明

版本

组合

0

Group

2.0

顶点

1

2.0

骨骼

2

2.0

UV

3

2.0

额外UV1

4

2.0

额外UV2

5

2.0

额外UV3

6

2.0

额外UV4

7

2.0

材质

8

2.0

切换

9

Flip

2.1

脉冲

10

2.1

偏移与偏移值

变形对每一个持有的作用元素(顶点、骨骼、UV这些)称为偏移(Offset),我感觉不是十分准确,不过PmxEditor都是这么翻译的,那就入乡随俗了。

针对不同的作用元素,会拥有不用的偏移值(偏移量):

组合(Group)

名称

类型

说明

变形索引

索引-变形

参阅索引类型

影响

float

对被索引变形的权重

PmxEditor中操作方法是:右键表情->归纳到表情组合,或者直接右键->新建制作表情->组合(G)

顶点(Vertex)

名称

类型

说明

顶点索引

索引-顶点

参阅索引类型

移动

vec3

变化的相对位置

骨骼(Bone)

名称

类型

说明

骨骼索引

索引-骨骼

参阅索引类型

移动

vec3

变化的相对位置

移动

vec4

相对旋转四元数

UV(及拓展UV)

名称

类型

说明

顶点索引

索引-顶点

参阅索引类型

~

vec4

做什么取决于UV拓展

没太读明白,UV的值需要4个,我用的时候只有两个:

材质(Material)

名称

类型

说明

材质索引

索引-材质

参阅索引类型,-1代表所有材质

混合方法

byte

0是乘法,1是加法

漫反射(扩散色)

vec4

镜面光(反射色)

vec3

镜面光强度

float

环境光(环境色)

vec3

边缘颜色

vec4

边缘大小

float

纹理色调

vec4

环境色调

vec4

贴图色调

vec4

不是很清楚环境色调(Environment tint)的意义,毕竟前面材质中光照贴图都给翻译成环境贴图了。

切换(Flip)

名称

类型

说明

变形索引

索引-变形

参阅索引类型

影响

float

对模型的影响

和分组很像,这里大致有应用。

脉冲(Impulse)

名称

类型

说明

刚体索引

索引-刚体

参阅索引类型

本地标志

byte

移动速度

vec3

转动扭矩

vec3

变形数据

依旧先以一个有符号的int开始

名称

类型

说明

本地变形名称

text

通用变形名称

text

面板位置

byte

{1,2,3,4},表情在MMD面板中处于的位置

变形类型

byte

参阅上面的变形类型说明

偏移量个数

int

元素的个数

偏移量数据

~[N]

N是偏移量个数,可以为0,具体参照上面的偏移量说明

10.表示枠

一开始看着英文还很蒙(Display Frame),寻思展示帧是什么。表示枠其实就是在MMD中,模型可以注册的物件的分组(只有骨骼和变形,相机、

光照不属于模型),在MMD左侧栏,那些可以展开的就是表示枠:

帧类型

类型

说明

版本

0

索引-骨骼

2.0

1

索引-变形

2.0

骨骼帧数据

名称

类型

说明

骨骼索引

索引-骨骼

参阅索引类型

变形帧数据

名称

类型

说明

变形索引

索引-变形

参阅索引类型

帧数据

名称

类型

说明

帧类型

byte

参阅帧类型

帧数据

~

参阅帧数据类型

表示枠数据

以一个有符号int开始作为表示枠个数

名称

类型

说明

表示枠本地名称

text

表示枠通用名称

text

特殊标识

byte

0表示普通帧,1表示特殊帧

帧数量

int

记录有多少个帧

帧数据

~[N]

N是帧数据个数,类型参照帧数据说明

11.刚体

刚体形状类型

类型

说明

版本

0

2.0

1

盒子

2.0

2

胶囊

2.0

物理模式

名称

类型

说明

版本

0

追踪骨骼

刚体黏在骨骼上

2.0

1

物理演算

刚体使用重力

2.0

2

物理+骨骼

刚体使用重力摆动骨骼

2.0

刚体数据

刚体部分以一个int开始,用于定义有多少刚体

名称

类型

说明

刚体本地名称

text

刚体通用名称

text

关联骨骼索引

索引-骨骼

参阅索引类型

群组ID

byte

非碰撞组

short

非碰撞组的掩码

形状

byte

参阅刚体形状类型

形状大小

vec3

XYZ边界

形状位置

vec3

XYZ位置

形状旋转

vec3

弧度制

质量

float

移动衰减

float

旋转衰减

float

反应力

float

摩擦力

float

物理模式

byte

参阅刚体物理模式

形状大小固定vec3不变,不过不同的刚体形状,对应的有效字节数不同,球是1字节有效(半径),箱体是3字节(长高宽),胶囊是2字节有效(半径、高)

12.关节点

关节点(Joint)是用来连接刚体的“钉子”。

关节点类型

类型

说明

版本

0

Spring 6DOF

2.0

1

6DOF

2.1

2

P2P

点结合

2.1

3

ConeTwist

轴旋转

2.1

4

Slider

轴移动

2.1

5

Hinge

轴旋转

2.1

关节点数据

关节点数据以一个有符号的int开头,标识关节点数量

名称

类型

说明

关节点本地名称

text

关节点通用名称

text

关节点类型

byte

参阅关节点类型

刚体索引A

索引-刚体

参阅索引类型

刚体索引B

索引-刚体

位置

vec3

旋转

vec3

弧度制

位置最小值

vec3

位置最大值

vec3

旋转最小值

vec3

旋转最大值

vec3

定位弹簧

vec3

弹力

旋转弹簧

vec3

13.软体

软体基于Bullet Physics,随PMX 2.1一起推出。

我暂时还不会物理方面的知识,暂时机翻,这部分等学一学Bullet后再来补全。

形状类型

类型

说明

版本

0

TriMesh

三角网格

2.1

1

Rope

2.1

标识

类型

说明

版本

0

B-Link

2.1

1

Cluster creation

2.1

2

Link crossing

2.1

空气动力学模型

类型

说明

版本

0

V-Point

2.1

1

V-TwoSided

2.1

2

V-OneSided

2.1

3

F-TwoSided

2.1

4

F-OneSided

2.1

锚固刚体

名称

类型

说明

刚体索引

索引-刚体

参照索引类型

顶点索引

索引-顶点

Near mode

byte

顶点针脚

名称

类型

说明

顶点索引

索引-顶点

参照索引类型

软体数据

名称

类型

说明

软体本地名称

text

软体通用名称

text

形状

byte

参阅软体形状类型

材质索引

索引-材质

参阅索引类型

byte

组ID

碰撞体掩码

short

标识

flag

见软体标识

B-link create distance

int

Number of clusters

int

总质量

float

Collision margin

float

空气动力学模型

int

参照空气动力学模型

VCF配置

float

速度修正系数

DP配置

float

阻尼系数

DG配置

float

阻力系数

LF配置

float

提升系数

PR配置

float

压力系数

VC配置

float

音量对话系数

DF配置

float

动摩擦系数

MT配置

float

姿势匹配系数

CHR配置

float

刚性接触硬度

KHR配置

float

动力学接触硬度

SHR配置

float

软接触硬度

AHR配置

float

锚固硬度

Cluster SRHR_CL

float

软硬度与刚硬度

Cluster SKHR_CL

float

柔软与动力学硬度

Cluster SSHR_CL

float

柔软与柔软的硬度

Cluster SR_SPLT_CL

float

软与刚冲动分裂

Cluster SK_SPLT_CL

float

软与动能冲动分裂

Cluster SS_SPLT_CL

float

软与软冲动分裂

Interation V_IT

int

速度求解器迭代

Interation P_IT

int

定位求解器迭代

Interation D_IT

int

漂移求解器迭代

Interation C_IT

int

群集解算器迭代

Material LST

int

线性刚度系数

Material AST

int

面积/角度刚度系数

Material VST

int

体积刚度系数

Anchor rigid body count

int

锚固刚体个数

Anchor rigid bodies

~[N]

N是锚固刚体计数。参见锚固刚体说明

Vertex pin count

int

顶点引脚个数

Vertex pins

~[N]

N是顶点引脚计数。参见顶点引脚说明

14.样例代码

因为不清楚软体方面,所以没有读取软体数据,不过数据组织套路都一样,有需求可以自己写。代码只考虑2.1版本,2.0版本根据各处版本描述自行更改适应程序。

因为需要先实验一次,所以用Python,写起来更快一些;C++需要考虑组织程序以及数据类型,Opengl还没学到家,现在只读出了顶点数据,等写出差不多的模型读取器后,再把C++程序放上来。或者直接上github搜索已有现成的mmd读取器saba,参考src/Saba/Model/MMD/PMXFile.h文件,我的C++pmx文件读取程序也是参考这个写法。

class PMX:

def __init__(self):

pass

#Version

#Index

#Model_Name

#Model_Name_Universal

#Comments_Local

#Comments_Universal

#Vertices

#Surfaces

#Textures

#Materials

#Bones

#Morphs

#DisplayFrames

#Rigid_Bodies

#Joints

@staticmethod

def from_file(filename):

res = PMX()

f = open(filename, 'rb')

Signature = f.read(4)

if Signature.decode() != "PMX ":

raise Exception("model type is not PMX")

else:

print("Loading PMX")

res.Version = struct.unpack('

Global_Count = int.from_bytes(f.read(1), byteorder='little', signed=False)

Globals = list(f.read(Global_Count))

Index = res.Index = {'Text Encoding' : "UTF16" if Globals[0] == 0 else "UTF8",

'Appendix UV' : Globals[1],

'Vertex Index Size' : Globals[2],

'Texture Index Size' : Globals[3],

'Material Index Size' :Globals[4],

'Bone Index Size' : Globals[5],

'Morph Index Size' : Globals[6],

'Rigid Body Index Size' : Globals[7],

}

res.Model_Name = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(res.Index['Text Encoding'])

res.Model_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(res.Index['Text Encoding'])

res.Comments_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

res.Comments_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

#索引类型对应unpack的大小

bone_index_type = {1: 'b', 2: 'h', 4:'i'}[Index['Bone Index Size']]

vertex_index_type = {1:'B', 2:'H', 4:'i'}[Index['Vertex Index Size']]

texture_index_type = {1: 'b', 2: 'h', 4:'i'}[Index['Texture Index Size']]

morph_index_type = {1: 'b', 2: 'h', 4:'i'}[Index['Morph Index Size']]

material_index_type = {1: 'b', 2: 'h', 4:'i'}[Index['Material Index Size']]

rigid_body_index_type = {1: 'b', 2: 'h', 4:'i'}[Index['Rigid Body Index Size']]

Vertex_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f"Vertex Count: {Vertex_Count}")

res.Vertices = []

for i in range(Vertex_Count):

Position = struct.unpack("

Normal = struct.unpack("

UV_Texture_Coordinate = struct.unpack("

Appendix_UV = []

for j in range(Index['Appendix UV']):

Appendix_UV.append(struct.unpack("

Weight_Type = int.from_bytes(f.read(1), byteorder='little', signed=True)

# struct: [(bone, weight),...]

Weight_Deform = []

if Weight_Type == 0:# BDEF1

BDEF1 = struct.unpack('<'+bone_index_type, f.read(Index['Bone Index Size']))[0]

Weight_Deform.append((BDEF1, 1))

elif Weight_Type == 1:# BDEF2

BDEF2 = struct.unpack('<'+bone_index_type*2+'f', f.read(Index['Bone Index Size']*2+4))

Weight_Deform.extend([(BDEF2[0], BDEF2[2]), (BDEF2[1], 1-BDEF2[2])])

elif Weight_Type == 2:# BDEF4

BDEF4 = struct.unpack('<'+bone_index_type*4+'ffff', f.read(Index['Bone Index Size']*4+16))

Weight_Deform.extend([(BDEF4[0], BDEF4[4]),

(BDEF4[1], BDEF4[5]),

(BDEF4[2], BDEF4[6]),

(BDEF4[3], BDEF4[7])])

elif Weight_Type == 3:# SDEF

SDEF = struct.unpack('<'+bone_index_type*2+'f'+'f'*9, f.read(Index['Bone Index Size']*2+40))

Weight_Deform.extend([(SDEF[0], SDEF[2]), (SDEF[1], 1 - SDEF[2]), {'C': SDEF[3:6], 'R0':SDEF[6:9], 'R1':SDEF[9:12]}])

elif Weight_Type == 4:#QDEF

QDEF = struct.unpack('<' + bone_index_type * 4 + 'ffff', f.read(Index['Bone Index Size'] * 4 + 16))

Weight_Deform.extend([(QDEF[0], QDEF[4]),

(QDEF[1], QDEF[5]),

(QDEF[2], QDEF[6]),

(QDEF[3], QDEF[7])])

elif Weight_Type == -1:

pass

else:

raise Exception(f'Weight Type {Weight_Type} not found')

Edge_Scale = struct.unpack('

res.Vertices.append({

'Position':Position,

'Normal':Normal,

'UV Texture Coordinate':UV_Texture_Coordinate,

'Appendix UV':Appendix_UV,

'Weight Type':Weight_Type,

'Weight Deform':Weight_Deform,

'Edge Scale':Edge_Scale

})

Surface_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Surface count: {Surface_Count}')

res.Surfaces = []

for i in range(Surface_Count//3):

res.Surfaces.append(struct.unpack('<'+vertex_index_type*3,f.read(3*Index['Vertex Index Size'])))

Texture_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Texture_Count: {Texture_Count}')

res.Textures = []

for i in range(Texture_Count):

res.Textures.append(f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding']))

Material_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Material Count: {Material_Count}')

res.Materials = []

for i in range(Material_Count):

Material_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

Material_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

Diffuser_Color = struct.unpack('

Specular_Color=struct.unpack('

Speculat_Strength=struct.unpack('

Ambient_Color=struct.unpack('

Drawing_Flags=f.read(1)[0]

Drawing_Flags = {

'No-Cull': Drawing_Flags & 0b00000001 == 0b00000001,

'Ground Shadow':Drawing_Flags & 0b00000010 == 0b00000010,

'Draw shadow':Drawing_Flags & 0b00000100 == 0b00000100,

'Receive Shadow':Drawing_Flags & 0b00001000 == 0b00001000,

'Has Edge':Drawing_Flags & 0b00010000 == 0b00010000,

'Vertex Color':Drawing_Flags & 0b00100000 == 0b00100000,

'Point Drawing':Drawing_Flags & 0b01000000 == 0b01000000,

'Line Drawing': Drawing_Flags & 0b10000000 == 0b10000000

}

Edge_Color = struct.unpack('

Edge_Scale = struct.unpack('

Texture_Index = struct.unpack('<'+texture_index_type, f.read(Index['Texture Index Size']))[0]

Environment_Index = struct.unpack('<' + texture_index_type, f.read(Index['Texture Index Size']))[0]

Environment_Blend_Mode = f.read(1)[0]

Toon_Reference = f.read(1)[0]

if Toon_Reference == 0:#reference texture

Toon_Value = struct.unpack('<'+texture_index_type, f.read(Index['Texture Index Size']))[0]

elif Toon_Reference == 1:#reference internal

Toon_Value = f.read(1)[0]

else:

raise Exception(f'Toon Reference {Toon_Reference} not found')

Meta_Data = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode('UTF16')

Material_Surface_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

res.Materials.append({

'Material Name Local':Material_Name_Local,

'Material Name Universal': Material_Name_Universal,

'Diffuser Color': Diffuser_Color,

'Specular Color': Specular_Color,

'Speculat Strength': Speculat_Strength,

'Ambient Color': Ambient_Color,

'Drawing Flags': Drawing_Flags,

'Edge Color': Edge_Color,

'Texture Index': Texture_Index,

'Environment Index': Environment_Index,

'Environment Blend Mode': Environment_Blend_Mode,

'Toon Reference': Toon_Reference,

'Toon Value': Toon_Value,

'Meta Data': Meta_Data,

'Surface Count': Material_Surface_Count

})

Bone_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Bone Count: {Bone_Count}')

res.Bones = []

for i in range(Bone_Count):

Bone_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

Bone_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

Position = struct.unpack('

Parent_Bone_Index = struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0]

Layer = int.from_bytes(f.read(4), byteorder='little', signed=True)

Bone_Flags = f.read(2)

Bone_Flags = {

'Indexed Tail Position': Bone_Flags[0] & 0b00000001 == 0b00000001,

'Rotatable': Bone_Flags[0] & 0b00000010 == 0b00000010,

'Translatable': Bone_Flags[0] & 0b00000100 == 0b00000100,

'Is Visible': Bone_Flags[0] & 0b00001000 == 0b00001000,

'Enabled': Bone_Flags[0] & 0b00010000 == 0b00010000,

'IK': Bone_Flags[0] & 0b00100000 == 0b00100000,

'Inherit Rotation': Bone_Flags[1] & 0b00000001 == 0b00000001,

'Inherit Translation': Bone_Flags[1] & 0b00000010 == 0b00000010,

'Fixed Axis': Bone_Flags[1] & 0b00000100 == 0b00000100,

'Local Coordinate': Bone_Flags[1] & 0b00001000 == 0b00001000,

'Physics After Deform': Bone_Flags[1] & 0b00010000 == 0b00010000,

'External Parent Deform': Bone_Flags[1] & 0b00100000 == 0b00100000,

}

if Bone_Flags['Indexed Tail Position'] is True:

Tail_Position = struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0]

else:

Tail_Position = struct.unpack('

if Bone_Flags['Inherit Rotation'] or Bone_Flags['Inherit Translation']:

Inherit_Bone = {

'Parent Index': struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0],

'Parent Influence': struct.unpack('

}

else:

Inherit_Bone = None

if Bone_Flags['Fixed Axis']:

Fixed_Axis = struct.unpack('

else:

Fixed_Axis = None

if Bone_Flags['Local Coordinate']:

Local_Coordinate = [struct.unpack('

else:

Local_Coordinate = None

if Bone_Flags['External Parent Deform']:

External_Parent = struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0]

else:

External_Parent = None

if Bone_Flags['IK']:

Target = struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0]

Loop = int.from_bytes(f.read(4), byteorder='little', signed=False)

Limit_Radian = struct.unpack('

Link_Count = int.from_bytes(f.read(4), byteorder='little', signed=False)

IK_Links = []

for j in range(Link_Count):

Bone_Index = struct.unpack('<'+bone_index_type,f.read(Index['Bone Index Size']))[0]

Has_Limit = bool(f.read(1)[0])

if Has_Limit:

Limit_Min = struct.unpack('

Limit_Max = struct.unpack('

IK_Links.append({

'Bone Index': Bone_Index,

'Has Limit': Has_Limit,

'Limit Min': Limit_Min if Has_Limit else None,

'Limit Max': Limit_Max if Has_Limit else None,

})

IK = {

'Target': Target,

'Loop': Loop,

'Limit Radian': Limit_Radian,

'Link Count': Link_Count,

'IK Links': IK_Links

}

else:

IK = None

res.Bones.append({

'Bone Name Local': Bone_Name_Local,

'Bone Name Universal': Bone_Name_Universal,

'Position': Position,

'Parent Bone Index': Parent_Bone_Index,

'Layer': Layer,

'Bone Flags': Bone_Flags,

'Tail Position': Tail_Position,

'Inherit Bone': Inherit_Bone,

'Fixed Axis': Fixed_Axis,

'Local Coordinate': Local_Coordinate,

'IK': IK

})

Morph_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Morph Count: {Morph_Count}')

res.Morphs = []

for i in range(Morph_Count):

Morph_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(Index['Text Encoding'])

Morph_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Panel_Type = f.read(1)[0]

Morph_Type = f.read(1)[0]

Offset_Size = int.from_bytes(f.read(4), byteorder='little', signed=False)

Offset_Data = []

for j in range(Offset_Size):

#注意这里面的索引类型多不一样

if Morph_Type in [0, 9]:#Group or Flip

Offset_Data.append({

'Morph Index': struct.unpack('<' + morph_index_type, f.read(Index['Morph Index Size']))[0],

'Influence': struct.unpack('

})

elif Morph_Type == 1:#Vertex

Offset_Data.append({

'Vertex Index': struct.unpack('<' + vertex_index_type, f.read(Index['Vertex Index Size']))[0],

'Translation': struct.unpack('

})

elif Morph_Type == 2:#Bone

Offset_Data.append({

'Bone Index': struct.unpack('<' + bone_index_type, f.read(Index['Bone Index Size']))[0],

'Translation': struct.unpack('

'Rotation': struct.unpack('

})

elif Morph_Type in [3, 4, 5, 6, 7]:#UV

Offset_Data.append({

'Vertex Index': struct.unpack('<' + vertex_index_type, f.read(Index['Vertex Index Size']))[0],

'Data': struct.unpack('

})

elif Morph_Type == 8:#Material

Offset_Data.append({

'Material Index': struct.unpack('<' + material_index_type, f.read(Index['Material Index Size']))[0],

'Mix Method': f.read(1)[0],

'Diffuse': struct.unpack('

'Specular': struct.unpack('

'Specularity': struct.unpack('

'Ambient': struct.unpack('

'Edge Color': struct.unpack('

'Edge Size': struct.unpack('

'Texture Tint': struct.unpack('

'Environment Tint': struct.unpack('

'Toon Tint': struct.unpack('

})

elif Morph_Type == 10: #Impulse

Offset_Data.append({

'Rigid Body Index': struct.unpack('<' + rigid_body_index_type, f.read(Index['Rigid Body Index Size'])),

'Local Flag': struct.unpack('

'Movement Speed': struct.unpack('

'Rotation torque': struct.unpack('

})

else:

raise Exception('morph type is not exist')

res.Morphs.append({

'Morph Name Local': Morph_Name_Local,

'Morph Name Universal': Morph_Name_Universal,

'Panel Type': Panel_Type,

'Morph Type': Morph_Type,

'Offset Size': Offset_Size,

'Offset Data': Offset_Data

})

DisplayFrame_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'DisplayFrame Count: {DisplayFrame_Count}')

res.DisplayFrames = []

for i in range(DisplayFrame_Count):

DisplayFrame_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

DisplayFrame_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Special_Flag = f.read(1)[0]

Frame_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

Frames = []

for j in range(Frame_Count):

Frame_Type = f.read(1)[0]

if Frame_Type == 1:

Frame_Data = struct.unpack('<' + morph_index_type, f.read(Index['Morph Index Size']))[0]

elif Frame_Type == 0:

Frame_Data = struct.unpack('<' + bone_index_type, f.read(Index['Bone Index Size']))[0]

else:

raise Exception('frame type is not exist')

Frames.append({

'Frame Type': Frame_Type,

'Frame Data': Frame_Data

})

res.DisplayFrames.append({

'DisplayFrame Name Local': DisplayFrame_Name_Local,

'DisplayFrame Name Universal': DisplayFrame_Name_Universal,

'Special Flag': Special_Flag,

'Frame Count': Frame_Count,

'Frames': Frames

})

Rigid_Body_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Rigid Body Count: {Rigid_Body_Count}')

res.Rigid_Bodies = []

for i in range(Rigid_Body_Count):

Rigid_Body_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Rigid_Body_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Related_Bone_Index = struct.unpack('<' + bone_index_type, f.read(Index['Bone Index Size']))[0]

Group_Id = f.read(1)[0]

Non_Collision_Group = struct.unpack('

Non_Collision_Group = [((1<

Shape = f.read(1)[0]

Shape_Size = struct.unpack('

Shape_Position = struct.unpack('

Shape_Rotation = struct.unpack('

Mass = struct.unpack('

Move_Attenuation = struct.unpack('

Rotation_Damping = struct.unpack('

Repulsion = struct.unpack('

Friction_Force = struct.unpack('

Physics_Mode = f.read(1)[0]

res.Rigid_Bodies.append({

'Rigid Body Name Local': Rigid_Body_Name_Local,

'Rigid Body Name Universal': Rigid_Body_Name_Universal,

'Related Bone Index': Related_Bone_Index,

'Group Id': Group_Id,

'Non Collision Group': Non_Collision_Group,

'Shape': Shape,

'Shape Size': Shape_Size,

'Shape Position': Shape_Position,

'Shape Rotation': Shape_Rotation,

'Mass': Mass,

'Move Attenuation': Move_Attenuation,

'Rotation Damping': Rotation_Damping,

'Repulsion': Repulsion,

'Friction Force': Friction_Force,

'Physics Mode': Physics_Mode

})

Joint_Count = int.from_bytes(f.read(4), byteorder='little', signed=True)

print(f'Joint_Count: {Joint_Count}')

res.Joints = []

for i in range(Joint_Count):

Joint_Name_Local = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Joint_Name_Universal = f.read(int.from_bytes(f.read(4), byteorder='little', signed=False)).decode(

Index['Text Encoding'])

Joint_Type = f.read(1)[0]

Rigid_body_index_A = struct.unpack('<' + rigid_body_index_type, f.read(Index['Rigid Body Index Size']))[0]

Rigid_body_index_B = struct.unpack('<' + rigid_body_index_type, f.read(Index['Rigid Body Index Size']))[0]

Position = struct.unpack('

Rotation = struct.unpack('

Position_Limit = (struct.unpack('

Rotation_Limit = (struct.unpack('

Position_Spring = struct.unpack('

Rotation_Spring = struct.unpack('

res.Joints.append({

'Joint Name Local': Joint_Name_Local,

'Joint Name Universal': Joint_Name_Universal,

'Joint Type': Joint_Type,

'Rigid body index A': Rigid_body_index_A,

'Rigid body index B': Rigid_body_index_B,

'Position': Position,

'Rotation': Rotation,

'Position Limit': Position_Limit,

'Rotation Limit': Rotation_Limit,

'Position Spring': Position_Spring,

'Rotation Spring': Rotation_Spring,

})

f.close()

return res

初始化:

if __name__ == '__main__':

pmx = PMX.from_file('model/model_test.pmx')

PMX对象的成员参考staticmethod上面的注释。