Saltar a contenido

Interfaz Dear ImGui

Empezamos descargando la librería https://github.com/ocornut/imgui.

Una vez descargada debemos descomprimir los ficheros .h y .cpp en nuestro proyecto, por ejemplo en src/libs/imgui.

Copiaremos también ese directorio los ficheros de backends llamados:

  • imgui_impl_sdl.h
  • imgui_impl_sdl.cpp
  • imgui_impl_sdlrenderer.h
  • imgui_impl_sdlrenderer.cpp

Debido a como he configurado el proyecto debo cambiar en imgui_impl_sdl.cpp y imgui_impl_sdlrenderer.cpp las referencias de #include <SDL.h> por #include <SDL2/SDL2.h>, también #include <SDL_syswm.h> por #include <SDL2/SDL_syswm.h>.

En el Makefile integraré los ficheros de esta biblioteca:

build:
    g++ -I src/include -L src/lib  -o bin/main *.cpp src/include/imgui/*.cpp -lmingw32 -lSDL2main -lSDL2 -lSDL2_ttf
run:
    ./bin/main.exe
clean:
    rm ./bin/main.exe

Es importante notar que esta biblioteca es buena para hacer debugging pero al compilarla tenemos que incluir el código en el ejecutable, lo que aumenta el tiempo de compilación y el tamaño resultante.

Una vez compile seguiremos el ejemplo oficial para integrarla con SDL2. Básicamente podemos añadir la inicialización en nuestra window.cpp:

#include "imgui.h"
#include "imgui_impl_sdl.h"
#include "imgui_impl_sdlrenderer.h"

#if !SDL_VERSION_ATLEAST(2, 0, 17)
#error This backend requires SDL 2.0.17+ because of SDL_RenderGeometry() function
#endif

Una vez cargado el renderer:

// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui_ImplSDL2_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer_Init(renderer);

Procesamos los eventos de ImGui justo en el bucle de eventos de SDL:

void Window::ProcessInput()
{
    fpsTimer.pause(); // Pausar para prevenir congelamiento
    while (SDL_PollEvent(&event))
    {
        ImGui_ImplSDL2_ProcessEvent(&event);
        switch (event.type)
        {
        case SDL_QUIT:
            running = false;
            break;
        case SDL_KEYDOWN:
            if (event.key.keysym.sym == SDLK_ESCAPE)
                running = false;
            break;
        }
    }
    fpsTimer.unpause(); // Continuar al recibir un evento
}

Durante el evento Update() podemos crear un ImGUI::NewFrame y crear una ventana donde por ahora podemos debugear la tasa de fotogramas mediante las funcionalidades de esta útil biblioteca gráfica:

void Window::Update()
{
    // Iniciar el temporizador de cap
    if (enableCap)
        capTimer.start();

    // Iniciamos un Frame de Imgui
    ImGui_ImplSDLRenderer_NewFrame();
    ImGui_ImplSDL2_NewFrame();
    ImGui::NewFrame();

    // Creamos ventana demo de ImGUI
    {
        ImGui::Begin("CPU 3D Rendering");
        ImGui::Separator();
        ImGui::Text(" %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
        ImGui::End();
    }

    // Old version
    avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f);
    ++countedFrames;

    // Custom objects update
    mesh.Update();
}

Ahora en Render() debemos renderizar la interfaz justo antes de las tareas de PostRender():

void Window::Render()
{
    // Renderizamos el frame de ImGui
    ImGui::Render();
    // Late rendering actions
    PostRender();
}

En las tareas de PostRender, justo antes de presentar el render SDL, haremos uso del backend renderer de ImGui para SDL:

void Window::PostRender()
{
    // Antes de presentar llamamos al SDL Renderer de ImGUI
    ImGui_ImplSDLRenderer_RenderDrawData(ImGui::GetDrawData());
    // Finalmente actualizar la pantalla
    SDL_RenderPresent(renderer);
}

Finalmente, durante el destructor, antes de liberar el renderer liberamos la memoria ocupada:

Window::~Window()
{
    // Liberamos ImGUI
    ImGui_ImplSDLRenderer_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();

    // Liberamos SDL
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
}

Si compilamos y ejecutamos el programa ahora:

En este punto creo que ya no necesitamos utilizar el módulo SDL TTF para dibujar el texto de los FPS, así que vamos a desactivarlo y a borrarlo de la compilación:

/* BORRAR TODAS ESTAS DEFINICIONES */

#include <SDL2/SDL_ttf.h>

SDL_Surface *textSurface;
SDL_Color textColor = {255, 255, 255}; 
TTF_Font *textFont;                   
float avgFPS = 0;                   
long countedFrames = 0;
Timer fpsTimer;

/* BORRAR TODAS ESTAS IMPLEMENTACIONES */

TTF_CloseFont(textFont);
TTF_Quit();

if (TTF_Init() < 0) {
    std::cout << "Error initializing SDL_ttf: " << TTF_GetError() << std::endl;
    running = false;
}

textFont = TTF_OpenFont("assets/FreeSans.ttf", 16);
if (!textFont) {
    std::cout << "Error loading font: " << TTF_GetError() << std::endl;
    running = false;
}

fpsTimer.start();
fpsTimer.pause();
fpsTimer.unpause();

avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f);
++countedFrames;

std::string avgFPSText = std::to_string(avgFPS).substr(0, std::to_string(avgFPS).size() - 4) + " fps";
textSurface = TTF_RenderText_Solid(textFont, avgFPSText.c_str(), textColor);
if (!textSurface) {
    std::cout << "Failed to render text: " << TTF_GetError() << std::endl;
}
SDL_Texture *textTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
SDL_Rect dest = {2, windowHeight - 21, textSurface->w, textSurface->h};
SDL_RenderCopy(renderer, textTexture, NULL, &dest);
SDL_FreeSurface(textSurface);
SDL_DestroyTexture(textTexture);

Ya que estamos quitaré otras opciones que no estoy utilizando, como la de la pantalla completa:

bool isFullscreen = false;

if (isFullscreen)
{
    windowWidth = Window_mode.w;
    windowHeight = Window_mode.h;
}

if (isFullscreen)
{
    SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
}

Y quitaremos que incluya las definiciones de SDL TTF en el Makefile:

build:
    g++ -I src/include -L src/lib  -o bin/main *.cpp src/include/imgui/*.cpp -lmingw32 -lSDL2main -lSDL2
run:
    ./bin/main.exe
clean:
    rm ./bin/main.exe

Finalmente cambiaré el tamaño de la pantalla a 1280x720 para tener espacio donde juguetear con la ventana de la interfaz:

Window window(1280, 720);

Opciones de renderizado

Ahora que tenemos una interfaz para debugear vamos a añadir diferentes opciones a nuetro programa.

Empecemos con un par de opciones para desactivar el cap de FPS y cambiarlo con un slider:

ImGui::Begin("CPU 3D Rendering");
ImGui::Checkbox("Limitar FPS", &this->enableCap);
ImGui::SliderInt("Límite de FPS", &this->fpsCap, 5, 300);

Otra opción para dibujar o no la cuadrícula de fondo, aunque deberemos crear una variable booleana para controlar esa opción:

// Var
class Window
{
public:
    bool drawGrid = true;
}

// Render
if (this->drawGrid)
    DrawGrid(0xFF616161);

// Update
ImGui::Checkbox("Dibujar cuadrícula", &this->drawGrid);

Ahora unas cuantas opciones para manejar el renderizado de los triángulos:

  • Dibujar vértices del modelo.
  • Dibujar wireframe del modelo.
  • Dibujar caras de los triángulos.
  • Activar el back-face culling.
class Window
{
public:
    bool drawWireframe = true;
    bool drawWireframeDots = true;
    bool drawFilledTriangles = true;
    bool enableBackfaceCulling = true;
}

En la interfaz añadiremos las opciones:

ImGui::Checkbox("Dibujar cuadrícula", &this->drawGrid);
ImGui::Checkbox("Dibujar vértices", &this->drawWireframeDots);
ImGui::Checkbox("Dibujar wireframe", &this->drawWireframe);
ImGui::Checkbox("Rellenar triángulos", &this->drawFilledTriangles);
ImGui::Checkbox("Back-face culling", &this->enableBackfaceCulling);

Y las implementaremos en el renderizado de la malla:

void Mesh::Update()
{
    // Loop all triangle faces of the mesh
    for (size_t i = 0; i < triangles.size(); i++)
    {

        /*** Back Face Culling Algorithm ***/
        if (window->enableBackfaceCulling)
        {
            triangles[i].ApplyCulling(window->cameraPosition);
            // Bypass the projection if triangle is being culled

            if (triangles[i].culling)
                continue;
        }
    }
}

void Mesh::Render()
{
    // Loop projected triangles array and render them
    for (size_t i = 0; i < triangles.size(); i++)
    {
        // If culling is true and enabled globally bypass the current triangle
        if (window->enableBackfaceCulling && triangles[i].culling)
            continue;

        // Triángulos
        if (window->drawFilledTriangles)
        {
            window->DrawFilledTriangle(
                triangles[i].projectedVertices[0].x, triangles[i].projectedVertices[0].y,
                triangles[i].projectedVertices[1].x, triangles[i].projectedVertices[1].y,
                triangles[i].projectedVertices[2].x, triangles[i].projectedVertices[2].y,
                0xFFFFFFFF);
        }

        // Wireframe
        if (window->drawWireframe)
        {
            window->DrawTriangle(
                triangles[i].projectedVertices[0].x, triangles[i].projectedVertices[0].y,
                triangles[i].projectedVertices[1].x, triangles[i].projectedVertices[1].y,
                triangles[i].projectedVertices[2].x, triangles[i].projectedVertices[2].y,
                0xFF0095FF);
        }

        // Vértices
        if (window->drawWireframeDots)
        {
            window->DrawRect(triangles[i].projectedVertices[0].x - 1, triangles[i].projectedVertices[0].y - 2, 5, 5, 0xFFFF0000);
            window->DrawRect(triangles[i].projectedVertices[1].x - 1, triangles[i].projectedVertices[1].y - 2, 5, 5, 0xFFFF0000);
            window->DrawRect(triangles[i].projectedVertices[2].x - 1, triangles[i].projectedVertices[2].y - 2, 5, 5, 0xFFFF0000);
        }
    }
}

Para acabar unos sliders para controlar la posición y velocidad de rotación del modelo, así como la posición de la cámara y el fov factor. Estos los podemos gestionar con un SliderFloat3, aunque necesitaremos implementar una interfaz con un array de 3 flotantes para cada uno:

class Window
{
public:
    // Vector3 cameraPosition{0, 0, 0};   // <---- borrar
    float modelPosition[3] = {0, 0, -5};
    float modelRotationSpeed[3] = {0.01, 0.01, 0.01};
    float cameraPosition[3] = {0, 0, 0};
    int fovFactor = 400;
}
ImGui::Separator();
ImGui::Text("Posición del modelo (X,Y,Z)");
ImGui::SliderFloat2("Pos", modelPosition, -2,  2);  // mejor no tocar Z
ImGui::Text("Velocidad de rotación (X,Y,Z)");
ImGui::SliderFloat3("Rot", modelRotationSpeed, 0, 0.05f);
ImGui::Separator();
ImGui::Text("Campo de visión");
ImGui::SliderInt("Fov", &this->fovFactor, 75, 1000);

Deberemos adaptar el funcionamiento del mesh con los valores de estos arreglos:

void Window::Update()
{
    // Update Model Rotation Speed
    mesh.SetRotationAmount(
        modelRotationSpeed[0], modelRotationSpeed[1], modelRotationSpeed[2]);
}

En cuanto a la cámara, los métodos del mesh que la utilizan son TranslateVertex y ApplyCulling así que cambiémoslos para utilizar un arreglo en lugar de un vector:

void TranslateVertex(int vertexIndex, float *distance)
{
    vertices[vertexIndex].x -= distance[0];
    vertices[vertexIndex].y -= distance[1];
    vertices[vertexIndex].z -= distance[2];
}

void ApplyCulling(float *cameraPosition)
{
    // Find the vector betweenn a triangle point and camera origin
    Vector3 cameraRay = Vector3(cameraPosition[0], cameraPosition[1], cameraPosition[2]) - this->vertices[0];
}

Finalmente adaptamos la transformación de traslación en el mesh:

triangles[i].TranslateVertex(j, window->modelPosition);

El resultado es genial y podemos manejar un montón de opciones en tiempo real:

Poco a poco iré añadiendo más opciones.


Última edición: 05 de Junio de 2022