Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

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

アニメーションの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.

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

GL_EXT_scalar_block_layoutについて

概要

Vulkan + Ray Tracingのコードを書き始めたときから気になっていたGL_EXT_scalar_block_layoutの拡張機能について調べてみた。

GL_EXT_scalar_block_layoutについて

仕様書はこちら
GLSL/GL_EXT_scalar_block_layout.txt at master · KhronosGroup/GLSL · GitHub

vulkanのray tracingでも見たことがあるような感じで、GitHubにテキストが置かれているだけの仕様書となっている。

OpenGL Shading Language Specificationを参照する形式で記述されていので、こちらも見ながら仕様書を読む必要がある。
https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf

Requirement

仕様書には、以下のように書かれている。

  • Written against SPIR-V 1.3.
  • Written against GLSL 4.6.
  • Written against Vulkan 1.1 with the VK_EXT_scalar_block_layout extension.

「この仕様書は~に対して書かれる」といった書かれ方だが、素直に上記のバージョンが必要であると読んでよさそう。

Overview

Adds a new block layout (scalar) for uniform, push constant, and storage
buffer blocks.
This new layout aligns values only to the scalar components of the block
and its composite members.

Additionally, this extension now allows uniform blocks to be decorated
with the std430 layout.

GL_EXT_scalar_block_layoutの拡張機能の肝は新しいblock layoutの追加であり、
そのblock layoutの追加はuniform, push constant, and storage buffer blocksに対して行われる。

またuniform blocksがstd430 layoutで修飾されていてもOKとある。

詳細

Section 4.4

表の1行目の1セルにscalarを追加とあるので、以下のようになる。

Layout Qualifier Qualifier Only Individual Variables Block Block Member Allowed Interfaces
shared
packed
std140
std430
scalar
X X uniform /
buffer

Section 4.4.5

Such a structure and each structure member have a base offset
and a base alignment, from which an aligned offset is computed
by rounding the base offset up to a multiple of the base
alignment.

scalerの修飾子をつけたuniformやbufferのoffsetやalignmentを定義しており、offsetとalignmentのルールはvulkanとのinterfaceで定めている内容に沿っている。

具体例の考察

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

例えば上記のようにglslシェーダの中に記述できる。
拡張機能の有効化によりlayout修飾子にscalarが追加されたので、layout()内でscalarを宣言できる。

bufferのメンバーはVertex3Dというユーザ定義の構造体である。
この場合

5. If the member is a structure, the base alignment of the structure is
N, where N is the largest base alignment value of any of its members.

とあり、最大のalignmentとなる要素がvec4であったとすれば、base alignmentはvec4のものとなる。
base alignmentが決まればaligned offsetも決まるため、そこからbase offsetもおそらく計算できそう。
このようにしてbase offsetとbase alignmentが決まるので、シェーダの中で

 Vertex3D v0 = vertices[0].v[0]

のようにして、vulkanからシェーダに渡しているデータの要素を取ってこれる。
ray tracingシェーダの場合、頂点の情報はraygen shaderから渡されない限り、各シェーダ(pixcelごと)が独自にbufferにアクセスする必要があるので、上記の拡張機能を利用できる。

参考リポジトリ

私が書いているサンプルコードは以下にあります。
GitHub - kodai731/Aqoole-Engine-Android-Vulkan-Rendering-Engine-: Android + Vulkan Rendering Engine

モデルのレンダリング完成(Android + Vulkan + Ray Tracing)

完成図

モデルのレンダリングだけでなく、キューブに反射するモデルも描けているところがray tracingポイントです。

1
2
3

前回からの変更点

調べてみると、Android 11からはandroid ndkが提供する画像デコーダライブラリが使える模様。
pngjpegで描かれるテクスチャを読み込んでvulkanにimage bufferとして渡すときに使用する。
画像デコーダー  |  Android NDK  |  Android Developers
上記で行えることはpng/jpegイメージを読み込んでvoid*に書き込むところまでなので、そのデータをvulkan bufferに渡すところは自分で用意する必要がある。

    AAsset* file = AAssetManager_open(app->activity->assetManager,
                                      imagePath, AASSET_MODE_STREAMING);
    AImageDecoder *decoder;
    if(AImageDecoder_createFromAAsset(file, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS)
        __android_log_print(ANDROID_LOG_DEBUG, "aqoole error", "error in decode image %d", 0);
    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
    int32_t width = AImageDecoderHeaderInfo_getWidth(info);
    int32_t height = AImageDecoderHeaderInfo_getHeight(info);
    AndroidBitmapFormat format =
            (AndroidBitmapFormat) AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
    size_t stride = AImageDecoder_getMinimumStride(decoder);  // Image decoder does not use padding by default
    size_t size = height * stride;
    void* pixels = malloc(size);
    if(AImageDecoder_decodeImage(decoder, pixels, stride, size) != ANDROID_IMAGE_DECODER_SUCCESS)
        __android_log_print(ANDROID_LOG_DEBUG, "aqoole error", "error in decode image %d", 0);
    AImageDecoder_delete(decoder);
    AAsset_close(file);
    //copy data to tmp buffer
    VkDeviceSize imageSize = size;
    int texWidth = width;
    int texHeight = height;
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    AEBuffer::CreateBuffer(mDevice, imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
                           stagingBufferMemory);
    AEBuffer::CopyData(mDevice, stagingBufferMemory, imageSize, (void*)pixels);
    //create image
    AEImage::CreateImage2D(mDevice, (uint32_t)texWidth, (uint32_t)texHeight, VK_FORMAT_B8G8R8A8_UNORM,
                           VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_USAGE_TRANSFER_DST_BIT |
                                                                               VK_IMAGE_USAGE_SAMPLED_BIT, VK_SHARING_MODE_EXCLUSIVE, VK_SAMPLE_COUNT_1_BIT, &mImage);
    //bind image to memory
    AEImage::BindImageMemory(mDevice, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                             &mImage, &mImageMemory);
    //command buffer create
    AECommandBuffer singleTimeCommandBuffer(mDevice, commandPool);
    //begin single time command
    AECommand::BeginSingleTimeCommands(&singleTimeCommandBuffer);
    AEImage::TransitionImageLayout(mDevice, &singleTimeCommandBuffer, VK_IMAGE_LAYOUT_UNDEFINED,
                                   VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &mImage);
    //copy buffer to image memory
    AEImage::CopyBufferToImage(mDevice, &singleTimeCommandBuffer, (uint32_t)texWidth, (uint32_t)texHeight,
                               &mImage, &stagingBuffer);
    AEImage::TransitionImageLayout(mDevice, &singleTimeCommandBuffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                   VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &mImage);
    //end command
    AECommand::EndSingleTimeCommands(&singleTimeCommandBuffer, queue);
    //create image view
    AEImage::CreateImageView2D(mDevice, &mImage, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT,
                               &mImageView, 1);
    //clean buffer
    vkFreeMemory(*mDevice->GetDevice(), stagingBufferMemory, nullptr);
    vkDestroyBuffer(*mDevice->GetDevice(), stagingBuffer, nullptr);
    free(pixels);

ただ上記は読み込み方法をstb_imageから変更しただけで、根本解決ではなかった。

rchitシェーダ

根本原因は配列の読み込みミスだった。
vertices[nonuniformEXT(objId)]とすることでrayが衝突したオブジェクトごとのvertexデータを取ってこれるが、今回はtextureデータを使用する関係で別のbufferにデータを格納していた。

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

今回の場合ではVerticesobj のブロックはモデルしか使わないので、アクセスはverticesobj[0].vobj[ind.x];などとすれば解決した。
オブジェクト番号と配列の番号が一致しないときはverticesobj[nonuniformEXT(objId)].vobj[ind.x];としてはダメ。

モデルの描画(Android + Ray Tracing)

モデルのレンダリング

f:id:Aqoole_Hateena:20220416224801p:plain
2
f:id:Aqoole_Hateena:20220416224732p:plain
1

思ったのと違うが、stbi_loadが使えない影響でstbi_load_from_memoryにしているからだろうか?
ただ部位ごとにはtextureで区切れているので、今はこれでよしとしようか。
ガラスで透過した絵が簡単に描けるところもray tracingの強み。

お借りしているモデルはこちら
https://skfb.ly/6S9UN
LICENSE Creative Commons Attribution
https://creativecommons.org/licenses/by/4.0/legalcode

今後の課題

単純な配列をshaderに渡せなかったので、今はmtl(マテリアル)の区切り位置のoffsetを決め打ちでハードコーディングしているので、shaderに渡せるようにしたい。
そのために、

#extension GL_EXT_scalar_block_layout : enable
#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_GOOGLE_include_directive : enable
layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];
layout(binding = 4, set = 0) buffer Indices {uint i[];} indices[];

このあたりの、拡張機能を用いてのshaderにbufferを渡している部分の調査と理解が必要。

オブジェクトが増えてくると、descriptor setとかlayoutとかのvulkan側のコーディングが大変になってくるので、何か工夫したい。
pipelineを分けるとか?