パレットの32バイトが指紋になる — PS1『デュープリズム』のディスクからマヤの3Dモデルを掘った
XRiftに置いている自作アバター「玉の巻物使い」は、1999年のPS1ソフト『デュープリズム』の登場キャラクター、マヤを造形の原点にしている。作り込むほど原典をちゃんと測りたくなった。頭と脚のどちらに三角形を厚く配っているのか。帽子の飾りにどれだけ予算を割いているのか。目分量でなく数字で知りたい。
手元にはゲーム実行中の描画コマンドを記録したログがあって、GPUに渡った頂点はそこから読める。ただしログに残る頂点は投影後の2D座標で、1記録につき1視点。正面の記録を何枚集めても後頭部の形は出てこない。360°の形が要るなら、ディスクの中に眠る3Dモデルそのものを読むしかない。それで所有ディスクを掘り始めた。解読の実働はAI(Claude)との分業で進めた。
先に立場を書いておく。これは所有ディスクの解析で、個人利用の範囲。取り出すのは数値と形の情報で、用途は自作アバターを裁く定規。抽出物は再配布しないし、後述のギャラリーも非公開のまま運用している。
ファイル名の無い6,656件と、32バイトの指紋
PS1のディスクだからISO9660でファイル一覧が見える、と思っていたらそうではなかった。このディスクのISO9660は形だけで、実体は隠しセクタに置かれたファイル表で引く。セクタ0x18から0x24にu32のエントリが並び、下位23bitがLBA(セクタ番号)。エントリは単調増加なので、次のエントリのLBAから自分のLBAを引けばファイルのセクタ数が出る。件数は6,656。そしてファイル名が無い。「どこから始まって何セクタか」だけが6,656件並んでいる。
名無しの6,656件の中からマヤをどう特定するか。効いたのはCLUT(カラーパレット)だった。PS1のテクスチャは16色パレットが基本で、1色2バイトの16色、計32バイト。実行時のVRAMの記録からマヤのテクスチャが使うパレットを採り、その32バイトでディスク全域をバイト検索すると当たる。テクスチャのピクセル列では当たらない。VRAM上と並びが違ううえ圧縮されているから。パレットだけは圧縮を免れて生のまま埋まっていて、これが指紋になる。

ひとつ拍子抜けしたのは、同じファイルがディスク上に何度も置かれていたこと。マヤの72KBのモデルファイルは27箇所にバイト単位で同一のコピーがあった。CD-ROMのシークを削るロード最適化らしい。指紋はどのコピーに当たってもいい。1つ見つかれば済む。
先人は居た。ただしモデル形式の手前まで
掘る前に先人を探した。見つかったのはS3Extractという抽出ツール。Xenogears・クロノクロス・デュープリズムの3本は同じスクウェア開発第3部の独自ファイルシステムを使っていて、S3Extractはそこからファイルを切り出せる。隠しセクタのファイル表という仕組み自体はnocashの文書にも載っている。ありがたいことに北米版のファイル名索引も同梱されていて、キャラクターモデルの.chrが172件、RUE・MINT・MAYAと実名付きで並ぶ。ここまでは先人の肩に乗れた。
ただしそこまでだった。切り出したファイルの中身、つまりモデル形式を解いた先人は見つからなかった。頂点がどう並び、骨がどう繋がり、テクスチャがどう畳まれているか。そこから先は自分で解くしかなかった。
答えはEXEに書いてあった
解読の要所は3つあった。1つ目はレコード長の話。
テクスチャのピクセルは独自のLZ圧縮で潰されていて、これは推定でなくゲームのEXE(これも隠しセクタに居る)をcapstone(多アーキテクチャ対応の逆アセンブルライブラリ)でMIPSの命令列に開き、展開ルーチンを見つけて移植した。形式は先頭にu32の展開後サイズ、あとはLSBから読むフラグビットでリテラルと参照を切り替える素直なLZ。データを眺めて圧縮形式を当てるより、展開するコードを読む方が速くて確実だった。
同じ手がモデルの面レコードでも効いた。面レコードは型バイトを持ち、型ごとに長さが違う。最初はこれを連立方程式のように推定した。「レコード列が領域にぴったり収まる」という条件から各型の長さを解くと、手元のキャラは読める。ところが全123種に広げると54種が失敗を返す。典型は「タイルIDが定義域を超える」。このときの作業仮説は「フォーマットには未知の版がある」だった。残り54種は別の版で、別の読み方が要るのだと…。
幻だった。版は最初から1つで、誤っていたのは推定したレコード長の3箇所。決着させたのはここでもEXEで、描画ディスパッチのswitch文を読むと14分岐のジャンプ表があり、各ケースのポインタ前進量がそのままレコード長になっている。0型は4バイトでなく3、9型は13でなく12、d型は20でなく21。数バイトの誤りが読み取り位置を累積的にずらし、ずれた位置から読んだタイルIDがデタラメな値になって「未知の版」という幻を生んでいた。同型の事故はもう一度あって、ある面グループを1バイトずれた位置から読み「タイル表に無い未知タイルが39面ある」と一時は結論していた。正しい開始位置で読み直すと未知タイルは全消滅。定義域を超える値が出たとき疑うべきは、値ではなく読み位置だった。
修正後の検証は機械にやらせた。全123ファイル359ブロックで、レコード列が次の構造体の直前に0〜3バイトのパディングを残して着地することを確認して359/359。EXEの直読はこの解読で4回勝ち筋になった。LZ圧縮、ボーン行列と回転規約、アニメ曲線、そしてレコード長。データ側からの逆推定が行き詰まったら、それを生成し消費するコードを読む。この件でいちばん効いた手筋。

骨格に姿勢が入っていない
2つ目の要所は、バグでなく仕様の側にあった。モデルを組み上げて立たせると足元が板状に広がる。腕も変な方向に伸びる。解読ミスを疑って読み直しを重ねたが、答えは「スケルトン区画は姿勢を持っていない」だった。スケルトン区画が扱う34ノードのうち回転エントリを持つのは3つだけで、残り31ノードは回転なし。一方で実行時の描画から姿勢を逆算すると、回転を持たないはずのノードが44°、57°、108°と回っている。つまり姿勢は骨格データの外、全部アニメーションが与えている。バインドポーズで正しく立たせることは原理的にできない。休止アニメの1tick目を姿勢として当てて、はじめてマヤは立った。

アニメは速度で書かれている
3つ目はアニメーション。曲線データの中身は、EXEの補間器を読んで解けた。キーフレームと補間ではなく、速度の区分線形符号化。1バイトのトークンが「速度を7bit符号付きで更新」「次のバイトと合わせて14bitで更新」「nティック待つ(速度は保持)」のどれかを表し、ランタイムは毎tick積分値に速度を足す。積分値がそのままチャネル値になる。速度列をひたすら積むだけの設計。
検証は実行状態の記録で取った。イベントアニメの69曲線をtick0から積分し、実行状態を2時点で記録したスナップショットと突き合わせる。それぞれtick24とtick8で、全チャネルのカーソル・待ちカウンタ・積分値・速度が完全一致した。1tickもずれない。ここまで合えば解読は終わり。歩行サイクルで脚が交互に出てスカートが揺れるGIFが出た。
発見がひとつ。PS1に物理演算は無く、帽子の房や後ろ髪や腰飾りの揺れはアニメ曲線で直接付けられている。しかも振幅はどの揺れモノも体幹より大きく、腰飾りはチャネル数でも体幹を上回る。誰かが一本ずつ手で付けた二次モーション。26年前の手仕事がバイト列にそのまま残っている。

検算は規約の誤りに無力だった
ここまで数字で押してきたが、数字が効かない領域があった。座標規約。
姿勢復元の検証は実行時の画面座標への投影残差で取っていて、中央値0.72ピクセルまで合っていた。それでも前後と左右が丸ごと逆でありうる。内部整合の検算が見ているのは「この解釈は自己矛盾していないか」だけで、全体を鏡映しにした解釈も180°回した解釈も同じ残差で通ってしまう。
これを覆したのは実機を触った記憶だった。「マヤは本を右手に持っている」でアタッチ先の左右が覆り、「ビューアの標準アバターに対して180°後ろを向いている」で正面の規約(+Z正面説)が覆り、「このキャラは刃を頭に刺していない」で武器アタッチ行の解釈が覆った。3回とも、指摘の時点で数値検算はすべて通っていた。子供の頃にゲームを遊んだ人間の記憶が、残差0.7pxの検算より上位にあった。正面と左右の判定は後から機械化できた。顔テクスチャを貼った面の重心zを測ると-54で、-Zが正面と即断できる。ただしその検査を思いつくには「顔はどちらを向いているはずか」という記憶が先に要る。
この解読は総当たりとEXE読解と全数検証をAIが持ち、実機の記憶と裁定を私が持つ分業だった。分業の生態そのもの、AI側の錯誤も含めた実録はもう1本の記事に書いた。

マヤがVRMになって、172種が並んだ
形式が解けたので出口を作った。マヤはVRMになった。PS1の剛体分割モデルは各頂点がちょうど1つの骨に属するのでウェイト塗りが不要で、VRM化と相性がいい。関節が割れない仕組みも実装が答えで、全パーツが変換済み頂点を共有プールの同じ番地へ書き込み後勝ちで上書きするから、割れないのでなく割れようがない。この規則ごとVRMに移植した。瞬き6フレームと口のピクセルもディスク内から発掘済みなので、顔もISOだけで焼ける。

そして一般化。マヤ固有の定数(実は偶然の産物だった)を3つ外すと、ほかのキャラも同じパイプラインで落ちた。ユニークな.chr全123種。後日「未抽出はある?」と数え直したら、マジックナンバーを持たない変種がさらに53種見つかった。うちモデルの49種を合流させて計172種。残り4種はスプライト系の別形式なので除外した。ボス級の大物や台座付きのフィギュア群が出てきて、最大は695面の龍だった。全種にアニメ再生付きの個別ビューアページを生成して、非公開のギャラリーに並べてある。


本来の目的も果たした。実測したマヤの三角形予算は頭部38.8%(UV面積では45.3%)、腕19.3%、脚18.9%、腰13.8%、上着9.2%。頭に4割弱、腕と脚に2割ずつの配分。裁く方も始めていて、玉の巻物使いの新版を原典のVRMを定規にした寸法の実測で測ると、靴とすねと帽子と手が原典の半分しかないと出た。そこを盛る改修をした。目分量では一生出なかった判定だと思う。

翌日、141マップが一日で落ちた
キャラが終わった翌日「マップに進もう」と言って、その日のうちに全141マップが3Dモデルとして落ちた。マップ形式(.mmd/.mtd)も解いた先人は見つからず、汎用ビューアのPSXPrevには読めなかったという報告があり、モデル投稿サイトに並ぶのは有志の自作だけ。それでもキャラの.chrに数日かけたのと比べると異様に速い。理由ははっきりしている。マップの面レコードは、EXEの描画ディスパッチをキャラと共有していた。分類表もインデックスもレコード長の確定表も、キャラ側で解いた資産がそのまま流用できた。機構を1つ解いてあったから、隣の形式が芋づるで落ちた。キャラで払った解読の複利。
マップ固有の罠もあった。最初に開いた広場マップは配置エントリ60個にピース60個で、「エントリとピースは1対1」に見えた。次にダンジョンを開くと6ピースに配置75個。つまり少数のピースを90°刻みの回転で置き直すインスタンシングが基本設計で、広場の60=60はただの偶然だった。森のマップでは「木」のピース1種が数十本置かれている。1例で当たった対応は、その1例の偶然かもしれない。
もうひとつ、ファイル名の無い世界ならではの問題があった。マップはジオメトリ(.mmd)とテクスチャ(.mtd)の2ファイルで1組なのに、どれとどれが組かはどこにも書いていない。ディスク上はだいたいmtd→mmdの順で隣接する(237/318組)が、間に別ファイルが挟まる組や、mtdを持たず前方の基本マップとテクスチャを共有するイベント変種がある。採った規則は「mmdの直前から遡り、タイル参照の被覆率95%以上を満たす最初のmtdを採る」。この機械裁定で141ユニーク全数の変換が成功し、噴水の広場も教会のステンドグラスも森の桟橋も目視で通った。展開したデータには描画用メッシュと別に歩行可能領域のメッシュまで入っていて、これはまだビューアに出していない。



26年前のディスクに全部残っていた
振り返ると、欲しかったものは全部ディスクにあった。モデルも骨も、瞬きのピクセルも、速度で書かれたアニメも、歩けるマップの床も、1999年にプレスされた名前の無い6,656件の中に残っていた。
立場は最初に書いたとおり。掘ったものは数値の定規として使い、抽出物は配布せず、ギャラリーも非公開のまま。定規としての仕事はもう始まっていて、玉の巻物使いは寸法の実測で一度裁かれて造形が変わった。部位別の三角形予算での裁きはまだ残っている。次はそれ。