[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 final result will render the triangle into the ImGui window

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:

You can move this Dear ImGui window around your screen and also out of the main window here.

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.

Previous
Previous

[C++] A std::tuple To Concatenate Validation Criteria

Next
Next

[C++] User-Defined Literals To Handle Units