ボーン名・表情名が化ける理由(15バイトのShift_JIS固定長)
モーションが「そのボーンだけ効かない」ときや、ツールを通したら名前が文字化けしたときの原因はだいたいここです。VMDの名前は長さフィールドを持たない15バイトの固定長で、UTF-8ではありません。
仕様は「15バイト・Shift_JIS・0埋め」
- 長さフィールドがありません。 15バイトの枠に入れて、残りを0(NUL)で埋めます。読む側は最初の0までを名前として扱います。
- 符号化はShift_JIS。 UTF-8ではないので、
TextEncoderではそのまま作れません(TextEncoderはUTF-8しか出力しません)。 - 日本語は1文字2バイトなので、日本語だけの名前は7文字までです。半角英数なら15文字入ります。
- IK/表示切替ブロックのボーン名だけは20バイトです(同じVMDの中で長さが違うので、共通の関数を使うときは引数にすること)。
- PMX側のボーン名は長さフィールド付きのUTF-16LEまたはUTF-8で、長さの制限はここまで厳しくありません。 つまり「PMXでは付けられるのに、VMDには書けない名前」が存在します。
実データでの名前の長さ
31本のVMDが動かすボーン名1,079種を、Shift_JISに変換してバイト数を数えた分布です。
| バイト数 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 種類 | 9 | 9 | 36 | 61 | 240 | 85 | 142 | 34 | 109 | 57 | 135 | 110 | 28 | 24 |
上限の15バイトにちょうど届いている名前が24種ありました。 いずれもスカートの末端で、左スカート1-3先 左スカート2-3先 のような名前です(「左スカート」で10バイト、1-3先 で5バイト)。14バイト以上に達しているものは52種。
これは「あと1文字足したら入らない名前が実在する」ということです。 補助ボーンを追加するときや命名を揃えるときに、7文字の壁は思ったより早く来ます。左スカート1-3先端 のように1文字足すと、そのボーンのキーフレームは書き出せなくなります。
表情名425種も同じ15バイトで、上限に達していたのは7種(ヘッドセットLED アームバンドLED アンクレットLED まつ毛-まばたき など)。IK名は20バイト枠に対して最長12バイトで、こちらは余裕があります。
全角と半角は別のボーン名になる
名前は完全一致で突き合わされます。実測したIK名は 右足IK(全角のIK)が普通でしたが、左ひざDIK(半角のDIK)のように混在しているものもありました。表情名には ウィンク2右 のように半角カナのものもありました。
見た目がほとんど同じでも、右足IK と 右足IK は別のボーンです。モーションが「そのボーンだけ効かない」ときは、まずここを疑ってください。モデルとモーションのボーン名を並べて突き合わせるのがいちばん早い確認です。
Shift_JISに無い文字は書けない
絵文字・一部の異体字(𠮷 など)・Unicodeの丸数字の一部は、Shift_JISに対応する符号がありません。書き出す側はそういう文字を ? に落とすか、書き出しを断るしかありません。
実測した1,079種のボーン名・425種の表情名は、すべてShift_JISへ往復できました。 MMDのボーン名は日本語の常用的な文字と英数に収まっているのが普通なので、実務ではまず当たりません。とはいえ自分で名前を組み立てて書き出すとき(このサイトのポーズ編集のように)は、往復できるかを必ず確かめる作りにしてください。
ここで気をつけること: 「エンコードに失敗したか」を戻り値の有無で判定してはいけません。 15バイトの固定長を作る関数は、変換できない文字があっても(? に落として)必ずバイト列を返します。 往復できるかを知りたいなら、作ったバイト列をもう一度デコードして元の文字列と比べるしかありません。
Shift_JISのエンコーダを外部ライブラリなしで作る
ブラウザには TextDecoder("shift_jis") が標準で入っていますが、エンコーダはありません。デコーダを使って逆引き表をその場で作るのがいちばん軽い方法です(2万件強のデコードで、実測数十ミリ秒。表は一度作れば使い回せます)。
ここに落とし穴があります。 候補となる2バイトの並びを全部つなげて1回でデコードし、「候補の数 = 出てきた文字の数」と仮定して突き合わせると壊れます。未定義の並びに当たったときのデコーダの挙動(置換文字をいくつ出すか、後続バイトを読み直すか)が1対1にならず、そこから先の対応が丸ごとずれます。 実際にこれでボーン名が化けました。1組ずつデコードしてください。
const pair = new Uint8Array(2);
const dec = new TextDecoder("shift_jis");
for (let hi = 0x81; hi <= 0xfc; hi++) {
for (let lo = 0x40; lo <= 0xfc; lo++) {
if (lo === 0x7f) continue; // 0x7f は2バイト目に使われない
pair[0] = hi; pair[1] = lo;
const ch = dec.decode(pair); // ★1組ずつ。まとめてデコードしない
if (ch.length !== 1 || ch === "\uFFFD") continue; // 未定義の並び
map.set(ch, (hi << 8) | lo);
}
}1バイト側はASCII(0x20〜0x7E)と半角カナ(0xA1〜0xDF)です。半角カナを忘れると ウィンク2右 のような実在する表情名が書けなくなります。
モデル名は20バイトで、途中で切れている
VMDのヘッダ直後にある20バイトはモデル名です。こちらも固定長なので、長いモデル名はマルチバイト文字の途中で切られています。 そのままデコードすると末尾に置換文字(U+FFFD、�)が出ます。画面に出すなら落としてください。
編集して書き戻すときは、読み込んだ20バイトをそのまま書き出すのが安全です。デコードして再エンコードすると、切れていた文字の扱いで元と違うバイト列になります。