-
Notifications
You must be signed in to change notification settings - Fork 36
ImGui
Fusee implements Dear ImGui.NET
via NuGet.
⚠️ Currently onlyDesktop
is supported.
For a documentation and/or examples of Dear ImGui
visit:
-
General documentation/intro
: https://github.com/ocornut/imgui#readme -
Dear ImGui Wiki
: https://github.com/ocornut/imgui/wiki -
Dear ImGui FAQ
: https://github.com/ocornut/imgui/blob/master/docs/FAQ.md -
Intermediate Mode paradigm
: https://github.com/ocornut/imgui/wiki/About-the-IMGUI-paradigm -
Using Begin() and BeginChild()
: https://github.com/ocornut/imgui/wiki/Tips#using-beginbeginchild -
CPP demo application
: https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp -
Simple example inside this readme
: Simple example
Note: Dear ImGui
is written in and for C++. However, due to the direct translation of ImGui.NET
all functions can be called with the exact naming as the C++ examples/code snippets. Replace all ImGui::
namespace calls with ImGui.
static class calls. All enum calls to ImGuiStyleVar_...
with ImGuiStyleVar.
enums, etc. For the data types, replace ImGui::Vec2
et al. with System.Numerics.Vector2/3/4/<T>
.
Another very good resource for learning is Dear ImGui
's GitHub issue tracker: https://github.com/ocornut/imgui/issues
i) Generate a new Desktop
project. Replace the RenderCanvasImp
and InputDriverImp
with the ImGui
flavored onces:
app.CanvasImplementor = new Fusee.ImGuiDesktop.ImGuiRenderCanvasImp(icon);
app.ContextImplementor = new Fusee.Engine.Imp.Graphics.Desktop.RenderContextImp(app.CanvasImplementor); // careful RCI stays the desktop variant
Input.AddDriverImp(new Fusee.ImGuiDesktop.ImGuiInputImp(app.CanvasImplementor));
ii) Generate a Core
file and a FuseeControl
Generate a new RenderCanvas
ImGuiCore
file. This is our main render loop. Within this class the ImGui
is being rendered. The Fusee
window however, is being rendered to a WritableTexture
and displayed as an image inside an ImGui.Image()
. This is achieved by generating another class which inherits from ImGuiDesktop.Templates.FuseeControlToTexture
and implements a render loop that renders to a texture and returns the IntPtr
from this texture for usage with ImGui.Image()
. The render loop inside this FuseeControl
is triggered from the ImGuiCore
file (see diagram and example code below). Attention: one must override the Resize()
method in ImGuiCore
and pass the changed width and height values to the FuseeControl
. Otherwise the window isn't resized properly after rendering everything inside FuseeControl
!
[FuseeApplication(Name = "FUSEE ImGui Example",
Description = "A very simple example how to use ImGui within a Fusee application.")]
public class ImGuiCore : RenderCanvas
{
// check if mouse is inside FuseeControl, if not -> prevent input
private static bool _isMouseInsideFuControl;
private FuseeControl _fuControl;
private async void Load()
{
// generate FuseeControl instance which renders to texture
_fuControl = new FuseeControl(RC);
_fuControl.Init();
}
public override async Task InitAsync()
{
Load();
await base.InitAsync();
}
public override void Update()
{
// update FuseeControl
_fuControl.Update(_isMouseInsideFuControl);
}
public override void Resize(ResizeEventArgs e)
{
// Resize event, must be set!
_fuControl.UpdateOriginalGameWindowDimensions(e.Width, e.Height);
}
public override async void RenderAFrame()
{
// new window
ImGui.Begin("FuseeWindow");
// get size of current window
var fuseeViewportSize = ImGui.GetWindowSize();
// get IntPtr to texture
var textureWithFuseeContent = _fuControl.RenderToTexture((int)size.X, (int)size.Y);
// draw image with size of current window, adapt uv coordinates to fit Fusee's OpenGL viewport
ImGui.Image(textureWithFuseeContent, fuseeViewportSize,
new Vector2(0, 1),
new Vector2(1, 0));
// check if mouse is inside window, if true, accept update() inputs
_isMouseInsideFuControl = ImGui.IsItemHovered();
ImGui.End();
}
}
This looks and feels like an "usual" Fusee
application with the exception, that we do render inside a RenderTexture
via a Camera
and return the IntPtr
to the WritableTexture
in the RenderAFrame()
method.
internal class FuseeControl : ImGuiDesktop.Templates.FuseeControlToTexture, IDisposable
{
private SceneContainer _rocketScene;
private SceneRendererForward _renderer;
private WritableTexture _renderTexture;
private Transform _camPivotTransform;
public int Width;
public int Height;
private const float RotationSpeed = 7;
private const float Damping = 0.8f;
// angle variables
private static float _angleHorz, _angleVert, _angleVelHorz, _angleVelVert;
private const float ZNear = 1f;
private const float ZFar = 1000;
private readonly float _fovy = M.PiOver4;
private Camera _cam;
private bool disposedValue;
public CoreControl(RenderContext ctx) : base(ctx)
{
_rc = ctx;
}
public override void Init()
{
_rocketScene = AssetStorage.Get<SceneContainer>("RocketFus.fus");
_camPivotTransform = new Transform();
_cam = new Camera(ProjectionMethod.Perspective, ZNear, ZFar, _fovy) { BackgroundColor = new float4(0, 0, 0, 0) };
var camNode = new SceneNode()
{
Name = "CamPivoteNode",
Children = new ChildList()
{
new SceneNode()
{
Name = "MainCam",
Components = new List<SceneComponent>()
{
new Transform() { Translation = new float3(0, 2, -10) },
_cam
}
}
},
Components = new List<SceneComponent>()
{
_camPivotTransform
}
};
_rocketScene.Children.Add(camNode);
_renderer = new SceneRendererForward(_rocketScene);
}
// check if mouse is inside FuseeControl (done and passed by ImGuiControl), if not, prevent any input
public override void Update(bool allowInput)
{
if (!allowInput)
{
_angleVelHorz = 0;
_angleVelVert = 0;
return;
}
if (Input.Mouse.LeftButton)
{
_angleVelHorz = RotationSpeed * Input.Mouse.XVel * Time.DeltaTimeUpdate * 0.0005f;
_angleVelVert = RotationSpeed * Input.Mouse.YVel * Time.DeltaTimeUpdate * 0.0005f;
}
else
{
var curDamp = (float)System.Math.Exp(-Damping * Time.DeltaTimeUpdate);
_angleVelHorz *= curDamp;
_angleVelVert *= curDamp;
}
_angleHorz += _angleVelHorz;
_angleVert += _angleVelVert;
}
// render to RenderTexture and return the `TextureHandle`
protected override ITextureHandle RenderAFrame()
{
_camPivotTransform.RotationQuaternion = QuaternionF.FromEuler(_angleVert, _angleHorz, 0);
_renderer.Render(_rc);
return _renderTexture.TextureHandle;
}
// re-create RenderTexture on each resize
protected override void Resize(int width, int height)
{
if (width <= 0 || height <= 0)
return;
Width = width;
Height = height;
_renderTexture?.Dispose();
_renderTexture = WritableTexture.CreateAlbedoTex(_rc, Width, Height);
// attach RenderTexture to camera, everything the camera sees goes into this texture
// which is being returned as an IntPtr to the data inside the RenderAFrame() method
_cam.RenderTexture = _renderTexture;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_renderTexture.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
How to start.
ImGui
works like a state machine: "Push and pop settings, generate commands and execute them, draw inside a window until the window is finished, etc.".
void Render()
{
// ImGui needs to have at least one window / panel in which elements can be renderer
// When no window is present, a default Debug window is being created
// in this example we generate a new window with ImGui.Begin()
// The way to specify a window size is by setting the size beforehand
// Set size of the next window
ImGui.SetNextWindowSize(new Vector2(200, 200));
// Generate a new window, title is "Title"
ImGui.Begin("Title");
// everything that follows is drawn in this window
// Generate an input field which expects an int value
// The value is being passed and updated via a reference to the actual value
// Therefore, do not generate any variables inside the main loop, as they are being overwritten every frame
// and we can't use them. Just generate a private attribute "_myIntValue" inside your class.
// The initial value is also the initial default value. This is important for e. g. a selection dropdown menu.
// ImGui references the index inside an array. The initial value inside the index specifies the default selected menu item
ImGui.InputInt("Enter an int here", ref _myIntValue);
// Generate a new Button
// If the button is being clicked the method returns true for a single frame.
// Therefore, if this button shouldn't trigger a one time action but a state, one needs to set all state variables by themselves
if(ImGui.Button("Click me!"))
{
Console.WriteLine("Someone clicked me!");
_buttonClickedState = !_buttonClickedState;
}
// For any style changes for any element one has to use the built in stack
// Push changes to it, pop styles from it, if not longer needed
// Color all following text elements red
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 0, 0, 1).ToUintColor());
ImGui.Text("Red text");
// Stop coloring text elements
// The parameter defines how many elements are being popped
// Do not pop more than available -> exception
ImGui.PopStyleColor(1);
// Same as style color. However be careful, the PushStyleVar methods accepts an "object" as the second parameter
// WindowRounding for example expects a float value. Other ImGuiStyleVars expect a Vector2, an array or something else.
// If one pushes the wrong datatype the best outcome is no visible changes, the worst is an exception while setting or, worse, after popping
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 2f);
ImGui.PopStyleVar();
// For placing elements on the same line, use SameLine()
// One can pass a x-offset to this method for spacing between the elements
ImGui.Text("This text is ");
ImGui.SameLine();
ImGui.Text("besides this text");
// Add a new line
ImGui.NewLine();
// This is dropdown selection, the default value is specified by the default value of the index ref variable (as described above)
// After user input one needs to process the index which is best be done by using the new switch-case syntax
// Method parameters are as follows: Name, index, array with values which can be selected, size of array (needed because of cpp invocation)
ImGui.Combo("Combo Selection Box", ref _comboSelectionIdx, new string[] { "Item1", "Item2", "Item3" }, 3);
// just an example, no production code ;)
var selectedString = _comboSelectionIdx switch
{
0 => "Item1",
1 => "Item2",
2 => "Item2",
_ => "<null>",
};
ImGui.Text("EOF");
// For raw geometric figures use the following method
// Add a filled circle with radius 2 and pink color to the current window only.
// There also exist methods for foreground, background, ... -drawing via GetForegroundDrawList(), etc.
var windowPos = ImGui.GetWindowPos();
ImGui.GetWindowDrawList().AddCircleFilled(windowPos, 2, new Vector4(1, 0, 1, 1).ToUintColor());
// Finish current window
ImGui.End();
}
// variables used inside Render()
private int _myIntValue;
private bool _buttonClickedState;
private int _comboSelectionIdx = 2; // set "Item3" as default
For a file selection one can use the rather wonky and undocumented Fusee.ImGuiDesktop.Templates.ImGuiFileDialog
. However, if the platform is windows and runtime and size is no issue, add <UseWpf>true</UseWpf>
to your project file.
This enables WPF
and one can use the simple OpenFileDialog
inside an ImGui
project:
// Todo: pin to *.txt extension, add all checks, etc.
OpenFileDialog openFileDialog = new();
// example code, do not call this construct every frame!
if(openFileDialog.ShowDialog())
ImGui.TextWrapped(File.ReadAllText(openFileDialog.FileName));
Besides the usual WritableTexture
a user can utilize the class WritableMultisampleTexture
which enables anti aliasing for the texture rendering and generates smooth edges.
Just change the following lines inside the example above:
WritableMultisampleTexture _renderTexture;
_renderTexture = WritableMultisampleTexture.CreateAlbedoTex(_rc, Width, Height, 8); // pass samplingFactor [1, 8]
For multiple cameras define them as usually, however, set each Camera
's RenderTarget
to the same RenderTexture
which renders multiple times inside the same texture.
_cam1.Viewport(0, 0, 50, 50);
_cam2.Viewport(0, 50, 25, 25);
_cam3.Viewport(12, 1, 60, 24);
// _renderTexture is always as big as the whole window, therefore viewport works as expected
_cam1.RenderTexture = _renderTexture;
_cam2.RenderTexture = _renderTexture;
_cam3.RenderTexture = _renderTexture;
Unfortunately Fusee
's ImGui
implementation is unable to work directly with our internal IImageData
but only with IntPtr
s to already bound and uploaded (to the GPU) textures. Therefore one needs to use the wrapper ExposedTexture
. This wrapper exposes the internal handle to the bound texture. Hence the name. Use as any Texture
, however, do not forget to call RC.RegisterTexture()
after loading, which registers the ExposedTexture
to the RenderContext
. This is necessary as we do not render this Texture
via our usual SceneRenderer
procedure.
// Inside ImGuiControl.cs
private ExposedTexture _imageTexture;
private static Vector2 _imgUv1 = new(0, 1);
private static Vector2 _imgUv2 = new(1, 0);
public async void Load()
{
var img = await AssetStorage.GetAsync<ImageData>("FuseeIconTop32.png");
_imageTexture = new ExposedTexture(img);
// register texture to the RenderContext, do not neglect, otherwise no image :)
RC.RegisterTexture(_imageTexture);
}
public void RenderAFrame()
{
ImGui.Begin("WindowWithImage");
var hndl = ((TextureHandle)_imageTexture.TextureHandle).TexHandle;
//Note: by default images will be upside down. Pass uv coodinates to fix this.
ImGui.Image(new IntPtr(hndl), new Vector2(_imageTexture.Width, _imageTexture.Height), _imgUv1 , _imgUv2);
ImGui.End();
}
Currently there is no easy way to change the font. It is set inside the class Fusee.ImGuiDesktop.ImGuiRenderCanvasImp
from the DoInit()
method. Change the path to the desired font and font size there.
To use two or more fonts one currently has to combine them. For a documentation visit: https://github.com/ocornut/imgui/blob/master/docs/FONTS.md#font-loading-instructions
Dear ImGui.NET
is implemented inside the Fusee.ImGuiDesktop
project. It utilizes an ImGuiController
class which in itself has a render loop with custom OpenGL
commands. All we get from the Dear ImGui.NET
implementation are DrawData
arrays with vertices and triangles representing the current state of the 2D GUI
. All the rendering, state-setting, shader binding, etc. needs to be done by hand. On the one hand we use the RenderContext
to present all our states and settings, on the other hand we have this low level implementation which sets state, too. Be very careful when changing anything inside the ImGuiController
as it quickly destroys states or assumptions inside the RenderContext
. This leads directly to the next chapter.
Inside the ImGuiController
a shader is set for rendering ImGui
. As this isn't done via our RenderContext
the dirty flag for a new shader isn't being set, and the RenderContext
thinks the old ShaderProgram
is still bound and ready to go. As this isn't the case, the ImGuiController
needs to notify the RenderContext
that the ShaderProgram
has been changed from the outside. However, all the logic is encapsulated in internal
values. Therefore, we use the friend
-pattern (InternalsVisibleTo
) and let the ImGuiController
set the CurrentShaderProgram
of the RenderContext
. For this to work, the ImGuiDesktop
namespace needs to have access to the CurrentShaderProgram
variable inside RenderContext
as well as the Handle
itself inside Desktop.ShaderHandleImp
.
Tell the RenderContext
that our current shader program has changed.
if (prgmHndl == null)
prgmHndl = new ShaderHandleImp() { Handle = ImGuiController.ShaderProgram };
_rc.CurrentShaderProgram = prgmHndl;
- Using FUSEE
- Tutorials
- Examples
- In-Depth Topics
- Input and Input Devices
- The Rendering Pipeline
- Render Layer
- Camera
- Textures
- FUSEE Exporter Blender Add on
- Assets
- Lighting & Materials
- Serialization and protobuf-net
- ImGui
- Blazor/WebAssembly
- Miscellaneous
- Developing FUSEE