概要
AndroidにVulkanベースでImGuiを導入できた。
ImGuiとはMITライセンスで公開されている、C++で書けるGUIライブラリである。
https://github.com/ocornut/imgui
ImGuiはexampleとして、いくつかのplatformとgraphics APIの組み合わせを紹介している。
https://github.com/ocornut/imgui/tree/master/examples
この中にAndroid + Vulkanの組み合わせはないが、やってみると導入は可能だったので、手順を簡単にまとめる。
結果
私の前回記事の続きにimguiの領域を追加。
https://qiita.com/Aqoole/items/d8f020b24d2861d20322
オブジェクトはray tracing pipelineで、imgui領域のレンダリングはimguiが内部で作成しているpipelineで実行している。
Androidのアプリの実行で複数windowの表示はできないので、私が以前に書いた記事とは異なり、オブジェクトの結果とimguiの領域を同じwindowに書いている。
https://qiita.com/Aqoole/items/13b491c6c392f689de8e
環境
- Windows 10 (10.0.19043 N/A ビルド 19043)
- Android Studio Bumblebee | 2021.1.1 Patch 3
- minSdkVersion : 30 (Android 11)
- NDK Version : 25.0.8221429
ここに書いているのはこの環境で実施しましたという実績値で、imguiの動作にはあまり関係ないものと思われる。(もっと古い版数でも実装可能と思われる)
ImGuiの導入
初期化
メインループに入る前に、初期化を実施する。
Androidでは結果にあるように、オブジェクトのレンダリングと同じimageにimguiの領域も描画しないといけないので、
- logical device
- surface
- swapchain
- depth image
- render pass
- frame buffer
は先に作成しておき、オブジェクトと共用で用いる。
descriptor poolなどは、共用でもimgui専用に作成しても、どちらでもよいと思う。
//create descriptor pool
//descriptor pool for imgui
VkDescriptorPoolSize pool_sizes[] =
{
{ VK_DESCRIPTOR_TYPE_SAMPLER, 10 },
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 10 },
{ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 10 },
{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 10 },
{ VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 10 },
{ VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 10 },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 10 },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 10 },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 10 },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 10 },
{ VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 10 }
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 1000 * IM_ARRAYSIZE(pool_sizes);
pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes);
pool_info.pPoolSizes = pool_sizes;
mPool = std::make_unique<AEDescriptorPool>(device, (uint32_t)IM_ARRAYSIZE(pool_sizes), pool_sizes);
//init imgui
ANativeWindow_acquire(platformWindow);
mContext = ImGui::CreateContext();
ImGui::SetCurrentContext(mContext);
//vulkan init
ImGui::StyleColorsDark();
ImGui::GetStyle().TouchExtraPadding = ImVec2(4.0f, 4.0f);
ImGui_ImplVulkan_InitInfo init_info = {};
init_info.Instance = *instance->GetInstance();
init_info.PhysicalDevice = *mDevice->GetPhysicalDevice();
init_info.Device = mDevice->GetDeviceNotConst();
init_info.QueueFamily = mQueue->GetQueueFamilyIndex();
init_info.Queue = mQueue->GetQueue(0);
init_info.PipelineCache = VK_NULL_HANDLE;
init_info.DescriptorPool = *mPool->GetDescriptorPool();
init_info.Allocator = nullptr;
init_info.MinImageCount = 2;
init_info.Subpass = 0;
init_info.ImageCount = mSwapchain->GetSize();
init_info.CheckVkResultFn = nullptr;
init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ImGui_ImplVulkan_Init(&init_info, *mRenderPass->GetRenderPass());
//android init
ImGui_ImplAndroid_Init(platformWindow);
//upload fonts
tmpCommandPool = std::make_unique<AECommandPool>(mDevice, mQueue);
tmpCommandBuffer = std::make_unique<AECommandBuffer>(mDevice, mCommandPool.get());
AECommand::BeginCommand(tmpCommandBuffer.get());
ImGui_ImplVulkan_CreateFontsTexture(*tmpCommandBuffer->GetCommandBuffer());
AECommand::EndCommand(tmpCommandBuffer.get());
VkSubmitInfo end_info = {};
end_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
end_info.commandBufferCount = 1;
end_info.pCommandBuffers = mCommandBuffer->GetCommandBuffer();
//vkEndCommandBuffer(*mCommandBuffer->GetCommandBuffer());
vkQueueSubmit(mQueue->GetQueue(0), 1, &end_info, VK_NULL_HANDLE);
vkDeviceWaitIdle(*mDevice->GetDevice());
ImGui_ImplVulkan_DestroyFontUploadObjects();
コマンドバッファーへの記録
コマンドの記録
imguiで表示する内容はframeごとに異なることが期待されるので(経過時間など)、メインループで呼び出す関数内でコマンドの記録を行い、オブジェクトのコマンドを実行するのと同じタイミングで実行する。
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_));
//Record ImGUI Contents
RecordImguiCommand(nextIndex, touchPositions, isTouched);
VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkCommandBuffer cmdBuffers[2] = {*gCommandBuffers[nextIndex]->GetCommandBuffer(), *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;
以下の部分でオブジェクトとimguiの両方のコマンドを記録し、submitしている。
VkCommandBuffer cmdBuffers[2] = {*gCommandBuffers[nextIndex]->GetCommandBuffer(), *gImgui->GetCommandBuffer()->GetCommandBuffer()};
ImGui描画の指定
先ほどのsubmitするコマンドバッファーにimguiで描画したい内容を記録する。
(gImguiは私が定義しているラッパークラスなので気にしないでください。コマンドバッファーに内容が記録されればよいです。)
void RecordImguiCommand(uint32_t imageNum, glm::vec2* touchPositions, bool& isTouched)
{
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 = 0, .height = 0}},
.clearValueCount = 2,
.pClearValues = clearVals};
vkCmdBeginRenderPass(*cb, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
auto io = ImGui::GetIO();
ImGui_ImplAndroid_NewFrame();
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowPos(ImVec2(100, 500), ImGuiCond_FirstUseEver);
ImGui::Begin("Parameters");
ImGui::Button("reset time", ImVec2(350.0f, 100.0f));
if (ImGui::Button("pause"))
{
// paused = !paused;
}
ImGui::Text("time = %.3f", (lastTime - startTime) * 0.001);
ImGui::SameLine();
ImGui::End();
ImGui::Render();
ImDrawData* drawData = ImGui::GetDrawData();
ImGui_ImplVulkan_RenderDrawData(drawData, *cb);
vkCmdEndRenderPass(*cb);
vkEndCommandBuffer(*cb);
}
課題
ImGuiの領域の描画には成功したが、タッチイベントでボタンを押すところはどうしても実装できなかった。
かなり重要な部分だけに、何とかしたいところ。
どうもimgui内ではマウスクリックのイベントでボタンを押した判定をしているようだが、マウスクリックのイベントとタッチのイベントを関連づける方法が見当たらなかった。
Android(Java)側でタッチポジションを取ってくることはできるので、imguiの機能を使わずに領域をタッチしたかどうかを判定することは可能だが、領域の位置を管理することが面倒なのとバグの温床になりそうな気がするので、できればImGui側に管理を任せたいところ。
引き続き調査はするが、できなさそうであればアプリ側で管理しなければならないか。
最後に
はてなブログにも色々書いているので、興味のある方はご覧ください。
https://blog.hatena.ne.jp/Aqoole_Hateena/aqoole-hateena.hatenablog.com/
ソースコードはGitHubに上げております。
https://github.com/kodai731/Aqoole-Engine-Android-Vulkan-Rendering-Engine-