Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

glTFでのアニメーション

glTFでアニメーション

colladaに引き続き、glTFでもアニメーションを行うことができた。

モデルの権利表記

配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-Animation

Unlicense license
https://unlicense.org/

glTFでanimationを行うときの注意点

nodeとjointとの関係のようなものは別の機会にまとめたいと思うが、注意点としては以下のようなものがある。

  • inverse bind matrixはjointの順番に並んでいる
  • animationのtargetはnodeの番号
  • inverse bind matrixは親nodeからの継承も含めて計算されている
  • rotationのQuaternionからの変換はwikiにある変換を用いればよい

Rotation formalisms in three dimensions - Wikipedia


colladaでanimationを行う際には
[Animation] colladaファイルの読み取りからAnimationを行うまで - Qiita
でまとめた通り

OBJECT SPACE -> BONE SPACE -> OBJECT SPACE
geometry (J_{A} * J_{B1} * J_{C1})^{-1} b A_{A} * A_{B1} * A_{C1} p

となるが、glTFでは

OBJECT SPACE -> BONE SPACE -> OBJECT SPACE
geometry J_{C1}^{-1} b A_{A} * A_{B1} * A_{C1} p

となる。

glTFでのアニメーション

glTFでアニメーション

colladaに引き続き、glTFでもアニメーションを行うことができた。

モデルの権利表記

配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-Animation

Unlicense license
https://unlicense.org/

glTFでanimationを行うときの注意点

nodeとjointとの関係のようなものは別の機会にまとめたいと思うが、注意点としては以下のようなものがある。

  • inverse bind matrixはjointの順番に並んでいる
  • animationのtargetはnodeの番号
  • inverse bind matrixは親nodeからの継承も含めて計算されている
  • rotationのQuaternionからの変換はwikiにある変換を用いればよい

Rotation formalisms in three dimensions - Wikipedia


colladaでanimationを行う際には
[Animation] colladaファイルの読み取りからAnimationを行うまで - Qiita
でまとめた通り

OBJECT SPACE -> BONE SPACE -> OBJECT SPACE
geometry (J_{A} * J_{B1} * J_{C1})^{-1} b A_{A} * A_{B1} * A_{C1} p

となるが、glTFでは

OBJECT SPACE -> BONE SPACE -> OBJECT SPACE
geometry J_{C1}^{-1} b A_{A} * A_{B1} * A_{C1} p

となる。

glTFでのモデルの描画

glTFでモデルの描画

joint情報を読み込み、静止モデルの描画をしてみた。

モデルの権利表記

配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-Animation

Unlicense license
https://unlicense.org/

glTFフォーマットの読み込み方

tiny glTFを用いてglTFのモデルを読み込んでいる。ライセンスはMITライセンス。
https://github.com/syoyo/tinygltf

使い方は以下のgit hubにあるコードを参考にした。ライセンスはApache 2.0。
https://github.com/techlabxe/vk_raytracing_book_1/blob/master/Common/include/util/VkrModel.h
https://github.com/techlabxe/vk_raytracing_book_1/tree/master/Common/src/util

glTFの構造については仕様書にまとめられている。
glTF™ 2.0 Specification

アニメーションの読み込みと描画に成功すれば、情報を改めてまとめ直したいと思っているが、とりあえずコードは以下のようなもの。

    using namespace tinygltf;
    std::vector<glm::uvec4> tmpJoint;
    std::vector<glm::vec4> tmpWeight;
    const uint8_t* jointSrc;
    const glm::vec4* weightSrc;
    for(auto& primitive : model.meshes[0].primitives){
        //each primitive
        Geometry geo = {};
        for(auto& attr : primitive.attributes) {
            std::string attName = attr.first;
            //positions
            if (std::regex_search(attName, std::regex("position", std::regex::icase))) {
                const auto &posAccr = model.accessors[attr.second];
                const auto &posBufView = model.bufferViews[posAccr.bufferView];
                size_t offsetByte = posAccr.byteOffset + posBufView.byteOffset;
                const auto *src = reinterpret_cast<const glm::vec3 *>(&(model.buffers[posBufView.buffer].data[offsetByte]));
                size_t vertexSize = posAccr.count;
                for (uint32_t i = 0; i < vertexSize; i++) {
                    geo.positions.emplace_back(src[i]);
                }
                //indices
                const auto &indexAccr = model.accessors[primitive.indices];
                const auto &indexBufView = model.bufferViews[indexAccr.bufferView];
                size_t indexOffsetByte = indexAccr.byteOffset + indexBufView.byteOffset;
                const auto *indexSrc = reinterpret_cast<const uint16_t *>(&(model.buffers[indexBufView.buffer].data[indexOffsetByte]));
                for (uint32_t i = 0; i < indexAccr.count; i++)
                    geo.indices.emplace_back((uint32_t) indexSrc[i]);
            }
            //texture coord
            if (std::regex_search(attName, std::regex("texcoord", std::regex::icase))) {
                const auto &tcAccr = model.accessors[attr.second];
                const auto &tcBufView = model.bufferViews[tcAccr.bufferView];
                size_t offsetByte = tcAccr.byteOffset + tcBufView.byteOffset;
                const auto *tcSrc = reinterpret_cast<const glm::vec2 *>(&model.buffers[tcBufView.buffer].data[offsetByte]);
                for (uint32_t i = 0; i < tcAccr.count; i++)
                    geo.texCoords.emplace_back(tcSrc[i]);
            }
            //joint information
            if(std::regex_search(attName, std::regex("joint", std::regex::icase))){
                const auto& jointAccr = model.accessors[attr.second];
                const auto& jointBufView = model.bufferViews[jointAccr.bufferView];
                size_t offsetByte = jointAccr.byteOffset + jointBufView.byteOffset;
                //in case of acc.componentType = TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE 
                jointSrc = reinterpret_cast<const uint8_t*>(&model.buffers[jointBufView.buffer].data[offsetByte]);
                for(uint32_t i = 0; i < jointAccr.count; i++){
                    uint32_t index = i * 4;
                    glm::uvec4 u(jointSrc[index], jointSrc[index + 1], jointSrc[index + 2], jointSrc[index + 3]);
                    tmpJoint.emplace_back(u);
                }
            }
            //weight information
            if(std::regex_search(attName, std::regex("weight", std::regex::icase))){
                const auto& weightAccr = model.accessors[attr.second];
                const auto& weightBufView = model.bufferViews[weightAccr.bufferView];
                size_t offsetByte = weightAccr.byteOffset + weightBufView.byteOffset;
                weightSrc = reinterpret_cast<const glm::vec4*>(&model.buffers[weightBufView.buffer].data[offsetByte]);
                for(uint32_t i = 0; i < weightAccr.count; i++)
                    tmpWeight.emplace_back(weightSrc[i]);
            }
        }
        mGeometries.emplace_back(geo);
    }

今後の作業

joint情報を読み込んで、compute shaderで頂点を計算するところまでできたので、次はanimationの情報を読み込んでモデルを動かせるようにしたい。
以前にcolladaで同じことをしたが、かなり難しく時間がかかったので覚悟はしているが、手元のスマホ(android)でモデルを動かせるようになりたい。

Animationの完成

Animationの実装

今までの実装では時間の取得の仕方が間違っていた。
時間の表示を正常にするとコマ飛びすることなくアニメーションが表示された。

時間の取得方法

実装当初に色々調べた際に、あまり深く調べることなく以下のstack overflowのanswerをそのまま流用していた。
How to get the current time in native Android code? - Stack Overflow

#include

// from android samples
/* return current time in milliseconds */
static double now_ms(void) {

struct timespec res;
clock_gettime(CLOCK_REALTIME, &res);
return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6;

}

しかしコメント文にあるように、このコードではミリセカンドの単位で時間が取得できる。
アニメーションを行うときにはセカンドの単位で表示していたので、1000倍の速さについていけずコマ飛びしていた。

timespec構造体

仕様は以下に書かれてある。
cpprefjp.github.io

tv_sec エポックからの経過秒。値は0以上
tv_nsec ナノ秒単位で表される秒未満の値 値の範囲は[0, 999'999'999]

tv_nsecはナノセカンドの単位で整数値で取得されるので、 1.0 * 10^{-9}を掛ける必要がある。
つまり clock_gettime を実行したときの時刻tは

double t = res.tv_sec + res.tv_nsec / 1e9;

で取得できる。

compute shaderでanimation部分を計算してみた

compute shaderでanimation部分の計算

vertex shaderとfragment shaderを用いるのであれば、vertex shaderでanimation後の位置に更新した後fragment shaderで色を付ければよいと思うが、raytracing pipelineでレンダリングする場合にはどの時点で位置を計算するのが良いのかわからなったので、compute shaderで計算するようにした。
もしかすれば、raytracing pipelineの中で頂点座標の位置を更新する方法もあるかもしれないが、rayがヒットするタイミングがどうなんだろうと思ったので、raytracing pipelineに入る前にcompute pipelineを動かし、compute pipelineの計算が終わった後にraytracing pipelineの処理を行うようにした。

compute shader部分

簡単に紹介

layout(binding = 1, set = 0, scalar) buffer VerticesobjDst {Vertex3DObj vobjDst[];} verticesobjDst[];
:
      vec3 v1 = w * (mats[0].m[jj] * vec4(v0, 1.0)).xyz;
      vec3 v2 = w * (matsNext[0].m[jj] * vec4(v0, 1.0)).xyz;
      if (animNum.a == 4){
        v3 += v1 + ((v2 - v1) * (1.0 - time.t));
      } else {
        v3 += v1 + ((v2 - v1) * (time.t - KeyFrames[animNum.a]));
      }
:
    verticesobjDst[0].vobjDst[ii].pos = v3;

mats:今のkey frameのanimation matrix
matsNext:次のkey frameのanimation matrix
今のkey frameと次のkey frameの位置を計算し、今の時間分だけ線形に補間している。
compute shader内ではraytracing pipelineでも用いるstorage bufferを共有しているので、storage bufferの値を直接更新することで、device(GPU)側のメモリだけで処理が完結できていると思っている。

個人的な反省

今回は別のところで、間違ったことをしており時間がかかってしまった。
bufferのアップデートの処理でindexを間違えており、animation matrixを更新するはずがずっとjointの重み付けのbufferをanimation matrixの値で更新していた。
そのせいでbufferの値が滅茶苦茶になっていたが、まさか更新先のbufferが間違えているとは思わず、調査にすごく時間がかかってしまった。
気づいてしまえばindexを間違えていただけなのだが、同期処理のタイミングを疑ってみたり、shader内でのstorage bufferの更新処理を疑ってみたり、メモリアラインメントを疑ってみたりetc...
凡ミスをしないようなアーキテクチャが必要なんだろうと思うが、今回で言えばバッファーをindexで管理せずに名前などで管理するなどになるか。

Animationの補間を実装してみた

アニメーション補間の実装

interpolation(補間)を実装してみた結果がこちら。

interpolationは

  1. 今のkey frame1(t1)を基にしたanimation行列を計算するときに、次のkey frame2(t2)のanimation行列も計算する。
  2. key frame1による位置v1を計算する。
  3. key frame2による位置v2を計算する。
  4. 今の時間tから一次的に計算する。
v = v1 + ((t - t1)  / (t2 - t1 )) * (v2 - v1)

cowboyの位置は固定ではなくなったかもしれないが、そもそもカクカクしていることには変わらない。
今はframeを制限せず、描画できるかぎりをレンダリングしているが、ある程度(例えば30fpsなどに)抑えた方がよいのだろうか。
ただAndroidGPU Watchの機能で見てみても、11 fps程度しか出ていないので、そもそも計算に時間がかかりすぎている疑惑もある。
今のところGPUで計算 -> CPUで集計 -> バッファーに格納し直し としているので、GPU計算のみでAnimationの位置が計算できるようになるのが一番の近道であるように思える。

課題

今まで実装して確認したところ、compute shader内で共有のバッファーオブジェクトにデータを書き込むと結果が反映されず、データのwriteはできなかった。
ただ、これを解決できることがボトルネック解消の最短の近道なので、compute shader内で計算結果の格納(write)をできる道を探ってみる。

Animationの描画(補間なし)2

cowboy君に色を付けた

前回のcowboy君はおかしな色がついていたが、正常な色になるようにした。

方法の考察

前回はgeometryごとにvertexを作成していたので、vertexの数が少なく小メモリーとなっていたが、今回の方法ではインデックスバッファーに0から始まるシリアルの数を入れているので、頂点を重複して登録している。
メモリ効率は悪くなるが、1頂点につき複数のテクスチャを貼るので、こうする他ないのか?
バッファーとしてどのように持つのがもっとも効率がよいのだろうか。