Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

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ポーズ

Phoenix 静止状態のレンダリング

Androidに表示した画面

colladaファイルからモデル情報を読み取り、その結果をそのまま表示した図

phoenix 1

モデルの権利表記

This model was created by NORBERTO-3D
https://sketchfab.com/3d-models/phoenix-bird-844ba0cf144a413ea92c779f18912042

LICENSE Creative Commons Attribution
https://creativecommons.org/licenses/by/4.0/legalcode

colladaファイルについて

colladaファイルを選んだ理由は

  • バイナリー形式でないので、自分のライブラリで読み込める
  • animationをサポートしている

という理由からcolladaファイルを読み込んでキャラを動かせるようになることを目標に取り組んできた。
glTF 2.0でも同じことができるはずなので、ひと段落すれば取り組んでみてもよいかも。

ただ、xmlで書かれているcollada形式の3Dモデルを読み込んでから、Vulkan APIを用いてのレンダリングはとてもやることが多く、時間がかかりまくっている状況。
colladaファイルを読み取る過程で調べた、各属性の依存関係のようなものはいつかまとめて画像一枚で説明するようなものをブログにアップしたいと思っているが、animationの実装が終われば取り組んでみたい。

animation付きのcolladaファイルについて

3Dモデルはsketchfabというサイトに、絵師様たちがアップロードしてくださっている。
Sketchfab - The best 3D viewer on the web
3Dモデルはfbxの拡張子でアップされることが多いと思うが、fbxはバイナリ形式である。(グラフィクスライブラリ内部では、Autodesk社のSDKを用いてパースすると思われる。それかASCIIファイルに変換するか。)
個人的には、今作成している個人ライブラリにはオープンなフォーマットを使いたかったので、animationまで内包しているcollada形式で読み込みたいのだが、animationを保ったままfbxからcolladaに変換する方法を探すのにとても時間がかかった。(見つけた方法も、たぶんすべての3Dモデルをカバーできているわけではないと思ってる。)
Blenderを用いてfbxファイルをimportし、collada形式でexportすればmesh情報などは正確なのだが、出力されたcolladaファイルをBlenderでimportしてanimationを実行してみると、何故かバグった状態で実行される。
Blenderで読み込めないものを個人製作したエンジンで読み込めるわけがないので、Blender上で正確にanimationを実行できるようなcolladaファイルの作成の仕方を探していた。
結論的には

sketchfabでfbxではなくglTF形式でダウンロードし、BlenderでglTFをcolladaに変換する

方法で実現できた。

ここからanimation用の行列を読み取り、座標変換を繰り返す部分の勉強と実装が待っており、時間がかかると思うが、とりあえず(予想以上に時間がかかったが)正しい情報を持ったモデルが用意できたことには一安心。

Blenderでfbx => collada変換するとanimationがうまく変換できない

対処法

プログラムでモデルをレンダリングしたい方向けの記事。
モデルはsketchfabというサイトでモデルをダウンロードしていることを想定。
sketchfab.com

対処法としては

fbxフォーマットでなくglTF 2.0のフォーマットでダウンロードする。

その後glTF -> colladaで変換すると、アニメーションまで正確に変換できる。
fbxからのcollada変換(アニメーション付き)が、どう頑張ってもどう調べても上手くいかなかったので、結局元ファイルを変えるという選択肢しかなかった。

GL_EXT_nonuniform_qualifierについて

概要

vulkan + ray tracingのチュートリアルのコードを読んだときに気になっていた、GL_EXT_nonuniform_qualifierについて仕様の調査を行う。

仕様書

以下のGitHubが仕様書のようだ。
GLSL/GL_EXT_nonuniform_qualifier.txt at master · KhronosGroup/GLSL · GitHub

この仕様書内では以下のOpenGL Shading Language Specificationの仕様書を参照する。
https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf

Overview

This extension adds a "nonuniform" type qualifier and constructor, which
is required by the Vulkan API to be used when indexing descriptor
bindings with an index that is not dynamically uniform.

This extension also allows arrays of resources declared using unsized
arrays to become run-time sized arrays.

とある。
nonuniformという修飾子を追加して、bindされるresourcesをdynamicに扱えるようにする。

layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];

例えばこのように、vulkan側ではレイアウトだけ定義しているresourcesをshader側でサイズの指定なしに宣言することができて

    Vertex3D v0 = vertices[nonuniformEXT(objId)].v[ind.x];

shader内ではこのようにしてresourcesにアクセスできる。
verticesを複数の要素からなる配列として扱える。

Vukan API側のextension

Vulkan APIを使用するプログラムの側でも拡張機能を使用する対応が必要。
VK_EXT_descriptor_indexing(3)

device_extensions.push_back("VK_EXT_descriptor_indexing");

のようにVK_EXT_descriptor_indexingの拡張機能を有効にする。

Description

This extension adds several small features which together enable applications to create large descriptor sets containing substantially all of their resources, and selecting amongst those resources with dynamic (non-uniform) indexes in the shader. There are feature enables and SPIR-V capabilities for non-uniform descriptor indexing in the shader, and non-uniform indexing in the shader requires use of a new NonUniformEXT decoration defined in the SPV_EXT_descriptor_indexing SPIR-V extension.

GLSLでの内容と同じことが記述されているが、shader内にてdescriptor setで渡すresourcesにdynamicにアクセスし、使用できる。

Section 4.1.9 Arrays

Section 4.1自体はGLSLでの基本となる型を定義している章となる。

仕様書のこの章としては、サイズが定義されていない場合の扱いについて、書き足すものとなる。
以下で今の仕様と異なっている箇所をピックアップする。

An array whose size is not specified in a declaration is _unsized_.
Unsized arrays can either be implicitly sized or run-time sized.
A _run-time sized_ array has its size determined by a buffer or
descriptor set bound via the API.

宣言時に全体のサイズを定義しない箇所が追記されている。
この場合は配列の最大indexでもってサイズが定義されるか、(vulkan)APIによってランタイムで定められる。

Unsized arrays must not be passed as an argument to a function,

unsized arrayは関数に引数として渡せない。

Section 4.x Nonuniform qualifier

nonuniformEXT qualifierが追加となる。
nonuniformEXT修飾子は変数や式がdynamic uniformでないときに使用する。

The nonuniformEXT qualifier can also be used with constructor syntax to
assert that an expression is not dynamically uniform. For example:

layout(location = 0) flat in int i;
layout(set = 0, binding = 0) uniform sampler2D tex[2];

color = texture(tex[nonuniformEXT(i)], ...);

This constructor syntax takes a single argument of any type and returns
the value with the same type, qualified with nonuniformEXT.

サンプルコード内にもあるような使い方としては以上が挙げられ、解説されている。