🗻

Vulkan + GLFWでDear ImGuiを使う

2024/08/14に公開

初めに

この記事はGLFW環境上でのVulkanにおいてDear ImGui(v1.19.0)のセットアップの仕方について解説する記事です。あまりVulkanやGLFWのセッティングには触れませんのでVulkan Tutorialなどを参照してください。注意点として著者は完全初心者なので間違っている可能性があることに留意してください。

Dear ImGui

ImGuiとはImmediate(Mode)GUIの略称で、アプリケーションが関数的に即座に呼び出せるGUIフレームワークことを指します(偶にDear ImGuiをImGuiと呼んでいるのを見かけますが、あくまでImGuiはGUIの種類であってImGuiは他にもいっぱいあります)。
https://collquinn.gitlab.io/portfolio/my-article.html

特にDear ImGuiが代表的です。これはC++で動くシンプルなImGuiライブラリで、OpenGLやDirectX, Vulkanなど多種多様なグラフィックスAPIをサポートしています。個人開発ではとてもよく使われているイメージがあります。
https://github.com/ocornut/imgui

今回の記事ではGLWF環境でのVulkanでDear ImGuiをセットアップする方法について書いていこうと思います。

Dear ImGuiのクローン~プロジェクト設定

以下のリンクからDear ImGuiをクローンしてください。
https://github.com/ocornut/imgui

imgui/をインクルードフォルダに設定してください。Dear ImGuiはcppファイルも存在しているため、imgui/直下にあるh,cppファイルをプロジェクトに追加してください。Visual Studioなら選択後右クリックでプロジェクトに追加する項目があります。

また、各APIに対応した実装はbuckendsファイルにあり、使用したいAPIに対応するh,cppファイルを追加でプロジェクトに追加する必要があります。今回はGLFWとVulkanなのでimgui_impl_glfw.h,.cppimgui_impl_vulkan.h,cppですね。


後はimgui.hと各バックエンドのヘッダーファイルをincludeすればプロジェクトの設定としては以上になります。

#include <imgui.h>
#include <backends/imgui_impl_glfw.h>
#include <backends/imgui_impl_vulkan.h>

Dear ImGuiのInitialze

まず最初にやることはComtextの生成です。変数としてImGuiContext*を持って置き、ImGui::CreateContextでContextを生成します。このContextを現在のImGui上のContextとして扱えるようにSetCurrentContextで登録してあげます。(Contextがいっぱいあるとどうなるんでしょうか)

ImGuiContext* m_context;
m_context = ImGui::CreateContext();
ImGui::SetCurrentContext(m_context);

その後、カラーテーマを設定してあげます。今回はダークカラーで行きます(imgui_draw.cppを見るとほかにもStyleColorsClassicなどあるみたいです。)

ImGui::StyleColorsDark();

ここからは各バックエンドの処理に入っていくわけですが、基本的にDear ImGuiは対象のグラフィックスAPIのパイプラインに従って描画する形で組み込まれるため、APIのオブジェクトを渡してあげたり描画処理に組み込んだりしてあげる必要があります。基本的にはGLFW及びVulkanのInitializeが終わった後にImGuiとのバックエンド設定を行ってあげるべきです。

まずはGLFWの設定ですが、Vulkanの場合以下のようにGLFWで使用しているGLFWwindowのポインターを登録してあげます。

ImGui_ImplGlfw_InitForVulkan((GLFWwindow* window), true);

次はVulkanの設定です。Vulkanのいくつかのオブジェクトを渡していきます。最低限必要なものとして

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • QueueFamily
  • VkQueue
  • VkDescriptorPool ※ImGui用のものを作る必要あり
  • RenderPass

を渡さなくてはいけません。VkDescriptorPoolを除く各オブジェクトは実際に使うアプリケーションと共有して問題ありません。

DescriptorPoolについてはImGuiが使うリソースの管理に使われるため、専用に作ってあげる必要があります。ただ、ImGui側で使うリソースがどれぐらいあるとかは(多分)取得できないので、Vulkan GuideによるとクソデカDescriptorPoolを作る必要があるらしいです(ここまでデカくなくてもいい気がしますが)。

		std::vector<vk::DescriptorPoolSize> poolSize = {
			{vk::DescriptorType::eSampler,1000},
			{vk::DescriptorType::eCombinedImageSampler,1000},
			{vk::DescriptorType::eSampledImage,1000},
			{vk::DescriptorType::eStorageImage,1000},
			{vk::DescriptorType::eUniformTexelBuffer,1000},
			{vk::DescriptorType::eStorageTexelBuffer,1000},
			{vk::DescriptorType::eUniformBuffer,1000},
			{vk::DescriptorType::eStorageBuffer,1000},
			{vk::DescriptorType::eUniformBufferDynamic,1000},
			{vk::DescriptorType::eStorageBufferDynamic,1000},
			{vk::DescriptorType::eInputAttachment,1000}
		};

		vk::DescriptorPoolCreateInfo poolInfo = {};
		poolInfo.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
		poolInfo.setMaxSets(1000);
		poolInfo.poolSizeCount = static_cast<uint32_t>(poolSize.size());
		poolInfo.pPoolSizes = poolSize.data();

		m_imGuiDescriptorPool = m_device->createDescriptorPoolUnique(poolInfo);

DescriptorPoolが作れたらImGui_ImplVulkan_InitInfo構造体に各オブジェクトと設定を記載していきます(*が付いているのはvulkan.hppの構造体を使っているだけなのであまり気にしないでください、単なるスマートポインター(みたいなもん)です)。

ImGui_ImplVulkan_InitInfo
init_info.Instance = *m_instance;
init_info.PhysicalDevice = m_physicalDevice;
init_info.Device = *m_device;
init_info.QueueFamily = m_queueIndex;
init_info.Queue = m_queue;
init_info.PipelineCache = VK_NULL_HANDLE;
init_info.DescriptorPool = *m_imGuiDescriptorPool;
init_info.Allocator = nullptr;
init_info.MinImageCount = 2;
init_info.ImageCount = m_swapchainImages.size();
init_info.RenderPass = *m_renderPass; 
init_info.CheckVkResultFn = nullptr;

これをImGui_ImpleVulkan_Init()に渡してあげてVulkanのセットアップを行います。

		ImGui_ImplVulkan_Init(&init_info);

どうも旧バージョンではRenderPassがここで追加で指定する必要があったみたいですがv1.19.0ではRenderPassはinitInfoに書かれるようになったっぽいです。これでバックエンドの登録は終わりました。

最後にImGui側のセットアップとして、Font用のテクスチャをVRAMにアップロードする処理を行います。(ただまあしなくても描画自体はできるので忘れても何とかはなります)

推測ではありますがImGuiはMSDFかなんかのFont用のテクスチャがあり、それをアップロードしておいてShader側でそれを使って文字を書くみたいな処理をしているんだと思います。なのでアップロードしておく必要があるというわけです。

というわけでその処理ですがv1.19.0ではシンプルに以下の関数を呼び出すだけで恐らくOKだと思います(多分...)。

ImGui_ImplVulkan_CreateFontsTexture();

昔のバージョンだとアップロードの処理を明示的にコマンドのSubmitで行ったりとかする必要があったみたいですが、今のバージョンでは特にそういう感じでもなさそうで、アップロード処理などは中で書かれているっぽいです。それらしい関数とかも他にないので多分これで問題ないと思われます(SRAMのリソース開放も多分いらない?)。

以上でInitializeは終わりです。

m_context = ImGui::CreateContext();
ImGui::SetCurrentContext(m_context);

ImGui::StyleColorsDark();

ImGui_ImplGlfw_InitForVulkan((GLFWwindow* window), true);

std::vector<vk::DescriptorPoolSize> poolSize = {
			{vk::DescriptorType::eSampler,1000},
			{vk::DescriptorType::eCombinedImageSampler,1000},
			{vk::DescriptorType::eSampledImage,1000},
			{vk::DescriptorType::eStorageImage,1000},
			{vk::DescriptorType::eUniformTexelBuffer,1000},
			{vk::DescriptorType::eStorageTexelBuffer,1000},
			{vk::DescriptorType::eUniformBuffer,1000},
			{vk::DescriptorType::eStorageBuffer,1000},
			{vk::DescriptorType::eUniformBufferDynamic,1000},
			{vk::DescriptorType::eStorageBufferDynamic,1000},
			{vk::DescriptorType::eInputAttachment,1000}
};

vk::DescriptorPoolCreateInfo poolInfo = {};
poolInfo.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
poolInfo.setMaxSets(1000);
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSize.size());
poolInfo.pPoolSizes = poolSize.data();

m_imGuiDescriptorPool = m_device->createDescriptorPoolUnique(poolInfo);

ImGui_ImplVulkan_InitInfo
init_info.Instance = *m_instance;
init_info.PhysicalDevice = m_physicalDevice;
init_info.Device = *m_device;
init_info.QueueFamily = m_queueIndex;
init_info.Queue = m_queue;
init_info.PipelineCache = VK_NULL_HANDLE;
init_info.DescriptorPool = *m_imGuiDescriptorPool;
init_info.Allocator = nullptr;
init_info.MinImageCount = 2;
init_info.ImageCount = m_swapchainImages.size();
init_info.RenderPass = *m_renderPass; 
init_info.CheckVkResultFn = nullptr;

ImGui_ImplVulkan_Init(&init_info);

ImGui_ImplVulkan_CreateFontsTexture();

Dear ImGuiの描画

Dear ImGuiの描画はVulkanの描画処理に乗っける形で行われます。一旦一フレームの処理を次のように書くとします。

void DrawFrame(){

    draw(); //コマンドの処理とかはここでやるとする
}

最初はまずフレームの開始をImGuiに送信してあげます。

void DrawFrame(){
    ImGui_ImplGlfw_NewFrame();
    ImGui_ImplVulkan_NewFrame();
    ImGui::NewFrame();

    draw(); //コマンドの処理とかはここでやるとする
}

その後、ImGuiで表示するコンテンツを登録してあげます。例として適当なのをいくつか入れます

void DrawFrame(){
    ImGui_ImplGlfw_NewFrame();
    ImGui_ImplVulkan_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("Hello, world!");
    float f = 0.0f;
    ImGui::DragFloat("Drag",&f);
    bool b = false;
    ImGui::Checkbox("Check Box", &b);
    ImGui::Text("Yeah");
    ImGui::End();

    draw(); //コマンドの処理とかはここでやるとする
}

こうしてImGuiで表示するGUIを設定しました。この後はそれを描画するコマンドを送信します。draw関数内はコマンドバッファにコマンドを積み込んでいるものとして考えます。

void draw(){

		m_commandBuffer->begin(vk::CommandBufferBeginInfo{});
        //いろいろ描画処理
		m_commandBuffer->end();
}

ImGuiのレンダリングは次のようにImGui::Render()ImGui_ImplVulkan_RenderDrawDataで行います(Renderは別にこのタイミングじゃなくてImGui::Endの後ならいいと思う)。2つ目の関数でcommandBufferを送っているのを見るとここでコマンドがつぎ込まれているというようなイメージだと思います。

void draw(){

		m_commandBuffer->begin(vk::CommandBufferBeginInfo{});
        //いろいろ描画処理
		ImGui::Render();
		ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), *m_commandBuffer);
		m_commandBuffer->end();
}

これで実行すればようやくImGuiが出てきます やった~


後ろのやつは気にしないでください。ノイズのテストをしていたので...

後は適当なタイミングでリリース処理を書いてあげれば終わり。

		ImGui_ImplVulkan_DestroyFontsTexture();
		ImGui_ImplVulkan_Shutdown();
		ImGui_ImplGlfw_Shutdown();
		ImGui::DestroyContext();

Vulkan Raytracingをしたときは?

Vulkan Raytracingをする場合、多分RenderPassを作っていないと思うのでImGuiのためにRenderPassを生成してください。後はほとんど同じで、描画時はRaytraceした後にRenderPassを開始して同様のコマンドを送ってあげれば動きました。こんなかんじ

		m_commandBuffer->begin(vk::CommandBufferBeginInfo{});
		vkutils::setImageLayout(*m_commandBuffer, image, vk::ImageLayout::ePresentSrcKHR, vk::ImageLayout::eGeneral);

		m_commandBuffer->bindPipeline(vk::PipelineBindPoint::eRayTracingKHR, *m_pipeline);

		m_commandBuffer->bindDescriptorSets(
			vk::PipelineBindPoint::eRayTracingKHR,
			*m_pipelineLayout,
			0,
			*descSet,
			nullptr
		);

		m_commandBuffer->traceRaysKHR(
			raygenRegion,
			missRegion,
			hitRegion,
			{},
			m_desc.Width, m_desc.Height, 1
		);

		vk::RenderPassBeginInfo renderPassInfo{};
		renderPassInfo.setRenderPass(*m_imGuiRenderPass);
		renderPassInfo.setFramebuffer(frameBuffer); 
		vk::Rect2D rect({0,0},{(uint32_t)m_desc.Width,(uint32_t)m_desc.Height});

		renderPassInfo.setRenderArea(rect);

		m_commandBuffer->beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);

		ImGui::Render();
		ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), *m_commandBuffer);

		m_commandBuffer->endRenderPass();

		m_commandBuffer->end();

地味に仕様としてImGuiのコマンド送信後はImageLayoutがPresentSrcKHRになるということがあるので注意してください(なんかそれでエラーが出てた)。

Discussion