すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう

こちらは クラスター Advent Calendar 2023 の1ページ目の6日目の記事です!

qiita.com

前日は @neguse_kさんの「Blenderでポーズを作ってUnityに取り込む」でした!Blenderでポーズを作成されている方はこちらの記事を参考にぜひclusterにも組み込んでみてください!


こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアになってちょうど1年が経ちました。エンジニアとしてできることも増え、やりがいのある日々を送っています!

さて、本記事のテーマは「"VRM" の構造をのぞいてみよう」です。ガチな解説ではなくおおまかにVRMの中身をイメージできるぐらいで紹介したいと思います。

使用するツール

今回はUnityとUniVRMは使用しません!

使用するVRM

VRMについて

VRMとは、VRMコンソーシアムが提唱している人型アバターを定義するファイルフォーマットとなっていて、clusterでもアバターの形式として使用されています。 VRMに対応した様々なアプリケーションでアバターを表示することができます。

例: VMagicMirrorでアリシア・ソリッドちゃんの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のみでは明確な用途は不明なため、さらに別の項目から使用されます。

  "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で確認してみましょう。

  1. AliciaSolid_vrm-0.51.vrmからAliciaSolid_vrm-0.51_edited.vrmをコピーして作成
  2. Hex Editorを開く
  3. Ctrl+F → 置換モードに切替 → 置換を実施
  4. Ctrl+Sで保存する
  5. 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_vrmextensions.VRMC_springBoneVRMC_materials_mtoonなどに変更された
  • マテリアルのパラメータはglTF標準のmaterials項目を使用するようになった
    • 色の乗算値を例にすると extensions.VRM.materialProperties[*].vectorProperties._Colormaterials[*].pbrMetallicRoughness.baseColorFactor のようにglTF標準のパラメータが使用されるようになった
    • Unity特有のシェーダープロパティ名は使用しなくなった

変更点の詳細については VRM-1.0の変更点 で確認できます。

VRM1.0はvrm-specificationにて仕様がより明確に記載され、MToonについても項目が定義されたりとUnity製アプリ以外でもVRMを利用できるように見直されています。

ハロクラで発表されているようにclusterもVRM1.0対応を進めていますため、リリースされたらぜひVRM1.0アバターでclusterを楽しんでください!

参考

雑感

VRMの構造について僕なりに紹介してみましたがいかがでしたでしょうか?
clusterのエンジニアになってプロダクト開発でもVRMに触れることも増えたので、せっかくなので知見の共有も兼ねて記事にしてみました。

VRMVRM Meetupも開催されたりと盛り上がりを見せているので、今後も注目していきたいです!

記事をご覧いただきありがとうございました!

明日のAdvent Calendar 2023 7日目は @uzzuさんの「UnityのPlay Asset DeliveryをtargetSdk34に対応させる」です。clusterのAndroidスペシャリストの記事をどうぞお楽しみに!(Play Asset Deliveryについてはuzzuさんにとても助けられました)

それでは~