ImGuiRenderer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. using Microsoft.Xna.Framework;
  2. using Microsoft.Xna.Framework.Graphics;
  3. using Microsoft.Xna.Framework.Input;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Runtime.InteropServices;
  7. using ImGuiNET;
  8. namespace OpenVIII.Core
  9. {
  10. /// <summary>
  11. /// ImGui renderer for use with XNA-likes (FNA & MonoGame)
  12. /// </summary>
  13. public class ImGuiRenderer
  14. {
  15. private Game _game;
  16. // Graphics
  17. private GraphicsDevice _graphicsDevice;
  18. private BasicEffect _effect;
  19. private RasterizerState _rasterizerState;
  20. private byte[] _vertexData;
  21. private VertexBuffer _vertexBuffer;
  22. private int _vertexBufferSize;
  23. private byte[] _indexData;
  24. private IndexBuffer _indexBuffer;
  25. private int _indexBufferSize;
  26. // Textures
  27. private Dictionary<IntPtr, Texture2D> _loadedTextures;
  28. private int _textureId;
  29. private IntPtr? _fontTextureId;
  30. // Input
  31. private int _scrollWheelValue;
  32. private List<int> _keys = new List<int>();
  33. public ImGuiRenderer(Game game)
  34. {
  35. var context = ImGui.CreateContext();
  36. ImGui.SetCurrentContext(context);
  37. _game = game ?? throw new ArgumentNullException(nameof(game));
  38. _graphicsDevice = game.GraphicsDevice;
  39. _loadedTextures = new Dictionary<IntPtr, Texture2D>();
  40. _rasterizerState = new RasterizerState()
  41. {
  42. CullMode = CullMode.None,
  43. DepthBias = 0,
  44. FillMode = FillMode.Solid,
  45. MultiSampleAntiAlias = false,
  46. ScissorTestEnable = true,
  47. SlopeScaleDepthBias = 0
  48. };
  49. SetupInput();
  50. }
  51. #region ImGuiRenderer
  52. /// <summary>
  53. /// 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
  54. /// </summary>
  55. public virtual unsafe void RebuildFontAtlas()
  56. {
  57. // Get font texture from ImGui
  58. var io = ImGui.GetIO();
  59. io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out var width, out var height, out var bytesPerPixel);
  60. // Copy the data to a managed array
  61. var pixels = new byte[width * height * bytesPerPixel];
  62. unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); }
  63. // Create and register the texture as an XNA texture
  64. var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color);
  65. tex2d.SetData(pixels);
  66. // Should a texture already have been build previously, unbind it first so it can be deallocated
  67. if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value);
  68. // Bind the new texture to an ImGui-friendly ID
  69. _fontTextureId = BindTexture(tex2d);
  70. // Let ImGui know where to find the texture
  71. io.Fonts.SetTexID(_fontTextureId.Value);
  72. io.Fonts.ClearTexData(); // Clears CPU side texture data
  73. }
  74. /// <summary>
  75. /// 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
  76. /// </summary>
  77. public virtual IntPtr BindTexture(Texture2D texture)
  78. {
  79. var id = new IntPtr(_textureId++);
  80. _loadedTextures.Add(id, texture);
  81. return id;
  82. }
  83. /// <summary>
  84. /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated
  85. /// </summary>
  86. public virtual void UnbindTexture(IntPtr textureId)
  87. {
  88. _loadedTextures.Remove(textureId);
  89. }
  90. /// <summary>
  91. /// Sets up ImGui for a new frame, should be called at frame start
  92. /// </summary>
  93. public virtual void BeforeLayout(GameTime gameTime)
  94. {
  95. ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
  96. UpdateInput();
  97. ImGui.NewFrame();
  98. }
  99. /// <summary>
  100. /// 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
  101. /// </summary>
  102. public virtual void AfterLayout()
  103. {
  104. ImGui.Render();
  105. unsafe { RenderDrawData(ImGui.GetDrawData()); }
  106. }
  107. #endregion ImGuiRenderer
  108. #region Setup & Update
  109. /// <summary>
  110. /// Maps ImGui keys to XNA keys. We use this later on to tell ImGui what keys were pressed
  111. /// </summary>
  112. protected virtual void SetupInput()
  113. {
  114. var io = ImGui.GetIO();
  115. _keys.Add(io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab);
  116. _keys.Add(io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left);
  117. _keys.Add(io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right);
  118. _keys.Add(io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up);
  119. _keys.Add(io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down);
  120. _keys.Add(io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp);
  121. _keys.Add(io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown);
  122. _keys.Add(io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home);
  123. _keys.Add(io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End);
  124. _keys.Add(io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete);
  125. _keys.Add(io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Back);
  126. _keys.Add(io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter);
  127. _keys.Add(io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape);
  128. _keys.Add(io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A);
  129. _keys.Add(io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C);
  130. _keys.Add(io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V);
  131. _keys.Add(io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X);
  132. _keys.Add(io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y);
  133. _keys.Add(io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z);
  134. // MonoGame-specific //////////////////////
  135. _game.Window.TextInput += (s, a) =>
  136. {
  137. if (a.Character == '\t') return;
  138. io.AddInputCharacter(a.Character);
  139. };
  140. ///////////////////////////////////////////
  141. // FNA-specific ///////////////////////////
  142. //TextInputEXT.TextInput += c =>
  143. //{
  144. // if (c == '\t') return;
  145. // ImGui.GetIO().AddInputCharacter(c);
  146. //};
  147. ///////////////////////////////////////////
  148. ImGui.GetIO().Fonts.AddFontDefault();
  149. }
  150. /// <summary>
  151. /// Updates the <see cref="Effect" /> to the current matrices and texture
  152. /// </summary>
  153. protected virtual Effect UpdateEffect(Texture2D texture)
  154. {
  155. _effect = _effect ?? new BasicEffect(_graphicsDevice);
  156. var io = ImGui.GetIO();
  157. // MonoGame-specific //////////////////////
  158. var offset = .5f;
  159. ///////////////////////////////////////////
  160. // FNA-specific ///////////////////////////
  161. //var offset = 0f;
  162. ///////////////////////////////////////////
  163. _effect.World = Matrix.Identity;
  164. _effect.View = Matrix.Identity;
  165. _effect.Projection = Matrix.CreateOrthographicOffCenter(offset, io.DisplaySize.X + offset, io.DisplaySize.Y + offset, offset, -1f, 1f);
  166. _effect.TextureEnabled = true;
  167. _effect.Texture = texture;
  168. _effect.VertexColorEnabled = true;
  169. return _effect;
  170. }
  171. /// <summary>
  172. /// Sends XNA input state to ImGui
  173. /// </summary>
  174. protected virtual void UpdateInput()
  175. {
  176. var io = ImGui.GetIO();
  177. var mouse = Mouse.GetState();
  178. var keyboard = Keyboard.GetState();
  179. for (var i = 0; i < _keys.Count; i++)
  180. {
  181. io.KeysDown[_keys[i]] = keyboard.IsKeyDown((Keys)_keys[i]);
  182. }
  183. io.KeyShift = keyboard.IsKeyDown(Keys.LeftShift) || keyboard.IsKeyDown(Keys.RightShift);
  184. io.KeyCtrl = keyboard.IsKeyDown(Keys.LeftControl) || keyboard.IsKeyDown(Keys.RightControl);
  185. io.KeyAlt = keyboard.IsKeyDown(Keys.LeftAlt) || keyboard.IsKeyDown(Keys.RightAlt);
  186. io.KeySuper = keyboard.IsKeyDown(Keys.LeftWindows) || keyboard.IsKeyDown(Keys.RightWindows);
  187. io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
  188. io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f);
  189. io.MousePos = new System.Numerics.Vector2(mouse.X, mouse.Y);
  190. io.MouseDown[0] = mouse.LeftButton == ButtonState.Pressed;
  191. io.MouseDown[1] = mouse.RightButton == ButtonState.Pressed;
  192. io.MouseDown[2] = mouse.MiddleButton == ButtonState.Pressed;
  193. var scrollDelta = mouse.ScrollWheelValue - _scrollWheelValue;
  194. io.MouseWheel = scrollDelta > 0 ? 1 : scrollDelta < 0 ? -1 : 0;
  195. _scrollWheelValue = mouse.ScrollWheelValue;
  196. }
  197. #endregion Setup & Update
  198. #region Internals
  199. /// <summary>
  200. /// Gets the geometry as set up by ImGui and sends it to the graphics device
  201. /// </summary>
  202. private void RenderDrawData(ImDrawDataPtr drawData)
  203. {
  204. // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers
  205. var lastViewport = _graphicsDevice.Viewport;
  206. var lastScissorBox = _graphicsDevice.ScissorRectangle;
  207. _graphicsDevice.BlendFactor = Color.White;
  208. _graphicsDevice.BlendState = BlendState.NonPremultiplied;
  209. _graphicsDevice.RasterizerState = _rasterizerState;
  210. _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
  211. // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays)
  212. drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale);
  213. // Setup projection
  214. _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
  215. UpdateBuffers(drawData);
  216. RenderCommandLists(drawData);
  217. // Restore modified state
  218. _graphicsDevice.Viewport = lastViewport;
  219. _graphicsDevice.ScissorRectangle = lastScissorBox;
  220. }
  221. private unsafe void UpdateBuffers(ImDrawDataPtr drawData)
  222. {
  223. if (drawData.TotalVtxCount == 0)
  224. {
  225. return;
  226. }
  227. // Expand buffers if we need more room
  228. if (drawData.TotalVtxCount > _vertexBufferSize)
  229. {
  230. _vertexBuffer?.Dispose();
  231. _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f);
  232. _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None);
  233. _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size];
  234. }
  235. if (drawData.TotalIdxCount > _indexBufferSize)
  236. {
  237. _indexBuffer?.Dispose();
  238. _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f);
  239. _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None);
  240. _indexData = new byte[_indexBufferSize * sizeof(ushort)];
  241. }
  242. // Copy ImGui's vertices and indices to a set of managed byte arrays
  243. var vtxOffset = 0;
  244. var idxOffset = 0;
  245. for (var n = 0; n < drawData.CmdListsCount; n++)
  246. {
  247. var cmdList = drawData.CmdListsRange[n];
  248. fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size])
  249. fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)])
  250. {
  251. Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size);
  252. Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort));
  253. }
  254. vtxOffset += cmdList.VtxBuffer.Size;
  255. idxOffset += cmdList.IdxBuffer.Size;
  256. }
  257. // Copy the managed byte arrays to the gpu vertex- and index buffers
  258. _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size);
  259. _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort));
  260. }
  261. private unsafe void RenderCommandLists(ImDrawDataPtr drawData)
  262. {
  263. _graphicsDevice.SetVertexBuffer(_vertexBuffer);
  264. _graphicsDevice.Indices = _indexBuffer;
  265. var vtxOffset = 0;
  266. var idxOffset = 0;
  267. for (var n = 0; n < drawData.CmdListsCount; n++)
  268. {
  269. var cmdList = drawData.CmdListsRange[n];
  270. for (var cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++)
  271. {
  272. var drawCmd = cmdList.CmdBuffer[cmdi];
  273. if (!_loadedTextures.ContainsKey(drawCmd.TextureId))
  274. {
  275. throw new InvalidOperationException($"Could not find a texture with ID '{drawCmd.TextureId}', please check your bindings");
  276. }
  277. _graphicsDevice.ScissorRectangle = new Rectangle(
  278. (int)drawCmd.ClipRect.X,
  279. (int)drawCmd.ClipRect.Y,
  280. (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X),
  281. (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y)
  282. );
  283. var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]);
  284. foreach (var pass in effect.CurrentTechnique.Passes)
  285. {
  286. pass.Apply();
  287. #pragma warning disable CS0618 // // FNA does not expose an alternative method.
  288. _graphicsDevice.DrawIndexedPrimitives(
  289. primitiveType: PrimitiveType.TriangleList,
  290. baseVertex: vtxOffset,
  291. minVertexIndex: 0,
  292. numVertices: cmdList.VtxBuffer.Size,
  293. startIndex: idxOffset,
  294. primitiveCount: (int)drawCmd.ElemCount / 3
  295. );
  296. #pragma warning restore CS0618
  297. }
  298. idxOffset += (int)drawCmd.ElemCount;
  299. }
  300. vtxOffset += cmdList.VtxBuffer.Size;
  301. }
  302. }
  303. #endregion Internals
  304. }
  305. }