Edited at

C#でImGui (ImGui in C#)


はじめに

C#でOpenGL + ImGuiをやろうとしたら情報がほとんど無かったのでまとめてみました。

環境

・OpenTK v3.0.1

・OpenTK.GLControl v3.0.1

・ImGui.NET v1.71.0

・.Net framework 4.7.1

動作イメージ


Nugetパケージの導入

Nugetから以下のパッケージを導入します。

・OpenTK

・OpenTK.GLControl

・ImGui.NET

このとき、ImGui.NETは.NetStandard版しか提供されていないので、ビルド環境が.Net frameworkだと大量のdllが呼ばれます。調べた所、.Net framework 4.7.1以上にしておけばまだdllの数は15個くらいに減るみたいです。また、格納されたdllのうち、実際に使用されるdllはもっと少ないので、配布時に削除したらよいと思います。


ImGuiManagerの作成

いろんなExampleのソースを読んで作成しました。

OpenGL4世代向けです。


ImGuiManager.cs

  public class ImGuiManager : IDisposable

{
//ImGuiで画面更新が必要な場合、このイベントを呼び、GLControl.Invalidate()を実行してもらう。
public event EventHandler DrawRequested;
public static List<IImGuiDrawable> ImDrawList = new List<IImGuiDrawable>();
public bool ImWantMouse => ImGui.GetIO().WantCaptureMouse;
public bool ImWantKeyboard => ImGui.GetIO().WantCaptureKeyboard;

private int vboHandle;
private int vbaHandle;
private int elementsHandle;
private int attribLocationTex;
private int attribLocationProjMtx;
private int attribLocationVtxPos;
private int attribLocationVtxUV;
private int attribLocationVtxColor;
private int shaderProgram;
private int shader_vs;
private int shader_fs;
private int fontTexture;
//C#のsizeofでは取得できないのでMarshalの方を使う
private int imDrawVertSize = System.Runtime.InteropServices.Marshal.SizeOf(default(ImDrawVert));

//画面サイズ=GLControlのサイズ
private System.Numerics.Vector2 displaySize;
private bool show_demo_window = true;
private bool show_another_window = false;
//日本語グリフの文字コードリスト
private IntPtr japaneseGlyph;

System.Numerics.Vector3 clear_color = new System.Numerics.Vector3(0.45f, 0.55f, 0.60f);
public ImGuiManager(IMGLControl glc)
{
ImGui.CreateContext();
ImGui.StyleColorsLight();
var io = ImGui.GetIO();
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; // We can honor GetMouseCursor() values (optional)
io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;// We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.KeyMap[(int)ImGuiKey.Tab] = (int)System.Windows.Forms.Keys.Tab;
io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)System.Windows.Forms.Keys.Left;
io.KeyMap[(int)ImGuiKey.RightArrow] = (int)System.Windows.Forms.Keys.Right;
io.KeyMap[(int)ImGuiKey.UpArrow] = (int)System.Windows.Forms.Keys.Up;
io.KeyMap[(int)ImGuiKey.DownArrow] = (int)System.Windows.Forms.Keys.Down;
io.KeyMap[(int)ImGuiKey.PageUp] = (int)System.Windows.Forms.Keys.Prior;
io.KeyMap[(int)ImGuiKey.PageDown] = (int)System.Windows.Forms.Keys.Next;
io.KeyMap[(int)ImGuiKey.Home] = (int)System.Windows.Forms.Keys.Home;
io.KeyMap[(int)ImGuiKey.End] = (int)System.Windows.Forms.Keys.End;
io.KeyMap[(int)ImGuiKey.Insert] = (int)System.Windows.Forms.Keys.Insert;
io.KeyMap[(int)ImGuiKey.Delete] = (int)System.Windows.Forms.Keys.Delete;
io.KeyMap[(int)ImGuiKey.Backspace] = (int)System.Windows.Forms.Keys.Back;
io.KeyMap[(int)ImGuiKey.Space] = (int)System.Windows.Forms.Keys.Space;
io.KeyMap[(int)ImGuiKey.Enter] = (int)System.Windows.Forms.Keys.Return;
io.KeyMap[(int)ImGuiKey.Escape] = (int)System.Windows.Forms.Keys.Escape;
io.KeyMap[(int)ImGuiKey.A] = (int)System.Windows.Forms.Keys.A;
io.KeyMap[(int)ImGuiKey.C] = (int)System.Windows.Forms.Keys.C;
io.KeyMap[(int)ImGuiKey.V] = (int)System.Windows.Forms.Keys.V;
io.KeyMap[(int)ImGuiKey.X] = (int)System.Windows.Forms.Keys.X;
io.KeyMap[(int)ImGuiKey.Y] = (int)System.Windows.Forms.Keys.Y;
io.KeyMap[(int)ImGuiKey.Z] = (int)System.Windows.Forms.Keys.Z;
displaySize.X = glc.Width;
displaySize.Y = glc.Height;
io.DisplaySize = displaySize;
createDeviceObjects();
createFontsTexture();
io.ImeWindowHandle = glc.Handle;
setStyle();
//WinFormイベントの登録
addControlEvents(glc);
}

#region Initialize
private void addControlEvents(IMGLControl glc)
{
//イベントの登録
glc.MouseDown += Glc_MouseDown;
glc.MouseUp += Glc_MouseUp;
glc.MouseMove += Glc_MouseMove;
glc.MouseWheel += Glc_MouseWheel;
glc.KeyDown += Glc_KeyDown;
glc.KeyUp += Glc_KeyUp;
glc.SizeChanged += Glc_SizeChanged;
glc.CharInputed += Glc_CharInputed;
}

private void createDeviceObjects()
{
int last_texture, last_array_buffer;
last_texture = GL.GetInteger(GetPName.TextureBinding2D);
last_array_buffer = GL.GetInteger(GetPName.ArrayBufferBinding);
//シェーダコードはOpenGLのバージョンに合わせて調整する
const string vertex_shader_glsl_440_core =
@"#version 440 core
layout (location = 0) in vec2 Position;
layout (location = 1) in vec2 UV;
layout (location = 2) in vec4 Color;
layout (location = 10) uniform mat4 ProjMtx;
out vec2 Frag_UV;
out vec4 Frag_Color;
void main()
{
Frag_UV = UV;
Frag_Color = Color;
gl_Position = ProjMtx * vec4(Position.xy,0,1);
}"
;
const string fragment_shader_glsl_440_core =
@"#version 440 core
in vec2 Frag_UV;
in vec4 Frag_Color;
layout (location = 20) uniform sampler2D Texture;
layout (location = 0) out vec4 Out_Color;
void main()
{
Out_Color = Frag_Color * texture(Texture, Frag_UV.st);
}"
;
//シェーダのコンパイルとリンク
shader_vs = GL.CreateShader(ShaderType.VertexShader);
GL.ShaderSource(shader_vs, vertex_shader_glsl_440_core);
GL.CompileShader(shader_vs);
var info = GL.GetShaderInfoLog(shader_vs);
if (!string.IsNullOrWhiteSpace(info))
Debug.WriteLine($"GL.CompileShader [VertexShader] had info log: {info}");

shader_fs = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(shader_fs, fragment_shader_glsl_440_core);
GL.CompileShader(shader_fs);
info = GL.GetShaderInfoLog(shader_fs);
if (!string.IsNullOrWhiteSpace(info))
Debug.WriteLine($"GL.CompileShader [VertexShader] had info log: {info}");
shaderProgram = GL.CreateProgram();
GL.AttachShader(shaderProgram, shader_vs);
GL.AttachShader(shaderProgram, shader_fs);
GL.LinkProgram(shaderProgram);
info = GL.GetProgramInfoLog(shaderProgram);
if (!string.IsNullOrWhiteSpace(info))
Debug.WriteLine($"GL.LinkProgram had info log: {info}");

//ImGuiのサンプルではglGetUniformLocationでLocationを取得しているが
//OpenGL4はシェーダで直値指定できるので直値で指定してしまう。
attribLocationTex = 20; //glGetUniformLocation(g_ShaderHandle, "Texture");
attribLocationProjMtx = 10; //glGetUniformLocation(g_ShaderHandle, "ProjMtx");
attribLocationVtxPos = 0; // glGetAttribLocation(g_ShaderHandle, "Position");
attribLocationVtxUV = 1;// glGetAttribLocation(g_ShaderHandle, "UV");
attribLocationVtxColor = 2;//= glGetAttribLocation(g_ShaderHandle, "Color");
vboHandle = GL.GenBuffer();//SetupRenderStateで使用される
vbaHandle = GL.GenVertexArray();//SetupRenderStateで使用される
elementsHandle = GL.GenBuffer();//SetupRenderStateで使用される
// Restore modified GL state
GL.BindTexture(TextureTarget.Texture2D, last_texture);
GL.BindBuffer(BufferTarget.ArrayBuffer, last_array_buffer);
}
bool createFontsTexture()
{
// Build texture atlas
unsafe
{
var io = ImGui.GetIO();
//Font setup
var config = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig());
// fill with data
config.OversampleH = 2; //横方向のオーバーサンプリング、高画質になるらしい
config.OversampleV = 1;
config.RasterizerMultiply = 1.2f;//1より大きくすると太くなる。imGuiはフォント描画にアンチエイリアスがかかって薄くなるのでこれで対処
config.FontNo = 2;//ttc(ttfが複数集まったやつ)ファイルの場合、この番号でフォントを指定できる。この場合MS UIGothicを指定
config.PixelSnapH = true;//線が濃くなれば良いが効果不明
//サンプルのコード
//font = io.Fonts.AddFontFromFileTTF(@"c:\windows\fonts\msgothic.ttc", 12.0f, config, io.Fonts.GetGlyphRangesJapanese());
//imgui で日本語が「?」になる場合の対処 を適用(https://qiita.com/benikabocha/items/a25571c1b059eaf952de)
//以下のクラスを作る
// public static readonly ushort[] glyphRangesJapanese = new ushort[] {
// 0x0020, 0x007E, 0x00A2, 0x00A3, 0x00A7,....};
IntPtr japaneseGlyph = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(ushort)) * FontGlyphs.glyphRangesJapanese.Length);
//Copy()の引数にushort[]が無いので下記のキャストで無理やり渡す
Marshal.Copy((short[])(object)FontGlyphs.glyphRangesJapanese, 0, japaneseGlyph, FontGlyphs.glyphRangesJapanese.Length);
font = io.Fonts.AddFontFromFileTTF(@"c:\windows\fonts\msgothic.ttc", 12.0f, config, japaneseGlyph);
//imgui内部でメモリを直接使用しているらしく、Freeすると落ちる
//Marshal.FreeCoTaskMem(ptr);
config.Destroy();

byte* pixels;
int width, height;
io.Fonts.GetTexDataAsRGBA32(out pixels, out width, out height); // Load as RGBA 32-bits (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory.

// Upload texture to graphics system
int last_texture;
GL.GetInteger(GetPName.TextureBinding2D, out last_texture);

fontTexture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, fontTexture);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
//# ifdef GL_UNPACK_ROW_LENGTH
// GL.PixelStore(PixelStoreParameter.PackRowLength, 0);
//#endif
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, (IntPtr)pixels);

// Store our identifier
io.Fonts.TexID = (IntPtr)fontTexture;

// Restore state
GL.BindTexture(TextureTarget.Texture2D, last_texture);
}
return true;
}

private void setStyle()
{
var style = ImGui.GetStyle();
style.WindowPadding = new System.Numerics.Vector2(5f,5f);
style.FramePadding = new System.Numerics.Vector2(3f,2f);
style.ItemSpacing = new System.Numerics.Vector2(4f,4f);
style.WindowRounding = 4f;
style.TabRounding = 2f;
style.Colors[(int)ImGuiCol.WindowBg] = new System.Numerics.Vector4(0.94f, 0.94f, 0.94f, 0.78f);
style.Colors[(int)ImGuiCol.FrameBg] = new System.Numerics.Vector4(1.00f, 1.00f, 1.00f, 0.71f);
style.Colors[(int)ImGuiCol.ChildBg] = ImGui.ColorConvertU32ToFloat4(0x0f000000);
}

#endregion Initialize

#region Control events
private void Glc_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
System.Numerics.Vector2 mousePos = new System.Numerics.Vector2(e.X, e.Y);
var io = ImGui.GetIO();
io.MousePos = mousePos;
DrawRequested?.Invoke(this, null);
}

private void Glc_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
var io = ImGui.GetIO();
int button = 0;
button = getButtonNo(e, button);
io.MouseDown[button] = false;
DrawRequested?.Invoke(this, null);
}

private void Glc_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
var io = ImGui.GetIO();
int button = 0;
button = getButtonNo(e, button);
io.MouseDown[button] = true;
DrawRequested?.Invoke(this, null);
}

private void Glc_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
{
var io = ImGui.GetIO();
if (!io.WantCaptureMouse)
return;
io.MouseWheel += e.Delta / 120.0f;
DrawRequested?.Invoke(this, null);
}

private void Glc_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
var io = ImGui.GetIO();
if (e.KeyValue < 256)
{
io.KeysDown[e.KeyValue] = true;

//io.AddInputCharacter((uint)e.KeyValue);
}
io.KeyAlt = e.Alt;
io.KeyCtrl = e.Control;
io.KeyShift = e.Shift;
DrawRequested?.Invoke(this, null);
}

private void Glc_CharInputed(object sender, char e)
{
var io = ImGui.GetIO();
io.AddInputCharacter(e);
}
private void Glc_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
{
var io = ImGui.GetIO();
if (e.KeyValue < 256)
io.KeysDown[e.KeyValue] = false;
io.KeyAlt = e.Alt;
io.KeyCtrl = e.Control;
io.KeyShift = e.Shift;
DrawRequested?.Invoke(this, null);
}
private void Glc_SizeChanged(object sender, EventArgs e)
{
var io = ImGui.GetIO();
displaySize.X = ((System.Windows.Forms.Control)sender).Width;
displaySize.Y = ((System.Windows.Forms.Control)sender).Height;
io.DisplaySize = displaySize;
}

private static int getButtonNo(System.Windows.Forms.MouseEventArgs e, int button)
{
switch (e.Button)
{
case System.Windows.Forms.MouseButtons.Right:
button = 1;
break;
case System.Windows.Forms.MouseButtons.Middle:
button = 2;
break;
case System.Windows.Forms.MouseButtons.XButton1:
button = 3;
break;
case System.Windows.Forms.MouseButtons.XButton2:
button = 4;
break;
case System.Windows.Forms.MouseButtons.Left:
case System.Windows.Forms.MouseButtons.None:
default:
break;
}

return button;
}

#endregion Control events

#region Draw call
private void ImGui_ImplOpenGL3_SetupRenderState(ImDrawDataPtr draw_data, uint vertex_array_object)
{
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
GL.Enable(EnableCap.Blend);
GL.BlendEquation(BlendEquationMode.FuncAdd);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Disable(EnableCap.CullFace);
GL.Disable(EnableCap.DepthTest);
GL.Enable(EnableCap.ScissorTest);
GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);

//↓サイズ変更の時だけ設定すればいいのでは?
// Setup viewport, orthographic projection matrix
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
//glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height);
float L = draw_data.DisplayPos.X;
float R = draw_data.DisplayPos.X + draw_data.DisplaySize.X;
float T = draw_data.DisplayPos.Y;
float B = draw_data.DisplayPos.Y + draw_data.DisplaySize.Y;
//ここだけはOptenTKのstructにする
OpenTK.Matrix4 ortho_projection = new OpenTK.Matrix4(
2.0f / (R - L), 0.0f, 0.0f, 0.0f,
0.0f, 2.0f / (T - B), 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
(R + L) / (L - R), (T + B) / (B - T), 0.0f, 1.0f
);

GL.UseProgram(shaderProgram);
GL.Uniform1(attribLocationTex, 0);
GL.UniformMatrix4(attribLocationProjMtx, false, ref ortho_projection);
GL.BindSampler(0, 0); // We use combined texture/sampler state. Applications using GL 3.3 may set that otherwise.

// Bind vertex/index buffers and setup attributes for ImDrawVert
GL.BindVertexArray(vbaHandle);
GL.BindBuffer(BufferTarget.ArrayBuffer, vboHandle);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, elementsHandle);
GL.EnableVertexAttribArray(attribLocationVtxPos);
GL.EnableVertexAttribArray(attribLocationVtxUV);
GL.EnableVertexAttribArray(attribLocationVtxColor);

GL.VertexAttribPointer(attribLocationVtxPos, 2, VertexAttribPointerType.Float, false, imDrawVertSize, 0);//ImDrawVertのoffsetの値は直接入力
GL.VertexAttribPointer(attribLocationVtxUV, 2, VertexAttribPointerType.Float, false, imDrawVertSize, 8);//0番目(pos)はVector2なのでfloat*2 = 8
GL.VertexAttribPointer(attribLocationVtxColor, 4, VertexAttribPointerType.UnsignedByte, true, imDrawVertSize, 16);//1番目(puv)はVector2なので 8 + float*2 = 16
}

private void ImGui_ImplOpenGL3_RenderDrawData(ImDrawDataPtr draw_data)
{
// Backup GL state
int last_active_texture = GL.GetInteger(GetPName.ActiveTexture);
int last_program = GL.GetInteger(GetPName.CurrentProgram);
int last_texture = GL.GetInteger(GetPName.TextureBinding2D);
int last_sampler = GL.GetInteger(GetPName.SamplerBinding);
int last_array_buffer = GL.GetInteger(GetPName.ColorArrayBufferBinding);
int[] last_polygon_mode = new int[2]; GL.GetInteger(GetPName.PolygonMode, last_polygon_mode);
//int[] last_viewport = new int[4]; GL.GetInteger(GetPName.Viewport, last_viewport);
int[] last_scissor_box = new int[4]; GL.GetInteger(GetPName.ScissorBox, last_scissor_box);
int last_blend_src_rgb = GL.GetInteger(GetPName.BlendSrcRgb);
int last_blend_dst_rgb = GL.GetInteger(GetPName.BlendDstRgb);
int last_blend_src_alpha = GL.GetInteger(GetPName.BlendSrcAlpha);
int last_blend_dst_alpha = GL.GetInteger(GetPName.BlendDstAlpha);
int last_blend_equation_rgb = GL.GetInteger(GetPName.BlendEquationRgb);
int last_blend_equation_alpha = GL.GetInteger(GetPName.BlendEquationAlpha);
bool last_enable_blend = GL.IsEnabled(EnableCap.Blend);
bool last_enable_cull_face = GL.IsEnabled(EnableCap.CullFace);
bool last_enable_depth_test = GL.IsEnabled(EnableCap.DepthTest);
bool last_enable_scissor_test = GL.IsEnabled(EnableCap.ScissorTest);
bool clip_origin_lower_left = true;
GL.ActiveTexture(TextureUnit.Texture0);

// Setup desired GL state
// Recreate the VAO every time (this is to easily allow multiple GL contexts to be rendered to. VAO are not shared among GL contexts)
// The renderer would actually work without any VAO bound, but then our VertexAttrib calls would overwrite the default one currently bound.
uint vertex_array_object = 0;
ImGui_ImplOpenGL3_SetupRenderState(draw_data, vertex_array_object);

// Will project scissor/clipping rectangles into framebuffer space
var clip_off = draw_data.DisplayPos; // (0,0) unless using multi-viewports
var clip_scale = draw_data.FramebufferScale; // (1,1) unless using retina display which are often (2,2)

// Render command lists
for (int n = 0; n < draw_data.CmdListsCount; n++)
{
var cmd_list = draw_data.CmdListsRange[n];

// Upload vertex/index buffers Indexバッファはほぼ100%ushortなのでushortとしてしまう。
GL.BufferData(BufferTarget.ArrayBuffer, cmd_list.VtxBuffer.Size * imDrawVertSize, cmd_list.VtxBuffer.Data, BufferUsageHint.StreamDraw);
GL.BufferData(BufferTarget.ElementArrayBuffer, cmd_list.IdxBuffer.Size * sizeof(ushort), cmd_list.IdxBuffer.Data, BufferUsageHint.StreamDraw);

for (int cmd_i = 0; cmd_i < cmd_list.CmdBuffer.Size; cmd_i++)
{
var pcmd = cmd_list.CmdBuffer[cmd_i];
if (pcmd.UserCallback != IntPtr.Zero)//ユーザコールバックはスキップ
{
// User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
//if (pcmd.UserCallback == ImGui. ImDrawCallback_ResetRenderState)
// ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object);
//else
//pcmd->UserCallback(cmd_list, pcmd);
Debug.WriteLine("UserCallback" + pcmd.UserCallback.ToString());
}
else
{
// Project scissor/clipping rectangles into framebuffer space
System.Numerics.Vector4 clip_rect = new System.Numerics.Vector4();
clip_rect.X = (pcmd.ClipRect.X - clip_off.X) * clip_scale.X;
clip_rect.Y = (pcmd.ClipRect.Y - clip_off.Y) * clip_scale.Y;
clip_rect.Z = (pcmd.ClipRect.Z - clip_off.X) * clip_scale.X;
clip_rect.W = (pcmd.ClipRect.W - clip_off.Y) * clip_scale.Y;

if (clip_rect.X < displaySize.X && clip_rect.Y < displaySize.Y && clip_rect.Z >= 0.0f && clip_rect.W >= 0.0f)
{
// Apply scissor/clipping rectangle
if (clip_origin_lower_left)
GL.Scissor((int)clip_rect.X, (int)(displaySize.Y - clip_rect.W), (int)(clip_rect.Z - clip_rect.X), (int)(clip_rect.W - clip_rect.Y));
else

GL.Scissor((int)clip_rect.X, (int)clip_rect.Y, (int)clip_rect.Z, (int)clip_rect.W); // Support for GL 4.5 rarely used glClipControl(GL_UPPER_LEFT)

// Bind texture, Draw
GL.BindTexture(TextureTarget.Texture2D, pcmd.TextureId.ToInt32());
//Indexバッファはほぼ100 % ushortなのでushortとしてしまう。
GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, new IntPtr(pcmd.IdxOffset * sizeof(ushort)), (int)pcmd.VtxOffset);
//If glDrawElementsBaseVertex not supported
//GL.DrawElements(BeginMode.Triangles, pcmd.ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)));
}
}
}
}

// Restore modified GL state
GL.UseProgram(last_program);
GL.BindTexture(TextureTarget.Texture2D, last_texture);
GL.BindSampler(0, last_sampler);
GL.ActiveTexture((TextureUnit)last_active_texture);
GL.BindBuffer(BufferTarget.ArrayBuffer, last_array_buffer);
GL.BlendEquationSeparate((BlendEquationMode)last_blend_equation_rgb, (BlendEquationMode)last_blend_equation_alpha);
GL.BlendFuncSeparate((BlendingFactorSrc)last_blend_src_rgb, (BlendingFactorDest)last_blend_dst_rgb, (BlendingFactorSrc)last_blend_src_alpha, (BlendingFactorDest)last_blend_dst_alpha);
if (last_enable_blend) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend);
if (last_enable_cull_face) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace);
if (last_enable_depth_test) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest);
if (last_enable_scissor_test) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest);
GL.PolygonMode(MaterialFace.FrontAndBack, (PolygonMode)last_polygon_mode[0]);
//GL.Viewport(last_viewport[0], last_viewport[1], last_viewport[2], last_viewport[3]);
GL.Scissor(last_scissor_box[0], last_scissor_box[1], last_scissor_box[2], last_scissor_box[3]);
GL.DisableVertexAttribArray(attribLocationVtxPos);
GL.DisableVertexAttribArray(attribLocationVtxUV);
GL.DisableVertexAttribArray(attribLocationVtxColor);

}
#endregion Draw call

#region sample code
float f = 0.0f;
int counter = 0;
private ImFontPtr font;

public void ImDraw()
{
ImGui.NewFrame();
// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
if (show_demo_window)
ImGui.ShowDemoWindow();
// 2. Show a simple window that we create ourselves. We use a Begin/End pair to created a named window.
{
ImGui.Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.
//ImGui.PushFont(font);

ImGui.Text("This is some useful text."); // Display some text (you can use a format strings too)
ImGui.Text("サンプルテキスト亜"); // Display some text (you can use a format strings too)
ImGui.Checkbox("Demo Window", ref show_demo_window); // Edit bools storing our window open/close state
ImGui.Checkbox("Another Window", ref show_another_window);

ImGui.SliderFloat("float", ref f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f
ImGui.ColorEdit3("clear color", ref clear_color); // Edit 3 floats representing a color

if (ImGui.Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated)
counter++;
ImGui.SameLine();
ImGui.Text($"counter = {counter}");

ImGui.Text($"Application average {1000.0f / ImGui.GetIO().Framerate:F3} ms/frame ({ImGui.GetIO().Framerate} FPS)");
//ImGui.PopFont();
ImGui.End();
}

// 3. Show another simple window.
if (show_another_window)
{
ImGui.Begin("Another Window", ref show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
ImGui.Text("Hello from another window!");
if (ImGui.Button("Close Me"))
show_another_window = false;
ImGui.End();
}

// Rendering
ImGui.Render();
//OpenGLの画面クリアなどは上位モジュールで実施
//GL.ClearColor(glc.BackColor);
//GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
ImGui_ImplOpenGL3_RenderDrawData(ImGui.GetDrawData());

//OpenGLのSwapは上位モジュールで実施
//glc.SwapBuffers();
}
#endregion sample code

#region Destroy
private void destroyDeviceObjects()
{
GL.DeleteVertexArray(vbaHandle);
GL.DeleteBuffer(vboHandle);
GL.DeleteBuffer(elementsHandle);
GL.DetachShader(shaderProgram, shader_vs);
GL.DetachShader(shaderProgram, shader_fs);
GL.DeleteProgram(shaderProgram);
}
void destroyFontsTexture()
{
var io = ImGui.GetIO();
GL.DeleteTexture(fontTexture);
io.Fonts.TexID = IntPtr.Zero;
fontTexture = 0;
}

public void Dispose()
{
ImDrawList.Clear();
destroyFontsTexture();
Marshal.FreeCoTaskMem(japaneseGlyph);
destroyDeviceObjects();
ImGui.DestroyContext();
}
#endregion



OpenTK側の処理

FormにOpenTK.GLControlを貼り付けます。おそらくツールボックスにいないので適当なコントロールを張り付けてForm.designer.csを直接編集して型をOpenTK.GLControlに変える方が早いかもしれません。

ゲームの場合はメインループを作成します。ゲーム以外の場合はGLControlのPaintイベントでOpenGLの描画を実施します。

GLControlのPaintイベントを使用する場合、ImGuiの描画更新に対応するため、ImGui側からGLControl側へ描画リクエスト(GLControl.Invalidate())を送る必要があります。

今回のサンプルではPaintイベントベースを前提として進めます。

OptenTKの初期化や描画方法はここでは割愛します。

まず、フォームのLoadイベントにてImGuiManagerを作成します。

ImGuiManagerのコンストラクタ引数にはOpenTK.GLControlを渡します。


Form1.cs-Loadイベント

    ImGuiManager imgui;

private void Form1_Load(object sender, EventArgs e)
{
imgui = new ImGuiManager(glControl);
imgui.DrawRequested += (arg1, arg2) => glc.Invalidate();
//OpenTKの初期化処理を記述(省略)
}


今回描画はPaintイベントで実施しますので、フォームのPaintイベントにてOptenTKの描画開始処理(画面のクリア等)→ImGuiの描画→Swap(画面への描画反映)を実施します。


Form1.cs-Paintイベント

    private void Form1_Paint(object sender, PaintEventArgs e)

{
//OpenTKの描画開始処理を書く
GL.ClearColor(glControl.BackColor);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
//ImGuiの描画
imgui.ImDraw();
//OpenTKのSwap
glControl.Swap();
}


日本語用グリフ作成対象定義の作成

コード内部のコメントにも書いていますがデフォルトのio.Fonts.GetGlyphRangesJapanese()は漢字の文字が足りないようです。→ imgui で日本語が「?」になる場合の対処

記事を参考にして、C#で以下のようなクラスを作っておきます。


FontGlyphs.cs

internal class FontGlyphs

{
public static readonly ushort[] glyphRangesJapanese = new ushort[] {
0x0020, 0x007E, 0x00A2, 0x00A3, 0x00A7,....};
}


ImGuiManagerの中身について


  • 画面サイズはGLContrlのサイズをそのまま適用します。画面サイズ変更はGLControl.SizeChangedイベントで実施します。

  • フォントの描画はアンチエイリアスが利いているせいで、かすれた薄い描画になります。アンチエイリアスを切る対応が難しそうなので、フォント追加時に指定できるImFontConfig.RasterizerMultiplyというパラメータで太さを太くします。

  • フォントのファイル名に.ttcを指定する場合、ImFontConfig.FontNoでttc内部のどのフォントを指定するか決められるようです。MSUIGothicやメイリオUIを指定したい場合はFontNoを2にしましょう。サンプルは以下です

        var config = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig());

// fill with data
config.OversampleH = 2; //横方向のオーバーサンプリング、高画質になるらしい
config.OversampleV = 1;
config.RasterizerMultiply = 1.2f;//1より大きくすると太くなる。imGuiはフォント描画にアンチエイリアスがかかって薄くなるのでこれで対処
config.FontNo = 2;//ttc(ttfが複数集まったやつ)ファイルの場合、この番号でフォントを指定できる。この場合MS UIGothicを指定
config.PixelSnapH = true;//線が濃くなれば良いが効果不明

MSUIGothicを使用する場合は以下のようにします。C#で作った配列は固定アドレス空間にコピーしてから渡す必要があります。

        IntPtr japaneseGlyph = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(ushort)) * FontGlyphs.glyphRangesJapanese.Length);

//Copy()の引数にushort[]が無いので下記のキャストで無理やり渡す
Marshal.Copy((short[])(object)FontGlyphs.glyphRangesJapanese, 0, japaneseGlyph, FontGlyphs.glyphRangesJapanese.Length);
font = io.Fonts.AddFontFromFileTTF(@"c:\windows\fonts\msgothic.ttc", 12.0f, config, japaneseGlyph);


  • マウスやキーボードのイベントはGLControlのMouseMove, MouseDown, MouseUp, MouseWheel, KeyDown, KeyUpイベントを使用します。

  • マウスホイールは私の環境では1刻みでDelta=120が取得できるのですが、ImGuiは1刻み1.0fのようなので
    io.MouseWheel += e.Delta / 120.0f; とします。

  • ImGui_ImplOpenGL3_SetupRenderState()のGL.VertexAttribPointer()でoffsetを入れるところはC++のようなマクロ相当の用意するのは大変なので手計算の直値入力としています。

  • OpenGLのIndexバッファの型はushort決め打ちにしています。ほとんどの場合問題ないはずです。


文字入力の対応

キーボード入力はio.AddInputCharacter();で与えるのですが、FormやGLControlのProcessMessageをオーバーライドしてWM_CHARメッセージから取得する必要があります。また、GLControlは普通のWinFormsのUserControlですのでIMEが無効化されています。

参考:C#でIMEの入力を受けるユーザーコントロールの作成

上記記事を元にOpenTKのGLControlを継承してIME対応版を作成します。

WM_CHARメッセージをキャッチして新たに追加したCharInputedイベントに文字を流します。


IMGLControl.cs

  public class IMGLControl : OpenTK.GLControl

{
public event EventHandler<char> CharInputed;
public IMGLControl() : base()
{
ControlRemoved += IMGLControl_ControlRemoved;
}
#region IME関係
private const int WM_IME_COMPOSITION = 0x010F;
private const int GCS_RESULTREADSTR = 0x0200;
private const int WM_IME_STARTCOMPOSITION = 0x10D; // IME変換開始
private const int WM_IME_ENDCOMPOSITION = 0x10E; // IME変換終了
private const int WM_IME_NOTIFY = 0x0282;
private const int WM_IME_SETCONTEXT = 0x0281;
private const int WM_CHAR = 0x0102;
private const int WM_UNICHAR = 0x0109;

public enum ImmAssociateContextExFlags : uint
{
IACE_CHILDREN = 0x0001,
IACE_DEFAULT = 0x0010,
IACE_IGNORENOCONTEXT = 0x0020
}

[StructLayout(LayoutKind.Sequential)]
public struct C_RECT
{
public int _Left;
public int _Top;
public int _Right;
public int _Bottom;
}

[StructLayout(LayoutKind.Sequential)]
public struct C_POINT
{
public int x;
public int y;
}

const uint CFS_POINT = 0x0002;

public struct COMPOSITIONFORM
{
public uint dwStyle;
public C_POINT ptCurrentPos;
public C_RECT rcArea;
}

[DllImport("Imm32.dll")]
private static extern IntPtr ImmGetContext(IntPtr hWnd);
[DllImport("Imm32.dll")]
private static extern int ImmGetCompositionString(IntPtr hIMC, int dwIndex, StringBuilder lpBuf, int dwBufLen);
[DllImport("Imm32.dll")]
private static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);
[DllImport("imm32.dll")]
private static extern IntPtr ImmCreateContext();
[DllImport("imm32.dll")]
private static extern bool ImmAssociateContextEx(IntPtr hWnd, IntPtr hIMC, ImmAssociateContextExFlags dwFlags);
[DllImport("imm32.dll")]
public static extern int ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM lpCompositionForm);

IntPtr himc = IntPtr.Zero;

private void IMGLControl_ControlRemoved(object sender, ControlEventArgs e)
{
if (himc != IntPtr.Zero)
{
ImmReleaseContext(this.Handle, himc);
himc = IntPtr.Zero;
}
//base.Dispose(disposing);
}

~IMGLControl()
{
if (himc != IntPtr.Zero)
{
ImmReleaseContext(this.Handle, himc);
himc = IntPtr.Zero;
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_IME_SETCONTEXT:
{
//Imeを関連付ける
IntPtr himc = ImmCreateContext();
ImmAssociateContextEx(this.Handle, himc, ImmAssociateContextExFlags.IACE_DEFAULT);
base.WndProc(ref m);
break;
}
case WM_IME_STARTCOMPOSITION:
{
//入力コンテキストにアクセスするためのお約束
IntPtr hImc = ImmGetContext(this.Handle);

//コンポジションウィンドウの位置を設定
COMPOSITIONFORM info = new COMPOSITIONFORM();
info.dwStyle = CFS_POINT;
info.ptCurrentPos.x = 10;
info.ptCurrentPos.y = 10;
ImmSetCompositionWindow(hImc, ref info);

//コンポジションウィンドウのフォントを設定
//ImmSetCompositionFont(hImc, m_Focus->GetFont()->GetInfoLog());

//入力コンテキストへのアクセスが終了したらロックを解除する
ImmReleaseContext(Handle, hImc);
base.WndProc(ref m);
break;
}
case WM_CHAR:
case WM_UNICHAR:
char c = (char)(m.WParam);
CharInputed?.Invoke(this, c);
break;
default:
//IME以外のメッセージは元のプロシージャで処理
base.WndProc(ref m);
break;
}
}
#endregion
}


ImGui側はIMGLControlで追加したCharInputedイベントを元にImGuiへ通知します。

private void addControlEvents(IMGLControl glc)

{//~省略
glc.CharInputed += Glc_CharInputed;
}

private void Glc_CharInputed(object sender, char e)
{
var io = ImGui.GetIO();
io.AddInputCharacter(e); //ImGuiへ通知
}


苦労した点

OpenTKのGL.GetIntegerで落ちたり落ちなかったりするケースがあり困りました。原因はGL.EnableVertexAttribArray()でした。使用しないVertexAttribArrayがある場合はかならずGL.DisableVertexAttribArray()を呼び出して無効化しておかないと内部のメモリがおかしくなって関係のない所AccessViolationが発生して落ちます。


変更履歴

2019.8.13 サンプルコードについて、OpenGLのVBAが適用されておらず、正常に描画できない問題を修正した。

2019.8.13 IME対応版にサンプル全入れ替え