using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace ImGuiNET.SampleProgram.XNA { /// /// ImGui renderer for use with XNA-likes (FNA & MonoGame) /// 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 _loadedTextures; private int _textureId; private IntPtr? _fontTextureId; // Input private int _scrollWheelValue; private List _keys = new List(); public ImGuiRenderer(Game game) { _game = game ?? throw new ArgumentNullException(nameof(game)); _graphicsDevice = game.GraphicsDevice; _loadedTextures = new Dictionary(); _rasterizerState = new RasterizerState() { CullMode = CullMode.None, DepthBias = 0, FillMode = FillMode.Solid, MultiSampleAntiAlias = false, ScissorTestEnable = true, SlopeScaleDepthBias = 0 }; SetupInput(); } #region ImGuiRenderer /// /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done /// public virtual void RebuildFontAtlas() { // Get font texture from ImGui var io = ImGui.GetIO(); var texData = io.FontAtlas.GetTexDataAsRGBA32(); // Copy the data to a managed array var pixels = new byte[texData.Width * texData.Height * texData.BytesPerPixel]; unsafe { Marshal.Copy(new IntPtr(texData.Pixels), pixels, 0, pixels.Length); } // Create and register the texture as an XNA texture var tex2d = new Texture2D(_graphicsDevice, texData.Width, texData.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.FontAtlas.SetTexID(_fontTextureId.Value); io.FontAtlas.ClearTexData(); // Clears CPU side texture data } /// /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw /// public virtual IntPtr BindTexture(Texture2D texture) { var id = new IntPtr(_textureId++); _loadedTextures.Add(id, texture); return id; } /// /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated /// public virtual void UnbindTexture(IntPtr textureId) { _loadedTextures.Remove(textureId); } /// /// Sets up ImGui for a new frame, should be called at frame start /// public virtual void BeforeLayout(GameTime gameTime) { ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; UpdateInput(); ImGui.NewFrame(); } /// /// 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 /// public virtual void AfterLayout() { ImGui.Render(); unsafe { RenderDrawData(ImGui.GetDrawData()); } } #endregion ImGuiRenderer #region Setup & Update /// /// Maps ImGui keys to XNA keys. We use this later on to tell ImGui what keys were pressed /// protected virtual void SetupInput() { var io = ImGui.GetIO(); _keys.Add(io.KeyMap[GuiKey.Tab] = (int)Keys.Tab); _keys.Add(io.KeyMap[GuiKey.LeftArrow] = (int)Keys.Left); _keys.Add(io.KeyMap[GuiKey.RightArrow] = (int)Keys.Right); _keys.Add(io.KeyMap[GuiKey.UpArrow] = (int)Keys.Up); _keys.Add(io.KeyMap[GuiKey.DownArrow] = (int)Keys.Down); _keys.Add(io.KeyMap[GuiKey.PageUp] = (int)Keys.PageUp); _keys.Add(io.KeyMap[GuiKey.PageDown] = (int)Keys.PageDown); _keys.Add(io.KeyMap[GuiKey.Home] = (int)Keys.Home); _keys.Add(io.KeyMap[GuiKey.End] = (int)Keys.End); _keys.Add(io.KeyMap[GuiKey.Delete] = (int)Keys.Delete); _keys.Add(io.KeyMap[GuiKey.Backspace] = (int)Keys.Back); _keys.Add(io.KeyMap[GuiKey.Enter] = (int)Keys.Enter); _keys.Add(io.KeyMap[GuiKey.Escape] = (int)Keys.Escape); _keys.Add(io.KeyMap[GuiKey.A] = (int)Keys.A); _keys.Add(io.KeyMap[GuiKey.C] = (int)Keys.C); _keys.Add(io.KeyMap[GuiKey.V] = (int)Keys.V); _keys.Add(io.KeyMap[GuiKey.X] = (int)Keys.X); _keys.Add(io.KeyMap[GuiKey.Y] = (int)Keys.Y); _keys.Add(io.KeyMap[GuiKey.Z] = (int)Keys.Z); // MonoGame-specific ////////////////////// _game.Window.TextInput += (s, a) => { if (a.Character == '\t') return; ImGui.AddInputCharacter(a.Character); }; /////////////////////////////////////////// // FNA-specific /////////////////////////// //TextInputEXT.TextInput += c => //{ // if (c == '\t') return; // ImGui.AddInputCharacter(c); //}; /////////////////////////////////////////// ImGui.GetIO().FontAtlas.AddDefaultFont(); } /// /// Updates the to the current matrices and texture /// protected virtual Effect UpdateEffect(Texture2D texture) { _effect = _effect ?? new BasicEffect(_graphicsDevice); var io = ImGui.GetIO(); // MonoGame-specific ////////////////////// var offset = .5f; /////////////////////////////////////////// // FNA-specific /////////////////////////// //var offset = 0f; /////////////////////////////////////////// _effect.World = Matrix.Identity; _effect.View = Matrix.Identity; _effect.Projection = Matrix.CreateOrthographicOffCenter(offset, io.DisplaySize.X + offset, io.DisplaySize.Y + offset, offset, -1f, 1f); _effect.TextureEnabled = true; _effect.Texture = texture; _effect.VertexColorEnabled = true; return _effect; } /// /// Sends XNA input state to ImGui /// protected virtual void UpdateInput() { var io = ImGui.GetIO(); var mouse = Mouse.GetState(); var keyboard = Keyboard.GetState(); for (int i = 0; i < _keys.Count; i++) { io.KeysDown[_keys[i]] = keyboard.IsKeyDown((Keys)_keys[i]); } io.ShiftPressed = keyboard.IsKeyDown(Keys.LeftShift) || keyboard.IsKeyDown(Keys.RightShift); io.CtrlPressed = keyboard.IsKeyDown(Keys.LeftControl) || keyboard.IsKeyDown(Keys.RightControl); io.AltPressed = keyboard.IsKeyDown(Keys.LeftAlt) || keyboard.IsKeyDown(Keys.RightAlt); io.SuperPressed = keyboard.IsKeyDown(Keys.LeftWindows) || keyboard.IsKeyDown(Keys.RightWindows); io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); io.MousePosition = new System.Numerics.Vector2(mouse.X, mouse.Y); io.MouseDown[0] = mouse.LeftButton == ButtonState.Pressed; io.MouseDown[1] = mouse.RightButton == ButtonState.Pressed; io.MouseDown[2] = mouse.MiddleButton == ButtonState.Pressed; var scrollDelta = mouse.ScrollWheelValue - _scrollWheelValue; io.MouseWheel = scrollDelta > 0 ? 1 : scrollDelta < 0 ? -1 : 0; _scrollWheelValue = mouse.ScrollWheelValue; } #endregion Setup & Update #region Internals /// /// Gets the geometry as set up by ImGui and sends it to the graphics device /// private unsafe void RenderDrawData(DrawData* 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; _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) ImGui.ScaleClipRects(drawData, 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; } private unsafe void UpdateBuffers(DrawData* drawData) { // 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++) { var 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(DrawData* drawData) { _graphicsDevice.SetVertexBuffer(_vertexBuffer); _graphicsDevice.Indices = _indexBuffer; int vtxOffset = 0; int idxOffset = 0; for (int n = 0; n < drawData->CmdListsCount; n++) { var cmdList = drawData->CmdLists[n]; for (int cmdi = 0; cmdi < cmdList->CmdBuffer.Size; cmdi++) { var drawCmd = &(((DrawCmd*)cmdList->CmdBuffer.Data)[cmdi]); 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: vtxOffset, minVertexIndex: 0, numVertices: cmdList->VtxBuffer.Size, startIndex: idxOffset, primitiveCount: (int)drawCmd->ElemCount / 3 ); #pragma warning restore CS0618 } idxOffset += (int)drawCmd->ElemCount; } vtxOffset += cmdList->VtxBuffer.Size; } } #endregion Internals } }