Mira.vrm を VRoid Hub にアップロードしたら弾かれました。
手元では何も問題が起きていませんでした。ビューアで開けば正しく表示されるし、自前の VRM 検証も通っていました。
エラーはこれです。
POSITION の Z: 宣言 -0.391 / 実際 -0.238
データは正しくて、データについての宣言が間違っていました。
glTF の accessor は「範囲」を自己申告している
glTF では、頂点データの実体とは別に、accessor が min と max を宣言します。
{
"type": "VEC3",
"count": 12043,
"min": [-0.512, 0.000, -0.391],
"max": [ 0.512, 1.621, 0.284]
}
仕様は、この宣言が実データと一致していることを要求します。ずれているとファイルとしては不正です。
なぜ手元で気づけないのか
ここが本題です。
レンダリングは実データを読みます。 min/max は境界の情報であって、絵を出すのには使われません。だから宣言がずれていても、描画結果は完全に正常です。
- ビューアで開く → 正しく出る
- 自前の検証を通す → 通る
- 実機で動かす → 動く
手元にあるものは、全部「正常」と答えます。 見た目で検算できる種類の情報ではないからです。
気づけるのは、検証器を持っている配布先に弾かれたときだけ。つまり気づく場所が、公開の直前になります。
どこで壊れたか
頂点を動かす加工スクリプトでした。書き戻すときに min/max を引き直していなかっただけです。
順番に辿ると、こうなっていました。
| 状態 | |
|---|---|
| 入力のベースファイル | ずれ無し |
| 加工の中間生成物 | ずれあり |
| 最終成果物 | ずれあり |
入力側が綺麗だったので、加工の過程で入ったことが確定しました。壊れた場所が特定できると、直すべきものが「ファイル」ではなく「スクリプト」だと分かります。
直すときに頂点は触らない
min/max を引き直すだけなので、頂点データは1バイトも変えません。
node app/scripts/fix-accessor-bounds.mjs <file.vrm> --check
--check を付けると検査だけして、ずれていれば終了コードが 0 以外になります。外すと直ります。
直した後に BIN チャンクの sha256 を取って、加工前と一致することを確認しました。宣言だけが変わっている、という保証です。ここを確認しておかないと「直すつもりでモデルを壊した」可能性が残ります。
教訓を残すだけでは足りない
これが一番言いたいところです。
「頂点を動かしたら min/max を引き直す」とノートに書いて終わりにすると、次も同じ場所で止まります。 人が覚えていないといけない対策は、覚えていない日に破れます。
verify:release-assets に組み込みました。モデルが壊れているとそもそもビルドが始まりません。
壊れたモデルを置いて、本当に落ちることも確認しました。検査を足したときは、落ちることを確かめるところまでやらないと、足したことになりません。
一般化すると
この形の落とし穴は glTF に限りません。
見た目で検算できないメタデータは、加工のたびに再計算するか、検査を機械に任せる。
- 境界(min / max / bounding box)
- 件数(count / length / total)
- ハッシュ(チェックサム、ETag)
- オフセット(byteOffset、インデックス)
どれも、ずれていても正常に見えます。 そして正常に見えるものは、公開の直前まで誰も疑いません。
踏んだ落とし穴だけを書いています。 同じところで止まった人の時間が少しでも減れば十分です。
他の記事も同じ形で、実際に測った数字と、直した手順だけを置いています。
(この話が出てきたのは Wisp という個人開発のアプリです)