神話のプログラム言語 OdinでDear ImGuiを動かす
前回予定していた通り、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以上
1-1.上記サイトからgitクローンを使って、ソースを自分のプロジェクトフォルダにダウンロードします。
ダウンロード前に、pythonモジュールのplyをインストールしておきましょう。(ライブラリ作成時に必要です)
$ 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ライブラリを作成します。
$ cd /project_folder/imgui
$ python build.py
$ dir imgui_windows_x64.lib
imgui\imgui_windows_x64.libが出来ていれば成功です。
1-3.出来上がったライブラリを元に、サンプルを動作させてみましょう。
「imgui/examples」まで移動し、glfw_opengl3
を実行します。
※サンプルは複数ありますが、バックエンドが現時点では、glfw以外は動作しませんでした。
$ cd /project_folder/imgui/examples
$ odin run .\glfw_opengl3\
1-4.正常に動作した事になります。
sharedフォルダに/project_folder/imguiを丸ごとコピーしますし、import "shared:imgui"
に変更して動作させてみる。
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のレンダー部分を外部に出すようにします。
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」にして、以下に記載
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」ファイルとして作成し、以下に記載。
レイアウトは、下記サイトを参考に記述しています。
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」ファイルを作成し、下記に記載。
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 -+- main.odin
+- render.odin
+- style.odin
+- menu.odin
$ odin run .\sample01\
3.imGuiで画像とテーブルビューを表示
先ほどの「sample01」フォルダを「sample02」フォルダにコピーします。
「sample01/main.odin」から「main.odin」ファイル内に1行追加します。(画像をロードする関数を追加します。)
・・・一部抜粋・・・
imStyle() // スタイルの設定 外部で記載
// 画像の読み込み処理をmain.odinに追加
out_texture, out_width, out_height, ok := LoadTextureFromFile("C:\\youre_folder\\king.png")
・・・一部抜粋・・・
「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」ファイルを新たに追加。
標準入力でファイルを読み込んだ後、イメージデータに変換し、テキスチャに変換しています。
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 -+- main.odin
+- render.odin
+- style.odin
+- menu.odin
+- image.odin
$ odin run .\sample02\
おわりに
ソースをべたに書き込んでいるので、長文になってますが、やってる事は大したことないので、誰でも作成できます。
まあ、Odin言語でImGuiを今後プログラムしたいと言う方の参考になれば良いかな、と思って書いてみました。
Discussion