glTF モデルの骨格アニメーション

推奨事項: NSDT シーン エディター を使用して3D アプリケーション シーンを迅速に構築する

この記事では、風車アニメーションの制作プロセスを詳しく説明します。

もちろん、これはハードコードするのが非常に簡単です (2 つのオブジェクト、1 つは静的、もう 1 つは回転します)。ただし、後でさらにアニメーションを追加する予定なので、適切な解決策を実装することにしました。

以前は、モデルとアニメーションに安っぽいアドホック バイナリ形式を使用していましたが、最近、いくつかの理由からglTF モデルに切り替えました。

  • 解析が簡単: JSON メタデータ + 生のバイナリ頂点データ
  • レンダリングは簡単です。モデルはグラフィックス API に直接マッピングされる形式で保存されます。
  • 十分にコンパクトです (重いもの (頂点データ) はバイナリ形式で保存されます)
  • 幅広くてスケーラブルです
  • スケルトンアニメーションをサポートしています

glTF モデルを使用すると、 他の人がゲームを簡単に拡張できる (MOD の作成など) ことができます。

残念ながら、glTF を使用してスケルトン アニメーションに関する適切なリソースを見つけることは不可能のようです。すべてのチュートリアルはいくつかの古い形式をカバーしており、glTF 仕様はほとんどが非常に長く正確で、アニメーション データの解釈が非常に簡潔です。これは専門家にとっては明らかなはずですが、私は専門家ではありません。もしあなたもそうでないなら、これはあなたのための記事です :)

ところで、私はこれを正しく行う方法を見つけるために、Sascha Willems の Vulkan + glTF サンプル レンダラーでアニメーション コードをリバース エンジニアリングすることになりました。

glTF モデル スケルトン アニメーション

スケルタル アニメーションが何であるかをすでに知っている場合は、このセクションをスキップしても問題ありません :)

glTF モデルのスケルトン アニメーションは、3D モデルをアニメーション化する最も一般的な方法です。これは概念的には非常に単純です。実際のモデルをアニメーション化する代わりに、モデルの非常に単純化された仮想スケルトンをアニメーション化します。モデル自体は、骨と肉のように接着されています。おおよそ次のようになります。

モデルの頂点がさまざまなボーンにどのように付着しているかを次に示します (赤は接着剤が多く含まれており、青は接着剤がありません)。

通常、各メッシュ頂点は、よりスムーズなアニメーションを提供するために異なるウェイトを持つ複数のボーンに接着され、頂点の最終的な変換はこれらのボーン間で補間されます。各頂点を 1 つのボーンに接着するだけの場合、モデルの異なる部分 (通常は人間の肩、肘、膝など) 間のトランジションでアニメーション化したときに不快なアーティファクトが生じます。

このアプローチのもう 1 つの重要な部分は階層的です。ボーンはツリーを形成し、子ボーンは親の変換を継承します。このモデル例では、2 つのボーンはスケルトンのルートであるボーンの子です。ボーンのみが明示的に上下に回転され、この回転はボーンから継承されます。earheadheadearshead

これは、ほとんどのゲーム エンジンがオブジェクト階層を使用するのと同じ理由です。ヘルメットに蚊を抱えたまま、移動中の輸送船の車に乗っている人がいる場合、これらすべてのオブジェクトの動きを個別に定義するのは非常に面倒で、間違いが発生しやすくなります。代わりに、船の動きを定義し、オブジェクトを割り当てて階層を形成し、子オブジェクトが親の動きを継承するようにします。蚊はヘルメットの子、ヘルメットは人間の子、というように。

同様に、腕のボーンごとに正しい回転を計算するよりも、人の肩が回転していること (腕全体が肩の子であること) を指定する方がはるかに簡単です。

glTF モデルの長所と短所

アニメーション フレームごとにすべての頂点位置を保存する代替手段である モーフ ターゲットアニメーションと比較すると、次のような利点があります。

  • 必要な保管スペースが少なくて済みます - スケルトンはモデルよりもはるかに小さいです
  • フレームごとに必要なデータ フローが少なくなります (アニメーション全体を GPU に保存する方法はありますが、メッシュ全体ではなくボーンのみ)
  • (おそらく) アーティストにとって使いやすくなりました
  • アニメーションを特定のモデルから切り離します。同じ歩行アニメーションを、頂点数が異なる多くの異なるモデルに適用できます。
  • プロシージャル アニメーションに統合するのがはるかに簡単です。たとえば、キャラクターの足が地形を横切るのを制限したい場合、スケルトン アニメーションを使用すると、いくつかのボーンに制限を追加するだけで済みます。

ただし、いくつかの欠点があります。

  • 使用しているアニメーション形式を正しく解析/デコードする必要があります (これは思っているよりも難しいです)
  • アニメーション化されたモデルごとにボーンごとに変換を計算する必要がありますが、これにはコストがかかり、扱いが難しい場合があります (ただし、これを行うにはコンピューティング シェーダーを使用できると思います)。
  • 何らかの方法でボーン データを GPU に転送する必要があります。これは頂点属性ではないため、ユニフォームには適さない可能性があります。
  • 頂点シェーダーでボーン変換を適用する必要があるため、このシェーダーは通常より 4 倍遅くなります (それでも、一般的なフラグメント シェーダーよりははるかに安価です)。

ただし、思っているほど悪くはありません。ボトムアップで実装する方法を詳しく見てみましょう。

glTFモデルボーン変換

したがって、何らかの方法でメッシュの頂点を動的に変換する必要があります。各ボーンには特定の変換が定義されており、通常はスケール、回転、平行移動で構成されます。スケーリングや移動が必要なく、ボーンが回転するだけの場合でも (これは多くの現実的なモデルでは妥当です。肩を肩の受け穴から 0.5 メートル離してみてください!)、さまざまな回転中心の周りで回転が発生する可能性があります。 (例: 腕が肩の周りを回転すると、手も手の骨の原点の周りではなく、肩の周りを回転します)。つまり、とにかく変換​​する必要があります。

これらすべてをサポートする最も一般的な方法は、各ボーンの 3x43x4 アフィン変換行列を単純に保存することです。この変換は通常、スケーリング、回転、平行移動の組み合わせ (この順序で適用されます) であり、同次座標の行列として表現されます (平行移動を行列などで表現するための数学的トリック)。

行列 (つまり、3 つの浮動小数点数) を使用する代わりに、合計 7、8、または 10 の浮動小数点を与えます。ただし、後で説明するように、コンポーネントの総数が 4 の倍数である場合、これらの変換をシェーダーに渡すのが簡単になります。したがって、私のお気に入りのオプションは、移動 + 回転 + 均一スケール (8 つの浮動小数点数) または本格的な行列 (12 の浮動小数点) です。

とにかく、これらの変換では、親の変換もすでに考慮されている必要があります (これは後で行います。親を考慮しないローカル変換とは対照的に、これらをグローバル変換と呼びます。したがって、次のような再帰式があります。これ:

globalTransform(ボーン)=globalTransform(親)⋅localTransform(ボーン)

グローバル変換(ボーン) = グローバル変換(親) ⋅ ローカル変換(ボーン)

ボーンに親がない場合は、 と同じです。これらの の起源については、この記事で後ほど説明します。globalTransformlocalTransformlocalTransform

glTFモデル組み合わせ変換

ちなみに、上の式は少し誤解を招きやすいかもしれません。変換を行列として保存する場合、2 つの 3x43x4 行列をどのように乗算すればよいでしょうか? これは行列の乗算の規則に違反しています。それらを (移動、回転、スケール) トリプルとして保存する場合、どのように構成すればよいでしょうか?

行列の場合、3x43x4 の行列を使用することが実際には最適化になります。本当に必要なのは、乗算が簡単な 4x44x4 行列です。偶然にも、アフィン変換は常に次の形式になります。

したがって、行 4 を実際に保存するのは意味がありませんが、計算を行うときにそれを復元する必要があります。実際、可逆アフィン変換は、可逆行列のすべてのグループのサブグループです。

行列のレシピは次のとおりです。(0001)(0001) 行を追加し、結果の 4×44×4 行列を乗算し、結果の最後の行を破棄します。これも (0001)(0001) になります。左の行列を右の行列の列に明示的に適用することで、これをより効率的に行う方法がいくつかありますが、一般的な式は同じです。

では、変換を移動、回転、スケールとして明示的に保存し、それらをどのように乗算するかについてはどうでしょうか? そう、公式は一つしかないのです!変換を (T,R,S) (平行移動ベクトル、回転演算子、スケール係数) として表しましょう。点 p に対するこの変換の効果は (T,R,S)⋅p=T+R⋅(S⋅p) です。このような 2 つの変換を組み合わせるとどうなるかを見てみましょう。

均一なスケーリングが回転によって入れ替わるという事実を利用しました。実は、どんなものでも通勤可能なんです!

したがって、この形式で 2 つの変換を乗算する公式は次のようになります。

R は回転四元数ではなく、回転演算子であることに注意してください。回転された四元数 Q の場合、回転の構成は変わりませんが、ベクトルに対する回転の作用は変わります。

また、このトリックは不均一なスケーリングでは機能しないことにも注意してください。本質的に、R が回転であり、S が不均一にスケーリングされる場合、積 S⋅R を R'⋅S' のようなものとして表現する方法はありません。他の回転 R' と不均一なスケーリング S'
この場合、行列を使用する方が簡単です。

頂点シェーダ

これは簡単な説明です!実際のコード、特に頂点シェーダーを見てみましょう。GLSL を使用しますが、ここでは特定の言語やグラフィックス API は重要ではありません。

何らかの方法でボーンごとのグローバル変換をシェーダーに渡したとしましょう (これについてはすぐに説明します)。また、どの頂点がどのボーンに接続されており、どのようなウェイトであるかを知る何らかの方法も必要です。これは通常、2 つの追加の頂点属性 (ボーン ID 用とボーン ウェイト用) を使用して行われます。通常、モデルあたり 256 を超えるボーンは必要なく、ウェイトにはそれほどの精度は必要ないため、ID には整数の属性を使用し、ウェイトには正規化された属性を使用できます。ほとんどのグラフィックス API ではプロパティは最大でも 4D であるため、通常は頂点を 4 つ以下のボーンに接着することのみを許可します。たとえば、ボーンが 2 つのボーンにのみ固定されている場合は、重みが 0 の 2 つのランダムなボーン ID を追加し、それを day と呼びます。uint8uint8

十分に言った:

// somewhere: mat4x3 globalBoneTransform[]

uniform mat4 uModelViewProjection;

layout (location = 0) in  vec3 vPosition;
// ...other attributes...
layout (location = 4) in ivec4 vBoneIDs;
layout (location = 5) in  vec4 vWeights;

void main() {
    vec3 position = vec3(0.0);
    for (int i = 0; i < 4; ++i) {
        mat4x3 boneTransform = globalBoneTransform[vBoneIDs[i]];
        position += vWeights[i] * (boneTransform * vec4(vPosition, 1.0));
    }

    gl_Position = uModelViewProjection * vec4(position, 1.0);
}

GLSL では、ベクトルの場合、  v,v[0] は vx と同じであり、vは等しい[1] です v.y 。

ここで私たちがやることは、

  1. 頂点が接続されている 4 つのボーンを反復処理します。
  2. ボーンの ID を読み取り、そのグローバル変換を取得しますvBoneIDs[i]
  3. 同次座標の  頂点位置にグローバル変換を適用するvec4(vPosition, 1.0)
  4. 重み付けされた結果を生成された頂点に追加するposition
  5. 通常の MVP 行列を結果に適用します

プロセス全体はスキニング、より具体的にはリニア ブレンド スキニング とも呼ばれます。

GLSL では、matNxM は N 列と M 行を意味するため、mat4x3 は実際には 3x4 行列になります。私は標準が好きです。

重みの合計が 1 になるかどうかわからない場合は、最後にそれらの合計で割ることもできます (ただし、重みの合計が 1 になることを確認したほうがよいでしょう)。

    position /= dot(vWeights, vec4(1.0));

重みの合計が 1 に等しくない場合、歪みが発生します。基本的に、頂点はモデルの原点に近づくか遠ざかります (合計が 1 未満か 1 より大きいかによって決まります)。これは、透視投影と、アフィン変換は線形空間を形成しないが、アフィン空間は形成するという事実に関係しています。

法線もある場合は、法線も同様に変換する必要があります。唯一の違いは、positionがpointであるのに対し、normalはVectorであるため、同次座標での表現が異なることです (w 座標として 1 を追加する代わりに 0 を使用します)。スケーリングを考慮して、後で正規化することもできます。

// somewhere: mat4x3 globalBoneTransform[]

uniform mat4 uModelViewProjection;

layout (location = 0) in  vec3 vPosition;
layout (location = 1) in  vec3 vNormal;
// ...other attributes...
layout (location = 4) in ivec4 vBoneIDs;
layout (location = 5) in  vec4 vWeights;

vec3 applyBoneTransform(vec4 p) {
    vec3 result = vec3(0.0);
    for (int i = 0; i < 4; ++i) {
        mat4x3 boneTransform = globalBoneTransform[vBoneIDs[i]];
        result += vWeights[i] * (boneTransform * p);
    }
    return result;
}

void main() {
    vec3 position = applyBoneTransform(vec4(vPosition, 1.0));
    vec3 normal = normalize(applyBoneTransform(vec4(vNormal, 0.0)));

    // ...
}

不均一なスケーリングを使用する場合、または目の空間の照明を行う場合は、状況が少し複雑になることに注意してください。

トランスフォームをシェーダーに渡す

私は主に OpenGL 3.3 を使用してグラフィック コンテンツを作成しているため、このセクションの詳細は OpenGL に固有のものですが、一般的な概念はどのグラフィック API にも当てはまると思います。

ほとんどのスケルトン アニメーション チュートリアルでは、スケルトンの変換に均一な配列を使用することを推奨しています。これは単純な作業方法ですが、少し問題が生じる可能性があります。

  • OpenGL にはユニフォームの数に制限があります。OpenGL 3.0 は少なくとも 1024コンポーネントを保証します。これは大まかに言うと行列の個々の要素を意味します。したがって、12 個のコンポーネントが必要となるため、各モデルのボーンによって制約を受けます。量が多いので、実際には十分かもしれません。ただし、多くのユニフォームはすでに他のもの (マトリックス、テクスチャなど) に使用されているため、通常はそれほど多くの無料ユニフォームはありません。実際には、通常 4096 ~ 16384 個のコンポーネントがあります。mat4x31024/12 ~ 85
  • アニメーション モデルごとに均一な配列を更新する必要があります。これは、多くの OpenGL 呼び出しが発生し、インスタンス化が行われないことを意味します。

この問題は、均一バッファを使用することである程度解決できます。

  • 特化すると、より多くのメモリを使用できるようになりますが、それでもそれほど多くはなく、通常は 64 KB のバッファです。
  • すべてのボーン変換をユニフォームにアップロードする代わりに、すべてのモデルのすべての変換を一度にバッファーにアップロードできます。モデルごとに glBindBufferRange を呼び出して、そのモデルのボーン データが存在する場所を指定する必要があるため、インスタンス化は行われません。

OpenGL 4.3 以降を使用している場合は、すべての変換をシェーダ ストレージ バッファ オブジェクトに簡単に保存できます。このオブジェクトのサイズは基本的に無制限です。それ以外の場合は、バッファ テクスチャを使用できます。これは、任意のデータ バッファにアクセスし、それを 1Dテクスチャとして偽装する方法ですバッファ テクスチャ自体は何も保存せず、既存のバッファを参照するだけです。それはこのように動作します:

  1. 共通の OpenGL を作成し、均一なスケーリングを持つ行ごとの行列 (12 フロート) または TRS トリプレット (8 フロート) としてフレームごとに保存されたすべてのモデルの骨格変換をそれに設定します。GL_ARRAY_BUFFER
  2. を作成して呼び出します - はこのテクスチャのピクセル形式で、ピクセルあたり 4 フロート (12 バイト) です (つまり、行列あたり 3 ピクセル、または TRS トリプルあたり 2ピクセル)GL_BUFFER_TEXTUREglTexBuffer(GL_BUFFER_TEXTURE, GL_RGBA32F, bufferID);RGBA32F
  3. シェーダーでユニフォームにテクスチャを貼り付けますsamplerBuffer
  4. シェーダー内の対応するピクセルを読み取り、ボーン変換に変換します。texelFetch

インスタンス化されたレンダリングの場合、このシェーダーは次のようになります。

uniform samplerBuffer uBoneTransformTexture;
uniform int uBoneCount;

mat4x3 getBoneTransform(int instanceID, int boneID) {
    int offset = (instanceID * uBoneCount + boneID) * 3;
    mat3x4 result;
    result[0] = texelFetch(uBoneTransformTexture, offset + 0);
    result[1] = texelFetch(uBoneTransformTexture, offset + 1);
    result[2] = texelFetch(uBoneTransformTexture, offset + 2);
    return transpose(result);
}

マトリックスを 4x3 マトリックス (GLSL で) として組み立てますが、テクスチャから行を読み取ってマトリックスの列に書き込み、それを転置して行と列を切り替えていることに注意くださいこれは単に GLSL が列優先の行列を使用しているためです。mat3x4

レビュー

確認してみましょう:

  • モデルをアニメーション化するには、各頂点を仮想スケルトンの最大 4 つのボーンに 4 つの異なるウェイトで接続します。
  • 各ボーンは、頂点に適用する必要があるグローバル変換を定義します。
  • 各頂点に対して、それに接続されている 4 つのボーンの変換を適用し、重みを使用して結果を平均します。
  • 変換を TRS トリプルまたは 3x4 アフィン変換行列として保存します
  • 変換は均一配列、均一バッファ、バッファ テクスチャ、またはシェーダ ストレージ バッファに保存されます。

私たちに残されたのは、こうした世界的な変革がどこから来るのかということです。

世界的な変革

実際、グローバル変換がどこから来るのかはすでにわかっています。グローバル変換はローカル変換から計算されます。

globalTransform(bone)=globalTransform(親)⋅localTransform(bone)globalTransform(bone)=globalTransform(親)⋅localTransform(bone)

これを計算する単純な方法は、すべての変換に対する再帰関数を計算することに似ています。

mat4 globalTransform(int boneID) {
    if (int parentID = parent[boneID]; parentID != -1)
        return globalTransform(parentID) * localTransform[boneID];
    else
        return localTransform[boneID];
}

または同じことですが、末尾再帰を手動で展開します。

for (int boneID = 0; boneID < nodeCount; ++boneID) {
    globalTransform[boneID] = identityTransform();
    int current = boneID;
    while (current != -1) {
        globalTransform[boneID] = localTransform[current] * globalTransform[boneID];
        current = nodeParent[current];
    }
}

どちらの方法も問題ありませんが、必要以上に多くの行列乗算を計算します。すべてのフレームとすべてのアニメーション モデルでこれを行う必要があることを忘れないでください。

グローバル変換を計算するより良い方法は、親から子への変換です。親のグローバル変換がすでに計算されている場合、必要なのはボーンごとに 1 つの行列乗算を行うことだけです。

// ... somehow make sure parent transform is already computed
if (int parentID = parent[boneID]; parentID != -1)
    globalTransform[boneID] = globalTransform[parentID] * localTransform[boneID];
else
    globalTransform[boneID] = localTransform[boneID];

子よりも親が確実に計算されるようにするには、ボーン ツリーに対して DFS を実行して、ボーンを正しく順序付ける必要があります。おそらくより簡単な解決策は、ボーン ツリー (親が子の前に来るようにボーンを列挙したもの) のトポロジカルな順序を事前に計算し、それをフレームごとに使用することです。(ところで、トポロジカルな順序付けの計算は、とにかく DFS を使用して行われます。より簡単な解決策は、ボーン ID が事実上トポロジカルな順序付けであることを確認することです。つまり、それが常に保持されます。これは、ボーン (およびメッシュ頂点) プロパティをロードすることで実行できます。)再注文するか、アーティストにボーンをこのように注文するように依頼してください:)さて、モデルのボーン、それだけです。parent[boneID] < boneID

後者の場合、実装は最も単純 (そして最速) です。

for (int boneID = 0; boneID < nodeCount; ++boneID) {
    if (int parentID = parent[boneID]; parentID != -1)
        globalTransform[boneID] = globalTransform[parentID] * localTransform[boneID];
    else
        globalTransform[boneID] = localTransform[boneID];
}

しかし、ローカルなコンバージョンはどこから来るのでしょうか?

ローカル変換

ここで、事態は少し奇妙になります(すでに奇妙ではなかったかのように)。ご存知のとおり、多くの場合、ワールド座標ではなく、特別な座標系でローカル ボーン変換を指定すると便利です。腕を回転させる場合、ローカル座標系の原点は足の下ではなく回転の中心になるため、この変換を明示的に解釈する必要はありません。また、遠くにいる人に手を振るなど、上下に回転させる場合は、モデルがモデル空間にあるかワールド空間にあるかに関係なく、ローカル空間の何らかの軸 (おそらく X) を中心に回転させたいと考えています。の方向。

私が言いたいのは、各ボーンに特別な座標系 (CS) を持たせこの座標系を使用してボーンのローカル変換を記述したいということです。

ただし、モデルの頂点はモデルの座標系 (これがこの座標系の定義) 内に配置されます。したがって、最初に頂点をボーンのローカル座標系に変換する方法が必要です。これは、非常にクールに聞こえるため、逆バインディング行列と呼ばれます。

さて、頂点をボーンのローカル CS に変換し、このローカル CS でアニメーション変換を適用しました (後で説明します)。それだけですか?次のことは、これを親ボーンの変換と組み合わせることであり、親ボーンは独自の座標系になることを覚えておいてください。したがって、もう 1 つ必要なことがあります。それは、頂点をボーンのローカル CS から親のローカル CS に変換することです。ちなみに、これは逆バインディング行列を使用して行うことができます。頂点をボーンのローカル CS からモデル CS に変換してから、親のローカル CS に変換します。

ConvertToParentCS(ノード)=inverseBindMatrix(親)⋅inverseBindMatrix(ノード)−1convertToParentCS(ノード)=inverseBindMatrix(親)⋅inverseBindMatrix(ノード)−1

次のように考えることもできます: 特定のボーンが頂点をそのネイティブ CS に変換し、アニメーションを適用してから元に戻し、次にその親が頂点を独自のローカル CS に変換し、独自のアニメーションを適用してから変換します。戻る、元に戻す、など。

実際、この変換を glTF で明示的に使用する必要はありませんconverToParentが、考えておくと便利です。

後もう一つ。場合によっては、(アーティストまたは 3D モデリング ソフトウェアにとって) モデルのデフォルト状態ではなく、何らかの変換状態 (バインディングポーズと呼ばれる) で頂点をボーンにアタッチすると便利な場合があります。したがって、ボーンごとに頂点を、そのボーンが頂点があると想定している CS に変換する別の変換が必要になる場合があります。わかりにくく聞こえるかもしれませんが、我慢してください。実際にはこの移行は必要ありません :)

クリックするとクマが表示されます

Blender はワールド空間の頂点位置をバインディング ポーズとして使用します。モデルが原点から X 軸に沿って 20 単位離れている場合、元の頂点位置は X=20 付近になり、逆バインディング行列がこれを補正します。これにより、事実上、Blender からエクスポートされたアニメーション モデルはアニメーションなしでは使用できなくなります。

glTF モデル変換のレビュー

全体として、次の一連の変換が頂点に適用されます。

  1. モデルにバインドされたポーズに変換します
  2. ボーン ローカル CS (逆結合行列) に変換します。
  3. 実際のアニメーションを適用します (ローカル CS で指定)
  4. ボーン ローカル CS から逆変換する
  5. ボーンに親ボーンがある場合は、親ボーンに対して手順 2 ~ 5 を繰り返します。

ここで、問題は、各形式がこれらの形式を指定する独自の方法を定義していることです。実際、これらの変換の一部は存在すらしない可能性があります。それらは他の変換に含める必要があります。

最後に、glTF について話しましょう。

glTF モデル 101

glTF は、OpenGL、OpenCL、Vulkan、WebGL、SPIR-V などを開発した Khronos Group によって開発された、非常に優れた 3D シーン作成フォーマットです。これが素晴らしいフォーマットだと思う理由は記事の冒頭ですでに述べましたので、もう少し詳しく説明しましょう。

これはglTF-2.0の仕様です。これは非常に優れており、仕様を読んで形式を学ぶだけです。

glTF シーンはノードで構成されており、ノードは抽象化であり、多くの意味を持ちます。ノードは、メッシュ、カメラ、ライト、スケルトン ボーンとしてレンダリングしたり、他のノードの親を単純に集約したりすることができます。各ノードには独自のアフィン変換があり、親ノード (親ノードがない場合はワールド原点) を基準とした位置、回転、スケールが定義されます。

glTF は、アクセサーを介してすべてのバイナリ データを記述します。基本的には、指定された型の配列 (要素間のギャップがゼロでない可能性があります) を含むバイナリ バッファーの一部への参照です (たとえば、コンポーネントがここではバイト単位である 100 個の連続配列)特定のバイナリなど)。vec4float326340

メッシュ ノードが骨格アニメーションを使用する場合、メッシュ ノードには骨格ボーンを指定する glTF ノードの ID のリストがあります。(実際には、メッシュはスキンを参照し、そのスキンにはジョイントが含まれています。これらのジョイントは階層を形成します。これらは依然として glTF ノードであるため、親と子を持つことができます。スケルトンやスケルトンノードはないことに注意してください。存在するのはボーン ノードだけです。 ; 同様に、アニメーション メッシュはボーン ノードやスケルトン ノードの子ではありませんが、それらを間接的に参照します (エクスポート ソフトウェアでは、Blender のように人工スケルトン ノードが追加される場合がありますが、glTF ではこれは必要ありません)。joints

メッシュ (実際にはメッシュ プリミティブ) の各頂点プロパティ (位置、法線、UV など) は別個のアクセサーです。メッシュがボーン アニメーションを使用する場合、メッシュにはボーン ID とウェイトのプロパティもあり、これらも一部のアクセサーです。ボーンの実際のアニメーションもアクセサーに保存されます。

スキン メッシュの説明に加えて、 glTF モデルには実際のアニメーション (基本的に、上記のリストの 3 番目の変換を変更する方法に関する指示) が含まれる場合があります。

glTF変換

glTF モデルが上記のリスト 1 ~ 5 のすべての変換をどのように保存するかを次に示します 。

  1. モデル バインディング ポーズはすでにモデルに適用されているか、逆バインディング行列に事前乗算されている必要があります。言い換えれば、glTF のバインディング ポーズのことは忘れてください。
  2. ボーンごとの逆バインディング行列は、別のアクセサー、つまり 4x4 行列の配列として指定されます (アフィン変換である必要があるため、最初の 3 行のみが対象となります)。
  3. 実際のアニメーションは外部で定義することも (手続き型アニメーションなど)、各ボーンの回転、移動、スケールのキーフレーム スプラインとして保存することもできます。ここで重要なことは、これらは...
  4. ...ローカル CS から親ローカル CS への遷移を結合します。そこでスケルトンアニメーションを組み合わせます。convertToParent
  5. 親はノード階層によって定義されますが、すでに変換を適用しているため、親の逆バインディング行列は必要ないため、親がある場合は親に対してステップ 3 ~ 5 を繰り返すだけです。convertToParent

したがって、glTF を使用すると、ボーンのグローバル変換は次のようになります。

コードではこれは次のようになります

// assuming parent[boneID] < boneID holds

// somehow compute the per-bone local animations
// (including the bone-CS-to-parent-CS transform)
for (int boneID = 0; boneID < boneCount; ++boneID) {
    transform[boneID] = ???;
}

// combine the transforms with the parent's transforms
for (int boneID = 0; boneID < boneCount; ++boneID) {
    if (int parentID = parent[boneID]; parentID != -1) {
        transform[boneID] = transform[parentID] * transform[boneID];
    }
}

// pre-multiply with inverse bind matrices
for (int boneID = 0; boneID < boneCount; ++boneID) {
    transform[boneID] = transform[boneID] * inverseBind[boneID];
}

この配列は、上記の頂点シェーダー内の配列です。transform[]globalBoneTransform[]

結局のところ、それほど複雑ではありません!一見ランダムに見える多数の行列を乗算する正しい順序を理解する必要があるだけです:)

glTFアニメーション

最後に、 glTF モデルに直接保存されているアニメーションを適用する方法について説明します。これらは、各ボーンの回転、スケール、および移動のキーフレーム スプラインとして割り当てられます

個々のスプラインはチャネルと呼ばれます。以下を定義します。

  • どのノードに適用されるか (例: スケルトン)
  • どのパラメータに影響しますか (回転、スケーリング、または移動)
  • キーフレームのタイムスタンプのアクセサー
  • キーフレーム値のアクセサー (回転の四元数、スケールまたは移動のベクトル)vec4vec3
  • 補間方法 – 、またはSTEPLINEARCUBICSPLINE

回転の場合、LINEAR実際には球面の直線性を意味します。 補間の場合 CUBICSPLINE 、キーフレームごとに 3 つの値 (スプライン値と 2 つの接線ベクトル) が保存されます。

したがって、ボーンのローカル変換を構築する方法は次のとおりです。

  • 現時点でのこのボーンの回転、移動、スケーリング スプラインをサンプリングします。
  • それらを組み合わせてローカル変換行列を形成します。

ベクトル変換 (x、y、z) の場合、対応する行列は次のとおりです。

不均一にスケーリングされたベクトル (x、y、z) の場合、行列は次のようになります。

回転クォータニオンについては、Wiki 記事で行列を見つけることができます。これは 3x3 行列で、次のように 4x4 行列の左上隅に配置します。

前に説明したように、これらの行列は 4x4 ですが、実際にはアフィン変換であるため、興味深いことは最初の 3 行でのみ発生します。

サンプルアニメーションスプライン

最後の点 (アニメーション化されたスプラインを効率的にサンプリングする) に対処するには、次のようなクラスにスプラインを収集できます。

template <typename T>
struct animation_spline {

    // ...some methods...

private:
    std::vector<float> timestamps_;
    std::vector<T> values_;
};

ここで、明らかな API の決定は、特定の時点でスプライン値を返すメソッドを作成することです。

template <typename T>
T value(float time) const {
    assert(!timestamps_.empty());

    if (time <= timestamps_[0])
        return values_[0];

    if (time >= timestamps_[1])
        return values_[1];

    for (int i = 1; i < timestamps_.size(); ++i) {
        if (time <= timestamps_[i]) {
            float t = (time - timestamps_[i - 1]) / (timestamps_[i] - timestamps_[i - 1]);
            return lerp(values_[i], values_[i + 1], t);
        }
    }
}

呼び出しは、補間のタイプと、これが回転であるかどうかに応じて変わります lerp 。

これは機能しますが、2 つの方法で改善できます。まず、キーフレームのタイムスタンプはソートされることが保証されているため、線形検索の代わりに二分検索を実行できます。

template <typename T>
T value(float time) const {
    auto it = std::lower_bound(timestamps_.begin(), timestamps_.end(), time);
    if (it == timestamps_.begin())
        return values_.front();
    if (it == timestamps_.end())
        return values_.back();

    int i = it - timestamps_.begin();

    float t = (time - timestamps_[i - 1]) / (timestamps_[i] - timestamps_[i - 1]);
    return lerp(values_[i - 1], values_[i], t);
}

次に、アニメーションを再生するときは、常に最初から最後まで線形に移動するため、現在のキーフレーム インデックスを保存することでアニメーションをさらに最適化できます。ただし、これはアニメーション自体のプロパティではないため、別のクラスを作成しましょう。

template <typename T>
struct animation_spline {

    // ...

private:
    std::vector<float> timestamps_;
    std::vector<T> values_;

    friend class animation_sampler<T>;
};

template <typename T>
struct animation_sampler {
    animation_spline<T> const & animation;
    int current_index;

    T sample(float time) {
        while (current_index + 1 < animation.timestamps_.size() && time > animation.timestamps_[current_index + 1])
            ++current_index;

        if (current_index + 1 >= animation.timestamps_.size())
            current_index = 0;

        float t = (time - timestamps_[current_index]) / (timestamps_[current_index + 1] - timestamps_[current_index]);
        return lerp(values_[current_index], values_[current_index + 1], t);
    }
};

元のリンク: glTF モデル スケルトン アニメーション (mvrlink.com)

おすすめ

転載: blog.csdn.net/ygtu2018/article/details/132999268