[Android + Vulkan + ImGui] AndroidでGUIをC++で

# 概要
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の組み合わせはないが、やってみると導入は可能だったので、手順を簡単にまとめる。

# 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1446998/0a10d98e-41c5-26a6-5c0c-10062754fca5.png)
私の前回記事の続きに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専用に作成しても、どちらでもよいと思う。

``` c++
	//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ごとに異なることが期待されるので(経過時間など)、メインループで呼び出す関数内でコマンドの記録を行い、オブジェクトのコマンドを実行するのと同じタイミングで実行する。

``` c++:DrawFrame() (メインループ)
  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している。

``` c++
VkCommandBuffer cmdBuffers[2] = {*gCommandBuffers[nextIndex]->GetCommandBuffer(), *gImgui->GetCommandBuffer()->GetCommandBuffer()};
```

### ImGui描画の指定
先ほどのsubmitするコマンドバッファーにimguiで描画したい内容を記録する。
(gImguiは私が定義しているラッパークラスなので気にしないでください。コマンドバッファーに内容が記録されればよいです。)

``` c++
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-