跳转至

本文由 简悦 SimpRead 转码, 原文地址 www.jianshu.com

一、前言

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 类型标准类型大小(字节)范围
bytecharuint8_t1
ubyteunsigned charuint8_t1
shortshortint16_t2
ushortunsigned shortuint16_t2
intintint32_t4
uintunsigned intuint32_t4
floatfloatfloat4IEEE754 标准定义 float 范围

(2)PMX 类型

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

类型名称类型说明类型结构大小(字节)
vec2XY 向量float, float8
vec3XYZ 向量float, float, float12
vec4XYZW 向量float, float, float, float16
text前四个字节说明字符串长度,编码定义
在后面的 Globas 全局定义中
int, byte[]4 + 字符长度
flag标志类型,每个 Byte 可含有 8 个标志
0 为 false,1 为 true
byte1
index索引类型,每个要素的索引不同,
后面详细说明
byte/ubyte/short/ushort/int1/2/4

(3)索引类型

有时候我们需要记住元素的索引,例如某个面是由哪几个点组成的,而表示一个点,就需要索引。
  索引的大小不是确定的,有些元素需要很多索引,因此索引类型也很大;而有些元素很少,索引节点的大小很小。
  同样的元素,例如顶点,有时一个模型只有几个顶点,有的有几万个顶点,因此同样是节点的索引,大小也可能不一样。PMX 将索引类型分为 3 种:Type1、Type2、Type4,分别占有 1、2、4 个字节,模型的索引的大小储存在 Globas 全局定义中,后面会提到。

类型名称说明类型 1类型 2类型 4类型 1 最大值类型 2 最大值类型 4 最大值空值
Vertex顶点ubyteushortint255655352147483647N/A
Bone骨骼byteshortint127327672147483647-1
Texture纹理byteshortint127327672147483647-1
Material材质byteshortint127327672147483647-1
Morph变形(表情)byteshortint127327672147483647-1
Rigidbody刚体byteshortint127327672147483647-1

2. 文件总体结构概览

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

Header版本说明
Model informationGlobals2.0模型全局信息
Vertex countVertices2.0顶点
Surface countSurfaces2.0
Texture countTextures2.0纹理
Material countMaterials2.0材质
Bone countBones2.0骨骼
Morph countMorphs2.0变形(表情)
Displayframe countDisplayframes2.0表示枠(huà)
Rigidbody countRigidbodies2.0刚体
Joint countJoints2.0关节点
SoftBody countSoftBodies2.0软体

软体应该是作为 Pmx 独有的,很明显的,PmdEditor 没有软体按钮。
  下面是真正开始解析文件每一部分格式。

3. 头部、全局信息

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

数据含义结构说明
签名byte[4]"PMX" [0x50, 0x4D, 0x58, 0x20]值为 "PMX 空格"
版本号float2.0、2.1
全局信息数量byte8(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),后面跟着每个顶点的定义
每个顶点的格式如下:

含义类型说明
位置vec3XYZ
法向量vec3XYZ
UV 坐标vec2XY
额外的 vec4vec4[N]N 的大小见上面的全局定义
变型权重类型byte0=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):

位置含义效果版本
0no-cull 禁用背面剔除双面描绘2.0
1Ground shadow 地面阴影将阴影投射到几何体上2.0
2Draw shadow 描绘阴影渲染到阴影贴图(本影标示)2.0
3Receive shadow 受到阴影渲染从阴影贴图接受阴影(本影)2.0
4Has edge 有边缘有边缘描绘(轮廓线有效)2.0
5Vertex colour 顶点颜色使用额外的 vec4 作为顶点的颜色2.1
6Point drawing 画点三个顶点都被画出2.1
7Line drawing 画线三个边被画出2.1
点线绘图(PMX2.1)

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

  • 普通面通过顶点 [A -> B -> C] 渲染
  • 通过点绘制,每个顶点被渲染为单独的点 [A,B,C],从而产生 3 个点。
  • 线条图将呈现 3 条线 [A-> B,B-> C,C-> A],产生 3 条线。

启用点渲染时,材质(不是顶点)的边尺度值将控制点的大小。
  如果设置了边缘标志,则点或线将具有边缘,这是未定义的行为。

材质数据

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

含义类型说明
本地材质名称text本地的材质名称,日语、中文等等
通用材质名称text通用的材质名称,一般是英文
漫反射颜色 Diffusevec4RGBA
镜面光(高光)颜色 Specularvec3RGB
镜面光强度 Specular strengthfloat镜面高光的大小
环境色 ambientvec3当光线不足时的阴影色(既基础色,让阴影不那么黑)
绘制标记flag见上面的材质标志
边缘颜色vec4RGBA
边缘比例float[0, 1]
索引 - 纹理index-texture参考索引类型
环境(高光贴图)索引index-texture与纹理索引相同,但用于环境映射
环境(高光贴图)混合模式byte0 = 禁用, 1 = 乘, 2 = 加, 3 = 额外的 vec4[注 1]
贴图引用byte0 = 引用纹理, 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
5IK是 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 骨

名称类型作用
目标骨骼索引索引 - 骨骼参阅索引类型
循环计数intIK 解算 CCD(循环坐标下降法)循环次数
限制角度floatIK 骨旋转角度限制
IK 链计数intIK 链接的骨骼数量
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。
  对于使用者,常认为变型有三种或五种等等,从文件存储来说似乎不止这些:

变型模式说明版本
组合0Group2.0
顶点12.0
骨骼22.0
UV32.0
额外 UV142.0
额外 UV252.0
额外 UV362.0
额外 UV472.0
材质82.0
切换9Flip2.1
脉冲102.1

偏移与偏移值

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

组合(Group)
名称类型说明
变形索引索引 - 变形参阅索引类型
影响float对被索引变形的权重

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

顶点(Vertex)
名称类型说明
顶点索引索引 - 顶点参阅索引类型
移动vec3变化的相对位置

骨骼(Bone)
名称类型说明
骨骼索引索引 - 骨骼参阅索引类型
移动vec3变化的相对位置
移动vec4相对旋转四元数

UV(及拓展 UV)
名称类型说明
顶点索引索引 - 顶点参阅索引类型
~vec4做什么取决于 UV 拓展

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

材质(Material)
名称类型说明
材质索引索引 - 材质参阅索引类型,-1 代表所有材质
混合方法byte0 是乘法,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
特殊标识byte0 表示普通帧,1 表示特殊帧
帧数量int记录有多少个帧
帧数据~[N]N 是帧数据个数,类型参照帧数据说明

11. 刚体

刚体形状类型

类型说明版本
02.0
1盒子2.0
2胶囊2.0

物理模式

名称类型说明版本
0追踪骨骼刚体黏在骨骼上2.0
1物理演算刚体使用重力2.0
2物理 + 骨骼刚体使用重力摆动骨骼2.0

刚体数据

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

名称类型说明
刚体本地名称text
刚体通用名称text
关联骨骼索引索引 - 骨骼参阅索引类型
群组 IDbyte
非碰撞组short非碰撞组的掩码
形状byte参阅刚体形状类型
形状大小vec3XYZ 边界
形状位置vec3XYZ 位置
形状旋转vec3弧度制
质量float
移动衰减float
旋转衰减float
反应力float
摩擦力float
物理模式byte参阅刚体物理模式

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

12. 关节点

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

关节点类型

类型说明版本
0Spring 6DOF2.0
16DOF2.1
2P2P点结合2.1
3ConeTwist轴旋转2.1
4Slider轴移动2.1
5Hinge轴旋转2.1

关节点数据

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

名称类型说明
关节点本地名称text
关节点通用名称text
关节点类型byte参阅关节点类型
刚体索引 A索引 - 刚体参阅索引类型
刚体索引 B索引 - 刚体
位置vec3
旋转vec3弧度制
位置最小值vec3
位置最大值vec3
旋转最小值vec3
旋转最大值vec3
定位弹簧vec3弹力
旋转弹簧vec3

13. 软体

软体基于 Bullet Physics,随 PMX 2.1 一起推出。
  我暂时还不会物理方面的知识,暂时机翻,这部分等学一学 Bullet 后再来补全。

形状类型

类型说明版本
0TriMesh三角网格2.1
1Rope2.1

标识

类型说明版本
0B-Link2.1
1Cluster creation2.1
2Link crossing2.1

空气动力学模型

类型说明版本
0V-Point2.1
1V-TwoSided2.1
2V-OneSided2.1
3F-TwoSided2.1
4F-OneSided2.1

锚固刚体

名称类型说明
刚体索引索引 - 刚体参照索引类型
顶点索引索引 - 顶点
Near modebyte

顶点针脚

名称类型说明
顶点索引索引 - 顶点参照索引类型

软体数据

名称类型说明
软体本地名称text
软体通用名称text
形状byte参阅软体形状类型
材质索引索引 - 材质参阅索引类型
byte组 ID
碰撞体掩码short
标识flag见软体标识
B-link create distanceint
Number of clustersint
总质量float
Collision marginfloat
空气动力学模型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_CLfloat软硬度与刚硬度
Cluster SKHR_CLfloat柔软与动力学硬度
Cluster SSHR_CLfloat柔软与柔软的硬度
Cluster SR_SPLT_CLfloat软与刚冲动分裂
Cluster SK_SPLT_CLfloat软与动能冲动分裂
Cluster SS_SPLT_CLfloat软与软冲动分裂
Interation V_ITint速度求解器迭代
Interation P_ITint定位求解器迭代
Interation D_ITint漂移求解器迭代
Interation C_ITint群集解算器迭代
Material LSTint线性刚度系数
Material ASTint面积 / 角度刚度系数
Material VSTint体积刚度系数
Anchor rigid body countint锚固刚体个数
Anchor rigid bodies~[N]N 是锚固刚体计数。参见锚固刚体说明
Vertex pin countint顶点引脚个数
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('<f', f.read(4))[0]

        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("<fff", f.read(12))

            Normal = struct.unpack("<fff", f.read(12))

            UV_Texture_Coordinate = struct.unpack("<ff", f.read(8))

            Appendix_UV = []
            for j in range(Index['Appendix UV']):
                Appendix_UV.append(struct.unpack("<ffff", f.read(16)))

            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('<f', f.read(4))[0]

            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('<ffff', f.read(16))
            Specular_Color=struct.unpack('<fff', f.read(12))
            Speculat_Strength=struct.unpack('<f', f.read(4))[0]
            Ambient_Color=struct.unpack('<fff',f.read(12))
            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('<ffff', f.read(16))
            Edge_Scale = struct.unpack('<f', f.read(4))[0]
            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('<fff', f.read(12))
            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('<fff', f.read(12))

            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('<f', f.read(4))[0]
                }
            else:
                Inherit_Bone = None

            if Bone_Flags['Fixed Axis']:
                Fixed_Axis = struct.unpack('<fff', f.read(12))
            else:
                Fixed_Axis = None

            if Bone_Flags['Local Coordinate']:
                Local_Coordinate = [struct.unpack('<fff', f.read(12)), struct.unpack('<fff', f.read(12))]
            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('<f', f.read(4))[0]
                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('<fff', f.read(12))
                        Limit_Max = struct.unpack('<fff', f.read(12))
                    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('<f', f.read(4))[0]
                     })
                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('<fff', f.read(12))
                    })
                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('<fff', f.read(12)),
                        'Rotation': struct.unpack('<ffff', f.read(16)),
                    })
                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('<ffff', f.read(16))
                    })
                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('<ffff', f.read(16)),
                        'Specular': struct.unpack('<fff', f.read(12)),
                        'Specularity': struct.unpack('<f', f.read(4))[0],
                        'Ambient': struct.unpack('<fff', f.read(12)),
                        'Edge Color': struct.unpack('<ffff', f.read(16)),
                        'Edge Size': struct.unpack('<f', f.read(4))[0],
                        'Texture Tint': struct.unpack('<ffff', f.read(16)),
                        'Environment Tint': struct.unpack('<ffff', f.read(16)),
                        'Toon Tint': struct.unpack('<ffff', f.read(16))
                    })
                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('<f', f.read(4))[0],
                        'Movement Speed': struct.unpack('<fff', f.read(12)),
                        'Rotation torque': struct.unpack('<fff', f.read(12)),
                    })
                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('<h', f.read(2))[0]
            Non_Collision_Group = [((1<<j) & Non_Collision_Group) != (1<<j) for j in range(14)]
            Shape = f.read(1)[0]
            Shape_Size = struct.unpack('<fff', f.read(12))
            Shape_Position = struct.unpack('<fff', f.read(12))
            Shape_Rotation = struct.unpack('<fff', f.read(12))
            Mass = struct.unpack('<f', f.read(4))[0]
            Move_Attenuation = struct.unpack('<f', f.read(4))[0]
            Rotation_Damping = struct.unpack('<f', f.read(4))[0]
            Repulsion = struct.unpack('<f', f.read(4))[0]
            Friction_Force = struct.unpack('<f', f.read(4))[0]
            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('<fff', f.read(12))
            Rotation = struct.unpack('<fff', f.read(12))
            Position_Limit = (struct.unpack('<fff', f.read(12)), struct.unpack('<fff', f.read(12)))
            Rotation_Limit = (struct.unpack('<fff', f.read(12)), struct.unpack('<fff', f.read(12)))
            Position_Spring = struct.unpack('<fff', f.read(12))
            Rotation_Spring = struct.unpack('<fff', f.read(12))
            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 上面的注释。

评论