Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

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

やりたいこと

ray tracingパイプラインでモデルをレンダリングしたい。
モデル自体は描画されるが、うまく色がつかない。
上部のcubesはガラスをイメージし、透過できるようにしている。

f:id:Aqoole_Hateena:20220412230509p:plain
モザイクのモデル

stb_imageの工夫

pngなどの画像データはstb_imageのライブラリを用いてるが、AndroidではそのままではFileやfopen()は使えないため、stbi_load()ではなくstbi_load_from_memoryで読み込んでいる。

    AAsset* file = AAssetManager_open(app->activity->assetManager,
                                      imagePath, AASSET_MODE_BUFFER);
    size_t fileLength = AAsset_getLength(file);
    auto fileContent = new unsigned char[fileLength];
    AAsset_read(file, fileContent, fileLength);
    AAsset_close(file);
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load_from_memory(fileContent, (int)fileLength, &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;

今後に向けて

モデルに色がつかない原因を調査する。
shaderの側でtextureにアクセスできていないか、vulkan appの側でうまく用意できていないか。

ガラスキューブをレイトレーシングで

ray tracingでガラスのレンダリング

f:id:Aqoole_Hateena:20220407230451p:plain
ガラスキューブ1
f:id:Aqoole_Hateena:20220407230526p:plain
ガラスキューブ2

ray tracingの真骨頂といえば、透過と反射だと(個人的には)思っていて、Androidでray tracingするからには、どこまで反射と屈折の回数を増やして計算できるのだろうと思っていた。
図の例では、3回反射と屈折を繰り返している。4回行うと途中でエラーしたので、この辺が限界のよう。
ray tracingでは、何も考えずにレイを飛ばしてヒットした先でレイを飛ばす、のようなことを行うとレイがミスする点がだんだん増えて図のように黒くなる点が増える?と思っているので、この辺りは要修正か。

環境

バイス : Samsung Galaxy S22
SoC : Exynos 2200 (Xclipse 920)

シェーダー

図のレンダリングはrchitシェーダーを2つ(AとBとする)使用してレンダリングしており、AのシェーダーからtraceRayEXTを呼び出し、レイがヒットした先でBのシェーダーを用いて色を取ってくる、といったことを繰り返している。
AMD系のGPUはrecursiveで呼び出すことができないので(昔試したときはそうだった)、このような手法にしている。
また時間があれば、シェーダーの中身も詳しく書きたい。

cubesに色がつかない

まとめ

複数オブジェクトを描画してみようとしているが、cubesに色がつかない

こんな感じ

f:id:Aqoole_Hateena:20220407044907p:plain
cubesに赤色がつかない

調べたこと

descriptor setのindex buffersをbindするときに

VUID-VkWriteDescriptorSet-dstArrayElement-00321

のメッセージが出ている。
khronos公式ページVkWriteDescriptorSet(3)でエラーメッセージを確認すると

The sum of dstArrayElement and descriptorCount must be less than or equal to the number of array elements in the descriptor set binding specified by dstBinding, and all applicable consecutive bindings, as described by https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#descriptorsets-updates-consecutive

VkWriteDescriptorSet の定義は以下。

// Provided by VK_VERSION_1_0
typedef struct VkWriteDescriptorSet {
    VkStructureType                  sType;
    const void*                      pNext;
    VkDescriptorSet                  dstSet;
    uint32_t                         dstBinding;
    uint32_t                         dstArrayElement;
    uint32_t                         descriptorCount;
    VkDescriptorType                 descriptorType;
    const VkDescriptorImageInfo*     pImageInfo;
    const VkDescriptorBufferInfo*    pBufferInfo;
    const VkBufferView*              pTexelBufferView;
} VkWriteDescriptorSet;

構造体の中のdstArrayElement+descriptorCountがdescriptor setの配列要素数以下にならなければならないようで、こちらはlayoutの作成時に指定する。

この記事を書きながら修正点に気づいたので、書き換えて実行してみるとcubesは赤く表示された

f:id:Aqoole_Hateena:20220407050654p:plain
赤いcubes

最後に

Hatena BlogはQiitaのようなcodeだけ書くような場所ではないと思うので、最後に雑感を少し。
プログラムの日記を書こうとすると、最後までビルドできないこともしばしばで、どのタイミングで記事にしようか迷うこともある。
今回は成功していない場合にでも投稿しようと思ったが、これでも意味はあるのだろうか?
同じエラーが出た人がいて、エラーのキーワードで検索したときに引っかかるかもしれないが、その場合には解決策が書かれていなければその人の問題は解決しない。。。
とりあえず書いておいて、解決すればリンクを貼るとかでもよいけど、それも大変そう。
まぁ何日か書きながら考えよう。

AndroidでRay Tracingできた

はじめに

ついにAndroidでリアルタイムray tracingできる時代が来た!ということでハローワールド的にray tracingで立方体を8つ描いた図がこちら。

f:id:Aqoole_Hateena:20220403233936p:plain
白背景に立方体4つをray tracingで描いてみた図

スマホは最近手に取れるようになったGalaxy S22を使っていますが、なんだか地味な画像だなと思われるかもしれません。ray tracingといえば高級なグラフィクスのゲームを連想される方がほとんどでしょうが、本来ray traceとはレンダリング手法のひとつで、反射や屈折といった目に見える景色をそのまま描画することに長けている描画方法のことです。上図はその手法を用いて簡単な図をレンダリングしてみました、ということになります。

Galaxy S22について

注意点ですが、日本で発売されるS22を購入しても(おそらく)ray tracingできません。この記事を見てS22が欲しくなる人はまれでしょうが、間違えて購入しないようにお願いします。
S22は欧州向けとそれ以外の地域向けで載っているSoCが異なっており、欧州向けは「Exynos2200(GPU : Xclipse920)」、それ以外は「Snapdragon」です。Xclipse920がAMDのRDNA2アーキテクチャーなので、ray tracingが可能です。
私はAmazon UKで購入しました。

Android Studioでの注意点

まだ描画に成功しただけで、プログラム的に細かい不備がたくさんある状態です。
開発環境はAndroid Studioです。
色々まとまってくれば、Qiitaにも久しぶりに投稿したいと思ってますが、主な注意点としては以下です。

  • 最新のndkが必要。(2022/4/3時点)

これ以上でないと、ray tracing pipelineの作成に必要な拡張機能が入っていないようです。

android {
    ndkVersion "25.0.8221429-beta2"
}

NDK のダウンロード  |  Android NDK  |  Android Developers

  • main/shaders配下にshaderファイルを置いても自動でコンパイルされない。

glslcで手動でコンパイルし、spvファイルを以下に格納する必要がある。

${PROJECT_DIR}\app\build\intermediates\assets\debug\mergeDebugAssets\shaders

Android の Vulkan シェーダー コンパイラ  |  Android NDK  |  Android Developers

  • validation layerとGoogleから出ているvulkan_wrapperを併用する場合、以下にvk_sdk_platform.hの追加が必要
${NDK_DIR}\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include\vulkan

今後に向けて

色の持ち方がBGRAになっているので、RGBAに直したいところ。
まだまだ画面に何か描けましたという状態ですが、ここまでくれば色々できるようになると思うので、スマホでどこまでできるか楽しみです。
2022/4/4追記
validation layerを入れると、拡張機能を用いているところのほとんどすべてでエラーのメッセージが出力される。実際にはエラーなしに動いているように見えるが、これはいったい?
調査が必要。
2022/4/5追記
enableしているはずなのに、vkGetDeviceAddressの拡張機能を有効にしろというメッセージが出力される。

Validation Error: [ VUID-vkGetBufferDeviceAddress-bufferDeviceAddress-03324 ] Object 0: handle = 0xea7170000000031, type = VK_OBJECT_TYPE_BUFFER; | MessageID = 0xa641531b | vkGetBufferDeviceAddressKHR: The bufferDeviceAddress feature must: be enabled. The Vulkan spec states: The bufferDeviceAddress or VkPhysicalDeviceBufferDeviceAddressFeaturesEXT::bufferDeviceAddress feature must be enabled (https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#VUID-vkGetBufferDeviceAddress-bufferDeviceAddress-03324)2754439499

ただ、有効化していないとそもそもbuffer addressをとってこれないし、debugで値を見てみるときちんと値が入っていることがわかる。
試しにvkGetBufferAddressEXTの関数を用いると、関数が見つからず使えなかった。
Vulkanのバージョンが上がるかvalidation layerのバージョンが上がるまで、callback関数の中でメッセージをフィルターする方向で進める。
ray tracing関係のextensionは軒並みvalidation errorが出ているように見えるので無視したいが、たまに実装が本当によくないことがあるのが事態をややこしくしている。

Android + VulkanでImguiの導入

出力例

なぜか最初の1frame目だけはimguiの部分がrenderingされていないが、2frame目以降は表示される。
fontが小さすぎて何書いているかわからない部分は今後の改善点。

f:id:Aqoole_Hateena:20220327210954p:plain
1frame目
f:id:Aqoole_Hateena:20220327211038p:plain
1frame目以降

IMGUIの基本

こちらを参照ください。
一言で言えば、C++GUI環境を構築するためのネイティブツールです。
github.com
以下は過去に私が書いたimgui関連の記事。
aqoole-hateena.hatenablog.com
qiita.com

Android + Vulkan

examplesには含まれていないが、Android + Vulkanの組み合わせでもrendering自体は可能。
どこまで使い勝手よくできるかは、今後の調査次第。

Window

Androidでimguiを導入する場合、windowはAndroidのシステムが提供してくれているANativeWindowを使うしかない。
そしてこの部分が今後どうなるかは不明だが、今の私が調べた限り、Vulkanがwindowと認識するものをAndroidのアプリの中でもうひとつ生成する方法が見つからなかった。
なので、ひとつのwindow(image)の中に

  • renderingしたい図形
  • Imguiの領域

の両方をrenderingする必要がある。

サンプルコード

imguiのexampleを眺めてみると、メインループの中でコマンドへの登録とqueueへのsubmitを行っている。
なのでこの出力例でもrenderingしたい図形とは別のコマンドバッファーにimguiのrenderingコマンドを登録するとともにすぐにsubmitする。

imguiのコマンドを登録する部分

void RecordImguiCommand(uint32_t imageNum)
{
  VkCommandBuffer* cb = gImgui->GetCommandBuffer()->GetCommandBuffer();
  VkCommandBufferBeginInfo cmdBufferBeginInfo{
          .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
          .pNext = nullptr,
          .flags = 0,
          .pInheritanceInfo = nullptr,
  };
  CALL_VK(vkBeginCommandBuffer(*cb, &cmdBufferBeginInfo));
  VkClearValue clearVals[2]{ {.color { .float32 {0.0f, 0.34f, 0.90f, 1.0f}}},{.depthStencil{.depth = 1.0f}}};
  VkRenderPassBeginInfo renderPassBeginInfo{
          .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
          .pNext = nullptr,
          .renderPass = render.renderPass_,
          .framebuffer = swapchain.framebuffers_[imageNum],
          .renderArea = {.offset { .x = 0, .y = 0,},
                  .extent = {.width = 150, .height = 200}},
          .clearValueCount = 2,
          .pClearValues = clearVals};
  vkCmdBeginRenderPass(*cb, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
  ImGui_ImplAndroid_NewFrame();
  ImGui_ImplVulkan_NewFrame();
  ImGui::NewFrame();
  static float f = 0.0f;
  static int counter = 0;
  ImGui::Begin("Parameters");                          // Create a window called "Hello, world!" and append into it.
  if (ImGui::Button("\nreset time\n"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
  {
        ResetCamera();
  }
  if (ImGui::Button("pause"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
//      paused = !paused;
  ImGui::SameLine();
  ImGui::End();
  ImGui::Render();
  ImDrawData* drawData = ImGui::GetDrawData();
  ImGui_ImplVulkan_RenderDrawData(drawData, *cb);
  vkCmdEndRenderPass(*cb);
  vkEndCommandBuffer(*cb);
}

メインループ内で呼び出す部分

bool VulkanDrawFrame(android_app *app, uint32_t currentFrame, bool& isTouched, bool& isFocused, glm::vec2* touchPositions,
                     glm::vec3* gravityData, glm::vec3* lastGravityData) {
  :
 中略
  :
  uint32_t nextIndex;
  // Get the framebuffer index we should draw in
  CALL_VK(vkAcquireNextImageKHR(device.device_, swapchain.swapchain_,
                                UINT64_MAX, render.semaphore_, VK_NULL_HANDLE,
                                &nextIndex));
  CALL_VK(vkResetFences(device.device_, 1, &render.fence_));
  RecordImguiCommand(nextIndex);
    VkPipelineStageFlags waitStageMask =
      VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
  VkCommandBuffer cmdBuffers[2] = {render.cmdBuffer_[nextIndex], *gImgui->GetCommandBuffer()->GetCommandBuffer()};
  VkSubmitInfo submit_info = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
                              .pNext = nullptr,
                              .waitSemaphoreCount = 1,
                              .pWaitSemaphores = &render.semaphore_,
                              .pWaitDstStageMask = &waitStageMask,
                              .commandBufferCount = 2,
                              .pCommandBuffers = cmdBuffers,
                              .signalSemaphoreCount = 1,
                              .pSignalSemaphores = &render.presentSemaphore_,};
  CALL_VK(vkQueueSubmit(device.queue_, 1, &submit_info, render.fence_));
  CALL_VK(
      vkWaitForFences(device.device_, 1, &render.fence_, VK_TRUE, 100000000));

  LOGI("Drawing frames......");

  VkResult result;
  VkPresentInfoKHR presentInfo{
      .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
      .pNext = nullptr,
      .waitSemaphoreCount = 1,
      .pWaitSemaphores = &render.presentSemaphore_,
      .swapchainCount = 1,
      .pSwapchains = &swapchain.swapchain_,
      .pImageIndices = &nextIndex,
      .pResults = &result,
  };
  vkQueuePresentKHR(device.queue_, &presentInfo);
  double currentTime = GetTime();
  //UpdateUI(app, 1000.0f / (float)(currentTime - lastTime));
  lastTime = currentTime;
  return true;
}

最後に

imgui領域のrenderingには成功したが、字が小さすぎるので、ここは改善していきたい。
また、ボタンを押しても指定した関数は実行されない。
Android + OpenGLESのexamplesにも書かれている通り、AndroidJava側で実行する関数の登録と呼び出しを実装しなければならなさそう。
これらの部分も今後調査して、実装していきたい。
私のGitHubプロジェクトはこちら。(ライセンスはApache2.0)
何かの参考になれば幸いです。
GitHub - kodai731/Aqoole-Engine-Android-Vulkan-Rendering-Engine-: Android + Vulkan Rendering Engine

popup windowの表示方法

動機

C/C++コード出身の人がndkを用いてandroidでコーディングする際に、追加情報などを画面に表示したいという需要はあると思う。
とりあえずコードがコンパイルできてプログラムが実行できれば、そこからは何とか勉強できる、というのが持論なので、ちょうどよさそうなサンプルコードを探していた。

サンプルコード

ndk samplesという、androidの公式が出しているサンプルコード集がある。
その中のteapotsというプロジェクトで、現在のFPSをpopup windowで表示するということを行っている。
Sample: Teapot  |  Android NDK  |  Android Developers
Giuhub Sampleはこちら
ndk-samples/teapots at main · android/ndk-samples · GitHub
おそらく権限の関係で公式のrepositoryから直接cloneできないので、私は一旦自分のrepositoryにforkしてからgit cloneした。

TeapotNativeActivity.java

このクラスに含まれている

public void showUI()

という関数がある。これがpopup windowを表示させる関数であるが、これをC++のコード側から呼び出す。

TeapotNativeActivity.cpp

ここでEngineというクラスの中でShowUI()という関数が用意されている。

void Engine::ShowUI() {
  JNIEnv* jni;
  app_->activity->vm->AttachCurrentThread(&jni, NULL);

  // Default class retrieval
  jclass clazz = jni->GetObjectClass(app_->activity->clazz);
  jmethodID methodID = jni->GetMethodID(clazz, "showUI", "()V");
  jni->CallVoidMethod(app_->activity->clazz, methodID);

  app_->activity->vm->DetachCurrentThread();
  return;
}

このような形で、これをmain関数の中で呼び出す。

updateFPS()

popup windowでwindowを作成した後は、そこに値を書き込む準備が必要である。
showUIと同じ要領でupdateFPSを、C++で呼び出して値を渡す。
今回はupdateなので、main loopの中で呼び出して値を更新する。

表示例

vulkanで試しているが、このように表示させることができる。
画面では、たくさんのcubesと左上にFPS表示をさせている。

f:id:Aqoole_Hateena:20220321194623p:plain
cubes + fps

あとがき

popup windowについて調査した理由

最初はimguiでデバッグ情報やinteract部分を担えるようなボタンやwindowを作成しようと思っていたが、これがなかなかに難航した。
single windowで、ユーザ定義の図形とimguiの図形の両方をレンダリングしようとすると、imageが交互に切り替わるような感じになり、非常に画面がチラつく。
ひとつのwindowにはひとつのswapchainしか作成できないので、swapchainを複数作成するという方法も不可能である。
複数作成しようとすると、VK_ERROR_NATIVE_WINDOW_IN_USE_KHRというエラーが表示される。
案としてはview portを変えて、ユーザ定義の領域とimguiのボタン用の領域を分けるか、新たにwindowを作成するかがあると思ったが、window作成の方が色々小回りが利くと思い、popup windowの作成方法を調べてみた。
このwindow領域にimguiのレンダリングが可能かどうかは、今後調べる必要がある。

git hub repository

上記のようなandroid + vulkanで色々描いている私のrepositoryはこちら
https://github.com/kodai731/Aqoole-Engine-Android-Vulkan-Rendering-Engine-

重力センサーの有効化と取得方法

結論

Androidの重力センサーの有効化と値の取得は、公式サンプルコードのままではうまくいかない。
サンプル: native-activity  |  Android NDK  |  Android Developers
先に結論を書くと、以下の過程が必要
(※ASensorEventQueue_enableSensor()は必須だが、ASensorEventQueue_registerSensor()は任意)

//initialize sensors
gSensors.gSensorManager = ASensorManager_getInstance();
gSensors.gSensorAccelerometer = ASensorManager_getDefaultSensor(gSensors.gSensorManager, ASENSOR_TYPE_ACCELEROMETER);
gSensors.gSensorGravity = ASensorManager_getDefaultSensor(gSensors.gSensorManager, ASENSOR_TYPE_GRAVITY);
gSensors.gSensorQueue = ASensorManager_createEventQueue(gSensors.gSensorManager, app->looper, LOOPER_ID_USER, NULL, NULL);
__android_log_print(ANDROID_LOG_DEBUG, "register sensor test",
                    std::to_string(ASensorEventQueue_registerSensor(gSensors.gSensorQueue, gSensors.gSensorAccelerometer, 10, 10)).c_str(), 0);
__android_log_print(ANDROID_LOG_DEBUG, "register sensor test gravity",
                    std::to_string(ASensorEventQueue_registerSensor(gSensors.gSensorQueue, gSensors.gSensorGravity, 10, 10)).c_str(), 0);
__android_log_print(ANDROID_LOG_DEBUG, "sensor test",
                    std::to_string(ASensorEventQueue_enableSensor(gSensors.gSensorQueue, gSensors.gSensorAccelerometer)).c_str(), 0);
__android_log_print(ANDROID_LOG_DEBUG, "sensor test gravity",
                    std::to_string(ASensorEventQueue_enableSensor(gSensors.gSensorQueue, gSensors.gSensorGravity)).c_str(), 0);

//main loop part
do {
  if ((ident = ALooper_pollAll(IsVulkanReady() ? 160 : -1, nullptr,
                      &events, (void**)&source)) >= 0) {
    if (source != NULL) source->process(app, source);
    if(ident == LOOPER_ID_USER)
    {
        __android_log_print(ANDROID_LOG_DEBUG, "gravity event count in loop = ", std::to_string(ASensorEventQueue_hasEvents(gSensors.gSensorQueue)).c_str(), 0);
        while(ASensorEventQueue_getEvents(gSensors.gSensorQueue, tempSensorEvent, 1) > 0) {
            gravityData.x = tempSensorEvent->vector.x;
            gravityData.y = tempSensorEvent->vector.y;
            gravityData.z = tempSensorEvent->vector.z;
            __android_log_print(ANDROID_LOG_DEBUG, "gravity2 :  ", (std::to_string(gravityData.x) + " " + std::to_string(gravityData.y) +
                                                                   " " + std::to_string(gravityData.z)).c_str(), 0);
        }
    }
  }
  __android_log_print(ANDROID_LOG_DEBUG, "gravity event count = ", std::to_string(ASensorEventQueue_hasEvents(gSensors.gSensorQueue)).c_str(),
                      0);
  // render if vulkan is ready
  if (IsVulkanReady()) {
    VulkanDrawFrame(currentFrame, isTouched, isFocused, touchPositions);
    currentFrame = (currentFrame + 1) % MAX_IN_FLIGHT;
  }
} while (app->destroyRequested == 0);

Androidのセンサーについて

使えるセンサーの一覧などがまとめられている。
モーション センサー  |  Android デベロッパー  |  Android Developers
ただNDKの観点では書かれていないので、C++で実装したい場合には別にまとめられているページを参照する。

Androidセンサー NDK

NDKの関数などが書かれている。
Sensor  |  Android NDK  |  Android Developers

注意事項

  • ASensorEventQueue_registerSensor()を用いる場合は、GradleのminSdkVersionが26以上であることが必要。

c++ - Android NDK undefined reference to ASensorEventQueue_registerSensor - Stack Overflow

The other possibility is that your minSdkVersion is lower than 26. ASensorEventQueue_registerSensor was not added until O, so it can't be linked unless your minSdkVersion is at least 26.

  • includeファイルは<android/sensor.h>

雑感とか

まとめるとたったこれだけのことなのだが、公式のコードでうまく動かないので、たどり着くまでにかなり時間がかかってしまった。
本当はlooperなど、色々調べたことも残したいが、とりあえずはこれで良いか。
公式を参照した後に、このメモを見てサクッとセンサーが使える人が増えることを願っています。