残差0.7pxの検算が通ってもモデルは後ろを向いていた — AIとバイナリを掘った分業の実録
PS1『デュープリズム』のディスクからキャラクターとマップの3Dモデルを掘り出しました。何をどう解読したかは別の記事に書いたので、こちらはその作業をAIと二人でどう分担していたかの記録です。相手はClaude Code、モデルはOpusとFable。着手から2日でキャラクター形式の全パイプライン(モデル・テクスチャ・骨格・姿勢・アニメ)が通り、4日目にはマップ形式が1日で落ちました。ただこの速度はAIが優秀だった、で終わる話ではないです。AIは途中で何度も派手に間違えていて、効いたのは分業の線のほうでした。分業といっても設計したわけではなく成り行きなんですけどね。
解析対象は自分が所有するディスクで、個人利用の範囲です。取り出すのは数値と形の情報だけ。自作ローポリアバターの配分を測る定規として使うのが目的で、抽出物は再配布しませんし、確認用のギャラリーも非公開で運用しています。
AIの仕事は総当たりと逆アセンブルと全数検査
このディスクにはファイル名がありません。ISO9660は形だけで、実体は隠しセクタのファイル表で引く方式。エントリは6,656件、名前の列はどこにも無い。目当てのモデルは中身の指紋で当てることにして、ここからがAIの仕事でした。
実行時のVRAMの記録から採ったCLUT(16色×2バイト=32バイト)を検索キーに、ディスクイメージ全域を走査する。テクスチャのピクセル列は並びが変わったり圧縮されたりで当たりませんが、パレットは生で埋まっているので指紋になります。終盤に外部テクスチャの在処を探したときも同じ手で、CLUT8本ぶんの列が連続で現れる場所はディスク全域でオフセット323941620のただ一箇所でした。32バイトの窓で数百MBをなめる仕事。人間は絶対やりたくない種類のものです。
データ側から推定が利かなくなったときは、実行ファイルを読みに行かせます。EXE(これも隠しセクタLBA35)をcapstone(多アーキテクチャ対応の逆アセンブルライブラリ)でMIPSの命令列に開いて、LZ展開ルーチン(0x8004881c)を特定しPythonへ移植する。面レコードの長さが割れなかったときは描画ディスパッチのswitch文(0x80026148・ジャンプ表0x80094290・14分岐)から各型のポインタ前進量を直読する。アニメーションは補間器(0x8002e44c/0x8002e56c)を読んで、速度を毎tick積分する区分線形の曲線バイトコードだと確定する。この逆アセンブルの読み通しもAIの仕事です。
そして検証。仮説はいちいち全数で検証させました。レコード長の確定表は全123ファイル359ブロックで「レコード列が次の構造体の直前に0〜3バイトのパディングで着地する」ことを機械検査して359/359。回転の規約は軸割当×乗算順×符号の72通りを、実行状態のスナップショット2時点×62ノードで総当たりして一意に収束(最大誤差0.0042、次点は0.30で桁違い)。修正を入れるたびに全172種を再エクスポートして、変わってはいけないキャラがバイト単位で同一かを比較する。マップは141ユニーク全数を変換し、ツール統合時の回帰検査はBINチャンク141個のバイト一致。この物量を毎回こなす胆力は私には無い。AIは疲れないので、この分業は素直でした。
人間の仕事は記憶と裁定
では人間は何をしていたか。検算が届かないところの審判です。
内部整合の検算は、規約の誤りに無力でした。姿勢の復元は実行時の画面座標への投影で残差中央値0.72pxまで合っていた。それでも規約の誤りは3回、どれも私の一言で露見しています。
1件目。VRM化したマヤに小物の本を持たせたら、一目で「解剖学的な左手に持っている。それにビューアの既定アバターに対して180°後ろを向いている」。マヤが本を右手に持つのはプレイした記憶です。これで「+Zが正面」という前提の誤りが確定しました(正しくは−Zが正面。以後、向きを疑うときは顔テクスチャを持つ面の重心zを測るのが最速手になった)。
2件目。腕と脚。座標から推定した部位ラベルで予算表まで作っていたのに、私が出したフィールド画面の描画記録へノード別の色を塗って重ねたら、腕と脚のラベルが丸ごと逆でした。構造からの推定は、実物に色を塗るまで仮説のまま。
3件目。「18732の刃が頭に刺さる」。武器の配置を決める144B区画(12バイト×12行)の行の読み方が誤っていて、内部の検算は全部通っているのに刃が頭に生えている。このキャラが二刀を両手に構える姿の記憶が無いと、そもそも異常だと気づけません。検算は全部緑なのに…。
3件に共通するのは、機械側の検査がすべて通ったまま結果が間違っていたこと。ground truth(正解の根拠)は実機の画面と、それを覚えている人間の頭の中にしかありませんでした。
もうひとつの人間の仕事は裁定です。どの穴を掘ってどの穴を寝かせるか。外部テクスチャが未回収のクリーチャー1体は、所在を特定したところで「一旦寝かせる」と決める。キャラが一段落したら「マップに進もう」と向きを変える。それと問い。「未抽出はある?」の一問で変種49種が発掘されて123種は全部ではなかったと判明し、「人型とそれ以外で3Dモデルの表現は一貫してるの?」の一問は誤った対症療法2件の撤去と根治に繋がりました。ギャラリーを眺めて「5121の右脚がない」「おたまの目が出てない」と指摘するのも私の側で、磨きに使った1日はこの種の指摘12件が全部フォーマット解読の残り穴に化けています。
AIの間違いも晒しておく
AI側が派手に間違えた話も残しておきます。3つ。
1バイトのずれが「未知の版」の幻を生みました。レコード長を全キャラ横断の連立で推定したら3箇所誤っていて、その数バイトがグリッドをずらし、54種のキャラで「タイルID超過」の読み出し失敗が出た。AIはこれを「まだ別の版がある」と読みました。実際は版は最初から1つで、誤っていたのはこちらの定数です。似た事件がもう1件あって、面レコードを1バイトずれた位置から読んで「未知タイルが17種ある」と結論していた。正しい開始位置で読み直すと未知IDは全消滅。表の定義域を超える値が出た時点で、値ではなく読み位置を疑うべきでした。
審判に使った物差しが粗くて、一度誤裁定しています。回転チャネルの軸割当を、画面座標への適合残差23.3px対30〜51pxという比較で「ch1=Z」と確定させていた。後からランタイム実測の72通り総当たりで裁き直すと正解はch1=Yで、誤差は0.0042対0.30。物差しが粗いまま裁いたのが敗因で、棄却の前に物差しそのものの較正が要りました。
偶然の一致にも騙されかけました。マップの配置エントリ数とピース数が、最初に開いた広場で偶然どちらも60。「エントリ数=ピース数」の前提で1日読み進めかけましたが、実際はダンジョンで6ピースを75回置くインスタンシングが基本設計で、あのまま行けば全部壊れていました。1例で当たった定数はその1例の偶然かもしれない。
タチが悪いのは、どの間違いも検算付きで自信満々に来ることです。だから前節の分業になります。
効いた型は3つ
逆算が縮退したら順算のコードを読む。データ側からの逆推定が行き詰まるたびにEXEの逆アセンブルへ切り替えて、これがLZ圧縮・回転規約・アニメ曲線と3連勝でした。「未知の版」の幻を潰したのも同じ型です。データは結果しか見せませんが、コードには手順が書いてある。
見た目で判断できないものは数字で殴る。四角形の頂点順は2通りの周回の周長分布(中央値247対297)で確定する。モデルの向きの検算は「顔面の平均zが負・左手のxが負・面の巻き順の外向き率が過半」という独立3条件の同時成立で機械化する。目で裁けない仮説を、統計と機械検査に裁かせる型です。
転んだ場所にはコードで防波堤を置く。ずれた位置からのLZ展開が偶然成功して2,214バイトのゴミを返した事件のあと、展開の成功判定は「展開長がヘッダのサイズ表記と一致」まで要求するよう厳格化しました。18時間分のゴミアニメを積分して2時間スタックした事件のあとは長さ上限のガード。144B区画の件では、解読メモに「0x01はインスタンス行」と書いてあったのに実装が見ておらず刃が頭に刺さったので、規則は散文でなくコードの側に守らせる。散文の教訓は次のセッションが読み飛ばしますが、コードのチェックは誰も覚えていなくても必ず走ります。
解読資産は翌日に複利で効いた
マップ形式(.mmd/.mtd)は着手から1日で141マップ全数のglb化まで落ちました。未解読の独自形式が1日で落ちたのは、前日までのキャラ形式(.chr)の資産がそのまま刺さったからです。分類表・インデックス・RGB/UVレコードの規則はEXEの描画ディスパッチをキャラと共有していて同一規則。レコード長の確定表もLZ展開器も検証の型も、全部流用が効きました。ちなみに.mmd/.mtdの中身を解いた資料は探した範囲で見つからなかったので(抽出ツールはあるものの構造の文書は無い)、この解読が最初のようです。
複利になったのは置き場の設計もあります。AIセッションの作業領域は揮発するので、解読の正典は最初からスクリプトのdocstringに置く運用にしました。構造も、潰した仮説も、騙された偶然の一致も、全部psxdisc.pyとpsxmap.pyの冒頭に書く。翌日の別セッションはそれを読んだ時点で前日の到達点から始まります。会話ログは流れて消えますが、コードに同居する文書は次の道具がそのまま読む。資産がセッションをまたいで残る形にしておいたのが、今回いちばん効いた設計判断だったと思います。
残っているもの
外部テクスチャが未回収のクリーチャーは2体残っています。翼のクリーチャー1体は在処(オフセット323941620)まで特定して寝かせてあって、幅可変ストリップの格納形式が最後の壁。もう1体は所在もまだ特定できていません。分業の線は最初から引けていたわけではなく、転ぶたびに「ここまでは機械、ここからは人間」と引き直されていきました。終わってみると、疲れる仕事が全部AIに行って、覚えている仕事と決める仕事が手元に残っただけです。続きは寝かせた穴からです。