Dear ImGui is a user interface library which can use several backends such as OpenGL and Vulkan. We will use it to display gui controls or to develop HUDs. It provides multiple widgets and the look and fell is easily customizable.
You can find the complete source code for this chapter here.
Imgui integration
The first thing is adding Java Imgui wrapper maven dependencies to the project pom.xml. We need to add compile time and runtime dependencies.
With Imgui we can render windows, panels, etc. like we render any other 3D model, but using only 2D shapes. We set the controls that we want to use and Imgui translates that to a set of vertex buffers that we can render using shaders. This is why it can be used with any backend.
For each vertex, Imgui defines its coordinates (2D coordinates), texture coordinates and the associated color. Therefore, we need to create a new class to model Gui meshes and to create the associated VAO and VBO. The class, named GuiMesh is defined like this.
As you can see, we use a single VBO but we define several attributes for the positions, texture coordinates and color. In this case, we do not populate the buffers with data, we will see later on how we will use it.
We need also to let the application create GUI controls and react to the user input. In order to support this, we will define a new interface named IGuiInstance which is defined like this:
The method drawGui will be used to construct the GUI, this where we will define the window and widgets that will be used to construct the GUI meshes. We will use the handleGuiInput method to process input events in the GUI. It returns a boolean value to state that the input has been processed by the GUI or not. For example, if we display an overlapping window we may not be interested in keep processing keystrokes in the game logic. You can use the return value to control that. We will store the specific implementation of IGuiInstance interface in the Scene class.
As you can see, most of the stuff here will be very familiar to you, we just set up the shaders and the uniforms. Since we will need to set up a custom key callback to handle ImGui input text controls, we need to keep track of a previous key callback in prevKeyCallBack to properly use it and free it. In addition to that, there is a new method called createUIResources which is defined like this:
In the method above is where we setup Imgui, we first create a context (required to perform any operation), and set up the display size to the window size. Imgui stores the status in an ini file, since we do not want the status to persist between runs we need to set it to null. The next step is to initialize the font atlas and set up a texture which will be used in the shaders so we can render properly texts, etc. The final step is to create the GuiMesh instance.
The createUniforms just creates a single two float for the scale (we will see later on how it will be used).
First we need to setup a GLFW key callback which first calls Window key call back to handle key events and translate GFLW key code sto Imgui ones. When setting a callback we obtain a reference to a previously established one so we can chain them. In this case we will invoke it if the key event is not handled by ImGui. We are not using char callbacks in other parts of the code, but if you do, remember to apply that chain schema also. After that, we set up the state of Imgui according to key pressed or released events. Finally, we need to setup a char call back so text input widgets can process those events.
The first thing that we do is to check if we have set up an implementation of the IGuiInstance interface. If there is no instance, we just return, there is no need to render anything. After that we call the drawGui method. That is, in each render call we invoke that method so the Imgui can update its status to be able to generate the proper vertex data. After binding the shader we first enable blending which will allow us to use transparencies. Just by enabling blending, transparencies still will not show up. We need also to instruct OpenGL about how the blending will be applied. This is done through the glBlendFunc function. You can check an excellent explanation about the details of the different functions that can be applied here.
After that, we need to disable depth testing and face culling for Imgui to work properly. Then, we bind the gui mesh which defines the structure of the data and bind the data and indices buffers. Imgui uses screen coordinates to generate the vertices data, that is x values cover the [0, screen width] range and y values cover the [0, screen height]. We will use the scale uniform to map from that coordinate system to the [-1, 1] range of OpenGL's clip space.
After that, we retrieve the data generated by Imgui to render the GUI. Imgui first organizes the data in what they call command lists. Each command list has a buffer where it stores the vertex and indices data, so we first dump data to the GPU by calling the glBufferData. Each command list defines also a set of commands which we will use to generate the draw calls. Each command stores the number of elements to be drawn and the offset to be applied to the buffer in the command list. When we have drawn all the elements we can re-enable the depth test.
Finally, we need to add a resize method which will be called any time the window is resized to adjust Imgui display size.
The vertex shader used for rendering the GUI is quite simple (gui.vert), we just transform the coordinates so they are in the [-1, 1] range and output the texture coordinates and color so they can be used in the fragment shader:
We also need to modify the Engine class to include IGuiInstance in the update loop and to use its return value to indicate if input has been consumed or not.
In the drawGui method we just setup a new frame, the window position and just invoke the showDemoWindow to generate Imgui's demo window. After ending the frame it is very important to call the render this is what will generate the set of commands upon the GUI structure defined previously. The handleGuiInput first gets mouse position and updates Imgui's IO class with that information and mouse button status. We also return a boolean that indicates that input has been capture by Imgui. Finally, we just need to update the input method to receive that flag. In this specific case, if input has already been consumed by the Gui, we just return.
With all those changes you will be able to see Imgui demo window overlapping the rotating cube. You can interact with the different methods and panels to get a glimpse of the capabilities of Imgui.