Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

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頂点につき複数のテクスチャを貼るので、こうする他ないのか?
バッファーとしてどのように持つのがもっとも効率がよいのだろうか。

Animationの描画(補間なし)

Animationの描画

ようやく、本当にようやくanimationの描画に成功したので、まだまだ課題は山積しているが、現状のカウボーイ君を載せる。

課題

まず、ご覧の通りわかりやすい課題があり

  • 補間がされていないのでカクカク
  • カウボーイ君の色が変

の改善が必要。

ただ

  • colladaファイルから情報を読み取り
  • それをcompute shaderで計算し、ray tracing pipelineで描画
  • 以上をAndroidのアプリで行う

の工程をフルスクラッチで行うことができた。

まだまだ学ぶことはあるが、ここまでできた証として残すとともに、これからもAndroid + Vulkan + Ray Tracingの知識と技術を充実させていく。

compute shaderによるanimationの計算

Compute Shader

ray tracingパイプラインのみでレンダリングしているとvertex shaderでのアニメーションの計算を行わないので、compute shaderでの計算方法を模索していた。
とりあえず、ソースデータからデスティネーションデータにデータをコピーするところまでは成功。

GPU計算結果のデバッグ

shaderのデバッグで困るのが、計算結果のデバッグIDEデバッグ機能などで見れないこと。
GPU側のメモリまでは見に行けないので、デバッグ専用にGPU計算結果を一度CPU側のメモリに格納し直してから見直す必要がある。(と思う)
(他に簡単な方法があればご教授ください)

VkDeviceMemory(Local(CPU)) => VkDeviceMemory(Device(GPU)) => Data(CPU)
CPU側で準備 コピー GPUで計算 コピー data

local deviceとして使用しているdevice memoryから、compute shaderで計算後にデータを格納し直す。
方法としては、CPU=>GPUでメモリをコピーしている向きと逆向きでデータをコピーすればOK

vkMapMemory(device, bufferMemory, 0, dataSize, 0, &tmpBuffer);
memcpy(data, tmpBuffer, dataSize);
vkUnmapMemory(device, bufferMemory);

memcpy()関数自体はCで用意されている関数で

#include
void *memcpy(void *buf1, const void *buf2, size_t n);

第一引数にコピー先のメモリブロックのポインタ
第二引数にコピー元のメモリブロックのポインタ
第三引数はコピーサイズ
https://bituse.info/c_func/56

なので、vkMapMemoryでVkDeviceMemory(Device(GPU))を指定した後、memcpy()のdataに、格納したい配列のアドレスを渡す。

colladaファイルからanimationを動かす方法

colladaファイルの特徴まとめ

  1. 行列はrow majorで格納されている => VulkanやOpenGLで用いる場合にはtransposeなどを行い、column majorへの変換が必要
  2. (実装方法によるが) > INV_BIND_MATRIXは使わなくてもよい

collada ファイルのanimation

プログラムを実装してみた後の、個人的な理解をまとめる。
間違いや改善点などがあれば、適宜修正していきたい。

まず、言葉の定義として以下を用いる。
同じ意味でも別の用語が使われる場合が多々あるので、ここでは以下の用語で統一する。
https://community.khronos.org/t/need-help-with-skeletal-animation/68668/3
の、偉大な先人の記述を参考にさせていただいている。
OBJECT SPACE(BIND POSE) : poseのこと。最終的な描画に用いられる座標
BONE SPACE(LOCAL SPACE や JOINT SPACEとも呼ばれる) : 各jointのoriginalの位置を指す座標
JOINT MATRIX : 各jointについて定義されている、BONE SPACE -> OBJECT SPACEに変換する行列
ANIMATION MATRIX : 各jointについて定義されている、BONE SPACE -> OBJECT SPACEにanimationの位置に変換する行列

まず、library_geometriesを読み込んだだけのmeshは、OBJECT SPACEにある。
初期状態のmeshのJOINT MATRIXは colladaファイルにおいてはlibrary_visual_scene > node > matrix に格納されている。

最終的なanimation poseはOBJECT SPACEにあり、その変換に必要なANIMATION MATRIXはcolladaファイルにおいては > matrix_output に記述されている。
ANIMATION MATRIXは BONE SPACE -> OBJECT MATRIXに変換する行列なので、まとめると

OBJECT SPACE -> BONE SPACE -> OBJECT SPACE
mesh 情報 ①の逆行列 original bone animation行列 animation pose


という経路での変換が必要。

なのでまずはmesh情報をBONE SPACEに戻す必要があるのでの行列の逆行列を使ってBONE SPACEに変換していく。

OBJECT SPACE -> BONE SPACE

ここで考慮が必要なのは、jointは階層構造になっており、行列は親の影響を受ける。
以下はサンプルとして、CPUでポジションを計算するプログラムで実装する場合には、再帰関数で実装すると都合がよさそう。

void objectToBone(Joint joint, glm::mat4 parentJointTransform){
    glm::mat4 jointTransform = parentJointTransform * joint.jointMatrix;
    glm::mat4 invJointTransform = glm::inverse(jointTransform);
    for(auto p : joint.jointPositions){
        bonePosition = invJointTransform * p;
    }
    for(auto &child : joint.children){
        objectToBone(child, jointTransform);
    }
}

といったイメージとなる。
ここで伝えたいことは、

  1. 親から渡されたjointTransformの逆行列は、一度右から自分のJOINT MATRIXを掛けた後に逆行列変換をするとよい。
  2. jointは階層構造なので、再帰関数にして子に伝えていくのがおそらく一番楽。

BONE SPACE -> JOINT SPACE

先の関数を修正する。

void objectToBone(Joint joint, glm::mat4 parentJointTransform, glm::mat4 parentAnimationTransform){
    parentJointTransform = parentJointTransform * joint.jointMatrix;
    glm::mat4 invJointTransform = glm::inverse(parentJointTransform);
    parentAnimationTransform = parentAnimationTransform * joint.animationMatrix;
    glm::mat4 finalTransform = parentAnimationTransform  * invJointTransform;
    for(auto p : joint.jointPositions){
        bonePosition = invJointTransform * p;
        newPosition += p.weight * finalTransform * bonePosition;
    }
    for(auto &child : joint.children){
        objectToBone(child, jointTransform);
    }
}

ここで伝えたいことは

  1. finalTransformの順番(Vulkan, OpenGLの場合)
  2. ここでは詳しく書いてはいないが、各点には影響を受けるjointの重さがあるので、それを加味してnewPositionの計算を行っている

ここでは座標変換を見るためにCPUで計算する関数を簡単に書いているが、実際にはshaderを用いて計算すると思うので、同様に展開していただけばと思う。

この変換を行えば、オブジェクトはanimationで定義されている位置に変換される。

アニメーションポーズのレンダリングの成功

アニメーションの1ポーズのレンダリングに、ようやく成功した。
colladaファイルからモデルを読み込み、vulkanでレンダリングしている。
何カ月もかかり、本当に長かった。

アニメーション用の座標変換などについても調べたので、これから記事にしたい。
それにしても、ようやく1ポーズが描けた。
今までは、関節が変に延びたり、首だけやたら長かったりと、本当に苦労した。

GitHub - TheThinMatrix/OpenGL-Animation: A simple example of skeletal animation using OpenGL (and LWJGL).
には最大限の感謝を!!

アニメーションの1ポーズ