[C++] Rendering An OpenGL Framebuffer Into A Dear ImGui Window
You can find the final result here on GitHub.
Once in a while I spend some time with OpenGL and computer graphics / game development. This became sort of a hobby. A while ago I came across Dear ImGui, a graphical user interface for C++. It allows you to create windows, menues, buttons, drop downs, etc, etc, ... So it's definitely worth to check this out if you need something like that in C++.
But it took me a fair amount of time, when it came down to custom render something into an ImGui window. Even though I found some StackOverflow posts, I didn't find a basic example which I can use to play around. But with reading documentation and patience I solved my problem.
I'm not an expert in computer graphics nor I'm a game developer. I provide this examples to the best of my knowledge and if you can help me to improve, I appreciate every comment.
So, in this article we'll create an OpenGL triangle, which then we put into an ImGui window. Please note, this is not an article about best practices, therefore I don't create classes or a certain structure. Also, I don't go into detail for the OpenGL stuff. If you're completely new to OpenGL I recommend an OpenGL beginner tutorial to get a basic understanding for it.
Thats being said, this is our plan, a simple triangle from a regular window on the left in a ImGui Window on the rigth.
The Triangle
For this article, I'll keep it simple. We start with the OpenGL hello world, a triangle. Pretty basic for now.
We have the vertices for the triangle, the shader (plus the compilation of the shader code) and the functions for the framebuffers in separate functions.
So first things first, our ID's and the shader code:
// our window dimensions
const GLuint WIDTH = 800;
const GLint HEIGHT = 600;
// global defined indices for OpenGL
GLuint VAO; // vertex array object
GLuint VBO; // vertex buffer object
GLuint FBO; // frame buffer object
GLuint RBO; // rendering buffer object
GLuint texture_id; // the texture id we'll need later to create a texture
// out of our framebuffer
GLuint shader; // the shader id
// first vertex shader code
const char* vertex_shader_code = R"*(
#version 330
layout (location = 0) in vec3 pos;
void main()
{
gl_Position = vec4(0.9*pos.x, 0.9*pos.y, 0.5*pos.z, 1.0);
}
)*";
// and fragment shader code
const char* fragment_shader_code = R"*(
#version 330
out vec4 color;
void main()
{
color = vec4(0.0, 1.0, 0.0, 1.0);
}
)*";
Now we continue with our main, the functions are implemented like in most OpenGL Tutorials. Check out the entire code on my GitHub repo.
void create_triangle()
{
// ... creates the vertex arrays and buffers ...
}
void add_shader(GLuint program, const char* shader_code, GLenum type)
{
// ... adds the actual shader to our program and compiles the shader code ...
}
void create_shaders()
{
// ... here we create our shaders ...
}
int main()
{
// ... glfw initialization
// ...
GLFWwindow *mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "My Window", NULL, NULL);
// ... glew initialization
// ...
// let's create the triangle and our shader
create_triangle();
create_shaders();
// and we can run our main loopt
while (!glfwWindowShouldClose(mainWindow))
{
// poll the events, set the clear cullor and
// run render our triangle
glfwPollEvents();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shader);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
// finally swap the buffers
glfwSwapBuffers(mainWindow);
}
// and cleanup, when we're done
glDeleteFramebuffers(1, &FBO);
glDeleteTextures(1, &texture_id);
glDeleteRenderbuffers(1, &RBO);
glfwDestroyWindow(mainWindow);
glfwTerminate();
return 0;
}
And we'll get this result, a green triangle, rendered in a basic window:
Dear ImGui
Now we'll add Dear ImGui. Therefore we also initialize Dear ImGui and we'll start with a basic Dear ImGui window:
int main()
{
// ... as before
// ImGui initialization, note: we use the docking branch
// this means we have the basic docking functionality as you have
// in a lot of other desktop applications
// and you can drag the ImGui Window out of the actual window
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// i (as always) prefer the dark mode
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable){
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// initialize ImGui's glfw/opengl implementation
ImGui_ImplGlfw_InitForOpenGL(mainWindow, true);
ImGui_ImplOpenGL3_Init("#version 330");
while (!glfwWindowShouldClose(mainWindow))
{
glfwPollEvents();
// and tell our program that we'll create a ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// we begin a new frame, and with Begin() we create a window
// ultimately we have to call End() and Render() to display it
ImGui::NewFrame();
ImGui::Begin("My Scene");
// Here we can render into the ImGui window
// ImGui Buttons, Drop Downs, etc. and later our framebuffer
ImGui::End();
ImGui::Render();
// ... rendering our triangle as before
// and we have to pass the render data further
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
GLFWwindow* backup_current_context = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_current_context);
}
glfwSwapBuffers(mainWindow);
}
// some ImGui cleanups here
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
// ... cleanups as before
return 0;
}
And this gives a Dear ImGui window in our example:
Framebuffer
And as a last step for this article, we'll render the triangle in a framebuffer, create a texture of the framebuffer and render this into the ImGui window.
We'll add some helper functiosn to create and bind the framebuffer. You can find a detailed explanation about framebuffers here on the OpenGL homepage.
// here we create our framebuffer and our renderbuffer
// you can find a more detailed explanation of framebuffer
// on the official opengl homepage, see the link above
void create_framebuffer()
{
glGenFramebuffers(1, &FBO);
glBindFramebuffer(GL_FRAMEBUFFER, FBO);
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0);
glGenRenderbuffers(1, &RBO);
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!\n";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
}
// here we bind our framebuffer
void bind_framebuffer()
{
glBindFramebuffer(GL_FRAMEBUFFER, FBO);
}
// here we unbind our framebuffer
void unbind_framebuffer()
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// and we rescale the buffer, so we're able to resize the window
void rescale_framebuffer(float width, float height)
{
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0);
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
}
Let's Put All Together
Now we can call our functions to create and handle a framebuffer. Right between ImGui::Begin()
and ImGui::End()
we add our framebuffer as image to the window.
I honestly don't know why, but the ordering matters:
First we render the ImGui window and then we render our triangle. To render into the framebuffer we call bind_framebuffer
before we render the triangle and unbind_framebuffer
when we're done.
int main()
{
// ... as before
create_triangle();
create_shaders();
// we create the framebuffer rigth after the triangle and the shader
create_framebuffer();
// ... ImGui initialization
while (!glfwWindowShouldClose(mainWindow))
{
// ... as before
ImGui::NewFrame();
ImGui::Begin("My Scene");
// we access the ImGui window size
const float window_width = ImGui::GetContentRegionAvail().x;
const float window_height = ImGui::GetContentRegionAvail().y;
// we rescale the framebuffer to the actual window size here and reset the glViewport
rescale_framebuffer(window_width, window_height);
glViewport(0, 0, window_width, window_height);
// we get the screen position of the window
ImVec2 pos = ImGui::GetCursorScreenPos();
// and here we can add our created texture as image to ImGui
// unfortunately we need to use the cast to void* or I didn't find another way tbh
ImGui::GetWindowDrawList()->AddImage(
(void *)texture_id,
ImVec2(pos.x, pos.y),
ImVec2(pos.x + window_width, pos.y + window_height),
ImVec2(0, 1),
ImVec2(1, 0)
);
ImGui::End();
ImGui::Render();
// now we can bind our framebuffer
bind_framebuffer();
// and we render our triangle as before
glUseProgram(shader);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
// and unbind it again
unbind_framebuffer();
// ... as before
glfwSwapBuffers(mainWindow);
}
// ... cleanup as before
return 0;
}
And this gives the reuslt:
You can find the final result here on GitHub.
And Thats It For Now
I hope that helped, if you can answer me why the ordering matters, that I first need to render the ImGui window and the the triangle, I'd appreciate that.
Let's see how to continue with that. I really like Dear ImGui, once I understood it, it's fairly simple to do an GUI in C++.
For now, thanks for reading.
Best Thomas.