👼

神話のプログラム言語 OdinでDear ImGuiを動かす

2024/12/25に公開

前回予定していた通り、Odin言語でDear ImGuiを導入と、プログラムの説明をします。

Dear ImGuiとは

Dear ImGuiは、Immediate Mode GUIの略称名で、ゲーム制作用アプリのUnreal Engineなどでも利用されています。(確かUnreal Engineから派生したものかな?、わかりませんけど。)
Dear ImGuiライブラリは単体アプリとして動作はしませんが、バックエンドと呼ばれる、SDLやOpenGL、DirectXなどのグラフィックエンジンを介して動作する事が可能です。
Dear ImGuiは多数のコンポーネントを持っており、各バックエンドのメニュー操作画面的な操作を行う事も可能です。
また、Dear ImGuiライブラリは、C++で作成されてはいますが、昨今では色々な言語でバインドされるようになってきました。もちろん、Odin言語のバインドもされていますが、まだ、全ての機能が搭載されているわけではありません。まだ、ほんの一部だけしか機能しません。

1.Dear ImGuiのインストール

Odin版のDear ImGuiをインストールする前に、下記ツールが必要になります。

  • VisutalStudioC++2022
  • Python3.10以上

https://gitlab.com/L-4/odin-imgui

1-1.上記サイトからgitクローンを使って、ソースを自分のプロジェクトフォルダにダウンロードします。

ダウンロード前に、pythonモジュールのplyをインストールしておきましょう。(ライブラリ作成時に必要です)

VSCodeのターミナル上で動作
$ cd /project_folder/
$ python -m pip install ply
$ git clone https://gitlab.com/L-4/odin-imgui.git

1-2.vcvarsall.batをWindows環境のPathに追加

※「C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build」をPATHに設定
PATHを追加後、DOSプロンプトを開いて、下記のように「python build.py」でlibライブラリを作成します。

DOSプロンプト上で動作
$ cd /project_folder/imgui
$ python build.py
$ dir imgui_windows_x64.lib

imgui\imgui_windows_x64.libが出来ていれば成功です。

1-3.出来上がったライブラリを元に、サンプルを動作させてみましょう。

「imgui/examples」まで移動し、glfw_opengl3を実行します。
※サンプルは複数ありますが、バックエンドが現時点では、glfw以外は動作しませんでした。

VSCodeのターミナル上で動作
$ cd /project_folder/imgui/examples
$ odin run .\glfw_opengl3\

1-4.正常に動作した事になります。

sharedフォルダに/project_folder/imguiを丸ごとコピーしますし、import "shared:imgui"に変更して動作させてみる。

glfw_opengl3/main.odin
package imgui_example_glfw_opengl3
 ・・・
import im "shared:imgui"                 // "shared:imgui"に書き換える
import "shared:imgui/imgui_impl_glfw"    // "shared:imgui"に書き換える
import "shared:imgui/imgui_impl_opengl3" // "shared:imgui"に書き換える

2.imGuiでプログラムを作成

次に、imGuiを使った独自のプログラムを作成してみます。
まず、プロジェクトフォルダ直下に「sample01」フォルダを作成し、先ほど「glfw_opengl3/main.odin」ファイルをコピーします。
コピー後に、日本語フォントの設定と、imguiのレンダー部分を外部に出すようにします。

sample01/main.odin
package main // package名をmainに変更
・・・部分的に抜粋・・・
  im.CreateContext()
  defer im.DestroyContext()
  io := im.GetIO()
  // 日本語フォントの設定(Windwosのメイリオフォントに設定)
  im.FontAtlas_AddFontFromFileTTF(io.Fonts, "C:\\Windows\\Fonts\\meiryo.ttc",
          18.0, nil, im.FontAtlas_GetGlyphRangesJapanese(io.Fonts))
  io.ConfigFlags += {.NavEnableKeyboard, .NavEnableGamepad}
  when !DISABLE_DOCKING {
    io.ConfigFlags += {.DockingEnable}
    io.ConfigFlags += {.ViewportsEnable}
    imStyle()  // スタイルの設定 外部で記載
    // 以下3行コメントアウト
    // style := im.GetStyle()
    // style.WindowRounding = 0
    // style.Colors[im.Col.WindowBg].w = 1
  }
  // im.StyleColorsDark()  // StyleColorsDarkもコメントアウト
・・・部分的に抜粋・・・
  for !glfw.WindowShouldClose(window) {
    glfw.PollEvents()

    imgui_impl_opengl3.NewFrame()
    imgui_impl_glfw.NewFrame()
    imRender(window)  // imGuiレンダリング 外部で記載
    // im.NewFrameからim.Renderまでをコメントアウト
    // im.NewFrame()

    // im.ShowDemoWindow()

    // if im.Begin("Window containing a quit button") {
    //  if im.Button("The quit button in question") {
    //    glfw.SetWindowShouldClose(window, true)
    //  }
    // }
    // im.End()

    // im.Render()
・・・部分的に抜粋・・・

imRender関数部分を別ファイル「render.odin」にして、以下に記載

sample01/render.odin
package main

import "core:fmt"
import "core:c"
import im "shared:imgui"
import "vendor:glfw"

// グローバルエリア
color: [4]f32 = {0, 1, 1, 1}
slider_value: c.int = 50
check_value := true
buf: [31]u8
radio: int = 1

imRender :: proc(window: glfw.WindowHandle) {

  im.NewFrame()
  im.SetWindowSize({500, 300})

  if im.Begin("imGuiの画面表示", nil, {.MenuBar}) {   // 画面の開始はbeginで始める
    imMenu(window)                                   // メニュー表示

    im.Text("ラベル(テキスト)")                      // ラベル表示
    im.ColorEdit4("テキスト色", &color, {.Float})     // カラーエディタを表示
    style := im.GetStyle()                           // 現スタイルを取得する
    style.Colors[im.Col.Text] = color                // 現スタイルから文字の色を設定
    if im.Button("ボタン", {80, 25}) {               // ボタン表示
      fmt.println("button click")
    }
    im.SameLine()                                    // 行を移動せずに、同一行を設定
    if im.Checkbox("チェックボックス", &check_value) { // チェックボックスを表示
      fmt.println("check box click")
    }
    if im.InputText("入力", cstring(&buf[0]), 30) {  // 入力欄を表示
      fmt.println("input:", string(buf[:]))
    }
    if im.RadioButton("ラジオa", radio == 1) do radio = 1
    im.SameLine()
    if im.RadioButton("ラジオb", radio == 2) do radio = 2
    im.SameLine()
    if im.RadioButton("ラジオc", radio == 3) do radio = 3
    im.SliderInt("スライダー", &slider_value, 0, 100, "スライダーの値:%d")
    im.ProgressBar(f32(slider_value)/100.0, {0, 20})  // プログレスバーとスライダーを同調させる
  }
  im.End()

  im.Render()
}

画面レイアウト部分を「style.odin」ファイルとして作成し、以下に記載。
レイアウトは、下記サイトを参考に記述しています。
https://github.com/GraphicsProgramming/dear-imgui-styles

sample01/style.odin
package main

import im "shared:imgui"
import "core:fmt"

imStyle :: proc() {
  style := im.GetStyle()
  style.Alpha = 1.0
  style.WindowRounding = 3
  style.GrabRounding = 1
  style.GrabMinSize = 20
  style.FrameRounding = 3
  style.Colors[im.Col.Text]                  = {0, 1, 1, 1}
  style.Colors[im.Col.TextDisabled]          = {0, 0.40, 0.41, 1}
  style.Colors[im.Col.WindowBg]              = {0, 0, 0, 1}
  style.Colors[im.Col.ChildBg]               = {0, 0, 0, 0}
  style.Colors[im.Col.Border]                = {0, 1, 1, 0.65}
  style.Colors[im.Col.BorderShadow]          = {0, 0, 0, 0}
  style.Colors[im.Col.FrameBg]               = {0.44, 0.80, 0.80, 0.18}
  style.Colors[im.Col.FrameBgHovered]        = {0.44, 0.80, 0.80, 0.27}
  style.Colors[im.Col.FrameBgActive]         = {0.44, 0.81, 0.86, 0.66}
  style.Colors[im.Col.TitleBg]               = {0.14, 0.18, 0.21, 0.73}
  style.Colors[im.Col.TitleBgCollapsed]      = {0, 0, 0, 0.54}
  style.Colors[im.Col.TitleBgActive]         = {0, 1, 1, 0.27}
  style.Colors[im.Col.MenuBarBg]             = {0, 0, 0, 0.20}
  style.Colors[im.Col.ScrollbarBg]           = {0.22, 0.29, 0.30, 0.71}
  style.Colors[im.Col.ScrollbarGrab]         = {0.00, 1.00, 1.00, 0.44}
  style.Colors[im.Col.ScrollbarGrabHovered]  = {0.00, 1.00, 1.00, 0.74}
  style.Colors[im.Col.ScrollbarGrabActive]   = {0, 1, 1, 1}
  style.Colors[im.Col.CheckMark]             = {0, 1, 1, 0.68}
  style.Colors[im.Col.SliderGrab]            = {0, 1, 1, 0.36}
  style.Colors[im.Col.SliderGrabActive]      = {0, 1, 1, 0.76}
  style.Colors[im.Col.Button]                = {0, 0.65, 0.65, 0.46}
  style.Colors[im.Col.ButtonHovered]         = {0, 1, 1, 0.43}
  style.Colors[im.Col.ButtonActive]          = {0, 1, 1, 0.62}
  style.Colors[im.Col.Header]                = {0, 1, 1, 0.33}
  style.Colors[im.Col.HeaderHovered]         = {0, 1, 1, 0.42}
  style.Colors[im.Col.HeaderActive]          = {0, 1, 1, 0.54}
  style.Colors[im.Col.ResizeGrip]            = {0, 1, 1, 0.54}
  style.Colors[im.Col.ResizeGripHovered]     = {0, 1, 1, 0.74}
  style.Colors[im.Col.ResizeGripActive]      = {0, 1, 1, 1}
  style.Colors[im.Col.PlotLines]             = {0, 1, 1, 1}
  style.Colors[im.Col.PlotLinesHovered]      = {0, 1, 1, 1}
  style.Colors[im.Col.PlotHistogram]         = {0, 1, 1, 1}
  style.Colors[im.Col.PlotHistogramHovered]  = {0, 1, 1, 1}
  style.Colors[im.Col.TextSelectedBg]        = {0, 1, 1, 0.22}
  style.Colors[im.Col.TableRowBg]   = {0, 0.65, 0.65, 0.46}
}

メニュー部分も「menu.odin」ファイルを作成し、下記に記載。

sample01/menu.odin
package main

import "core:fmt"

import im "shared:imgui"
import "vendor:glfw"

imMenu :: proc(window: glfw.WindowHandle) {
  
  if im.BeginMenuBar() {        // メニューバーの表示
    if im.BeginMenu("ファイル") {   // 上位メニューの表示
      if im.MenuItem("オープン", "Ctrl+O") {  // 下位のメニュー表示
        fmt.printf("Open clicked\n")
      }
      if im.MenuItem("クローズ", "Ctrl+S") {  // 下位のメニュー表示
        fmt.printf("Close clicked\n")
        glfw.SetWindowShouldClose(window, true) // glfw自体を終了
      }
      im.EndMenu()
    }
    im.EndMenuBar()
  }
}

「sample01」フォルダの構成が下記のようになってれば、問題ありません。

sample01フォルダのファイル構成
sample01 -+- main.odin
          +- render.odin
          +- style.odin
          +- menu.odin
実行
$ odin run .\sample01\

3.imGuiで画像とテーブルビューを表示

先ほどの「sample01」フォルダを「sample02」フォルダにコピーします。
「sample01/main.odin」から「main.odin」ファイル内に1行追加します。(画像をロードする関数を追加します。)

sample02/main.odin
・・・一部抜粋・・・
    imStyle()  // スタイルの設定 外部で記載
    // 画像の読み込み処理をmain.odinに追加
    out_texture, out_width, out_height, ok := LoadTextureFromFile("C:\\youre_folder\\king.png")
・・・一部抜粋・・・

「render.odin」ファイルには、追加でテーブルビューと画像ウィンドウの開閉ボタンを設けるように、以下に記載

sample02/render.odin
package main

import "core:fmt"
import "core:c"

import im "shared:imgui"
import "vendor:glfw"
import gl "vendor:OpenGL"

// グローバルエリア
color: [4]f32 = {0, 1, 1, 1}
slider_value: c.int = 0
check_value := true
buf: [31]u8
show_dialog := false
out_width, out_height: c.int
out_texture: u32

imRender :: proc(window: glfw.WindowHandle) {

  im.NewFrame()
  im.SetWindowSize({500, 350})

  if im.Begin("imGuiの画面表示", nil, {.MenuBar}) {
    imMenu(window)
    image_size: im.Vec2 = {770/2, 78}

    // ラベルと入力とボタンを1行で表示
    im.Text("ラベル")   // ラベルの表示
    im.SameLine()      // 位置を同じ行に設定
    if im.InputText("##Username", cstring(&buf[0]), 30) {
      fmt.println("input:", string(buf[:]))
    }
    im.SameLine()      // 位置を同じ行に設定
    if im.Button("ボタン", {80, 25}) {
      fmt.println("button click")
    }
    // テーブルビューの表示処理
    // TableFlags_RowBgとstyle.Colors[im.Col.TableRowBg]でカラム事に色を変更する
    if im.BeginTable("Table1", 3, im.TableFlags_Borders | im.TableFlags_RowBg) {
      im.TableSetupColumn("COL 1")
      im.TableSetupColumn("COL 2")
      im.TableSetupColumn("COL 3")
      im.TableHeadersRow()

      for row in 0 ..< 5 {
        if row > 0 do im.TableNextRow()
        for column in 0 ..< 3 {
          im.TableNextColumn()
          im.Text("Row %d Column %d", row, column)
        }
      }
      im.EndTable()
    }
    // ProgressBarの表示処理
    im.ProgressBar(f32(slider_value)/100.0, {500, 18})  // size:x=0にするとスライダーと同じ長さ
    if slider_value == 100 do slider_value = 0
    else do slider_value += 1

    // 画像ウィンドウの表示処理
    if im.Button("画像ウィンドウの開閉") {
      if show_dialog == true do show_dialog = false
      else do show_dialog = true
    }
    if show_dialog == true {
      // 別画面の表示
      im.Begin("画像ウィンドウ")
      im.SetWindowSize({315, 288})
      // 画像の表示
      im.Image(im.TextureID(uintptr(out_texture)), {f32(out_width), f32(out_height)})
      im.End()
    }
  }
  im.End()

  im.Render()
}

画像を読み込むために、「image.odin」ファイルを新たに追加。
標準入力でファイルを読み込んだ後、イメージデータに変換し、テキスチャに変換しています。

sample02/image.odin
package main

import "core:fmt"
import "core:os"
import "core:c"
import "vendor:stb/image"
import gl "vendor:OpenGL"

// イメージファイルをテキスチャに変換
// (イメージを読み込ませたいだけなのに、めちゃくちゃ面倒)
// 画像ファイルを通常のファイルオープンで読み込み、STBライブラリでイメージデータを変換した後、
// OpenGLでテキスチャに変換
// 参考資料:https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
LoadTextureFromFile :: proc(file_name: string) -> (u32, int, int, bool) {
  file_data: []byte
  if fd, ok := os.open(file_name, os.O_RDONLY); ok == nil {
    file_size, _ := os.seek(fd, 0, os.SEEK_END)
    file_data = make([]byte, file_size) or_else fmt.panicf("error: out of memory\n")
    defer delete(file_data)
    os.seek(fd, 0, os.SEEK_SET)
    total, _ := os.read(fd, file_data)

    image_data := image.load_from_memory(raw_data(file_data), c.int(file_size), &out_width, &out_height, nil, 4)

    gl.GenTextures(1, &out_texture)
    // defer gl.DeleteTextures(1, &out_texture)  // プログラム終了時には、どこかで削除が必要
    gl.BindTexture(gl.TEXTURE_2D, out_texture)
    gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
    gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, i32(out_width), i32(out_height), 0, gl.RGBA, gl.UNSIGNED_BYTE, image_data)
    image.image_free(image_data)
 
    return out_texture, int(out_width), int(out_height), true
  }
  return 0, 0, 0, false
}

「sample02」フォルダの構成が下記のようになってれば、問題ありません。

sample02フォルダのファイル構成
sample02 -+- main.odin
          +- render.odin
          +- style.odin
          +- menu.odin
          +- image.odin
実行
$ odin run .\sample02\

おわりに

ソースをべたに書き込んでいるので、長文になってますが、やってる事は大したことないので、誰でも作成できます。
まあ、Odin言語でImGuiを今後プログラムしたいと言う方の参考になれば良いかな、と思って書いてみました。

Discussion