LoginSignup
5

More than 5 years have passed since last update.

D言語でdear imgui (AKA ImGui)

Posted at

ImGuiが何なのかについては
OpenGLやDirectXなGUIにimguiが最強すぎる
をどうぞ。

本記事では、ImGuiのCラッパーである
https://github.com/Extrawurst/cimgui
のDバインディング
https://github.com/Extrawurst/DerelictImgui
を使ってみます。

今回のコード
https://github.com/ousttrue/dimgui_sample

Dバインディングとして同様の
https://github.com/d-gamedev-team/dimgui
というのもあった。
記事書きながらなんかさっき見たリポジトリと違うなぁと思ったら
本当に違うリポジトリだったという・・・

環境は、
Windows10(64bit) + DMD + dub + VisualD

サンプルを試してみる

cloneして試してみると・・・

> dub
Failed to load symbol igGetFont from shared library cimgui.dll

dependency walkerでcimgui.dllを確認すると確かに無い。

https://github.com/Extrawurst/DerelictImgui
のsubmoduleのバージョンでcimguiをビルドする。

igGetFontが存在することを確認した。
差し替えてみる。

動いた。

imgui.png

自分のプロジェクトで使う

今後使っていく上でもいろいろありそうなので、関連ソースを手元に置いておく方向でプロジェクトを構成する。

構成

> dub init dimgui_sample
> cd dimgui_sample
dimgui_sample> mkdir submodules
dimgui_sample> cd submodules
dimgui_sample/submodules> git submodule add https://github.com/glfw/glfw.git glfw
dimgui_sample/submodules> git submodule add https://github.com/Extrawurst/DerelictImgui DerelictImgui

dubのdependencyでDerelictImguiをクローンしたローカルの方を参照させる。

dub.sdl
name "dimgui_sample"
description "A minimal D application."
authors "ousttrue"
copyright "Copyright c 2016, ousttrue"
license "MIT"

dependency "derelict-glfw3" version="~>3.1.0"
dependency "derelict-gl3" version="~>1.0.12"
dependency "derelict-imgui" version="~master" path="submodules/DerelictImgui" # <- これ
dependency "gfm:math" version="~>6.1.2"

sdl形式を使っているのはこっちの方がコンパクトであることもあるが、
jsonをvisualdで開くと何故か死亡するからでもあるw

glfw3.dllとcimgui.dllをビルドするバッチも用意した。
premake5でvisual studioのプロジェクトを生成して、MSBuildでコマンドラインビルドする。

build.bat
pushd submodules
premake5.exe vs2015
set MSBUILD=C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe
"%MSBUILD%" _build_premake\SubModules.sln /p:Platform=Win32 /p:Configuration=Release
popd

premakeの定義は割愛。リポジトリのsubmodulesにあります。

最後、VisualDのソリューションを生成する。

> dub generate visuald

準備完了。

実装

https://github.com/Extrawurst/imgui_d_test
をベースにコードを整理してみる。

Glfw(Window管理)とOpenGL(Shader等のリソース管理・描画)とImGui(Glfwからイベントを受け取る、OpenGLの描画命令を生成する)がお互いに参照しないように分けた。

ImGUiを動作させるには2つの面が必要で、
1つが描画(OpenGLの呼び出し)、もうひとつがマウスやキーボード等の入力イベントの受け取り(Glfwが作った値を反映する)になる。

描画

描画関数の呼び出しには、

io.RenderDrawListsFn

にコールバックを指定する方法と、

io.RenderDrawListsFn

にnullを指定して

igRender()

の後で

igGetDrawData()

で取り出した描画命令を使って自分で呼び出す方法がある。後者を使った。

void RenderDrawLists(ImGuiIO *io, IRenderer renderer)
{
    auto data=igGetDrawData();

    renderer.begin(io.DisplaySize.x, io.DisplaySize.y);
    foreach (n; 0..data.CmdListsCount)
    {
        ImDrawList* cmd_list = data.CmdLists[n];

        auto countVertices = ImDrawList_GetVertexBufferSize(cmd_list);
        renderer.setVertices(ImDrawList_GetVertexPtr(cmd_list,0), countVertices * ImDrawVert.sizeof);

        auto countIndices = ImDrawList_GetIndexBufferSize(cmd_list);
        renderer.setIndices(ImDrawList_GetIndexPtr(cmd_list,0), countIndices * ImDrawIdx.sizeof);

        ImDrawIdx* idx_buffer_offset;
        auto cmdCnt = ImDrawList_GetCmdSize(cmd_list);       
        foreach(i; 0..cmdCnt)
        {
            auto pcmd = ImDrawList_GetCmdPtr(cmd_list, i);

            if (pcmd.UserCallback)
            {
                pcmd.UserCallback(cmd_list, pcmd);
            }
            else
            {
                renderer.draw(pcmd.TextureId
                              , cast(int)pcmd.ClipRect.x, cast(int)(io.DisplaySize.y - pcmd.ClipRect.w)
                                  , cast(int)(pcmd.ClipRect.z - pcmd.ClipRect.x), cast(int)(pcmd.ClipRect.w - pcmd.ClipRect.y)
                                      , pcmd.ElemCount, idx_buffer_offset
                                      );
            }

            idx_buffer_offset += pcmd.ElemCount;
        }
    }

    renderer.end();
}

インタフェースを定義して、後のVulkan化に備えた。

IRenderer.d
interface IRenderer
{
    void CreateDeviceObjects(uint vertexSize, uint uvOffset, uint colorOffset);
    void* CreateFonts(ubyte* pixels, int width, int height);
    nothrow void begin(float width, float height);
    nothrow void setVertices(void *vertices, int len);
    nothrow void setIndices(void *indices, int len);
    nothrow void draw(void* textureId
                      , int x, int y, int w, int h
                      , uint count, ushort* offset);
    nothrow void end();
}

入力イベント ~ 構築

ioのメンバを更新してigNewFrame()を呼び出すまでが入力イベント処理で、以降igRenderまでがGUI構築。

struct GuiContext
{
    double       m_time = 0.0f;
    bool[3]      m_mousePressed;
    float        m_mouseWheel = 0.0f;
    bool show_test_window;
    bool show_another_window;
    float[3] clear_color;
}


void updateGui(ImGuiIO *io
               , ref GuiContext context
               , double current_time
               , int[2] size, int[2] windowSize
               , double[2] pos, bool mouseDown0, bool mouseDown1, bool mouseDown2
               , bool hasFocus
               )
{
    // Setup display size (every frame to accommodate for window resizing)
    io.DisplaySize = ImVec2(cast(float)size[0], cast(float)size[1]);

    // Setup time step
    io.DeltaTime = context.m_time > 0.0 ? cast(float)(current_time - context.m_time) : cast(float)(1.0f/60.0f);
    context.m_time = current_time;

    // Setup inputs
    // (we already got mouse wheel, keyboard keys & characters from glfw callbacks polled in glfwPollEvents())
    if (hasFocus)
    {
        // Convert mouse coordinates to pixels
        auto mouse_x = pos[0] * cast(float)size[0] / windowSize[0];
        auto mouse_y = pos[1] * cast(float)size[1] / windowSize[1];
        // Mouse position, in pixels (set to -1,-1 if no mouse / on another screen, etc.)
        io.MousePos = ImVec2(mouse_x, mouse_y);
    }
    else
    {
        io.MousePos = ImVec2(-1,-1);
    }

    {
        io.MouseDown[0] = context.m_mousePressed[0] || mouseDown0;
        context.m_mousePressed[0] = false;
    }
    {
        io.MouseDown[1] = context.m_mousePressed[1] || mouseDown1;
        context.m_mousePressed[1] = false;
    }
    {
        io.MouseDown[2] = context.m_mousePressed[2] || mouseDown2;
        context.m_mousePressed[2] = false;
    }

    io.MouseWheel = context.m_mouseWheel;
    context.m_mouseWheel = 0.0f;

    //
    igNewFrame();
    //

    // 1. Show a simple window
    // Tip: if we don't call ImGui::Begin()/ImGui::End() the widgets appears in a window automatically called "Debug"
    {
        static float f = 0.0f;
        igText("Hello, world!");
        igSliderFloat("float", &f, 0.0f, 1.0f);
        igColorEdit3("clear color", context.clear_color);
        if (igButton("Test Window")) context.show_test_window ^= 1;
        if (igButton("Another Window")) context.show_another_window ^= 1;
        igText("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / igGetIO().Framerate, igGetIO().Framerate);
    }

// 省略

    igRender();
}

参考

UnityのImmediate mode GUIのコンセプトの説明がある。
最近までUnityのImGUIがレガシーな変な設計だと思ってました。すいません。

終わり

ImGuiはよさげ。
よい費用対効果のGUIとなりそうな感じがするのだ。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5