From 5b29f8e3cc245fd0bd5d9effbe345175aeb49d97 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 12 Feb 2019 20:58:13 -0800 Subject: [PATCH] Visual Studio support add renderer to project --- getFNA.sh | 19 +- project_name.sln | 4 + project_name/ImGui/DrawVertDeclaration.cs | 28 ++ project_name/ImGui/ImGuiRenderer.cs | 387 ++++++++++++++++++++++ project_name/project_name.csproj | 8 +- 5 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 project_name/ImGui/DrawVertDeclaration.cs create mode 100644 project_name/ImGui/ImGuiRenderer.cs diff --git a/getFNA.sh b/getFNA.sh index c0a480b..ba85897 100755 --- a/getFNA.sh +++ b/getFNA.sh @@ -68,7 +68,9 @@ function downloadImGui() { checkGit echo "Downloading ImGui..." - git -C $MY_DIR clone https://github.com/mellinoe/ImGui.NET.git --recursive + echo "Temporarily using ImGui.NET branch until ImGui.NET master is updated" + #git -C $MY_DIR clone https://github.com/mellinoe/ImGui.NET.git --recursive + git -C $MY_DIR clone -b fix-MonoGame-FNA https://github.com/prime31/ImGui.NET.git --recursive if [ $? -eq 0 ]; then echo "Finished downloading!\n" else @@ -168,12 +170,7 @@ if [ ! -d "$MY_DIR/project_name" ]; then exit 1 fi -# copy over ImGui files before renaming the project -echo "Copying ImGui renderer to project..." -cp "$MY_DIR/ImGui.NET/src/ImGui.NET.SampleProgram.XNA/DrawVertDeclaration.cs" "$MY_DIR/project_name/ImGui" -cp "$MY_DIR/ImGui.NET/src/ImGui.NET.SampleProgram.XNA/ImGuiRenderer.cs" "$MY_DIR/project_name/ImGui" -sed -i '' "s/cimgui/cimgui.dylib/g" ImGui.NET/src/ImGui.NET/Generated/ImGuiNative.gen.cs - + read -p "Enter the project name to use for your folder and csproj file or 'exit' to quit: " newProjectName if [[ $newProjectName = 'exit' || -z "$newProjectName" ]]; then exit 1 @@ -202,4 +199,10 @@ cd Nez.FNA git submodule init git submodule update -printf "\n\nManually run the following command:\n\nnuget restore Nez.FNA/Nez/Nez.sln && msbuild Nez.FNA/Nez/Nez.sln && msbuild /t:restore $newProjectName\n\n" +command -v pbcopy > /dev/null 2>&1 +if [ ! $? -eq 0 ]; then + printf "\n\nManually run the following command:\n\nnuget restore Nez.FNA/Nez/Nez.sln && msbuild Nez.FNA/Nez/Nez.sln && msbuild /t:restore $newProjectName\n\n" +else + echo "nuget restore Nez.FNA/Nez/Nez.sln && msbuild Nez.FNA/Nez/Nez.sln && msbuild /t:restore $newProjectName" | pbcopy + echo "command copied to your clipboard\n" +fi diff --git a/project_name.sln b/project_name.sln index 7d9eda8..bf0682f 100644 --- a/project_name.sln +++ b/project_name.sln @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA", "FNA\FNA.csproj", "{3 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nez.FNA", "Nez.FNA\Nez.FNA\Nez.FNA.csproj", "{11A5855C-B12C-4F8D-B935-56F3D0B671C3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET", "ImGui.NET\src\ImGui.NET\ImGui.NET.csproj", "{AD548A1D-01B2-410E-B3ED-ADAC05C3C4A3}" +EndProject Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {8C576ECC-147D-4B4A-8EC1-56533D26A178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -47,6 +49,8 @@ Global {35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {11A5855C-B12C-4F8D-B935-56F3D0B671C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11A5855C-B12C-4F8D-B935-56F3D0B671C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD548A1D-01B2-410E-B3ED-ADAC05C3C4A3}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {AD548A1D-01B2-410E-B3ED-ADAC05C3C4A3}.Debug|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/project_name/ImGui/DrawVertDeclaration.cs b/project_name/ImGui/DrawVertDeclaration.cs new file mode 100644 index 0000000..276e5df --- /dev/null +++ b/project_name/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + 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) + ); + } + } +} \ No newline at end of file diff --git a/project_name/ImGui/ImGuiRenderer.cs b/project_name/ImGui/ImGuiRenderer.cs new file mode 100644 index 0000000..0c6886a --- /dev/null +++ b/project_name/ImGui/ImGuiRenderer.cs @@ -0,0 +1,387 @@ +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) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _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 unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int 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 + } + + /// + /// 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(float deltaTime) + { + ImGui.GetIO().DeltaTime = deltaTime; + 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[(int)ImGuiKey.Tab] = (int)Keys.Tab); + _keys.Add(io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left); + _keys.Add(io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right); + _keys.Add(io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up); + _keys.Add(io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down); + _keys.Add(io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp); + _keys.Add(io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown); + _keys.Add(io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home); + _keys.Add(io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End); + _keys.Add(io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete); + _keys.Add(io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Back); + _keys.Add(io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter); + _keys.Add(io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape); + _keys.Add(io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A); + _keys.Add(io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C); + _keys.Add(io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V); + _keys.Add(io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X); + _keys.Add(io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y); + _keys.Add(io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z); + + + // 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); + }; + /////////////////////////////////////////// + + ImGui.GetIO().Fonts.AddFontDefault(); + } + + /// + /// 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.KeyShift = keyboard.IsKeyDown(Keys.LeftShift) || keyboard.IsKeyDown(Keys.RightShift); + io.KeyCtrl = keyboard.IsKeyDown(Keys.LeftControl) || keyboard.IsKeyDown(Keys.RightControl); + io.KeyAlt = keyboard.IsKeyDown(Keys.LeftAlt) || keyboard.IsKeyDown(Keys.RightAlt); + io.KeySuper = 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.MousePos = 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 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; + + _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; + } + + 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.CmdListsRange[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)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.CmdListsRange[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[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 + } +} \ No newline at end of file diff --git a/project_name/project_name.csproj b/project_name/project_name.csproj index fc687f1..0cc174c 100644 --- a/project_name/project_name.csproj +++ b/project_name/project_name.csproj @@ -72,19 +72,19 @@ - osx\%(RecursiveDir)%(Filename)%(Extension) + %(RecursiveDir)%(Filename)%(Extension) PreserveNewest - osx\%(RecursiveDir)%(Filename)%(Extension) + %(RecursiveDir)%(Filename)%(Extension) PreserveNewest - osx\%(RecursiveDir)%(Filename)%(Extension) + %(RecursiveDir)%(Filename)%(Extension) PreserveNewest - osx\%(RecursiveDir)%(Filename)%(Extension) + %(RecursiveDir)%(Filename)%(Extension) PreserveNewest