3D画像ファイルのアニメーションとは?
glTFファイルのアニメーションを解きほぐす
はじめに
2次元(以下2D)画像でも3次元(以下3D)画像でも、動画に変換された画像を鑑賞するというアニメーションとして知られる映像文化が広く普及しています。
連続する静止画を1秒間に数十コマの時間間隔で表示すれば、いわゆる映画として知られる動画になります。
昔の映画は静止画がそのまま記録されていたので膨大な情報量を持っていましたが、デジタル社会のニーズに応えるため、データ量の大幅な圧縮が必要になってこれまで多くのデータ削減技術が開発され、今も開発が続けられています。
2Dや3D画像のアニメーションも、静止した2Dや3D画像のデータをできるだけ増大させないように、数々の工夫を施して動画に変換している技術であると言えるでしょう。
アニメーションさせるデータを画像ファイルに組み込んで、所定のアニメーションを動かすという仕組みも開発されてきており、2D画像ファイルに対してはアニメーションGIFファイルやAPNGファイルなどがあります。
3D画像ファイルに対しても、アニメーションデータが組み込まれたファイルがあります。
以前ので、3Dデータフォーマットの決定版とされるglTFファイルフォーマットを調べましたが、そのglTFファイルにもアニメーションデータが格納されるようになっています。
そのglTFファイルについては、
引用:VRM
という記事があって、人型3Dアバター用のVRMファイルはglTFファイルをベースにしているのですが、
引用:VRM のアニメーション
と記載され、VRM自体にはAnimationが使われないとされています。
このVRMファイルに対して、
引用:VRM はローカル軸の方向を破棄すべきでない
という、アニメーションの要素の一つである「ボーン」についての問題提起のような記事がありました。
「ボーン」を使ったアニメーションは、骨格となるボーン(Bone)を設定してそのBoneに付随する3D物体の動きを制御するアニメーションです。
複数個のBoneを組み合わせて使用されるのが一般的で、人物や動物、その他無機物でも動きがある物体に適用されて広く普及しています。
Boneの用語は別の呼び方の例もあるのですが、ここではボーンアニメーションと呼んでおきます。
ウィキペディアには、
「この技術は一連の「骨格」を構築することにより利用される。この構築作業は「リギング」(rigging) と呼ばれることがある。」
「スケルトンを構成するそれぞれの骨にはキャラクターを可視化する為の要素を関連付けておく。この関連付けを「スキニング」と言う。」
引用:スケルタルアニメーション
と記されています。
アニメーション技術には上記のように多くの専門用語が登場して、何となくそれら要素技術が分かったつもりになっていても具体的な挙動を把握するのは容易ではありません。
そもそも3D画像ファイルのアニメーションはどういうものであって、glTFファイルのアニメーションデータはどういう構造になっているのでしょうか?ボーンアニメーションのデータはどのように扱われているのでしょうか?
そのglTFファイルのボーンアニメーションについては、
primitive の attributes にジョイントインデックス JOINTS と ウェイト値 WEIGHTS が必要です.JOINTS や WEIGHTS はセマンティクスと呼ばれ,1つの頂点データに同じセマンティクスのデータを複数入れることができます.これはセマンティクス名の後ろにインデックスを追加して指定します.」
引用:glTF 覚え書き
という解説記事や、
「頂点の情報にはボーンのインデックスとウェイト (影響度) が追加されます。」
「joints はボーンを構成するノードの識別子の配列です。」
引用:vertex skinning の覚え書き
という記事が参考になります。
今回は、glTFファイルのアニメーションデータの構造を解きほぐすことによってアニメーションの基本的で具体的な技術を学ぶことにします。
glTF 2.0ファイルフォーマットのアニメーションデータ
glTF 2.0ファイルフォーマットの仕様については、The glTF 2.0 Specificationに書かれています。
glTFのアニメーションデータは、
・animation.channels
・animation.samplers
のデータが必須となっています。
他に、
・animation.name
・animation.extensions
・animation.extras
などのデータがあります。
そのらのデータの意味や役割を以下に具体的に説明していきます。
3D物体の移動アニメーション
最初に簡単な例として3D物体を単純に移動させるだけのアニメーションを取り上げます。
具体例1(立方体の移動)
具体例1は立方体を一方向に単純に移動するだけのアニメーションです。
具体例1のglTFファイル生成:
以前のや、
と同様に、glTFファイルの生成には、著名なオープンソースの3DCGソフトであるBlenderから出力されたファイルを用います。
Blenderのデフォルト状態(筆者の環境でBlenderインストールした直後の状態なのでデフォルトとしておく)の立方体で、animationモードで0フレームを登録し、立方体をx位置1mにして120フレームに登録します。終了は140フレームに設定しました。
後で述べるglTFデータを分かり易くするため、1フレームでなく0フレームを開始点として登録しています。
また同様に分かり易くするため、アニメーション速度を一定にするように、デフォルト補間モードをリニアに設定しています。
animationモードは24Hzの周期になっているので、120フレームは120/24=5秒後に相当します。立方体が5秒間でx方向に1m移動するというアニメーションです。
レンダリングを操作し、120フレームに達してanimation記録が完了したら、メッシュモードなし、カメラ設定ONにしてglTFファイルであるサンプル1.glbをエクスポートしました。
Blenderでのアニメーションの様子を見ていただくために、Blenderの出力機能で動画であるaviファイルを出力し、そのaviファイルを適当な外部ツールでデータ圧縮されたwebmファイルに変換した動画を【図1】に表示します。
aviファイルはデータ容量が大きいので、ブラウザで表示させるためにデータ圧縮されたwebmファイルに変換しています。
具体例1のglTFファイルのデータ:
Blenderからエクスポートされたサンプル1.glbファイルのJSONデータを調べると、ジオメトリを示す"meshes"情報の"primitives"は、
"primitives"
{
"attributes":
{
"POSITION":0
},
"indices":1,
"material":0
}
となっていて、の具体例1とanimationしか変わっていないので、この"primitives"データは同じです。
アニメーションデータを示す"animations"情報は、
"animations": [
{
"channels": [
{
"sampler": 0,
"target": {
"node": 0,
"path": "translation"
}
}
],
"name": "CubeAction.001",
"samplers": [
{
"input": 2,
"interpolation": "LINEAR",
"output": 3
}
]
}
],
となっています。
"nodes"情報は、
カラム0が、
"nodes": [
{
"mesh": 0,
"name": "Cube"
}
],
カラム1省略
となっています。
animation.channelsデータは、"node": 0の物体(Cube)が"translation"(移動)する"CubeAction.001"という名のアニメーションであることを示しています。
animation.samplersデータは、"input": 2がアニメーションのフレーム時間、"output": 3がアニメーションのキーフレームと呼ばれるフレームの状態変化データを示しており、"accessors"情報のカラム2に"input"のデータ、カラム3に"output"のデータが格納されます。
"interpolation": "LINEAR"は、補間方法がリニアであることを示していますが意味はなさそうです。後述します。
"accessors"情報は、
"accessors": [
カラム0省略
カラム1省略
カラム2は、
{
"bufferView": 2,
"componentType": 5126,
"count": 121,
"max": [
5
],
"min": [
0
],
"type": "SCALAR"
},
カラム3は、
{
"bufferView": 3,
"componentType": 5126,
"count": 121,
"type": "VEC3"
}
となっています。
カラム2の"input"のデータは、最小値0秒、最大値5秒の121個のフレーム時間データがあることを示しています。
フレーム0からフレーム120までは121フレームあり、それぞれのフレーム時間が、"componentType": 5126により4Byteの浮動小数点(単精度float)で"bufferView": 2のバッファに格納されています。
その"bufferView": 2には、
0、0.04167、0.08333、・・・、5
のデータが格納されていて、0秒から5秒までの5/120=0.04167刻みで変化する時間データになっています。
カラム3の"output"のデータは、121個ある各フレーム毎のx位置、y位置、z位置の3個のデータが"componentType": 5126により4Byteの浮動小数点(単精度float)で"bufferView": 3のバッファに格納されています。
その"bufferView": 3には、
(0、0、0)、(0.00833、0、0)、(0.01667、0、0)、・・・、(1、0、0)
のデータが格納されていて、x位置だけが0mから1mまでの1/120=0.00833刻みで変化する位置データになっています。
具体例1のglTFファイルを用いたアニメーション:
生成されたglTFファイルを用いてアニメーションするには、three.jsを利用したJavascriptを用います。
three.jsは「ウェブブラウザ上でリアルタイムレンダリングによる3次元コンピュータグラフィックスを描画する、クロスブラウザ対応の軽量なJavaScriptライブラリおよびAPIである。」
引用:Three.js
と書かれており、広く利用されているライブラリです。
three.jsでサンプル1.glbファイルのデータ(geometry、material、camera、animation)と上記"output"のデータを読み取ります。
Blenderの24Hzに合わせるように描画周期を調整して、立方体の位置をその"output"のデータに逐次変更します。
この実行結果を【図2】に示します。ブラウザがこのJavascriptを実行して表示しています。
【図1】と同様のアニメーションがされていることが分かると思います。
具体例1の解説
キーフレームデータが格納されていない場合に、この"interpolation"が効くのでしょうか?
"min": [
0.0416666666666667
],
が格納されます。
これでは混乱を招くのではと思い、"min"が0となるようにフレーム0をここでは開始点に選んでいます。
具体例1ではこれを140に設定して、そのフレーム140のキーフレーム登録はしておりません。
Blender内でこのアニメーションを動作させると、【図1】に示すようにフレーム0から140まで移動するアニメーションになります。
glTFにはこの終了フレームデータは格納されておらず、終了点のフレーム120だけが終了フレームとみなされることになります。
ちなみにこのサンプル1.glbをBlenderでインポートしてアニメーションさせると、フレーム250までの移動動作になります。
フレーム120からフレーム250までのデータはないので、フレーム120の状態のままフレーム250まで時間だけが進んでアニメーションが終わります。
【図1】は【図2】と違って、フレーム120のところで(140-120)/24=0.833秒だけ停止していることになります。
こういう差異をなくすには、Blenderの終了フレームは終了点と同じにしておくことが必要です。
ボーンアニメーション
次に3Dアニメーションで基本的な技術の一つになっているボーンアニメーションを取り上げます。
ここではボーンアニメーションに用いられるglTFファイルの構造をできるだけ具体的に説明します。
具体例2(1個のボーンだけの回転)
具体例2は1個のボーン(Bone)だけがX軸周りに90゜回転するだけの単純なアニメーションで、まだBoneに付随する3D物体との関連付けは行っていません。
ボーンアニメーションは複数個のBoneを関節部を介して結合して用いられることが多いのですが、先ずは単独での動きを調べてBoneのデータがどのように扱われているかを示します。
具体例2のglTFファイルの生成:
Blenderを用い具体例1と同じ立方体で、「オブジェクトの追加」機能で1個のアーマチュア(BlenderではBoneのこと)を設定します。
立方体(Cube)は位置関係を示すために削除しないで非表示にしておきます。
デフォルト状態でアーマチュアボタンをクリックすると【図3】に示す上向き(Blenderでは+z方向)の8角形(上に延びた4角錐の底に小さな4角錐が連結した形)のBoneが設定されます。
このBoneの形状はいろいろな形に変更できますがここではデフォルトの8角形にしておきます。
位置、回転は0、スケールは1になっています。
Boneの動きを分かり易くするためにスケールを2に変更します。
立方体は頂点が±1.0になっているので、Boneが立方体の中心から立方体をはみ出た形になります。
具体例1と同様に、animationモードでアーマチュアの上記初期状態を0フレームを登録し、アーマチュアのx回転角を90゜にして120フレームに登録します。終了は140フレームに設定しました。
アニメーション速度を一定にするように、デフォルト補間モードをリニアに設定しています。
具体例1と同様に、レンダリングを操作し、120フレームに達してanimation記録が完了したら、メッシュモードなし、カメラ設定ONにしてglTFファイルであるサンプル2.glbをエクスポートしました。
具体例1と同様に、Blenderでのアニメーションの様子を見ていただくために、Blenderの出力機能で動画であるaviファイルを出力し、そのaviファイルを適当な外部ツールでデータ圧縮されたwebmファイルに変換した動画を【図4】に表示します。
この出力には、立方体は位置確認用のために配置しているので、「ビューポート表示」で「ワイヤーフレーム」にチェックしてワイヤーフレームにしておきます。
このワイヤーフレームの情報はglTFファイルには格納されません。
具体例2のglTFファイルのデータ:
Blenderからエクスポートされたサンプル2.glbファイルのJSONデータを調べると、ジオメトリを示す"meshes"情報の"primitives"は、
"primitives"
{
"attributes":
{
"POSITION":0
},
"indices":1,
"material":0
}
となっていて、具体例1と同じです。
具体例2ではボーンがあるのですが、まだ関連付ける物体を設定していないからボーンアニメーションは行っていないことになり、"primitives"にボーンアニメーションを制御する"JOINTS"データは入っていません。
"nodes"情報は、
"nodes": [
カラム0省略
カラム1省略
{
"name": "Bone"
},
{
"children": [
2
],
"name": "30a230fc30de30c130e530a2", ※アーマチュアの意
"scale": [
2,
2,
2
]
}
となっていて、Boneデータは"nodes"情報のカラム2、3に格納されています。
"Bone"と"アーマチュア"の2つが混在していて分かりにくいのですが、Blenderからこのような形にエクスポートされています。
"アーマチュア"のTRSプロパティであるPosition、rotationはそれぞれデフォルトの(0, 0, 0)、(0, 0, 0, 1)であるので省略され、変更していたscaleの(2, 2, 2)だけが格納されています。
アニメーションデータを示す"animations"情報は、
"animations": [
{
"channels": [
{
"sampler": 0,
"target": {
"node": 2,
"path": "translation"
}
},
{
"sampler": 1,
"target": {
"node": 2,
"path": "rotation"
}
},
{
"sampler": 2,
"target": {
"node": 2,
"path": "scale"
}
},
{
"sampler": 3,
"target": {
"node": 3,
"path": "rotation"
}
}
],
"name": "30a230fc30de30c130e530a2Action", ※アーマチュアActionの意
"samplers": [
{
"input": 2,
"interpolation": "LINEAR",
"output": 3
},
{
"input": 2,
"interpolation": "LINEAR",
"output": 4
},
{
"input": 2,
"interpolation": "LINEAR",
"output": 5
},
{
"input": 2,
"interpolation": "LINEAR",
"output": 6
}
]
}
],
となっています。
animation.channelsデータは、カラム0、カラム1、カラム2が"node": 1の"Bone"という名前のオブジェクトの"translation"(移動)、"rotation"(回転)、"scale"(拡大縮小)を示しています。カラム3は、"アーマチュア"という名前のオブジェクトの"rotation"(回転)を示していて、"アーマチュアaction"という名前のアニメーションであることを示しています。
animation.samplersデータは、"input": 2がアニメーションのフレーム時間、"output": 3、4、5、6がアニメーションのキーフレームと呼ばれるフレームの状態変化データを示しており、"accessors"情報のカラム2に"input"のデータ、カラム3、4、5、6に"output"のデータが格納されます。
"sampler": 0、1、2は"node": 2を示しているので、"Bone"という名前のオブジェクトに対する制御を表します。
"sampler": 3は"node": 3を示しているので、"アーマチュア"という名前のオブジェクトに対する制御を表すことになります。
従って、カラム3、4、5の"output"は"Bone"についての状態変化データ、カラム6の"output"は"アーマチュア"についての状態変化データになります。
"accessors"情報は、
"accessors": [
カラム0省略
カラム1省略
カラム2は、
{
"bufferView": 2,
"componentType": 5126,
"count": 121,
"max": [
5
],
"min": [
0
],
"type": "SCALAR"
},
カラム3は、
{
"bufferView": 3,
"componentType": 5126,
"count": 121,
"type": "VEC3"
},
カラム4は、
{
"bufferView": 4,
"componentType": 5126,
"count": 121,
"type": "VEC4"
},
カラム5は、
{
"bufferView": 5,
"componentType": 5126,
"count": 121,
"type": "VEC3"
},
カラム6は、
{
"bufferView": 6,
"componentType": 5126,
"count": 121,
"type": "VEC4"
},
となっています。
カラム2の"input"のデータは、具体例1と同じで最小値0秒、最大値5秒の121個のフレーム時間データがあることを示しています。
フレーム0からフレーム120までは121フレームあり、それぞれのフレーム時間が、"componentType": 5126により4Byteの浮動小数点(単精度float)で"bufferView": 2のバッファに格納されています。
その"bufferView": 2には、具体例1と同様に
0、0.04167、0.08333、・・・、5
のデータが格納されていて、0秒から5秒までの5/120=0.04167刻みで変化する時間データになっています。
カラム3の"output"のデータは、121個ある各フレーム毎のx位置、y位置、z位置の3個のデータが単精度floatで"bufferView": 3のバッファに格納されているのですが、121個全部が(0、0、0)になっていて、移動しないことを示しています。
カラム4の"output"のデータは、121個ある各フレーム毎の回転角を表すquaternionの4個のデータが単精度floatで"bufferView": 4のバッファに格納されているのですが、121個全部が(0、0、0、1)になっていて、回転しないことを示しています。
カラム5の"output"のデータは、121個ある各フレーム毎の拡大縮小を表すxスケール、yスケール、zスケールの3個のデータが単精度floatで"bufferView": 5のバッファに格納されているのですが、121個全部が(1、1、1)になっていて、初期状態のまま拡大縮小しないことを示しています。
"アーマチュア"の状態変化データであるカラム6の"output"のデータは、121個ある各フレーム毎の回転角を表すquaternionの4個のデータが単精度floatで"bufferView": 6のバッファに格納されており、
(0、0、0、1)、(0.006545、0、0、0.999979)、(0.01309、0、0、0.999914)、・・・、(0.707107、0、0、0.707107)
のデータが格納されていて、Y軸からZ軸に0゜から90゜まで徐々に回転していくデータになっています。
具体例2のglTFファイルを用いたアニメーション:
具体例2は1個のボーン(Bone)だけがX軸周りに90゜回転するだけの単純なアニメーションで、まだBoneに付随する3D物体との関連付けは行っていません。
ボーンアニメーションは複数個のBoneを関節部を介して結合して用いられることが多いのですが、先ずは単独での動きを調べてBoneのデータがどのように扱われているかを示します。
具体例1と同様に、生成されたglTFファイルを用いてアニメーションするには、three.jsを利用したJavascriptを用います。
three.jsでサンプル1.glbファイルのデータ(geometry、material、camera、animation)と上記アーマチュア(Bone)の"output"のデータを読み取ります。
Blenderの24Hzに合わせるように描画周期を調整して、逐次Boneの位置にその"output"のデータ(quaternionデータ)を用いた回転処理を施します。
この実行結果を【図5】に示します。ブラウザがこのJavascriptを実行して表示しています。
three.jsでは、Boneは緑から青へのグラディエーションになっている線で表示されます。
このBoneの形状もthree.jsで変更できますがデフォルト状態の線のままにしています。
【図4】と同様のBoneアニメーションがされていることが分かると思います。
立方体はBoneの位置関係を示すために、three.jsでワイヤフレームにした立方体を表示させています。
具体例2の解説
通常は、このBoneの動きに合わせて、付随する3D物体が動くアニメーションが構成されます。
具体例2では付随する3D物体を設定していないので、glTFファイルにはボーンアニメーションで用いられる、ジョイントインデックス JOINTS や ウェイト値 WEIGHTSは格納されていません。
具体例2のBoneは、BoneのTRSプロパティであるPosition、rotationはそれぞれデフォルトの(0, 0, 0)、(0, 0, 0, 1)であるので省略され、変更していたscaleの(2, 2, 2)だけが格納されています。
従って、Boneは中心(0, 0, 0)から大きさ2で上(BlenderではZ軸、glTFではY軸)方向に回転しないで延びた線ベクトルということになります。
3D物体と一緒に線ベクトルを表示するのは難しいので、Blenderでは8角形の形、three.jsでは緑から青へのグラディエーションの線で表示しています。
のように計121個の格納されています。
glTFファイルを用いてアニメーションする際は、そのquaternionデータを読み取って、読み取ったquaternionデータよりBoneの位置を計算して逐次表示しています。
具体例3(ボーンに付随する立方体の回転)
具体例3は、具体例2の1個のボーン(Bone)だけがX軸周りに90゜回転するアニメーションに対して、Boneに付随する立方体と関連付けを行ったアニメーションです。
ボーンアニメーションは、Boneの動きに関連付けされる3D物体の要素(頂点やポリゴンや立体)と、その関連付けの度合い(weightデータと呼ばれる)を定めることによって複雑な立体表面の動きが制御されます。
この機能を単純化して、1個のBoneに対して単純な立方体の頂点を関連付けることにして分かり易くしてみます。
具体例3のglTFファイルの生成:
Blenderを用い具体例2と同じに、立方体で1個のBoneを設定します。Boneの動きを分かり易くするためにスケールを2に変更します。
立方体とBoneを「オブジェクトモード」-「ペアレント」-「自動のウェイトで」に設定します。
これにより立方体の全頂点がBoneと関連付けられたことになります。
具体例2と同様に、animationモードでアーマチュアの上記初期状態を0フレームを登録し、アーマチュアのx回転角を90゜にして120フレームに登録します。終了は140フレームに設定しました。
アニメーション速度を一定にするように、デフォルト補間モードをリニアに設定しています。
具体例2と同様に、レンダリングを操作し、120フレームに達してanimation記録が完了したら、メッシュモードなし、カメラ設定ONにしてglTFファイルであるサンプル3.glbをエクスポートしました。
具体例1や2と同様に、Blenderでのアニメーションの様子を見ていただくために、Blenderの出力機能で動画であるaviファイルを出力し、そのaviファイルを適当な外部ツールでデータ圧縮されたwebmファイルに変換した動画を【図6】に表示します。
この出力も、「ビューポート表示」で「ワイヤーフレーム」にチェックしてワイヤーフレームにしておきます。
このワイヤーフレームの情報はglTFファイルには格納されません。
具体例3のglTFファイルのデータ:
Blenderからエクスポートされたサンプル3.glbファイルのJSONデータを調べると、ジオメトリを示す"meshes"情報の"primitives"は、
"primitives"
{
"attributes":
{
"POSITION":0
"JOINTS_0": 1,
"WEIGHTS_0": 2
},
"indices":3,
"material":0
}
となっていて、具体例1や2に対して"JOINTS_0": 1と"WEIGHTS_0": 2が加わっています。
"nodes"情報は、
"nodes": [
カラム0省略
{
"name": "Bone"
},
{
"mesh": 0,
"name": "Cube",
"skin": 0
},
{
"children": [
2
1
],
"name": "30a230fc30de30c130e530a2", ※アーマチュアの意
"scale": [
2,
2,
2
]
}
となっていて、Boneデータは具体例2と異なり、"nodes"情報のカラム1に"name": "Bone"が入り、カラム2に"Cube"の情報、カラム3に"children"が2と1に設定されています。
これにより、カラム1の"Bone"とカラム2の"Cube"が関連付けられていることになります。
BoneのTRSプロパティであるPosition、rotationは具体例2と同様にそれぞれデフォルトの(0, 0, 0)、(0, 0, 0, 1)であるので省略され、変更していたscaleの(2, 2, 2)だけが格納されています。
アニメーションデータを示す"animations"情報は、
"animations": [
{
"channels": [
{
"sampler": 0,
"target": {
"node": 1,
"path": "translation"
}
},
{
"sampler": 1,
"target": {
"node": 1,
"path": "rotation"
}
},
{
"sampler": 2,
"target": {
"node": 1,
"path": "scale"
}
},
{
"sampler": 3,
"target": {
"node": 3,
"path": "rotation"
}
}
],
"name": "30a230fc30de30c130e530a2Action", ※アーマチュアActionの意
"samplers": [
{
"input": 5,
"interpolation": "LINEAR",
"output": 6
},
{
"input": 5,
"interpolation": "LINEAR",
"output": 7
},
{
"input": 5,
"interpolation": "LINEAR",
"output": 8
},
{
"input": 5,
"interpolation": "LINEAR",
"output": 9
}
]
}
],
となっていて、番号はnodesの値などによって異なりますが、具体例2と同等です。
animation.channelsデータとanimation.samplersデータは、具体例2と同等なのですが番号が異なっているので、再度記載しておきます。
animation.channelsデータは、カラム0、カラム1、カラム2が"node": 1の"Bone"という名前のオブジェクトの"translation"(移動)、"rotation"(回転)、"scale"(拡大縮小)を示しています。カラム3は、"アーマチュア"という名前のオブジェクトの"rotation"(回転)を示していて、"アーマチュアAction"というnameのアニメーションであることを示しています。
animation.samplersデータは、"input": 5がアニメーションのフレーム時間、"output": 6、7、8、9がアニメーションのキーフレームと呼ばれるフレームの状態変化データを示しており、"accessors"情報のカラム5に"input"のデータ、カラム6、7、8、9に"output"のデータが格納されます。
"sampler": 0、1、2は"node": 1を示しているので、"Bone"という名前のオブジェクトに対する制御を表します。
"sampler": 3は"node": 3を示しているので、"アーマチュア"という名前のオブジェクトに対する制御を表すことになります。
従って、カラム6、7、8の"output"は"Bone"についての状態変化データ、カラム9の"output"は"アーマチュア"についての状態変化データになります。
"accessors"情報は、
"accessors": [
カラム0省略
カラム1は、
{
"bufferView": 1,
"componentType": 5121,
"count": 8,
"type": "VEC4"
},
カラム2は、
{
"bufferView": 2,
"componentType": 5126,
"count": 8,
"type": "VEC4"
},
カラム3省略
カラム4省略
カラム5は、
{
"bufferView": 2,
"componentType": 5126,
"count": 121,
"max": [
5
],
"min": [
0
],
"type": "SCALAR"
},
カラム6は、
{
"bufferView": 3,
"componentType": 5126,
"count": 121,
"type": "VEC3"
},
カラム7は、
{
"bufferView": 4,
"componentType": 5126,
"count": 121,
"type": "VEC4"
},
カラム8は、
{
"bufferView": 5,
"componentType": 5126,
"count": 121,
"type": "VEC3"
},
カラム9は、
{
"bufferView": 6,
"componentType": 5126,
"count": 121,
"type": "VEC4"
},
となっています。
カラム1は"JOINTS_0"のデータで、"componentType": 5121によりunsigned byteで8個の(0、0、0、0)が格納されています。
8個のデータはCubeの頂点に対応しており、その順番は"POSITION": 0に格納されている座標データの格納順です。
(0、0、0、0)は各頂点が関連付けられるBoneの番号で、ここでは1個のBoneなので1番目に0が格納されています。
2~4番目は不使用なので0が入っています。
カラム2は"WEIGHTS_0"のデータで、"componentType": 5126により4Byteの浮動小数点(単精度float)で8個の(1、0、0、0)が格納されています。
"JOINTS_0"データと同様に、8個のデータはCubeの頂点に対応しています。
(1、0、0、0)は各頂点がBoneに関連付けられる重み(weight)を示していて、1が100%、0が0%の重みになっています。
ここでは全頂点が100%Boneに関連付けられているので1番目に1が格納されています。
2~4番目は不使用なので0が入っています。
全頂点が同じ重みであるので8個のデータが全部(1、0、0、0)になっています。
カラム5の"input"のデータは、具体例1や2と同じで最小値0秒、最大値5秒の121個のフレーム時間データがあることを示しています。
カラム6の"output"、カラム7の"output"、カラム8の"output"、カラム9の"output"データはデータは、具体例2の"output"のデータと同じです。すなわち、"Bone"の"output"のデータの"translation"は、121個全部が(0、0、0)になっているので移動しない、"output"のデータの"rotation"は、121個全部が(0、0、0、1)になっているので回転しない、"output"のデータの"scale"は、121個全部が(1、1、1)になっているので拡大縮小しない、ことを示しています。
そして、"アーマチュア"の状態変化データであるカラム9の"output"のデータは、121個ある各フレーム毎の回転角を表すquaternionの4個のデータが単精度floatで"bufferView": 9のバッファに格納されており、
(0、0、0、1)、(0.006545、0、0、0.999979)、(0.01309、0、0、0.999914)、・・・、(0.707107、0、0、0.707107)
のデータが格納されていて、Y軸からZ軸に0゜から90゜まで徐々に回転していくデータになっています。
具体例3のglTFファイルを用いたアニメーション:
具体例3は1個のボーン(Bone)だけがX軸周りに90゜回転し、Boneに付随する立方体3D物体と全頂点100%の重みで関連付けたアニメーションです。
具体例1や2と同様に、生成されたglTFファイルを用いてアニメーションするには、three.jsを利用したJavascriptを用います。
three.jsでサンプル3.glbファイルのデータ(geometry、material、camera、animation)と上記アーマチュア(Bone)の"output"のデータと"JOINTS_0"データと"WEIGHTS_0"データを読み取ります。
Blenderの24Hzに合わせるように描画周期を調整して、逐次Boneの位置にその"output"のデータ(quaternionデータ)を用いた回転処理を施します。更に、Cubeの頂点の位置にもその"output"のデータ(quaternionデータ)を用いた回転処理を施します。
この実行結果を【図7】に示します。ブラウザがこのJavascriptを実行して表示しています。
【図6】と同様の立方体が回転するBoneアニメーションがされていることが分かると思います。
立方体は、three.jsでワイヤフレームにした立方体を表示させています。
具体例3の解説
具体例2にはなかったジョイントインデックス JOINTS や ウェイト値 WEIGHTSは格納されています。
同じ変位をすることになります。すなわち、Boneの90゜回転に合わせてCubeも90゜回転するアニメーションになるということです。
前述のglTF 2.0ファイルフォーマット仕様書には、
「The number of joints that influence one vertex is limited to 4 per set, so the referenced accessors MUST have VEC4 type and following component types:
• JOINTS_n: unsigned byte or unsigned short
• WEIGHTS_n: float, or normalized unsigned byte, or normalized unsigned short」
と書かれており、JOINTSやWEIGHTSの数は各頂点に対し最大4個のBoneが設定されます。
を立方体の各頂点の位置座標に施して、立方体の位置を逐次表示しています。
具体例4(ボーンに付随する立方体の回転で1個の頂点だけ回転しない)
具体例4は、具体例3の1個のボーン(Bone)だけがX軸周りに90゜回転し、Boneに付随する立方体と関連付けを行ったアニメーションに対して、1個の頂点だけ回転させないアニメーションです。
具体例3では立方体の8個の全頂点にweight=1が設定されているために、立方体全体がBoneに合わせて回転していましたが、頂点にweight=0が設定されている場合にどのようなボーンアニメーションになるかを示します。
具体例4のglTFファイルの生成:
Blenderを用い具体例3と同じに、立方体で1個のBoneを設定します。Boneの動きを分かり易くするためにスケールを2に変更します。
立方体とBoneを「オブジェクトモード」-「ペアレント」-「自動のウェイトで」に設定します。
これにより立方体の全頂点がBoneと関連付けられたことになりますが、その後Blenderの「ウェイトペイント」機能を使って1個の頂点だけをweight=0に設定します。
具体例3と同様に、animationモードでアーマチュアの上記初期状態を0フレームを登録し、アーマチュアのx回転角を90゜にして120フレームに登録します。終了は140フレームに設定しました。
アニメーション速度を一定にするように、デフォルト補間モードをリニアに設定しています。
具体例3と同様に、レンダリングを操作し、120フレームに達してanimation記録が完了したら、メッシュモードなし、カメラ設定ONにしてglTFファイルであるサンプル4.glbをエクスポートしました。
具体例3と同様に、Blenderでのアニメーションの様子を見ていただくために、Blenderの出力機能で動画であるaviファイルを出力し、そのaviファイルを適当な外部ツールでデータ圧縮されたwebmファイルに変換した動画を【図8】に表示します。
この出力も、「ビューポート表示」で「ワイヤーフレーム」にチェックしてワイヤーフレームにしておきます。
このワイヤーフレームの情報はglTFファイルには格納されません。
1個の頂点をweight=0に設定しているため、立方体がBoneに追従して変形しながら回転しています。
具体例4のglTFファイルのデータ:
Blenderからエクスポートされたサンプル4.glbファイルのJSONデータを調べると、具体例3のサンプル3.glbファイルと同じでした。
weight=0に設定したのにglTFファイルに反映されていません。
サンプル4.glbファイルをBlenderでインポートしてみても【図8】のような挙動にならず、全頂点がweight=1の【図6】と同じ挙動になります。
具体例4のglTFファイルを用いたアニメーション:
glTFファイルにweight=0が反映されていないので、three.jsで立方体のそのweight=0にした頂点だけを回転しないようにしてみました。
この実行結果を【図9】に示します。ブラウザがこのJavascriptを実行して表示しています。
【図8】と同様の立方体が変形しながら回転するBoneアニメーションがされていることが分かると思います。
立方体は、three.jsでワイヤフレームにした立方体を表示させています。
具体例4の解説
何か方法があるのかも知れませんが、今回はその追求を断念しました。
おわりに
作成したglTFファイルを用いたアニメーション描画には、JavaScriptライブラリであるthree.jsを使いました。
今回はthree.jsの高度な機能は使っておりませんが、基本的なアニメーション処理に触れることができました。
アニメーションの基本技術の一端を学べたと思います。
「まえがき」に書いたglTFアニメーションデータの不具合?についても今回の調査では分かりませんでした。
ボーンアニメーションについて更に掘り下げて探っていこうと思っています。