【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう
こちらは クラスター Advent Calendar 2023 の1ページ目の6日目の記事です!
前日は @neguse_kさんの「Blenderでポーズを作ってUnityに取り込む」でした!Blenderでポーズを作成されている方はこちらの記事を参考にぜひclusterにも組み込んでみてください!
こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアになってちょうど1年が経ちました。エンジニアとしてできることも増え、やりがいのある日々を送っています!
さて、本記事のテーマは「"VRM" の構造をのぞいてみよう」です。ガチな解説ではなくおおまかにVRMの中身をイメージできるぐらいで紹介したいと思います。
使用するツール
- Visual Studio Code
- Hex Editor
- Visual Studio Codeのエクステンションで、いわゆるバイナリエディタです。
- VMagicMirror
- VRMの確認用として使用します。
- 開発者の獏星(ばくすたー)さんもアドベントカレンダーで記事を投稿されています! → 「VRM Animation (.vrma)をUnity上で簡単に生成できるようにした話」
今回はUnityとUniVRMは使用しません!
使用するVRM
VRMについて
VRMとは、VRMコンソーシアムが提唱している人型アバターを定義するファイルフォーマットとなっていて、clusterでもアバターの形式として使用されています。 VRMに対応した様々なアプリケーションでアバターを表示することができます。
VRMアバターを作ったり使ったりするだけならいろいろなツールが提供されているので構造を知る必要はないんですが、知っておくとさらにVRMと仲良くなれるかも?
VRMはglTF-2.0がベースになっている
VRMコンソーシアムから公開されているVRM仕様を見ると以下のように記載されています。
glTF-2.0のバイナリ形式glbをベースにした、VR向けモデルフォーマットです。
つまり結論を言ってしまうと "VRMの構造" ≒ "glTF-2.0の構造" と言えます! ということでまずはglTF-2.0の構造について簡単に紹介します。
glTF-2.0 はバイナリとJSONの2つで構成されている
glTFは3Dコンテンツ向けのファイルフォーマットでKhronos Groupから提供されています。仕様書は glTF™ 2.0 Specificationにあります。glTFファイルには主に以下の情報が1ファイル内に格納できるように設計されています。
- オブジェクトのヒエラルキー
- メッシュ (法線やウェイトなども含む)
- テクスチャ
- マテリアル
- etc...
glTFはバイナリファイルですが、中身は「ヘッダ領域」を除いて「JSON領域」と「バイナリ領域」のチャンクで構成されています。
AliciaSolid_vrm-0.51.vrmをHex Editorで見ると、JSON文字列が格納されていることが確認できます。
そして、ある境界からテキスト部分ではなくバイナリ形式で格納された領域を確認することができます。
ここでJSON領域とバイナリ領域を少し深堀りします。
JSON領域
JSON領域には以下のような情報が格納されています。
項目名 | 説明 |
---|---|
buffer | シンプルなバイナリ領域のアドレス情報。ほとんどのVRMは1つだけ持っている。 |
bufferView | bufferをさらに区分けしたもの。 |
image | あるbufferViewをどの画像形式でロードするかの情報を持つ。画像形式はmimeTypeで表現される。 |
accessor | どのbufferViewをどうやって読み込むかの情報を持つ。 |
mesh | メッシュを構成するための情報を持つ。プリミティブ(マテリアルを割り当てる単位)毎に頂点位置(POSITION)、法線(NORMAL)、UV座標(TEXCOORD_0)などをaccessorを介して取得する。 |
etc... | - |
例として、以下はAliciaSolid_vrm-0.51.vrmのノード情報(nodes)のJSONの一部を整形表示したものになります。
ノードは位置や回転のほかにメッシュの有無の情報を持ちます。
Unityを知っている方向けに説明すれば、ノードはGameObjectのようなものでボーンとしても活用されたり、meshがあればMeshRenderer、さらにskinがあればSkinnedMeshRendererを持つようなイメージとなります。
"nodes": [ { "name": "mesh", "children": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ], "translation": [ 0, 0, 0 ], "rotation": [ 0, 0, 0, 1 ], "scale": [ 1, 1, 1 ], "extras": {} }, { "name": "body_top", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1], "mesh": 0, "skin": 0, "extras": {} }, // ... { "name": "Head", "children": [ 77, 78, 79, 80, 81, 89, 97, 99, 101, 103, 104, 108, 109, 113, 114 ], "translation": [ -9.235208e-9, 0.0388788, -0.00014353916 ], "rotation": [ 0, 0, 0, 1 ], "scale": [ 1, 1, 1 ], "extras": {} },
※↑のJSONは成形したもので、VRM内のJSON領域ではスペースや改行は基本的に除外されています
バイナリ領域
バイナリ領域(Binary Buffer)は何を表しているかというと、メッシュ頂点やテクスチャ、モーフがバイナリデータとして格納されています。
実はバイナリ領域単体では何のデータかはわかりません。バイナリ領域の読み込み方ですが、JSON項目のbuffer、bufferView + 各情報から確定 させる流れになります。
JSON項目からバイナリ領域を読み込む流れ
buffer
bufferはバイナリ領域の一番大きな単位です。以下のように、バイトの長さのみでとてもシンプルです。
前述したとおり、ほとんどのVRMは1つだけ持っています。
"buffers": [ { "byteLength": 7772784 } ],
bufferView
bufferViewはbufferを区切ったもので、大雑把なイメージは以下です。
以下のJSONを例にするとbufferViewの0番目は「0番目のbufferのオフセット0から344698バイト」、bufferViewの1番目は「0番目のbufferのオフセット344698から136063バイト」、...のように表現されます。
"bufferViews": [ { "buffer": 0, "byteOffset": 0, "byteLength": 344698 }, { "buffer": 0, "byteOffset": 344698, "byteLength": 136063 }, { "buffer": 0, "byteOffset": 480761, "byteLength": 1708146 }, // ...
あくまでbufferを区切っただけなので、bufferView単体でも何のデータとして読めばいいかはわかりません。後述するimageやaccessorなどから活用方法を確定させることになります。
image
テクスチャに使用される画像ファイルはimage項目から参照できます。
例えば"Alicia_body"というテクスチャ画像は「bufferViewの0番目をpng形式でロードする」となります。
"bufferView": 0, "mimeType": "image\/png"
の場合は、「bufferViewの0番目をpng画像として読み込む」という解釈になります。
"images": [ { "name": "Alicia_body", "bufferView": 0, "mimeType": "image\/png" }, { "name": "Sphere", "bufferView": 1, "mimeType": "image\/png" }, // ...
accessor
accessorは以下のように情報を扱います。
- typeで整数(int) or 浮動小数点(float)を決定
- componentTypeでScalar(1個毎) or Vector3(3個毎) or Matrix4x4(16個毎) or etcを決定
- 定義はAccessor Data Typesから確認できます。5126はfloatになります。
accessorのみでは明確な用途は不明なため、さらに別の項目から使用されます。
"accessors": [ { "bufferView": 7, "byteOffset": 0, "type": "VEC3", "componentType": 5126, "count": 4804, "max": [ 0.614650965, 1.31239367, 0.150282308 ], "min": [ -0.614748538, 0.9991788, -0.0840013 ], "normalized": false }, { "bufferView": 8, "byteOffset": 0, "type": "VEC3", "componentType": 5126, "count": 4804, "normalized": false }, // ...
mesh
primitivesのattributesとして頂点位置(POSITION)や法線(NORMAL)を持っており、それらの数値がaccessorのインデックス値を表しています。
"POSITION": 0
の場合は、「accessorsの0番目を頂点位置として使用する」という解釈になります。頂点位置なのでそのaccessorのtypeはVector3(VEC3)になっているはずです。
"meshes": [ { "name": "body_top.baked", "primitives": [ { "mode": 4, "indices": 5, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2, "JOINTS_0": 4, "WEIGHTS_0": 3 }, "material": 0 }, // ...
VRMはglTF拡張にVRM特有の情報を持つ
glTFは前述のとおりJSON領域を持っていますが、このJSONに追加情報を格納しても良いことになっています。この追加情報のことをglTF拡張(glTF Extensions)と呼びます。
VRMはこのglTF拡張部分にVRM特有の情報を持たせることで実現されていて、VRM0.xの場合はextensions.VRM
項目に格納されています。
以下はAliciaSolid_vrm-0.51.vrmから抽出した情報ですが、一部紹介します。
拡張項目名 | 説明 |
---|---|
humanoid | VRMのヒューマノイド(人型)のボーン情報 |
secondaryAnimation | VRMのSpringBoneの情報 |
etc... | - |
"extensions": { "VRM": { "exporterVersion": "UniVRM-0.51.0", "specVersion": "0.0", "meta": { "title": "Alicia Solid", "version": "1.10", "author": "© DWANGO Co., Ltd.", // ... }, "humanoid": { // ... }, "firstPerson": { // ... }, "blendShapeMaster": { // ... }, "secondaryAnimation": { // ... }, "materialProperties": [ ] } // ...
ということでVRMの構造を簡単に説明してみました。もう少しglTFに詳しくなりたいな~という方は gltf20-reference-guide.pdf が図もあったりして参考になるかと思います!
VRMはglTFの仕組みをうまく利用したファイルフォーマットだったんですね~。
おまけ
バイナリエディタでVRMを編集してみよう
さて、glTFの大まかな構造が分かったところで Hex Editor でAliciaSolid_vrm-0.51.vrmを編集してみましょう!
今回は編集結果がはっきりとわかるようにマテリアルの色の乗算値を赤色に変更してみます。
VRM0.xのMToonの色の乗算値は extensions.VRM.materialProperties[*].vectorProperties._Color
です。Hex Editorで文字列"_Color"
を検索すると見つけられます。
乗算値は白([1,1,1,1]
)になっていようなので、置換処理で "_Color":[1,1,1,1],
→ "_Color":[1,0,0,1],
に変更保存して、VMagicMirrorで確認してみましょう。
- AliciaSolid_vrm-0.51.vrmからAliciaSolid_vrm-0.51_edited.vrmをコピーして作成
- Hex Editorを開く
- Ctrl+F → 置換モードに切替 → 置換を実施
- Ctrl+Sで保存する
- VMagicMirrorでAliciaSolid_vrm-0.51_edited.vrmをロード
以下が読み込んだ結果です。しっかり赤色になっていますね。
実際にバイナリエディタでVRMを編集することはほとんどないと思いますが、「VRMの構造を知ってるとこんなこともできるよ~」という紹介でした。
VRM0.x と VRM1.0 について
まだclusterではVRM1.0に対応はしていませんが、せっかくなのでVRM1.0についても少し触れようと思います。
VRM1.0はVRM0.xよりさらにglTFに準拠したフォーマットになっています。具体的に言うと以下のような変更が入りました。
- UniVRM以外の一般的なglTFライブラリでもロードしやすいようにbufferViewの扱いが変更された
- glTF拡張での名前は
extensions.VRMC_vrm
、extensions.VRMC_springBone
、VRMC_materials_mtoon
などに変更された - マテリアルのパラメータはglTF標準の
materials
項目を使用するようになった- 色の乗算値を例にすると
extensions.VRM.materialProperties[*].vectorProperties._Color
→materials[*].pbrMetallicRoughness.baseColorFactor
のようにglTF標準のパラメータが使用されるようになった - Unity特有のシェーダープロパティ名は使用しなくなった
- 色の乗算値を例にすると
変更点の詳細については VRM-1.0の変更点 で確認できます。
VRM1.0はvrm-specificationにて仕様がより明確に記載され、MToonについても項目が定義されたりとUnity製アプリ以外でもVRMを利用できるように見直されています。
ハロクラで発表されているようにclusterもVRM1.0対応を進めていますため、リリースされたらぜひVRM1.0アバターでclusterを楽しんでください!
参考
雑感
VRMの構造について僕なりに紹介してみましたがいかがでしたでしょうか?
clusterのエンジニアになってプロダクト開発でもVRMに触れることも増えたので、せっかくなので知見の共有も兼ねて記事にしてみました。
VRMはVRM Meetupも開催されたりと盛り上がりを見せているので、今後も注目していきたいです!
記事をご覧いただきありがとうございました!
明日のAdvent Calendar 2023 7日目は @uzzuさんの「UnityのPlay Asset DeliveryをtargetSdk34に対応させる」です。clusterのAndroidスペシャリストの記事をどうぞお楽しみに!(Play Asset Deliveryについてはuzzuさんにとても助けられました)
それでは~