glTFでのアニメーション
glTFでアニメーション
colladaに引き続き、glTFでもアニメーションを行うことができた。
モデルの権利表記
配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-AnimationUnlicense 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 | b | p |
となるが、glTFでは
OBJECT SPACE | -> | BONE SPACE | -> | OBJECT SPACE |
geometry | b | p |
となる。
glTFでのアニメーション
glTFでアニメーション
colladaに引き続き、glTFでもアニメーションを行うことができた。
モデルの権利表記
配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-AnimationUnlicense 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 | b | p |
となるが、glTFでは
OBJECT SPACE | -> | BONE SPACE | -> | OBJECT SPACE |
geometry | b | p |
となる。
glTFでのモデルの描画
glTFでモデルの描画
joint情報を読み込み、静止モデルの描画をしてみた。
モデルの権利表記
配布元リポジトリ
https://github.com/TheThinMatrix/OpenGL-AnimationUnlicense 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); }
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はナノセカンドの単位で整数値で取得されるので、を掛ける必要がある。
つまり 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は
- 今のkey frame1(t1)を基にしたanimation行列を計算するときに、次のkey frame2(t2)のanimation行列も計算する。
- key frame1による位置v1を計算する。
- key frame2による位置v2を計算する。
- 今の時間tから一次的に計算する。
v = v1 + ((t - t1) / (t2 - t1 )) * (v2 - v1)
cowboyの位置は固定ではなくなったかもしれないが、そもそもカクカクしていることには変わらない。
今はframeを制限せず、描画できるかぎりをレンダリングしているが、ある程度(例えば30fpsなどに)抑えた方がよいのだろうか。
ただAndroidのGPU Watchの機能で見てみても、11 fps程度しか出ていないので、そもそも計算に時間がかかりすぎている疑惑もある。
今のところGPUで計算 -> CPUで集計 -> バッファーに格納し直し としているので、GPU計算のみでAnimationの位置が計算できるようになるのが一番の近道であるように思える。
課題
今まで実装して確認したところ、compute shader内で共有のバッファーオブジェクトにデータを書き込むと結果が反映されず、データのwriteはできなかった。
ただ、これを解決できることがボトルネック解消の最短の近道なので、compute shader内で計算結果の格納(write)をできる道を探ってみる。
Animationの描画(補間なし)2
cowboy君に色を付けた
前回のcowboy君はおかしな色がついていたが、正常な色になるようにした。
方法の考察
前回はgeometryごとにvertexを作成していたので、vertexの数が少なく小メモリーとなっていたが、今回の方法ではインデックスバッファーに0から始まるシリアルの数を入れているので、頂点を重複して登録している。
メモリ効率は悪くなるが、1頂点につき複数のテクスチャを貼るので、こうする他ないのか?
バッファーとしてどのように持つのがもっとも効率がよいのだろうか。