PMXファイルの構造(バイト単位の仕様)
PMXは「ヘッダで型の幅を宣言してから、可変長のブロックを順に並べる」という形式です。長さフィールドを持たないブロックが多いため、1つでも読み違えるとそこから先が全部ずれます。実物のモデルを自前で歩いて書き換えるパーサを作ったときに、実際に引っかかった点まで含めてまとめます。
前提
- 数値はリトルエンディアン。 整数は
uint8/uint16/int32、実数はfloat32。 - 文字列は「バイト長(uint32) + 本体」。VMDと違って固定長ではありません。文字コードはヘッダの宣言(UTF-16LE か UTF-8)に従います。
- インデックスの幅はモデルごとに違います。 1・2・4バイトのいずれかで、ヘッダに書かれています。ここを決め打ちすると、大きいモデルだけ壊れます。
- ブロックの区切りはバイト数ではなく件数だけで示されます。読み飛ばすにも中身を正しく歩く必要があります。
ヘッダ
| オフセット | 長さ | 型 | 内容 |
|---|---|---|---|
| 0 | 4 | ASCII | PMX (末尾は空白) |
| 4 | 4 | float32 | バージョン。2.0 または 2.1 |
| 8 | 1 | uint8 | この後に続く設定バイトの数。実測した5本はすべて 8 |
| 9 | 1 | uint8 | 文字コード。0 = UTF-16LE、1 = UTF-8 |
| 10 | 1 | uint8 | 追加UVの数(0〜4) |
| 11 | 1 | uint8 | 頂点インデックスの幅(1/2/4) |
| 12 | 1 | uint8 | テクスチャインデックスの幅 |
| 13 | 1 | uint8 | 材質インデックスの幅 |
| 14 | 1 | uint8 | ボーンインデックスの幅 |
| 15 | 1 | uint8 | モーフインデックスの幅 |
| 16 | 1 | uint8 | 剛体インデックスの幅 |
設定バイトの数(オフセット8)は8とは限らない前提で読むこと。 将来の拡張に備えて、8個読んだあと「宣言された長さ − 8」だけ読み飛ばすようにしておけば、増えても壊れません。
実測した5本の宣言はこうでした。頂点数より右の数字はインデックスの幅(バイト数)です。同じサイトに置いてあるモデル同士でも違います。
| モデル | 頂点数 | 頂点 | テクスチャ | 材質 | ボーン | モーフ | 剛体 |
|---|---|---|---|---|---|---|---|
alicia.pmx | 22,311 | 2 | 1 | 1 | 2 | 1 | 1 |
ad_org8.pmx | 40,688 | 2 | 1 | 1 | 2 | 2 | 2 |
ad_ik2.pmx | 39,540 | 2 | 1 | 1 | 2 | 2 | 2 |
blue_stage_simple.pmx | 4,669 | 2 | 1 | 1 | 1 | 1 | 1 |
5本とも文字コードは 0(UTF-16LE)、追加UVは 0 でした。ヘッダの直後には「モデル名・英語名・コメント・英語コメント」の4つの文字列がこの順で並びます。
インデックスは符号の扱いが幅によって違います。 ボーン・材質などの参照は「参照なし」を −1 で表すため符号付きで読みます。一方、頂点インデックス(面とモーフが使う)は符号なしです。1バイトなら uint8/int8、2バイトなら uint16/int16、4バイトは常に int32 です。
頂点
「頂点数(uint32)+ 本体」。1件のレイアウトは次のとおりですが、ウェイトの種別によって長さが変わります。
| 長さ | 型 | 内容 |
|---|---|---|
| 12 | float32 × 3 | 位置 |
| 12 | float32 × 3 | 法線 |
| 8 | float32 × 2 | UV |
| 16 × 追加UV数 | float32 × 4 × n | 追加UV(ヘッダで0なら無し) |
| 1 | uint8 | ウェイト種別(0〜4) |
| 可変 | — | ボーンとウェイト(下表) |
| 4 | float32 | エッジ倍率 |
| 種別 | 名前 | 中身 | 長さ(ボーン幅 = B) |
|---|---|---|---|
| 0 | BDEF1 | ボーン1本 | B |
| 1 | BDEF2 | ボーン2本 + ウェイト1つ(残りは 1 − w) | 2B + 4 |
| 2 | BDEF4 | ボーン4本 + ウェイト4つ | 4B + 16 |
| 3 | SDEF | BDEF2と同じ並び + C・R0・R1(float32 × 9) | 2B + 4 + 36 |
| 4 | QDEF | BDEF4と同じ並び(PMX 2.1) | 4B + 16 |
実測した5本の内訳です。SDEFは実際に使われています(1本のモデルで3,132頂点)。QDEFは5本とも0件でした。
| モデル | BDEF1 | BDEF2 | BDEF4 | SDEF |
|---|---|---|---|---|
alicia.pmx | 8,858 | 10,121 | 3,332 | 0 |
ad_org8.pmx | 19,200 | 16,813 | 1,543 | 3,132 |
ad_ik2.pmx | 18,997 | 16,649 | 1,484 | 2,410 |
blue_stage_simple.pmx | 4,669 | 0 | 0 | 0 |
SDEFを読み飛ばす実装にする場合も、36バイトぶんは必ず進めること。 ここを飛ばし忘れると、以降の頂点が全部ずれます。変形としてBDEF2扱いにするのは妥当な割り切りで(three.jsのMMDLoaderも同じ)、肩やひじの潰れ方が本家と厳密には一致しない、という差で済みます。
面とテクスチャ
- 面: 「頂点インデックスの総数(uint32)+ 本体」。3つで1つの三角形なので、面数は総数 ÷ 3 です。1件の幅は頂点インデックスの幅(多くは2バイト)。
- テクスチャ: 「件数(uint32)+ 文字列 × 件数」。中身はPMXからの相対パス(
Texture\body.pngのようにWindows流の区切り)です。
面は材質ごとに連続して並んでいます。 材質の情報には「自分が使う頂点インデックスの数」しか書かれておらず、開始位置は書かれていません。先頭から順に足していった累積が開始位置になります。この性質のおかげで、材質ごとの頂点を切り出すのは簡単です。
材質
| 長さ | 型 | 内容 |
|---|---|---|
| 可変 | 文字列 × 2 | 材質名・英語名 |
| 16 | float32 × 4 | 拡散色(R, G, B, 不透明度) |
| 16 | float32 × 3 + 1 | 反射色 + 反射強度 |
| 12 | float32 × 3 | 環境色 |
| 1 | uint8 | 描画フラグ(両面・地面影・セルフ影・エッジ など) |
| 16 | float32 × 4 | エッジ色 |
| 4 | float32 | エッジ幅 |
| B_tex × 2 | index | テクスチャ / スフィアテクスチャ |
| 1 | uint8 | スフィアモード(0 = 無効 / 1 = 乗算 / 2 = 加算 / 3 = サブテクスチャ) |
| 1 | uint8 | 共有トゥーンフラグ |
| 可変 | index または uint8 | フラグが0ならテクスチャインデックス、1なら1バイトの共有トゥーン番号 |
| 可変 | 文字列 | メモ |
| 4 | int32 | この材質が使う頂点インデックスの数(3の倍数) |
共有トゥーンフラグの分岐が最大の罠です。 ここを1バイト読み違えると、材質より後ろのブロック(ボーン・モーフ・剛体)が丸ごとずれます。「材質までは読めるのにボーンで落ちる」ときは、まずここを疑ってください。
材質は見た目のパーツの単位(髪・顔・スカート・籠手…)なので、「このモデルは何でできているのか」を読むにも、色や大きさを個別に変えるにも、この単位が合います。色を差し替えるときは拡散色のRGB 12バイトだけを書き換え、4つ目の不透明度は触らないこと — 半透明にしてある材質を勝手に不透明へ戻すと、髪の透け表現が壊れます。
ボーン
| 長さ | 型 | 内容 |
|---|---|---|
| 可変 | 文字列 × 2 | ボーン名・英語名 |
| 12 | float32 × 3 | 位置(モデル空間の絶対座標) |
| B | index | 親ボーン(−1 で親なし) |
| 4 | int32 | 変形階層 |
| 2 | uint16 | フラグ(ビットマスク) |
| 可変 | — | フラグに応じた追加ブロック(下表) |
| ビット | 意味 | 続くもの |
|---|---|---|
| 0x0001 | 接続先がボーン指定 | ボーンインデックス(B)。立っていなければ代わりに座標オフセット 12バイト |
| 0x0020 | IK | IKブロック(下記) |
| 0x0100 / 0x0200 | 回転付与 / 移動付与 | 付与親ボーン(B)+ 付与率(float32) |
| 0x0400 | 軸固定 | 軸ベクトル 12バイト |
| 0x0800 | ローカル軸 | X軸・Z軸 24バイト |
| 0x2000 | 外部親変形 | Key 4バイト |
IKブロックは「IKターゲット(B)+ ループ回数(int32)+ 単位角(float32)+ リンク数(int32)」に続けて、リンクごとに「ボーン(B)+ 角度制限の有無(uint8)」が並び、制限ありのときだけ下限・上限の24バイトが入ります。
ボーンの位置は絶対座標で、回転は持ちません。 ローカルの位置は「自分の座標 − 親の座標」で求めます。だからボーンを動かす種類の編集をするときは、親から順に積み上げる形になります。
「親は必ず自分より前にある」は成り立ちません。 実物のモデル(ad_ik2.pmx)に、親の添字が自分より後ろのボーンが1本ありました。先頭から1回なめるだけの実装は、親の座標が未計算のまま参照して落ちます。トポロジカル順に並べ直してから処理してください。
もう1つ実物で分かったこと: 足IKボーンは足の子ではありません(親は「全ての親」)。脚の長さを変えても勝手には追従しないので、動かす先(足首)と同じだけずらす必要があります。「動かす先へ重ねる」ではいけません — つま先IKのように、IKボーンと動かす先が元から同じ位置に無いものがあります。
モーフ
「モーフ名・英語名 + パネル(uint8)+ 種別(uint8)+ 要素数(int32)+ 要素 × n」。種別によって1要素の長さが変わります。
| 種別 | 内容 | 1要素の長さ |
|---|---|---|
| 0 | グループ | モーフindex + float32 |
| 1 | 頂点 | 頂点index + 移動量 12バイト |
| 2 | ボーン | ボーンindex + 移動 12 + 回転 16 |
| 3 | UV | 頂点index + float32 × 4 |
| 4〜7 | 追加UV1〜4 | 3と同じ並び |
| 8 | 材質 | 材質index + 1 + 拡散16 + 反射12 + 強度4 + 環境12 + エッジ色16 + エッジ幅4 + テクスチャ16 + スフィア16 + トゥーン16 |
| 9 | フリップ(PMX 2.1) | モーフindex + float32 |
| 10 | インパルス(PMX 2.1) | 剛体index + 1 + 12 + 12 |
種別4〜7(追加UVモーフ)を読み飛ばさない実装が実在します。 three.jsが内部で使うmmdparserがそれで、この種のモーフを持つモデルでは解析がずれます。自前で歩くなら、種別3と同じ並びとして進めれば問題ありません。実測した5本には4〜7は含まれていなかったので(内訳はグループ・頂点・ボーン・UV・材質のみ)、手元のモデルで再現しないからといって無いことにはできません。
表示枠・剛体・ジョイント
- 表示枠: 「枠名・英語名 + 特殊枠フラグ(uint8)+ 要素数(int32)」に続けて、要素ごとに「対象(uint8)+ index」。対象が0ならボーン、1ならモーフなので、読むインデックスの幅が要素ごとに変わります。
- 剛体: 名前2つ + 関連ボーン(B) + グループ(1) + 非衝突グループ(2) + 形状(1) + 大きさ12 + 位置12 + 回転12 + 質量・移動減衰・回転減衰・反発力・摩擦力(4 × 5) + 物理演算の種類(1)。
- ジョイント: 名前2つ + 種類(1) + 剛体A・B(剛体index × 2) + 位置12 + 回転12 + 移動制限・回転制限・ばね(12 × 6)。
剛体の「物理演算の種類」は 0 = ボーン追従、1 = 物理演算、2 = 物理+ボーン位置合わせ です。ここが0以外のボーン(髪・スカート)は物理が毎フレーム動かすので、ポーズをVMDへ書き出すときは除外するのが正解です。実際に、素立ちのつもりで採ったポーズが63ボーンになり、この条件で絞ったら10ボーンになりました(残った10本は足IKが曲げている脚のボーンで、これは書き出してよいものです)。
PMX 2.1にはこの後にソフトボディのブロックが続きますが、実物で見かけたことがなく、このサイトでも読んでいません。2.0のファイルにはそもそも存在しません。
書き換えるときの作法
- 「全部を構造体へ展開して書き戻す」をやらない。 走査時に書き換えたい場所のバイト位置だけを控えておき、そこだけ上書きするほうが安全です。読まなかったブロックや、float32の往復で下位ビットが変わる値を巻き込まずに済みます。
- 等倍(何も変えない設定)で焼き直したらバイト列が完全一致する、をテストで固定する。 走査位置が1つでもずれていれば必ず落ちる、いちばん厳しい検査です。このとき「変化なしの箇所は書き戻さない」分岐が要ります — 法線の再正規化やfloat32の往復だけで下位ビットが動き、実際に全モデルが「差分あり」になりました。
- 文字列を差し替えると長さが変わる。 モデル名を変えるだけでも後続のオフセットが全部ずれるので、位置を控えて上書きする方式のままでは足りません。前後を切って繋ぎ直す必要があります。