-
-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Monogame backend #28
Comments
Hi, cool thing. But you might be able to use the native backends if you can get your hands on the internal device and hwnd of xna/fna/monogame. (I really recommend using them, because sometimes ImGui introduces breaking changes/new features in backends and those are hard to maintain) |
Thanks, that is a good suggestion |
This would be nice to have a modern implementation! I'm the author of ImGuiNET/ImGui.NET#364 which worked a bit on getting the internals exposed and also overhauling the command line generator. However, I'm not an expert in unmanaged code and I really struggled to get things working. But every time I want to check out something new and cool from ImGui I totally don't want to jump in and break everything I have working 😆 |
@Thraka (PS: If you need any help with ImGui stuff feel free to just hop on my discord (readme) and ask me there) |
@MartelementAyy Please do provide your implementation. |
Thanks @JunaMeinhold With a MonoGame implementation of your library, I'll totally switch. Thanks! |
@Thraka You want the implementation of the native ImGuiRenderer from ImGui.NET ? DrawVertDeclarationDrawVertDeclaration : public static class DrawVertDeclaration
{
public static readonly VertexDeclaration Declaration;
public static readonly int Size;
static DrawVertDeclaration()
{
unsafe { Size = sizeof(ImDrawVert); }
Declaration = new VertexDeclaration(
Size,
// Position
new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0),
// UV
new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
// Color
new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0)
);
}
} ImGuiRenderer /// <summary>
/// ImGui renderer for use with XNA-likes (FNA & MonoGame)
/// </summary>
public class ImGuiRenderer
{
private Game _game;
// Graphics
private GraphicsDevice _graphicsDevice;
private BasicEffect _effect;
private RasterizerState _rasterizerState;
private byte[] _vertexData;
private VertexBuffer _vertexBuffer;
private int _vertexBufferSize;
private byte[] _indexData;
private IndexBuffer _indexBuffer;
private int _indexBufferSize;
// Textures
private Dictionary<ImTextureID, Texture2D> _loadedTextures;
private int _textureId;
private ImTextureID? _fontTextureId;
// Input
private int _scrollWheelValue;
private int _horizontalScrollWheelValue;
private readonly float WHEEL_DELTA = 120;
private Keys[] _allKeys = Enum.GetValues<Keys>();
public ImGuiRenderer(Game game)
{
var context = ImGui.CreateContext();
ImGui.SetCurrentContext(context);
_game = game ?? throw new ArgumentNullException(nameof(game));
_graphicsDevice = game.GraphicsDevice;
_loadedTextures = new Dictionary<ImTextureID, Texture2D>();
_rasterizerState = new RasterizerState()
{
CullMode = CullMode.None,
DepthBias = 0,
FillMode = FillMode.Solid,
MultiSampleAntiAlias = false,
ScissorTestEnable = true,
SlopeScaleDepthBias = 0
};
SetupInput();
}
#region ImGuiRenderer
/// <summary>
/// Creates a texture and loads the font data from ImGui. Should be called when the <see cref="GraphicsDevice" /> is initialized but before any rendering is done
/// </summary>
public virtual unsafe void RebuildFontAtlas()
{
// Get font texture from ImGui
var io = ImGui.GetIO();
byte* pixelData = null;
int width = 0;
int height = 0;
int bytesPerPixel = 0;
io.Fonts.GetTexDataAsRGBA32(ref pixelData, ref width, ref height, ref bytesPerPixel);
// Copy the data to a managed array
var pixels = new byte[width * height * bytesPerPixel];
unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); }
// Create and register the texture as an XNA texture
var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color);
tex2d.SetData(pixels);
// Should a texture already have been build previously, unbind it first so it can be deallocated
if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value);
// Bind the new texture to an ImGui-friendly id
_fontTextureId = BindTexture(tex2d);
// Let ImGui know where to find the texture
io.Fonts.SetTexID(_fontTextureId.Value);
io.Fonts.ClearTexData(); // Clears CPU side texture data
}
/// <summary>
/// Creates a pointer to a texture, which can be passed through ImGui calls such as <see cref="ImGui.Image" />. That pointer is then used by ImGui to let us know what texture to draw
/// </summary>
public virtual ImTextureID BindTexture(Texture2D texture)
{
var id = new ImTextureID(_textureId++);
_loadedTextures.Add(id, texture);
return id;
}
/// <summary>
/// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated
/// </summary>
public virtual void UnbindTexture(ImTextureID textureId)
{
_loadedTextures.Remove(textureId);
}
/// <summary>
/// Sets up ImGui for a new frame, should be called at frame start
/// </summary>
public virtual void BeforeLayout(GameTime gameTime)
{
ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
UpdateInput();
ImGui.NewFrame();
}
/// <summary>
/// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls
/// </summary>
public virtual void AfterLayout()
{
ImGui.Render();
unsafe { RenderDrawData(ImGui.GetDrawData()); }
}
#endregion ImGuiRenderer
#region Setup & Update
/// <summary>
/// Setup key input event handler.
/// </summary>
protected virtual void SetupInput()
{
var io = ImGui.GetIO();
// MonoGame-specific //////////////////////
_game.Window.TextInput += (s, a) =>
{
if (a.Character == '\t') return;
io.AddInputCharacter(a.Character);
};
///////////////////////////////////////////
// FNA-specific ///////////////////////////
//TextInputEXT.TextInput += c =>
//{
// if (c == '\t') return;
// ImGui.GetIO().AddInputCharacter(c);
//};
///////////////////////////////////////////
}
/// <summary>
/// Updates the <see cref="Effect" /> to the current matrices and texture
/// </summary>
protected virtual Effect UpdateEffect(Texture2D texture)
{
_effect = _effect ?? new BasicEffect(_graphicsDevice);
var io = ImGui.GetIO();
_effect.World = Matrix.Identity;
_effect.View = Matrix.Identity;
_effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f);
_effect.TextureEnabled = true;
_effect.Texture = texture;
_effect.VertexColorEnabled = true;
return _effect;
}
/// <summary>
/// Sends XNA input state to ImGui
/// </summary>
protected virtual void UpdateInput()
{
if (!_game.IsActive) return;
var io = ImGui.GetIO();
var mouse = Mouse.GetState();
var keyboard = Keyboard.GetState();
io.AddMousePosEvent(mouse.X, mouse.Y);
io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed);
io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed);
io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed);
io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed);
io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed);
io.AddMouseWheelEvent(
(mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA,
(mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA);
_scrollWheelValue = mouse.ScrollWheelValue;
_horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue;
foreach (var key in _allKeys)
{
if (TryMapKeys(key, out ImGuiKey imguikey))
{
io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key));
}
}
io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f);
}
private bool TryMapKeys(Keys key, out ImGuiKey imguikey)
{
//Special case not handed in the switch...
//If the actual key we put in is "None", return none and true.
//otherwise, return none and false.
if (key == Keys.None)
{
imguikey = ImGuiKey.None;
return true;
}
imguikey = key switch
{
Keys.Back => ImGuiKey.Backspace,
Keys.Tab => ImGuiKey.Tab,
Keys.Enter => ImGuiKey.Enter,
Keys.CapsLock => ImGuiKey.CapsLock,
Keys.Escape => ImGuiKey.Escape,
Keys.Space => ImGuiKey.Space,
Keys.PageUp => ImGuiKey.PageUp,
Keys.PageDown => ImGuiKey.PageDown,
Keys.End => ImGuiKey.End,
Keys.Home => ImGuiKey.Home,
Keys.Left => ImGuiKey.LeftArrow,
Keys.Right => ImGuiKey.RightArrow,
Keys.Up => ImGuiKey.UpArrow,
Keys.Down => ImGuiKey.DownArrow,
Keys.PrintScreen => ImGuiKey.PrintScreen,
Keys.Insert => ImGuiKey.Insert,
Keys.Delete => ImGuiKey.Delete,
//>= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0),
>= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A),
>= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0),
Keys.Multiply => ImGuiKey.KeypadMultiply,
Keys.Add => ImGuiKey.KeypadAdd,
Keys.Subtract => ImGuiKey.KeypadSubtract,
Keys.Decimal => ImGuiKey.KeypadDecimal,
Keys.Divide => ImGuiKey.KeypadDivide,
>= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1),
Keys.NumLock => ImGuiKey.NumLock,
Keys.Scroll => ImGuiKey.ScrollLock,
Keys.LeftShift => ImGuiKey.ModShift,
Keys.LeftControl => ImGuiKey.ModCtrl,
Keys.LeftAlt => ImGuiKey.ModAlt,
Keys.OemSemicolon => ImGuiKey.Semicolon,
Keys.OemPlus => ImGuiKey.Equal,
Keys.OemComma => ImGuiKey.Comma,
Keys.OemMinus => ImGuiKey.Minus,
Keys.OemPeriod => ImGuiKey.Period,
Keys.OemQuestion => ImGuiKey.Slash,
Keys.OemTilde => ImGuiKey.GraveAccent,
Keys.OemOpenBrackets => ImGuiKey.LeftBracket,
Keys.OemCloseBrackets => ImGuiKey.RightBracket,
Keys.OemPipe => ImGuiKey.Backslash,
Keys.OemQuotes => ImGuiKey.Apostrophe,
Keys.BrowserBack => ImGuiKey.AppBack,
Keys.BrowserForward => ImGuiKey.AppForward,
_ => ImGuiKey.None,
};
return imguikey != ImGuiKey.None;
}
#endregion Setup & Update
#region Internals
/// <summary>
/// Gets the geometry as set up by ImGui and sends it to the graphics device
/// </summary>
private void RenderDrawData(ImDrawDataPtr drawData)
{
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers
var lastViewport = _graphicsDevice.Viewport;
var lastScissorBox = _graphicsDevice.ScissorRectangle;
var lastRasterizer = _graphicsDevice.RasterizerState;
var lastDepthStencil = _graphicsDevice.DepthStencilState;
var lastBlendFactor = _graphicsDevice.BlendFactor;
var lastBlendState = _graphicsDevice.BlendState;
_graphicsDevice.BlendFactor = Color.White;
_graphicsDevice.BlendState = BlendState.NonPremultiplied;
_graphicsDevice.RasterizerState = _rasterizerState;
_graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
// Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays)
drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale);
// Setup projection
_graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
UpdateBuffers(drawData);
RenderCommandLists(drawData);
// Restore modified state
_graphicsDevice.Viewport = lastViewport;
_graphicsDevice.ScissorRectangle = lastScissorBox;
_graphicsDevice.RasterizerState = lastRasterizer;
_graphicsDevice.DepthStencilState = lastDepthStencil;
_graphicsDevice.BlendState = lastBlendState;
_graphicsDevice.BlendFactor = lastBlendFactor;
}
private unsafe void UpdateBuffers(ImDrawDataPtr drawData)
{
if (drawData.TotalVtxCount == 0)
{
return;
}
// Expand buffers if we need more room
if (drawData.TotalVtxCount > _vertexBufferSize)
{
_vertexBuffer?.Dispose();
_vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f);
_vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None);
_vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size];
}
if (drawData.TotalIdxCount > _indexBufferSize)
{
_indexBuffer?.Dispose();
_indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f);
_indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None);
_indexData = new byte[_indexBufferSize * sizeof(ushort)];
}
// Copy ImGui's vertices and indices to a set of managed byte arrays
int vtxOffset = 0;
int idxOffset = 0;
for (int n = 0; n < drawData.CmdListsCount; n++)
{
ImDrawListPtr cmdList = drawData.CmdLists[n];
fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size])
fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)])
{
Buffer.MemoryCopy(cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size);
Buffer.MemoryCopy(cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort));
}
vtxOffset += cmdList.VtxBuffer.Size;
idxOffset += cmdList.IdxBuffer.Size;
}
// Copy the managed byte arrays to the gpu vertex- and index buffers
_vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size);
_indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort));
}
private unsafe void RenderCommandLists(ImDrawDataPtr drawData)
{
_graphicsDevice.SetVertexBuffer(_vertexBuffer);
_graphicsDevice.Indices = _indexBuffer;
int vtxOffset = 0;
int idxOffset = 0;
for (int n = 0; n < drawData.CmdListsCount; n++)
{
ImDrawListPtr cmdList = drawData.CmdLists[n];
for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++)
{
ImDrawCmd drawCmd = cmdList.CmdBuffer[cmdi];
if (drawCmd.ElemCount == 0)
{
continue;
}
if (!_loadedTextures.ContainsKey(drawCmd.TextureId))
{
throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings");
}
_graphicsDevice.ScissorRectangle = new Rectangle(
(int)drawCmd.ClipRect.X,
(int)drawCmd.ClipRect.Y,
(int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X),
(int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y)
);
var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]);
foreach (var pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
#pragma warning disable CS0618 // // FNA does not expose an alternative method.
_graphicsDevice.DrawIndexedPrimitives(
primitiveType: PrimitiveType.TriangleList,
baseVertex: (int)drawCmd.VtxOffset + vtxOffset,
minVertexIndex: 0,
numVertices: cmdList.VtxBuffer.Size,
startIndex: (int)drawCmd.IdxOffset + idxOffset,
primitiveCount: (int)drawCmd.ElemCount / 3
);
#pragma warning restore CS0618
}
}
vtxOffset += cmdList.VtxBuffer.Size;
idxOffset += cmdList.IdxBuffer.Size;
}
}
#endregion Internals
} Also there is one key I couldn't find which is In the protected override void Initialize()
{
// TODO: Add your initialization logic here
_imGuiRenderer = new ImGuiRenderer(this);
_imGuiRenderer.RebuildFontAtlas();
base.Initialize();
} And in the _imGuiRenderer.BeforeLayout(gameTime);
ImGui.ShowDemoWindow();
_imGuiRenderer.AfterLayout(); Or the one using internal device and hwnd of monogame? If it is the latest one, I don't have it yet. |
I didn't realize the ImGui.NET renderer would just sort of port straight over. I was thinking there was more work for the backend. Regardless, thanks! |
Hello, thanks you for your great bindings.
I managed to pull out an implementation of ImGuiRenderer (based on the "official" ImGuiRenderer from ImGui.NET)
I'm nowhere near an expert on this topic, but it might be interesting to maybe add it to your examples ?
I might drop the code here if anyone interested in making a pull request and/or optimize it.
Sadly, config flag for viewports enable doesn't seem to work, but dockspace yes.
The text was updated successfully, but these errors were encountered: