OpenGL集成imgui

  LearnOpenGL教程采用了循序渐进的教学方式,由浅入深地讲解了OpenGL的使用。但是,在学习OpenGL的过程中不难发现,有时候只是简单的一个参数的改动,就不得不重新编译一遍程序,根据评论区的介绍发现了ImGui这款轻量级的GUI库,可以非常方便地和OpenGL程序进行交互。

ImGui

  Dear ImGui(ImGui)是一个轻量级、便于集成的C++ GUI库,用于创建即时编辑器、调试工具等工具。ImGui最初是为游戏开发而设计,但也被广泛用于各种领域,如工具开发、仿真、数据可视化等。Dear ImGui库的主要特点和优势:
  1.轻量级且易于集成:Dear ImGui是一个轻量级的库,只提供GUI绘制的基本功能,使其易于集成到各种项目中。
  2.简单的API:ImGui提供了简单而直观的API,使用户可以轻松创建各种GUI元素,如按钮、文本框、复选框等。
  3.即时渲染:ImGui使用即时渲染的方式工作,这意味着GUI元素在每一帧都会被重新绘制,从而实现动态更新和交互。
  4.跨平台:ImGui支持跨平台开发,可以在各种操作系统上运行,包括Windows、macOS、Linux等。
  5.自定义主题:ImGui允许用户轻松定制GUI的外观和样式,包括颜色、字体等,以满足不同项目的需求。
  6.无依赖:ImGui本身不依赖于任何图形库,但可以与常见的图形库(如OpenGL、DirectX)集成使用。
  在使用Dear ImGui时,你可以创建各种GUI元素,与用户交互并实时更新,从而为项目提供一个简单而有效的用户界面。ImGui的设计使得它特别适合用于快速原型设计、调试工具的开发,以及其他需要简单GUI的场景。
  另外,ImGUI已经写好了主流平台和图形API的后端,能够非常方便地在应用程序中集成imgui。

imgui和OpenGL

imgui使用说明

  imgui代码仓库中最外层文件目录中包含的.h .cpp文件即是imgui库所有主要的源文件。仓库中的backends中包含了主流平台和图形API的后端源代码,极大地方便了快速集成在自己的应用中。我们可以将imgui自身的源代码文件拷贝到自己的项目目录中。然后,选择合适的后端平台和图形API拷贝到自己的项目目录中。

imgui示例介绍

  本章节以GLFW+OpenGL为例,使用CMake和MSVC编译imgui对应的示例代码。
  1. GLFW版本3.4_x64位
  2. MSVC版本17.6.3(随VS2022安装)
  工程组织结构如下所示:

.
|-- CMakeLists.txt
|-- libraries
|   |-- glfw
|   |   |-- include
|   |   |   `-- GLFW
|   |   |       |-- glfw3.h
|   |   |       `-- glfw3native.h
|   |   `-- lib-vc2022
|   |       |-- glfw3.dll
|   |       |-- glfw3.lib
|   |       |-- glfw3_mt.lib
|   |       `-- glfw3dll.lib
|   `-- imgui
|       |-- backends
|       |   |-- imgui_impl_glfw.cpp
|       |   |-- imgui_impl_glfw.h
|       |   |-- imgui_impl_opengl3.cpp
|       |   |-- imgui_impl_opengl3.h
|       |   `-- imgui_impl_opengl3_loader.h
|       |-- imconfig.h
|       |-- imgui.cpp
|       |-- imgui.h
|       |-- imgui_demo.cpp
|       |-- imgui_draw.cpp
|       |-- imgui_internal.h
|       |-- imgui_tables.cpp
|       |-- imgui_widgets.cpp
|       |-- imstb_rectpack.h
|       |-- imstb_textedit.h
|       `-- imstb_truetype.h
`-- source
    `-- main.cpp

  其中,libraries目录用于存放第三方的库,GLFW下载了对应平台预编译好的库;source中存放源码,main.cpp为imgui仓库中examples/example_glfa_opengl3目录中的示例程序源代码。
  CMakeLists.txt文件的内容如下:

cmake_minimum_required(VERSION 3.15)
project(imguiOpenGL)

set(CMAKE_CXX_STANDARD 17)

# 设置默认的构建类型
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build (Debug or Release)" FORCE)
endif()

if(MSVC)
    add_compile_options(/wd4819) # cmake禁止msvc 4819警告(和字符集相关的警告)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # 设置可执行文件输出目录
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) # 设置库文件输出目录
endif()

# 设置include路径
include_directories(
    libraries/
    libraries/imgui
    libraries/imgui/backends
    libraries/glfw/include
)
# 设置要连接的库
set(GLFW_LIB ${CMAKE_CURRENT_SOURCE_DIR}/libraries/glfw/lib-vc2022/glfw3.lib)
set(LINKED_LIBS opengl32 ${GLFW_LIB})
# 将imgui编译成静态库
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libraries/imgui)
set(IMGUI_SOURCE_FILES
    ${IMGUI_DIR}/imgui.cpp
    ${IMGUI_DIR}/imgui_demo.cpp
    ${IMGUI_DIR}/imgui_draw.cpp
    ${IMGUI_DIR}/imgui_tables.cpp
    ${IMGUI_DIR}/imgui_widgets.cpp

    ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
    ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
)
add_library(imgui ${IMGUI_SOURCE_FILES})
# 创建可执行程序并连接到imgui GLFW OpenGL库
add_executable(${PROJECT_NAME} source/main.cpp)
if(MSVC)
    set_property(TARGET ${PROJECT_NAME} PROPERTY LINK_FLAGS /NODEFAULTLIB:MSVCRT.lib) # 禁用默认的msvc运行时库
endif()
target_link_libraries(${PROJECT_NAME} ${LINKED_LIBS} imgui)

  编译整个项目,运行最后生成的可执行文件imguiOpenGL,结果如下:

  界面上显示的所有示例元素都可以在imgui_demo.cpp文件中找到,仿照demo中的例子就可以编写自己的GUI程序了。根据main.cpp中的例子和官方文档的说明,imgui的整个渲染流程可以简单总结为:

At initialization:
  call ImGui::CreateContext()
  call ImGui_ImplXXXX_Init() for each backend.

At the beginning of your frame:
  call ImGui_ImplXXXX_NewFrame() for each backend.
  call ImGui::NewFrame()

At the end of your frame:
  call ImGui::Render()
  call ImGui_ImplXXXX_RenderDrawData() for your Renderer backend.

At shutdown:
  call ImGui_ImplXXXX_Shutdown() for each backend.
  call ImGui::DestroyContext()

旋转的Box

  学习OpenGL的过程中,可以将OpenGL的渲染过程加入到示例代码中,以创建一个可以通过GUI界面旋转Box的OpenGL程序为例:
  1. 添加了glad库;
  2. 添加了glm库;
  3. 添加了stb_image库和纹理;
  4. 添加了LearnOpenGL教程中封装的Shader类;
  5. 使用了自己编写的生成立方体顶点数据的类,使用了小米推出的字体MiSans
  工程的目录结构如下:

.
|-- CMakeLists.txt
|-- assets
|   |-- fonts
|   |   `-- MiSans-Regular.ttf
|   |-- shaders
|   |   |-- cube.frag
|   |   `-- cube.vert
|   `-- textures
|       `-- boxtexture.jpg
|-- libraries
|   |-- glad
|   |   |-- include
|   |   |   |-- KHR
|   |   |   |   `-- khrplatform.h
|   |   |   `-- glad
|   |   |       `-- glad.h
|   |   `-- src
|   |       `-- glad.c
|   |-- glfw
|   |   |-- include
|   |   |   `-- GLFW
|   |   |       |-- glfw3.h
|   |   |       `-- glfw3native.h
|   |   `-- lib-vc2022
|   |       |-- glfw3.dll
|   |       |-- glfw3.lib
|   |       |-- glfw3_mt.lib
|   |       `-- glfw3dll.lib
|   |-- glm
|   |   |-- glm库的源代码
|   |-- imgui
|   |   |-- backends
|   |   |   |-- imgui_impl_glfw.cpp
|   |   |   |-- imgui_impl_glfw.h
|   |   |   |-- imgui_impl_opengl3.cpp
|   |   |   |-- imgui_impl_opengl3.h
|   |   |   `-- imgui_impl_opengl3_loader.h
|   |   |-- imconfig.h
|   |   |-- imgui.cpp
|   |   |-- imgui.h
|   |   |-- imgui_demo.cpp
|   |   |-- imgui_draw.cpp
|   |   |-- imgui_internal.h
|   |   |-- imgui_tables.cpp
|   |   |-- imgui_widgets.cpp
|   |   |-- imstb_rectpack.h
|   |   |-- imstb_textedit.h
|   |   `-- imstb_truetype.h
|   `-- stb_image.h
`-- source
    |-- inc
    |   |-- box.h
    |   |-- shader.h
    |   `-- vec.h
    |-- main.cpp
    `-- src

  CMakeLists文件也做了适当修改,添加了对应的头文件路径,库编译,资源文件拷贝等:

cmake_minimum_required(VERSION 3.15)
project(imguiOpenGL)

set(CMAKE_CXX_STANDARD 17)

# 设置默认的构建类型
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build (Debug or Release)" FORCE)
endif()

if(MSVC)
    add_compile_options(/wd4819) # cmake禁止msvc 4819警告(和字符集相关的警告)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # 设置可执行文件输出目录
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) # 设置库文件输出目录
endif()

# 设置include路径
include_directories(
    libraries/
    libraries/imgui
    libraries/imgui/backends
    libraries/glm
    libraries/glad/include
    libraries/glfw/include

    source/inc
)

set(GLFW_LIB ${CMAKE_CURRENT_SOURCE_DIR}/libraries/glfw/lib-vc2022/glfw3.lib)
set(LINKED_LIBS opengl32 ${GLFW_LIB})

set(GLAD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libraries/glad)
set(GLAD_SOURCE_FILES
    ${GLAD_DIR}/src/glad.c
)
add_library(glad ${GLAD_SOURCE_FILES})

set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libraries/imgui)
set(IMGUI_SOURCE_FILES
    ${IMGUI_DIR}/imgui.cpp
    ${IMGUI_DIR}/imgui_demo.cpp
    ${IMGUI_DIR}/imgui_draw.cpp
    ${IMGUI_DIR}/imgui_tables.cpp
    ${IMGUI_DIR}/imgui_widgets.cpp

    ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
    ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
)
add_library(imgui ${IMGUI_SOURCE_FILES})

# 拷贝资源文件,将assets文件夹中的资源文件拷贝到可执行文件所在目录
file(COPY assets/ DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})

add_executable(${PROJECT_NAME} source/main.cpp)
if(MSVC)
    set_property(TARGET ${PROJECT_NAME} PROPERTY LINK_FLAGS /NODEFAULTLIB:MSVCRT.lib) # 禁用默认的msvc运行时库
endif()
target_link_libraries(${PROJECT_NAME} ${LINKED_LIBS} imgui glad)

  修改后的main.cpp文件如下:

#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#include <iostream>
#define GL_SILENCE_DEPRECATION
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <GLES2/gl2.h>
#endif
#include <glad/glad.h>
#include <GLFW/glfw3.h> // Will drag system OpenGL headers

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include "box.h"
#include "shader.h"
// [Win32] Our example includes a copy of glfw3.lib pre-compiled with VS2010 to maximize ease of testing and compatibility with old VS compilers.
// To link with VS2010-era libraries, VS2015+ requires linking with legacy_stdio_definitions.lib, which we do using this pragma.
// Your own project should not be affected, as you are likely to link with a newer binary of GLFW that is adequate for your version of Visual Studio.
#if defined(_MSC_VER) && (_MSC_VER >= 1900) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS)
#pragma comment(lib, "legacy_stdio_definitions")
#endif

// This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details.
#ifdef __EMSCRIPTEN__
#include "../libs/emscripten/emscripten_mainloop_stub.h"
#endif

static void glfw_error_callback(int error, const char* description)
{
    fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}

unsigned int loadTexture(char const *path);

// Main code
int main(int, char**)
{
    glfwSetErrorCallback(glfw_error_callback);
    if (!glfwInit())
        return 1;
    const char* glsl_version = "#version 130";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 3.0+ only

    // Create window with graphics context
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui GLFW+OpenGL3 example", nullptr, nullptr);
    if (window == nullptr)
        return 1;
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();
    //ImGui::StyleColorsLight();

    // Setup Platform/Renderer backends
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    /******************** -- Load OPENGL Functions-- ********************/
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    /******************** -- Load Fonts-- ********************/
    io.Fonts->AddFontFromFileTTF("../fonts/MiSans-Regular.ttf", 18.0f);
    /******************** -- VAO and VBO-- ********************/
    Box box;
    auto vertices = box.vertices;

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);
    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
    glEnableVertexAttribArray(0);
    // texture coordinate attribute
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3)));
    glEnableVertexAttribArray(2);

    /******************** -- Texture -- ********************/
    unsigned int texture = loadTexture("../textures/boxtexture.jpg");

    /******************** -- Shader-- ********************/
    Shader shaderProgram("../shaders/cube.vert", "../shaders/cube.frag");
    glEnable(GL_DEPTH_TEST);
    // Our state
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);

    // Main loop
#ifdef __EMSCRIPTEN__
    // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file.
    // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
    io.IniFilename = nullptr;
    EMSCRIPTEN_MAINLOOP_BEGIN
#else
    while (!glfwWindowShouldClose(window))
#endif
    {
        // Poll and handle events (inputs, window resize, etc.)
        // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
        // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
        // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
        // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
        glfwPollEvents();

        // Start the Dear ImGui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        static float xAngle = 0.0f;
        static float yAngle = 0.0f;
        static float zAngle = 0.0f;
        static float scale = 1.0f;
        {
            ImGui::Begin("Rotate Cube", 0, ImGuiWindowFlags_AlwaysAutoResize);
            ImGui::ColorEdit3("clear Color", (float*)&clear_color);
            ImGui::SliderFloat("X-Axis Angle", &xAngle, 0.0f, 360.0f);
            ImGui::SliderFloat("Y-Axis Angle", &yAngle, 0.0f, 360.0f);
            ImGui::SliderFloat("Z-Axis Angle", &zAngle, 0.0f, 360.0f);
            ImGui::SliderFloat("Model Scale", &scale, 0.1f, 5.0f);
            ImGui::End();
        }

        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

        shaderProgram.use();
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)display_w / (float)display_h, 0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 5.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::scale(model, glm::vec3(scale));
        model = glm::rotate(model, glm::radians(xAngle), glm::vec3(1.0f, 0.0f, 0.0f));
        model = glm::rotate(model, glm::radians(yAngle), glm::vec3(0.0f, 1.0f, 0.0f));
        model = glm::rotate(model, glm::radians(zAngle), glm::vec3(0.0f, 0.0f, 1.0f));

        shaderProgram.setMat4("projection", projection);
        shaderProgram.setMat4("view", view);
        shaderProgram.setMat4("model", model);

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        // Rendering
        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        glfwSwapBuffers(window);
    }
#ifdef __EMSCRIPTEN__
    EMSCRIPTEN_MAINLOOP_END;
#endif

    // Cleanup
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

unsigned int loadTexture(char const *path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); 
        glGenerateMipmap(GL_TEXTURE_2D);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        stbi_image_free(data);
    }

    return textureID;
}

  其中,顶点着色器和片段着色器代码如下:

# vertex shader
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec2 TexCoord;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}
# fragment shader
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;
uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
}

  UI部分的代码如下(更多UI元素可以参考imgui_demo.cpp,官方仓库的使用指导也推荐了一些热门的插件):

static float xAngle = 0.0f;
static float yAngle = 0.0f;
static float zAngle = 0.0f;
static float scale = 1.0f;
{
    ImGui::Begin("Rotate Cube", 0, ImGuiWindowFlags_AlwaysAutoResize);
    ImGui::ColorEdit3("clear Color", (float*)&clear_color);
    ImGui::SliderFloat("X-Axis Angle", &xAngle, 0.0f, 360.0f);
    ImGui::SliderFloat("Y-Axis Angle", &yAngle, 0.0f, 360.0f);
    ImGui::SliderFloat("Z-Axis Angle", &zAngle, 0.0f, 360.0f);
    ImGui::SliderFloat("Model Scale", &scale, 0.1f, 5.0f);
    ImGui::End();
}

  最终的效果如图所示:


当珍惜每一片时光~