glTF2フォーマットについて

glTFとは

 glTF (The GL Transmission Format)とは、ゲームやインタラクティブコンテンツなどの、コンテンツ再生用途に向いた3Dアセットフォーマットです。

 どのようなプラットフォームでも利用可能であり、基本的にJSONベースのフォーマットであるため、3Dファイル形式の中では比較的仕様を理解しやすいものになっています。

 正式名称が示す通り、元々はOpenGL(特にWebGL)での利用を想定し、それらの仕様に寄ったフォーマット(glTF1.0)でしたが、今回紹介する次世代バージョンglTF2.0は特定の3D APIやシェーダー言語への依存を極力無くしており、よりユニバーサルな仕様になっています。

 本記事では、以前のバージョンに比べて、表現力・機能面で大幅な飛躍を遂げた、glTF2.0について解説します。
ちなみに、前バージョンであるglTF1.0については、Qiitaにて解説記事をあげています ので、そちらをご覧ください。

3Dファイル形式をめぐる混沌

 CGの黎明期より、数多くの3Dファイルフォーマットが世に生まれ出ました。CGにおける技術的な要件は多岐にわたるため、全てを満たすファイルフォーマットというものはそうそうありません。各社・業界団体の思惑による力関係なども加わり、未だ持って統一の機会を見ることはありません。

 ですが、近年、大体のいくつかのフォーマットに収斂されてきたように思います。いわゆる3Dアセット(ポリゴン形状、テクスチャ、マテリアル、アニメーションなどを含む)をやり取りするフォーマットは主に2つのタイプに大別できるでしょう。

  • 制作用フォーマット:3Dコンテンツの制作時に作業者または3Dソフト間でやり取りするために用いられる。制作に役立つよう大量のメタデータを保持しており、再生用途としては冗長である。
  • ランタイム用フォーマット:ゲームやインタラクティブコンテンツ、商品閲覧などの、一般ユーザー向けのプログラムが高速に処理できるための軽量なことが特徴。

 glTFは、後者(ランタイム向けの軽量フォーマット)にあたります。

 ※ちなみに前者(制作用フォーマット)としては、Autodesk社のFBXフォーマットやSPI(ソニー・ピクチャーズ・イメージワークス)社とILM(インダストリアル・ライト・アンド・マジック)社が開発したAlembicフォーマット、Pixar社が公開しているUSDフォーマットなどが挙げられます。

再生用フォーマットとしてのglTFの躍進

 glTF2は、近年の動向(2019年末現在)を見ると、再生用3Dフォーマットとしては十分に業界標準としての立場にいます。

 また、国内ではVRアバター用3Dフォーマットとして、VRMフォーマットがありますが、このVRMフォーマットは実はglTF2フォーマットに準拠した拡張フォーマットです。

glTF2.0の特徴

 glTF2.0には、以下の特徴があります。

(glTF1.0の頃から持っている特徴)

  • 基本的に、JSONファイルとバイナリファイルから構成される (後述しますが、派生型の異なるファイル構成も可能です)
  • インターネット等でファイルに含まれる情報種別の特定に使われる「MIMEタイプ」に正式に登録されている。
  • ポリゴンデータ、テクスチャ、マテリアル(光の反射の性質)、アニメーション(スケルタルアニメーション含む)をサポート。
  • 仕様の拡張を認めており、そのための拡張用の記述形式があります。Khronosやその他のベンダーがいくつかの拡張仕様を提案しています (OpenGLの拡張のようなイメージ)。

(glTF2.0から備えた特徴)

  • 物理ベースマテリアルをサポートします。
  • ブレンドシェイプ(モーフィング)をサポートします。
  • 拡張仕様として、GoogleのDraco圧縮技術をサポートします。アセットデータの大幅なサイズの削減が可能です。

また、詳細な事項として、以下のような1.0と2.0に共通する仕様があります。

  • 右手座標系を使用します。つまり、xとyの外積はzを生成します。
  • y軸を上方向と定義します。
  • 全ての角度はラジアンで表現されます。
  • 反時計回りを正の回転とします。
  • すべての直線距離の単位はメートルです。
  • ノードのトランスフォーム値として設定される平行移動、回転、スケール(いわゆるTRS)の値ですが、そこから変換行列を作成する場合、T * R * Sの並びとして考え、後ろから掛け算します。つまり、頂点にはまずスケール行列をかけ、次に回転行列をかけ、最後に平行移動行列をかけます。

glTF2.0の構成

 glTFの具体的な構成は、いくつかのパターンに分けられます(用途によって選ぶことができます)。

基本形

 以下の構成を取ります。

  • .gltfファイル (glTFの主体をなす、概要としてのメタ情報が記述されたJSONファイル。他ファイルへの参照情報も含まれます)
  • .binファイル (頂点データやアニメーションデータなどが格納されたバイナリファイル)
  • 各種テクスチャ画像ファイル
  • シェーダーファイル(GLSLファイルなど)

派生系:Data URL埋め込み型

主体である.gltfファイル(JSONファイル)の中に、各種外部ファイルのデータをData URL形式として、直接文字列で埋め込んでしまう形式です。全ての外部ファイルを埋め込むこともできますし、一部のファイルのみ埋め込む形でも構いません。

派生型:単一バイナリ形式

 全ての構成要素を、バイナリデータとして、単一のファイルに納めたものです。拡張子は.glbです。とはいえ、基本型での.gltfファイルにあたるJSONデータの論理構造に変更はありません。.gltfファイルに相当する部分の後に、各種外部依存ファイルのデータのバイナリが続きます。ファイル先頭のヘッダ領域には、それらのデータ領域へのオフセット情報などが書かれています。バイナリデータとしてはかなりシンプルなデータ構成なので、読み書きに苦労することはないと思います。C/C++などのネイティブ言語はもちろん、JavaScriptなどの言語でも、バイナリを扱うAPIで問題なく読むことができます。

JSON部分の記述形式

 まずは最初に、ファイルの中身を実際に見てみましょう。

 以下に記載するのは、glTFコミュニティ公式が公開している、最も単純な三角形ポリゴンの.gltfファイル です。

{ "scenes" : [ { "nodes" : [ 0 ] } ], "nodes" : [ { "mesh" : 0 } ], "meshes" : [ { "primitives" : [ { "attributes" : { "POSITION" : 0 } } ] } ], "buffers" : [ { "uri" : "triangleWithoutIndices.bin", "byteLength" : 36 } ], "bufferViews" : [ { "buffer" : 0, "byteOffset" : 0, "byteLength" : 36, "target" : 34962 } ], "accessors" : [ { "bufferView" : 0, "byteOffset" : 0, "componentType" : 5126, "count" : 3, "type" : "VEC3", "max" : [ 1.0, 1.0, 0.0 ], "min" : [ 0.0, 0.0, 0.0 ] } ], "asset" : { "version" : "2.0" } }

 総じて言うと、

  • まず各データ種類(nodeやmesh)ごとに、それらを連想配列でまとめたnodesやmeshesといったプロパティが、JSONの第一階層に存在します。例えばnodesの中には、配列で複数の(上記の例では1つですが)nodeの具体的なメタ情報が定義されています。
  • 各データ項目(例えばnodesプロパティの中の個々のnodeなど)は、自身が保持する情報として、具体的な即値(行列値など)を保持している場合もあれば、他のデータ項目へのインデックス整数を保持している場合もあります。
  • あるデータ項目の定義(例えば、あるnode)を見たときに、それが持つあるプロパティが他のデータ項目のインデックスを指し示している( 例えば"nodes": [{ "mesh" : 0 }] )場合は、JSONの第一階層に存在する、対応するデータ種類のプロパティ(この場合は"meshes")の配列を、先ほどのインデックス({ "mesh" : 0 }の0)でアクセスすることで、その具体的な情報(そのnodeが持つmeshの具体的な情報)にアクセスすることができます。
  • 基本的に、glTFのJSONファイルの読み込み手続きは、この繰り返しです。
  • (ただし、 "buffers": [{"byteLength" : 36 }] などのように、他のデータ要素へのインデックスではなく、何らかの即地(この場合はバイト長)である場合もあります。この辺りの区別には、慣れが必要です。厳密には仕様書で規定されていますが…。)

※ ちなみに、glTF1.0では、JSONの第一階層の各種データプロパティ("nodes"など)は、複数のデータ項目を、glTF2.0のような配列ではなく、連想配列で持っていました。そのため、各種データ項目は他データ項目への参照として、インデックス整数ではなく、その連想配列の要素を指し示すためのキーとなる文字列を持っていました。おそらく、この形式は冗長すぎると判断され、glTF2.0では配列ベースに仕様変更されたのだと思われます。

glTF2.0の仕様書に、このインデックスによるアクセス及び、name属性の取り扱いについての記述があります。
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#indices-and-names 

理解を助けるチートシート

 また、glTFの公式Githubページでは、非常にわかりやすいチートシート が用意されています。

(R&D Slackメンバーのhamukunさん(Qiitaアカウント名@randall2835さん)が、公式チートシートの日本語訳版 を作成されていらっしゃいます!)
記事:次世代の3Dデータフォーマット決定版 glTF 2.0 の概要図を日本語訳してみた 

glTFファイルの作成方法

 概要がざっと掴めたところで、glTFファイルはどうやって作れば良いのでしょうか。

 方法は幾つかあります。メジャーな方法をいくつか挙げます。

Blenderから

 Khronos公式のBlender用のglTFエクスポーターです。最近のBlenderでは標準搭載されています。

 Githubプロジェクト:https://github.com/KhronosGroup/glTF-Blender-IO 

 初期からKhronosのglTF公式コミュニティで利用されている実績のあるエクスポーターです。Blender標準のPrincipled BSDFノードのまま利用できます。
詳細はBlenderドキュメントの該当ページ を御覧ください。

Unityから

UniGltf

https://github.com/ousttrue/UniGLTF 

UnityからglTFファイルを書き出すエクスポータです。

UnityからVRMファイルを書き出すUniVRMパッケージの一部としても利用されています。

その他の方法

 GithubのglTFプロジェクトのREADME.mdに、各種コンバーター・エクスポーターのリスト があります。

Mayaなど、Blender以外の3Dソフトを使っているケースでは

 現時点(2017年9月時点)では、glTF2.0のファイル生成については、Blender以外の3Dソフトだとちょっと辛いところかもしれません。
スタティックなポリゴンモデルであれば、おそらく問題ないと思います。ただ、スキニングやPBRマテリアルなどが入ってくると問題が生じることがあるかもしれません。

 glTF1.0で良いのであれば、OpenCOLLADAプラグインでCOLLADAファイルとして出力し、それを[COLLADA2GLTFコンバーター]でglTF1.0ファイルに出力する、というワークフローが割と安定しています(スケルタルアニメーションもいけます)。
ただ、[COLLADA2GLTFコンバーター]でglTF2.0対応がまだ現時点で完全でなく、出力したglTF2.0ファイルで、特にスケルタルアニメーション周りの問題が若干見られるようです。

 前述の通り、何らかの3Dファイル形式を通じてデータをBlenderに取り込み、Blenderの公式エクスポータでglTF2.0出力するのが良さそうなのですが、スケルタルアニメーションやボーン構造が問題なく取り込めるかはやって見ないとわからないですね。

 (意外と、後述するSketchfabを介する方法は試してみる価値があるかもしれません。本来、変換ツールとして使うものではないんですが…)

 もっとも、この辺りは日々事情が変わっていきますので、そのうち状況が改善する可能性があります。こちらの記事でも、情報のフォローアップをしていければと思います。

glTF2.0のPBRマテリアルを正しく出力するための方法

今のところ(2017年10月現在)、glTFコミュニティにおいてもPBRマテリアル情報の出力できちんとしたサポートを予定しているのはBlenderのみのようです。Blenderに搭載されいている物理ベースオフラインレンダラー「Cycles」が提供するPBRマテリアルであるPrinciple Node のマテリアル設定を、glTF2.0に問題なく出力できるよう、Blender用のglTFエクスポータープロジェクト は対応を進めているようです。

glTFアセットの入手方法

 自分でglTFファイルを作成するのが億劫だという方もいるでしょう。

 Sketchfab という、海外ではかなり有名な3Dビューアーサービスがあります。こちらが、作品のglTF形式でのダウンロードに対応いまして、非常に多くのglTF2.0ファイルを手に入れることができます。クリエイティブ・コモンズのライセンスで配布されているものも多いので、非常に使い勝手が良いのではないでしょうか(ご利用にあたっては、ライセンスは都度、作品ごとにご確認ください)。

 このSketchfabに3Dファイルを投稿して、それを自分でglTF2.0ファイルとしてダウンロードする、という変換方法もあるでしょうね(もちろん、アップロードした3Dデータは公開状態になってしまうわけですが、)。

バイナリデータへのアクセス

 さて、大体のデータ構造はわかりましたが、実際にglTFのモデルをWebGLなどの3D APIで画面に表示するためには、JSON部分のメタデータだけでは情報が足りません。
実際のモデルの頂点データなどは、.binファイルにバイナリデータとして収められています。つまり、バイナリデータへのアクセスが必要です。

 では具体的にどうやってアクセスすれば良いのか。そのためのバイナリ構造についての情報は、.gltfファイルのbuffersbufferViewsaccessorsの項目が教えてくれます。

https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#binary-data-storage 

なお、glTFにおけるバイナリデータは、全てリトルエンディアンとして扱います。

バイナリデータの構成

glTFでは.gltf(JSON)によるメタデータとそこから参照される実データ(3D形状やアニメーション、スキン情報など)の2つからなります。

後者の実データはバイナリデータ(仕様書ではBinary Blobと表現しています)です。.gltfファイル中のメタデータ上は1つまたは複数のbufferがそれらに対応します。
一般的には.binファイルに実際のバイナリデータが収められており、その場合は各buffer部分のuriプロパティに.binファイルへのパスが記載されています。

メモリレイアウトを決める3要素:Buffer、 BufferView、 Accessor

このバイナリデータ領域(buffer)には、任意の数のジオメトリ、アニメーション、スキン関連のデータなど、異なる種類のデータがまとめて格納されています。

Bufferの中のそれぞれのデータの区画はBufferViewという単位で区切られています。

またBufferViewの区画のデータの中で、さらにいくつかの細かい種類にデータが分かれている場合があります。それらの異なるデータ種類に適切にアクセスできるよう、アクセス位置を制御するために必要な情報がAccessorです。

Buffer, BufferView, Accessorの関係を図にするとこのようになります。

Memory (4).jpg

AccessorにはbyteOffsetというパラメータがあり、BufferViewの先頭アドレスから見たときのバイトオフセット(データの開始位置を後ろにずらすバイト数)が指定されています。
これとあわせてBufferViewbyteStrideというバイトサイズ(その種類のデータの次の要素までのバイト距離)を考慮することで、各データ種類の次の要素を走査することができます。

頂点データ(位置座標や法線ベクトルなどを含む)のデータ範囲を示すのがBufferViewであり、その頂点データ内の位置座標や法線ベクトルなどにアクセスする際に情報がAccessorに含まれています。

このBufferViewbyteStrideAccessorbyteOffset次第で、頂点データの各頂点属性(位置座標や法線ベクトル、テクスチャ座標)などを、通常の詰め方(SoA: Structure of Array)ではなくインターリーブ構成(AoS: Array of Structure)にすることも可能です。

buffers

 まずは、buffersについて見てみましょう。以下にサンプル例を示します。

"buffers": { "Box": { "byteLength": 648, "type": "arraybuffer", "uri": "Box.bin" } },

 Box.bin という実際の.binバイナリファイルが指定されており、またそのデータ長が648バイトであると書かれていますね。まぁ、ここは難しくないですね。

bufferViews

 次に、bufferViewsを見てみます。サンプルを示します。

"bufferViews": [ { "buffer": 0, "byteLength": 25272, "byteOffset": 0, "target": 34963 }, { "buffer": 0, "byteLength": 76768, "byteOffset": 25272, "byteStride": 32, "target": 34962 } ]

 このbufferViewは何かというと、前述のbufferが示す大元のバイナリデータの、サブセット領域を定義するものです。
"buffer"で大元のバイナリデータへの参照(buffersデータ配列中のインデックス)を、
"byteLength"でこのサブセット領域のバイト長を、
"byteOffset"でこのサブセット領域が大元のバイナリデータの先頭から何バイト目から始まるのか、
をそれぞれ示しています。
また、"target": 3496334963 は OpenGLマクロ定数でいうところの ELEMENT_ARRAY_BUFFERを指しており、つまりインデックスバッファとして使われることを意味しています。
"target": 3496234962ARRAY_BUFFERつまり、頂点バッファとして使われることを意味しています。

 "byteStride"はデータ要素間のバイト距離(例えば、頂点の位置座標データなら、n頂点目とn+1頂点目の開始バイト間の距離)を表します。
ちなみにOpenGLやWebGLでのbyteStrideの考え方では、0の場合は特別扱いで1つ1つのデータはぴったり隣り合っていると解釈します。しかしglTF2.0ではbyteStrideの最小値が4bytesであり、0は許容されていません。OpenGLやWebGLのように0を用いた運用が役立つのはvec3(頂点座標や法線座標)やvec2(テクスチャ座標)などの異なる要素数の頂点属性を同じバッファ内に混載させる場合です。glTF2.0では0が許容されていないので、異なる要素数(データサイズ)の頂点属性を同じBufferViewに含めることはできません。データサイズが異なる頂点属性は別々のBufferViewに配置することになります。
byteStrideによる固定サイズでアクセス幅をずらし全ての頂点属性を隣合わせるAoS構成であれば、異なる要素数(データサイズ)の頂点属性を同一のBufferfViewに配置することが可能です。

※glTF1.0では、byteStrideフィールドはaccessor項目にありましたが、glTF2.0でbufferView項目に移動しました。

glTFの値で時々出てくる謎の整数値は何?

例えば、先ほどの

"bufferViews": [ { "buffer": 0, "byteLength": 72, "byteOffset": 0, "target": 34963 } ]

 でいうなら、 34963 ですよね。glTFでは、こういう謎の整数値がバンバン出てきます。こういうのを見つけたら、ここ を参照してください。そうです。つまりこれらはWebGL(OpenGL)のマクロ定数です。

 例えば 34963 を調べてみると、 ELEMENT_ARRAY_BUFFER を意味していることがわかります。ぱっと見の視認性は悪いですが、プログラムで実際にパースするときは、文字列ではなく整数値なので、直接の判別処理ができるため、ヘタに文字列であるよりこちらの方がいいですね。
ただ、GL系以外の3D API処理系でglTFを扱う際は、同様の対応表をプログラムに用意させるか、gl.hなどのOpenGLのヘッダファイルをインクルードする必要があるため、そこはちょっと不便かもしれませんね。

accessors

 さて、次はaccessorsを見てみます。サンプルを示します。

"accessors": [ { "bufferView": 0, "byteOffset": 0, "componentType": 5123, "count": 12636, "max": [ 4212 ], "min": [ 0 ], "type": "SCALAR" }

 accessorは、前述の“bufferViews”で定義されているバッファのサブ領域内から、具体的にどのような形で、1つ1つのデータ(例えば頂点座標など)を読んでいけばいいかを定義しています。

 "accessor_25"を見てみると、
"bufferView": で参照するバイナリデータのサブセット領域(bufferViewsデータ配列中のインデックス)を指定しています。
そして、
"byteOffset"で、そのサブセット領域の先頭から何バイト目から読み込みを開始すべきかを指定しています。
前項のbufferViewの"byteOffset"とこのaccessorの"byteOffset"の合算オフセット位置、そして前項のbufferViewの"byteStride"が、それぞれOpenGL/WebGLのgl.vertexAttribPointer関数で指定する、オフセットとストライドに対応すると思ってください。
(なので、今の所ほとんどのデータはそうなっていないですが、もしかしたらそのうちインターリーブなデータレイアウトのファイルも出てくるかもしれませんね。)
"type": は、データがスカラー型なのか、ベクトル型(VEC2/VEC3/VEC4)なのかを示しています。
"componentType": 5123はOpenGL/WebGLマクロ定数でいうところのUNSIGNED_SHORT型を意味しています(そして"type"は"SCALAR"なので、おそらくこれは頂点インデックスの情報なのでしょうね)。
"count"は、何個分データが入っているか、ですね。
"max"と"min"は、このaccessor内の全データでの最大値と最小値ですね。 

ここまで情報が提示されていれば、ちゃんとバイナリデータからデータを取ってくることができます。

こうしたバイナリデータへのアクセス方法については、glTF2.0仕様書の以下の箇所の記述が役立ちます。
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#binary-data-storage