#author("2026-10-05T18:04:04+09:00","","")
* PMX格式详解 [#qcc9542a]
&size(13){''PMX 2.0 / 2.1 的全部 chunk 结构与二进制规格''};

#contents

** 什么是 PMX [#t3fa80fc]

''PMX'' 是取代 PMD 的''现行标准模型格式''。
于 2011 年前后登场,如今已成为 MMD 模型事实上的标准。

|~项目|~内容|
|扩展名|.pmx|
|字节序|小端序|
|字符串编码|UTF-16LE 或 UTF-8(由 header 指定)|
|版本|2.0 / 2.1|
|设计|由 PMXEditor 的作者完成|
|特征|全部 chunk 均支持''可变长索引'',可应对大规模模型|

** 整体结构 [#t9d022a5]

PMX 由''chunk 的链式结构''构成。

#code(){{
┌─────────────────────────┐
│ Header                  │  ← 魔数・版本・设置
├─────────────────────────┤
│ Model Info              │  ← 模型名・注释
├─────────────────────────┤
│ Vertex Data             │  ← 顶点
├─────────────────────────┤
│ Face Data               │  ← 面(顶点索引)
├─────────────────────────┤
│ Texture Data            │  ← 贴图的路径
├─────────────────────────┤
│ Material Data           │  ← 材质
├─────────────────────────┤
│ Bone Data               │  ← 骨骼
├─────────────────────────┤
│ Morph Data              │  ← 变形
├─────────────────────────┤
│ Display Frame Data      │  ← 显示框
├─────────────────────────┤
│ Rigid Body Data         │  ← 刚体
├─────────────────────────┤
│ Joint Data              │  ← 关节
├─────────────────────────┤
│ Soft Body Data          │  ← 软体(仅 2.1)
└─────────────────────────┘
}}

> '''注意''':PMX 的结构是''每个 chunk 的开头都带一个表示个数的 int32''。
> 也就是说,按「顶点数 → 顶点数据…、面索引数 → 面数据…」的顺序读取。
> 中途哪怕偏移 1 字节,''其后全都会损坏''。

** Header [#h948c2a1]

|~字段|~类型|~内容|
|magic|4 bytes|`PMX `(0x50 0x4D 0x58 0x20)|
|version|float|2.0 或 2.1|
|globalsCount|byte|通常为 8|
|encoding|byte|0 = UTF-16LE、1 = UTF-8|
|additionalUVCount|byte|0〜4|
|vertexIndexSize|byte|1 / 2 / 4|
|textureIndexSize|byte|1 / 2 / 4|
|materialIndexSize|byte|1 / 2 / 4|
|boneIndexSize|byte|1 / 2 / 4|
|morphIndexSize|byte|1 / 2 / 4|
|rigidBodyIndexSize|byte|1 / 2 / 4|

*** 索引大小的含义 [#o13e15d7]

''索引大小''指定了该种类的索引用''多少字节表示''。

|~大小|~可表示范围|
|1|0〜255|
|2|0〜65535|
|4|0〜4294967295|

> '''设计意图''':PMD 采用''固定 2 字节''。
> 因此无法制作顶点数 65536 以上的模型。
> PMX 通过改为''可变长''撤除了这一限制。
> 实现时必须''读取 header 中的值,并据此分别读取''。

*** 有无符号 [#sc174ea2]

- ''顶点索引'' — ''无符号''
- ''贴图・材质・骨骼・变形・刚体索引'' — ''有符号''(因为要用 -1 表示「无」)

** Model Info [#c0b3fd85]

|~字段|~内容|
|modelNameLocal|本地名(日语等)|
|modelNameUniversal|通用名(英文字母)|
|commentLocal|本地注释|
|commentUniversal|通用注释|

*** TextBuf 的结构 [#j994f8aa]

字符串以''可变长''方式存储。

#code(){{
int32  length   ← 字节数(不是字符数)
byte[] text     ← 实际字节序列(遵循 header 的 encoding)
}}

> '''实现的陷阱''':`length` 是''字节数''。
> 若是 UTF-16LE 则''1 字符 = 2 字节'',UTF-8 则''1 字符 = 1〜4 字节''。
> ''与字符数混淆会立即损坏''。

** Vertex Data [#hd94dc34]

|~字段|~类型|~内容|
|vertexCount|int32|顶点数|
|(以下重复 vertexCount 次)|—|—|
|position|float3|坐标|
|normal|float3|法线|
|uv|float2|UV|
|additionalUV|float4 × N|附加 UV(N = header 中的值)|
|weightType|byte|权重种类|
|(权重数据)|可变|参见下文|
|edgeScale|float|轮廓边倍率|

*** 权重种类与数据 [#ff0c7522]

|~weightType|~名称|~数据|
|0|BDEF1|boneIndex × 1|
|1|BDEF2|boneIndex × 2 + weight × 1(float)|
|2|BDEF4|boneIndex × 4 + weight × 4(float)|
|3|SDEF|boneIndex × 2 + weight × 1 + C(float3) + R0(float3) + R1(float3)|
|4|QDEF|boneIndex × 4 + weight × 4(float)|

> '''注意''':SDEF 是在 BDEF2 的数据上''追加 3 个 float3''的结构。
> QDEF 与 BDEF4 是''同一结构''(只是解释方式不同)。

** Face Data [#e78ed160]

|~字段|~类型|~内容|
|faceIndexCount|int32|''索引的个数''(面数 × 3)|
|(以下重复 faceIndexCount 次)|vertexIndexSize|顶点索引(无符号)|

> '''重要''':`faceIndexCount` ''并不是面数''。
> 1 个三角形有 3 个顶点,因此''面数 = faceIndexCount / 3''。
> 此处混淆会导致读取损坏。

** Texture Data [#o312d23b]

|~字段|~类型|~内容|
|textureCount|int32|贴图数|
|(以下重复 textureCount 次)|TextBuf|''相对路径''|

> '''可移植性要点''':路径是''相对于模型文件的相对路径''。
> 写入绝对路径的模型''在其他 PC 上必定损坏''。
> 发布前请确认''为相对路径''。

** Material Data [#u1d40781]

|~字段|~类型|~内容|
|nameLocal|TextBuf|材质名(本地)|
|nameUniversal|TextBuf|材质名(通用)|
|diffuse|float4|漫反射色 RGBA|
|specular|float3|高光色|
|shininess|float|高光强度|
|ambient|float3|环境色|
|drawFlag|byte|绘制标志|
|edgeColor|float4|轮廓边颜色|
|edgeSize|float|轮廓边大小|
|textureIndex|textureIndexSize|贴图(-1 = 无)|
|sphereTextureIndex|textureIndexSize|球面贴图(-1 = 无)|
|sphereMode|byte|0:无效 1:乘算 2:加算 3:减算|
|sharedToonFlag|byte|0:独立 toon 1:共享 toon|
|(toon 索引)|—|随 flag 变化|
|memo|TextBuf|备注|
|faceVertexCount|int32|''该材质使用的面索引数''|

*** drawFlag 的位 [#o9d5d0d2]

|~位|~含义|
|0x01|双面绘制|
|0x02|地面阴影|
|0x04|自阴影贴图|
|0x08|自阴影|

*** toon 索引的分支 [#i93e99b7]

#code(){{
if (sharedToonFlag == 1)
    sharedToonIndex : byte          // 共享 toon 的编号(0〜9)
else
    toonTextureIndex : textureIndexSize   // 引用贴图数组
}}

> '''注意''':`faceVertexCount` ''不是顶点数,而是面索引数''。
> 实际面数为 ''faceVertexCount / 3''。

** Bone Data [#l3221fa6]

|~字段|~类型|~内容|
|nameLocal|TextBuf|骨骼名|
|nameUniversal|TextBuf|骨骼名(通用)|
|position|float3|坐标|
|parentBoneIndex|boneIndexSize|父级(-1 = 无)|
|layer|int32|变形层级|
|boneFlag|uint16|标志|
|(末端位置)|—|随 flag 变化|
|(赋予)|—|随 flag 变化|
|(轴)|—|随 flag 变化|
|(局部轴)|—|随 flag 变化|
|(外部父级)|—|随 flag 变化|
|(IK 数据)|—|随 flag 变化|

*** boneFlag 的位 [#xcffdbf4]

|~位|~含义|
|0x0001|连接目标为''骨骼''(为 0 则为''相对位置'')|
|0x0002|可旋转|
|0x0004|可移动|
|0x0008|显示|
|0x0010|可操作|
|0x0020|IK|
|0x0080|局部赋予|
|0x0100|旋转赋予|
|0x0200|移动赋予|
|0x0400|轴固定|
|0x0800|局部轴|
|0x1000|物理后变形|
|0x2000|外部父级变形|

*** 条件字段 [#xd50b6fc]

#code(){{
// 末端位置
if (flag & 0x0001)  tailBoneIndex : boneIndexSize
else                tailPosition  : float3

// 赋予
if (flag & 0x0100) or (flag & 0x0200)
    grantParentBoneIndex : boneIndexSize
    grantWeight          : float

// 轴固定
if (flag & 0x0400)  axisDirection : float3

// 局部轴
if (flag & 0x0800)  localX : float3, localZ : float3

// 外部父级变形
if (flag & 0x2000)  externalParentKey : int32
}}

*** IK 数据 [#d5d46772]

#code(){{
if (flag & 0x0020) {
    ikTargetBoneIndex : boneIndexSize
    ikLoopCount       : int32
    ikLimitAngle      : float        // 单位角(弧度)
    ikLinkCount       : int32
    for (i = 0; i < ikLinkCount; i++) {
        ikLinkBoneIndex : boneIndexSize
        hasLimit        : byte       // 0:无限制 1:有限制
        if (hasLimit) {
            limitMin : float3        // 弧度
            limitMax : float3        // 弧度
        }
    }
}
}}

> '''注意''':角度为''弧度(radian)''。
> UI 上虽以度(degree)显示,
> 但二进制中存放的是''弧度''。

** Morph Data [#j54ef09d]

|~字段|~类型|~内容|
|nameLocal|TextBuf|变形名|
|nameUniversal|TextBuf|变形名(通用)|
|panel|byte|0:系统 1:眉 2:眼 3:口 4:其他|
|morphType|byte|变形种类|
|offsetCount|int32|偏移数|
|(偏移)|可变|各类型结构不同|

*** morphType 一览 [#qa825d96]

|~值|~名称|~偏移|
|0|组|morphIndex + weight(float)|
|1|顶点|vertexIndex + offset(float3)|
|2|骨骼|boneIndex + translation(float3) + rotation(float4)|
|3|UV|vertexIndex + offset(float4)|
|4〜7|附加 UV1〜4|与 UV 同结构|
|8|材质|参见下文|
|9|翻转(2.1)|morphIndex + weight|
|10|冲量(2.1)|rigidBodyIndex + localFlag + velocity + torque|

*** 材质变形的偏移 [#s165c34b]

#code(){{
materialIndex : materialIndexSize
calcMode      : byte          // 0:乘算 1:加算
diffuse       : float4
specular      : float3
shininess     : float
ambient       : float3
edgeColor     : float4
edgeSize      : float
textureCoef   : float4
sphereCoef    : float4
toonCoef      : float4
}}

*** 骨骼变形的旋转 [#k89878ae]

以''四元数(float4)''存储。
并非欧拉角。

** Display Frame Data [#c0b328a3]

|~字段|~类型|~内容|
|nameLocal|TextBuf|框名|
|nameUniversal|TextBuf|框名(通用)|
|specialFlag|byte|0:通常框 1:特殊框(左眼・右眼)|
|elementCount|int32|元素数|
|(元素)|可变|boneIndex 或 morphIndex|

#code(){{
for (i = 0; i < elementCount; i++) {
    type  : byte   // 0:骨骼 1:变形
    index : boneIndexSize 或 morphIndexSize
}
}}

** Rigid Body Data [#ba4642ed]

|~字段|~类型|~内容|
|nameLocal|TextBuf|刚体名|
|nameUniversal|TextBuf|刚体名(通用)|
|relatedBoneIndex|boneIndexSize|关联骨骼|
|group|byte|碰撞组(0〜15)|
|noCollisionGroupFlag|uint16|非碰撞组(16 位)|
|shape|byte|0:球 1:盒 2:胶囊|
|size|float3|大小|
|position|float3|位置|
|rotation|float3|旋转(弧度)|
|mass|float|质量|
|moveAttenuation|float|移动衰减|
|rotationDamping|float|旋转衰减|
|repulsion|float|反弹力|
|friction|float|摩擦力|
|physicsMode|byte|0:骨骼跟随 1:物理运算 2:物理+骨骼对齐|

** Joint Data [#efbbeb25]

|~字段|~类型|~内容|
|nameLocal|TextBuf|关节名|
|nameUniversal|TextBuf|关节名(通用)|
|type|byte|0:弹簧 6DOF|
|rigidBodyIndexA|rigidBodyIndexSize|刚体 A|
|rigidBodyIndexB|rigidBodyIndexSize|刚体 B|
|position|float3|位置|
|rotation|float3|旋转(弧度)|
|moveLimitLower|float3|移动限制 下限|
|moveLimitUpper|float3|移动限制 上限|
|rotationLimitLower|float3|旋转限制 下限(弧度)|
|rotationLimitUpper|float3|旋转限制 上限(弧度)|
|springMove|float3|弹簧移动|
|springRotation|float3|弹簧旋转|

** Soft Body Data(仅 PMX 2.1) [#o2b22961]

PMX 2.1 中新增了''软体(Soft Body)''。
其结构复杂,特点是带有''锚点骨骼''与''顶点对应关系''。

- 可将头发和布料作为''弹簧网格''进行模拟
- 支持的工具有限,''实际应用的例子很少''
- 多数情况可用刚体+关节替代

** 与 PMD 的主要区别 [#c8d389c1]

|~项目|~PMD|~PMX|
|字符编码|Shift-JIS 固定|UTF-16LE / UTF-8|
|索引宽度|固定 2 字节|可变(1/2/4)|
|附加 UV|无|最多 4|
|权重|BDEF1/2|BDEF1/2/4、SDEF、QDEF|
|材质双面绘制|有(受限)|通过 drawFlag 详细控制|
|显示框|独有结构|灵活结构|
|软体|无|2.1 中支持|

详见 [[PMD格式详解]]。

** 实现注意事项汇总 [#i1570a5b]

#region(编写 PMX 解析器时的检查清单)
+ '''字节序为小端序''' — 部分环境需要转换
+ '''TextBuf 的长度是字节数''' — 并非字符数
+ '''索引大小从 header 读取''' — 不要假定为固定值
+ '''顶点索引无符号,其他有符号'''
+ '''faceIndexCount 是面索引数''' — 并非面数
+ '''angle 是弧度''' — 并非度
+ '''根据 boneFlag 以''可变长''读取''' — 需实现条件分支
+ '''toon 的分支通过 sharedToonFlag 判定'''
+ '''骨骼变形的旋转是四元数'''
+ '''材质变形的偏移有 22 个字段''' — 不要弄错顺序
#endregion

** 验证方法 [#uc3b48dc]

编写完 PMX 解析器后,按以下顺序测试。

+ 读取''已知的简单模型'',将顶点数・面数与 PMXEditor 的显示进行对照
+ 确认''能否完整读到全部 chunk 末尾''(剩余字节数是否为 0)
+ 分别准备''索引大小为 1/2/4 的模型''并读取
+ 读取''包含 BDEF2 / SDEF / BDEF4 的模型''
+ 读取''包含材质变形・骨骼变形的模型''
+ 将读入的数据''写回'',比较是否与原始数据一致

> '''最可靠的验证''':''读入 → 写出 → 字节比较''。
> 若完全一致,即可保证解析器与写出器均正确。

** 相关页面 [#sb5e99da]

- [[PMD格式详解]] — 与旧格式的对比
- [[VMD动作数据格式]] — 动作侧的规格
- [[数据格式速查表]] — 一览表
- [[格式转换工具链]] — 转换时的信息缺失
- [[顶点权重详解]] — 权重的含义

#comment(num=5,noname,below)


トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS