#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)