companynameJ

3D画像ファイルのアニメーションとは?

glTFファイルのアニメーションを解きほぐす

 
 

はじめに

 
2次元(以下2D)画像でも3次元(以下3D)画像でも、動画に変換された画像を鑑賞するというアニメーションとして知られる映像文化が広く普及しています。
 
連続する静止画を1秒間に数十コマの時間間隔で表示すれば、いわゆる映画として知られる動画になります。
昔の映画は静止画がそのまま記録されていたので膨大な情報量を持っていましたが、デジタル社会のニーズに応えるため、データ量の大幅な圧縮が必要になってこれまで多くのデータ削減技術が開発され、今も開発が続けられています。
2Dや3D画像のアニメーションも、静止した2Dや3D画像のデータをできるだけ増大させないように、数々の工夫を施して動画に変換している技術であると言えるでしょう。
 
アニメーションさせるデータを画像ファイルに組み込んで、所定のアニメーションを動かすという仕組みも開発されてきており、2D画像ファイルに対してはアニメーションGIFファイルやAPNGファイルなどがあります。
3D画像ファイルに対しても、アニメーションデータが組み込まれたファイルがあります。
 
以前の3D画像ファイルの画像とは?で、3Dデータフォーマットの決定版とされるglTFファイルフォーマットを調べましたが、そのglTFファイルにもアニメーションデータが格納されるようになっています。
 
そのglTFファイルについては、

・「VRM - glTFをベースとした人型3Dアバターのファイルフォーマットの形式」
 引用:VRM

という記事があって、人型3Dアバター用のVRMファイルはglTFファイルをベースにしているのですが、

・「VRM は、Animation を使わないという仕様です。」
 引用:VRM のアニメーション

と記載され、VRM自体にはAnimationが使われないとされています。
 
このVRMファイルに対して、

・「一般的な人型の glTF モデルであれば持っている「ある情報」が VRM には欠けているのです。その情報とは『ボーンのローカル軸の方向』です。」
 引用:VRM はローカル軸の方向を破棄すべきでない

という、アニメーションの要素の一つである「ボーン」についての問題提起のような記事がありました。
 
「ボーン」を使ったアニメーションは、骨格となるボーン(Bone)を設定してそのBoneに付随する3D物体の動きを制御するアニメーションです。
複数個のBoneを組み合わせて使用されるのが一般的で、人物や動物、その他無機物でも動きがある物体に適用されて広く普及しています。
Boneの用語は別の呼び方の例もあるのですが、ここではボーンアニメーションと呼んでおきます。
 
ウィキペディアには、

「スケルタルアニメーション (英: skeletal animation) は、日本ではしばしばボーンアニメーションまたはスキンメッシュアニメーションとも呼ばれ、キャラクターのコンピュータアニメーションに用いられる技術である。骨格(ボーン)を階層的に連結したモデルであるスケルトンを艤装し、それを所定の姿勢(キーフレーム)へとアニメーションさせ、それに従いキャラクターの表面構成点群の位置を追従させ描画する事でキャラクターのアニメーションを実現する。この技術は人間をはじめ多くの一般的な生物的なモデルへ適用でき、一度アニメーションを作成すれば同様のアニメーションを任意のオブジェクトへ適用し、その形状を変化させる事が可能であり、スプーンや建築物のモデルもスケルトンさえ定義すればアニメーションを即座に適用可能である。」
「この技術は一連の「骨格」を構築することにより利用される。この構築作業は「リギング」(rigging) と呼ばれることがある。」
「スケルトンを構成するそれぞれの骨にはキャラクターを可視化する為の要素を関連付けておく。この関連付けを「スキニング」と言う。」
 引用:スケルタルアニメーション

と記されています。
 
アニメーション技術には上記のように多くの専門用語が登場して、何となくそれら要素技術が分かったつもりになっていても具体的な挙動を把握するのは容易ではありません。
 
そもそも3D画像ファイルのアニメーションはどういうものであって、glTFファイルのアニメーションデータはどういう構造になっているのでしょうか?ボーンアニメーションのデータはどのように扱われているのでしょうか?
 
そのglTFファイルのボーンアニメーションについては、

・「スキンアニメーション-頂点データにジョイントインデックスとウェイト値を含める
primitive の attributes にジョイントインデックス JOINTS と ウェイト値 WEIGHTS が必要です.JOINTS や WEIGHTS はセマンティクスと呼ばれ,1つの頂点データに同じセマンティクスのデータを複数入れることができます.これはセマンティクス名の後ろにインデックスを追加して指定します.」
 引用:glTF 覚え書き

という解説記事や、

・「vertex skinning ではモデルにボーンを埋め込んでボーンの動きに追随して各頂点が動くようになっています。」
「頂点の情報にはボーンのインデックスとウェイト (影響度) が追加されます。」
「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ファイル生成:

以前の3D画像ファイルの画像とは?や、3D画像ファイルのカメラデータとは?
と同様に、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】

 

具体例1のglTFファイルのデータ:

Blenderからエクスポートされたサンプル1.glbファイルのJSONデータを調べると、ジオメトリを示す"meshes"情報の"primitives"は、
 "primitives"
 {
  "attributes":
  {
   "POSITION":0
  },
  "indices":1,
  "material":0
 }
となっていて、3D画像ファイルの画像とは?の具体例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】と同様のアニメーションがされていることが分かると思います。


【図2】

 

具体例1の解説

・具体例1は一番単純なtranslationと呼ばれる移動アニメーションですが、TRS(Translation、Rotation、Scale)として知られる変形処理である回転(Rotation)や拡大縮小(Scale)のアニメーションも同様の構造になっています。

 

キーフレームデータの格納:今回の調査を開始した当初には、キーフレームデータが格納されているとは想定していませんでした。開始点と終了点のデータだけがglTFファイルに格納されて、そのデータをアプリソフトで読み取ってアニメーション処理しているのだろうと単純に考えていました。しかし、それではアニメーション動作のリニア補間とかベジェ補間の処理は、リニアであれば簡単だけれども複雑な動作を指定するのは困難であることに気付きました。上記"interpolation": "LINEAR",とありますが、Blenderでリニア補間でなくても"LINEAR"が格納されていたのでこのデータは無意味になります。キーフレームデータを用いて処理されます。
キーフレームデータが格納されていない場合に、この"interpolation"が効くのでしょうか?

 

0フレームの登録:Blenderのアニメーション操作ではデフォルトがフレーム1になっていて、フレーム1を開始点として登録すると紹介している記事が多いのですが、フレーム1を開始点として登録するとglTFデータには、
  "min": [
   0.0416666666666667
  ],
が格納されます。
これでは混乱を招くのではと思い、"min"が0となるようにフレーム0をここでは開始点に選んでいます。

 

終了フレーム:Blenderのアニメーション操作では終了フレームが設定できるようになっており、デフォルトはフレーム250になっています。
具体例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になっています。
 

imgP11_3
【図3】

 
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ファイルには格納されません。
 

【図4】

 

具体例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でワイヤフレームにした立方体を表示させています。


【図5】

 

具体例2の解説

・具体例2はボーンアニメーションの必須の要素であるボーン(Bone)を1個だけにして回転させる単純なアニメーションです。
通常は、このBoneの動きに合わせて、付随する3D物体が動くアニメーションが構成されます。
具体例2では付随する3D物体を設定していないので、glTFファイルにはボーンアニメーションで用いられる、ジョイントインデックス JOINTS や ウェイト値 WEIGHTSは格納されていません。

 

Boneデータの格納:Boneは、立方体などの3D物体とは異なり、Boneの位置と回転角と大きさで定義される線ベクトルで表されます。
具体例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では緑から青へのグラディエーションの線で表示しています。

 

Boneアニメーション:そのBoneが具体例2ではx軸周りに90゜回転して、そのキーフレームがBoneのquaternionデータの形で、(0、0、0、1)、(0.006545、0、0、0.999979)、(0.01309、0、0、0.999914)、・・・、(0.707107、0、0、0.707107)
のように計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ファイルには格納されません。

 

【図6】

 

具体例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でワイヤフレームにした立方体を表示させています。
 


【図7】

 

具体例3の解説

・具体例3はボーンアニメーションの必須の要素であるボーン(Bone)を1個だけにして回転させ、このBoneの動きに合わせて付随する立方体が回転するアニメーションです。
具体例2にはなかったジョイントインデックス JOINTS や ウェイト値 WEIGHTSは格納されています。

 

JOINTSとWEIGHTSデータの格納:具体例3では、立方体の8個の全頂点に対し、JOINTSは(0、0、0、0)、WEIGHTSは(1、0、0、0)になっており、全頂点がBoneに合わせて
同じ変位をすることになります。すなわち、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が設定されます。

 

Boneアニメーション:具体例2と同じBoneがx軸周りに90゜回転するキーフレームのBoneの計121個のquaternionデータ(0、0、0、1)、(0.006545、0、0、0.999979)、(0.01309、0、0、0.999914)、・・・、(0.707107、0、0、0.707107)
を立方体の各頂点の位置座標に施して、立方体の位置を逐次表示しています。

 

具体例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に追従して変形しながら回転しています。
 

【図8】

 

具体例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でワイヤフレームにした立方体を表示させています。
 


【図9】

 

具体例4の解説

・具体例4はボーンアニメーションの必須の要素であるボーン(Bone)を1個だけにして回転させ、このBoneの動きに合わせて付随する立方体が回転し、1個の頂点だけweight=0に設定されて回転しないアニメーションです。

 

weightがGlTFに反映されない:Blebderのウェイトペイントで頂点のweight値がエクスポートしたglTFファイルに反映していません。
何か方法があるのかも知れませんが、今回はその追求を断念しました。

 

おわりに

 

・今回は誰もが知っているアニメーションを取り上げて、特に3D画像のglTFファイルに格納されるアニメーション情報とはどういうものなのか調べてみました。

 

・今回サンプルにしたglTFファイルの作成には、著名なオープンソースの3DCGソフトであるBlenderを使いました。
作成したglTFファイルを用いたアニメーション描画には、JavaScriptライブラリであるthree.jsを使いました。
今回はthree.jsの高度な機能は使っておりませんが、基本的なアニメーション処理に触れることができました。

 

・アニメーションのキーフレームデータがBlenderから出力され、glTFファイルの"input"と"output"データとして格納されていることを知ったのは意外でありました。
アニメーションの基本技術の一端を学べたと思います。

 

・ボーンアニメーションでは、ボーンの動きに合わせて付随するスキンなどの物体の全部または一部が動くことは理解していましたが、ボーン単体のアニメーションとボーンに関連付けられた物体のアニメーションがあって、後者がいわゆるボーンアニメーションであることが理解でき、その後者の場合にglTFファイルにJOINTSやWEIGHTSデータが格納されることが分かりました。

 

・今回のボーンアニメーションの具体例4では、weight=0か1としていますが、0と1の間の数値の場合の挙動も調べたいと思っています。例えばweight=0.5であれば頂点の移動量が1/2になるのでしょうが、ボーンがある位置になった場合にweight値が変化するというケースもあるのでは?と想像されます。

 

・BlenderでweightがGlTFファイルに反映されない問題は不明です。
「まえがき」に書いたglTFアニメーションデータの不具合?についても今回の調査では分かりませんでした。
ボーンアニメーションについて更に掘り下げて探っていこうと思っています。

 
 

sub1title

closeicon

サンシャインブルー工房創設者、管理人の 青木ガンバロ と申します。
団塊世代の一技術者としてかつては開発業務に没頭しておりました。今はたそがれ期に入り、世間では簡単なことであっても、自分にとっては知らない、経験していないことが山ほどあると気付かされます。
限りある時間に、少しでもそういう未経験のテーマに挑戦してみようと思い、本工房を立ち上げることにしました。
 
いろいろな分野の学習を続けていると、何度も躓いてしまいます。素朴な疑問が湧いてくるのですが、その解答を得るのに手こずります。
膨大な情報の中から欲しい情報を探るのが難しくなっていると感じます。
 
技術を中心としたさまざまな課題を解きほぐし、より本質的な内容を明らかにすることによって、同じように学ぼうとする方々が、素早く答にたどり着けるような情報を発信していければと考えております。
 
このホームページもその挑戦の一つとして独学で制作してきました。
Web技術の多岐にわたる学習が必要であったり、関連する規格やツールなどの仕組みも進化して更新されていくので、最善解に近づくのは容易ではありません。
当初から躓きの連続でありましたが、多くの親切な方々の的確な記事にも助けられて進めてきました。
まだ多くの疑問点が残っております。逐次改善していくつもりです。
 
これまで多くの友人、先輩方の支えがあったお陰で何とか過ごしてくることができました。
人びとの繋がりの大切さを痛感しております。
どこまで頑張れるか分かりませんが、残る力を絞って進んでまいります。
これからも、皆さまのご援助、ご協力をよろしくお願い申し上げます。

sub2title

closeicon

2019-  4-16

・サンシャインブルー工房の個人事業開業

2022-12-22

・ホームページを公開

・ブログ「立方体に写真を貼り付けるツールを作りました」を公開

・ブログ「球体に写真を貼り付けるツールを作りました」を公開

2023-  2-  3

・ブログ「3DにおけるJPEGとは?」を公開

2023-  3-  1

・ブログ「3D画像ファイルの画像とは?」を公開

2023-  4-16

・ブログ「3D画像ファイルのカメラデータとは?」を公開

2023-  5-22

・ブログ「3D画像ファイルの点群とは?」を公開

2023-  6-28

・ブログ「3D画像ファイルのGeoTIFFとは?」を公開

2023-  7-28

・「役立つーる」ページを新設し、最初のオンラインツールGeoTIFFリーダーを公開

・ブログ「GeoTIFFリーダーを作りました」を公開

2023-  8-16

・ブログ「点群におけるLASとは?」を公開

2023-  9- 1

・ブログ「Googleアースに使われるKMLとは?」を公開

2024-  1-16

・ブログ「3D画像ファイルのアニメーションとは?」を公開

2024-  2- 8

・ブログ「ボーンアニメーションのオフセット行列とは?」を公開

2024-  7- 13

・ブログ「3D画像モデルの回転表示とは?」を公開

sub3title

closeicon






    皆さまからの、ご要望、ご依頼、ご質問、ご意見、ご提案などをお待ちしております。
    当方の事情や、お問い合わせの内容によりましては、返信を差し上げることができない場合もあります。あらかじめご了承ください。

    姓 例)日本

    名 例)太郎

    せい 例)にほん

    めい 例)たろう

    例)xxx@abcde.co,jp

    例)いろはに会社


     

    お問い合わせをいただき、ありがとうございます。

    messageOKこのお問い合わせの送信が完了しました。

    お問い合わせがエラーになりました。再度お試しください。

    messageNGこのお問い合わせの送信が失敗しました。

    sub4title

    closeicon
    1.著作権について
    当サイトは、お客さまご自身の画像が貼り付けられてダウンロードされた画像の情報を除き、当サイトに掲載されている、文章・画像・動画等の著作物の情報を無断転載することを禁止致します。
    当サイトは、ブラウザがFirefoxである場合に動画ファイルの生成のためにLPGLv3ライセンスのFFmpegライブラリを使用しています。
    2.リンクについて
    以下の場合を除き、当サイトを他のWebサイトに自由にリンクすることができます。
    (リンクをお断りするWebサイト)
    違法または反社会的な情報を提供するWebサイト
    当サイトの関係者や提供する情報に対して誹謗、中傷する内容を有するWebサイト
    当サイトであることが不明確であるWebサイト
    sub5title

    closeicon
    1.個人情報保護方針
    当サイトは、お客様からお預かりする個人情報の重要性個人情報の重要性を認識し、個人情報の保護に関する法律、その他の関係法令を遵守し、個人情報を安全かつ適切に取り扱います。
    2.個人情報の取得と利用目的
    個人情報を取得させていただくにあたっては、取得情報と利用目的を以下に定め、必要な個人情報のみを、適法かつ公正な手段により取得させていただきます。
     2.1.お問い合わせやご注文時の情報
    (取得情報)
    お客様ご自身によるお問い合わせやご依頼のための入力情報
    (利用目的)
    お客様へのご依頼に対応するため。
     2.2.ご利用履歴情報
    (取得情報)
    アクセス解析ツールGoogle Analyticsを用いた個人を特定しないトラフィックデータ
    (利用目的)
    ご利用状況の分析により当サイトの一層の改善や拡充を図るため
    このGoogle Analyticsの規約に関する詳細は、ここをクリックしてください。
     2.3.広告管理情報
    (取得情報)
    広告表示ツールGoogle AdSenseを用いた個人を特定しない広告管理データ
    (利用目的)
    お客様の興味に応じた商品やサービスの広告配信のため
    このGoogle AdSenseの規約に関する詳細は、ここをクリックしてください。
    3.個人情報の第三者への開示
    当サイトは、お客様からお預かりした個人情報を、個人情報保護法その他の法令に基づき開示が認められる場合、または、お客様からの同意を得た場合を除き、第三者に提供することはありません。
    4.クッキー(Cookie)について
    クッキー(Cookie)とは、当サイトにアクセスした際にお客様のブラウザに送信され、お客様のコンピューターに保存される情報のことです。
    当サイトでは、利用履歴の収集および広告の管理のためにそのクッキーを使用しています。
    当サイトが使用するクッキーのデータは、個人情報を含むものではありませんが、お客様がブラウザを操作することにより、クッキーの使用を制限(オプトアウト)することや、保存されたクッキーの情報を削除することも可能です。ただし、そのブラウザの設定によっては、当サイト一部の機能が使用できなくなる恐れがありますのでご注意ください。
    5.個人情報保護方針の変更
    当サイトは、法令の制定、改正等により、本個人情報保護方針を適宜見直し、予告なく変更する場合があります。本個人情報保護方針の変更は、変更後の内容が閲覧可能となった時点で有効になります。
    6.免責事項
    当サイトは、正確な情報を掲載するよう努めておりますが、誤情報の混入、情報の陳腐化が起こることがあります。当サイトに掲載された内容や、他のサイトに移動された場合の移動先サイトで提供される情報によって生じた損害等の一切の責任を負いかねますのでご了承ください。