Drawing 3D Textured Rotating Cube with Offscreen MSAA FBO using SDL3_image, GLM, GLAD, and OpenGL 3.3 Отрисовка 3D-куба с текстурой, вращением и закадровым MSAA FBO с использованием SDL3_image, GLM, GLAD и OpenGL 3.3

A step-by-step guide to implementing robust 4x Anti-Aliasing (MSAA) using offscreen Framebuffer Objects (FBOs). This technique bypasses platform-specific GLX display visual limitations (such as inside WSL2) while providing hardware-accelerated smooth edges on Windows and Ubuntu. Пошаговое руководство по реализации надёжного 4x сглаживания (MSAA) с использованием закадровых буферов кадра (FBO). Эта техника обходит ограничения визуала GLX (например, в WSL2) и обеспечивает аппаратное сглаживание краев под Windows и Ubuntu.

Step 1: Create Project Directory Structure Шаг 1: Создание структуры директории проекта

Create the project workspace and asset subfolder first: Сначала создайте рабочую папку проекта и подпапку для ассетов:

Windows (Command Prompt / PowerShell)

mkdir C:\projects\draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop
mkdir C:\projects\draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop\assets\textures
cd C:\projects\draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop

Ubuntu / WSL (Terminal)

mkdir -p ~/projects/draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop/assets/textures
cd ~/projects/draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop

Step 2: Download & Place Texture Asset Шаг 2: Скачивание и размещение текстурного ассета

Download the wooden crate texture set from OpenGameArt: Скачайте набор текстур деревянного ящика с сайта OpenGameArt:

OpenGameArt - Crate #5 (crate.7z) OpenGameArt - Crate #5 (crate.7z)

Extract crate.7z and place the texture file clear_01.png into the project's texture subfolder. Распакуйте архив crate.7z и поместите файл текстуры clear_01.png в подпапку текстур проекта.

Windows Path

C:\projects\draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop\assets\textures\clear_01.png

Ubuntu / WSL Command (Copying from Windows to WSL)

If you downloaded and placed clear_01.png inside your Windows project directory, copy it directly to your WSL Linux directory using the Windows mount path (/mnt/c/): Если вы скачали и поместили clear_01.png в Windows-папку проекта, скопируйте его напрямую в Linux-папку WSL через монтированный путь Windows (/mnt/c/):

cp /mnt/c/projects/draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop/assets/textures/clear_01.png \
   ~/projects/draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop/assets/textures/

Step 3: Create Source Files (main.cpp & CMakeLists.txt) Шаг 3: Создание исходных файлов (main.cpp и CMakeLists.txt)

Create these files using your preferred editor: Создайте эти файлы в любом удобном редакторе:

Windows (Terminal / Notepad)

notepad main.cpp
notepad CMakeLists.txt

Ubuntu (Terminal / Nano)

nano main.cpp
nano CMakeLists.txt
Quick Nano Commands (Ubuntu):Быстрые команды Nano (Ubuntu): Paste code with Right Mouse Button (RMB) → Press Ctrl + O and Enter to save → Press Ctrl + X to exit. Вставьте код правой кнопкой мыши (ПКМ) → Нажмите Ctrl + O и Enter для сохранения → Нажмите Ctrl + X для выхода.

1A. File Contents for main.cpp (Classic Loop) 1A. Содержимое файла main.cpp (Классический цикл)

#include <glad/glad.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>

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

const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;
uniform mat4 uMVP;

void main() {
    gl_Position = uMVP * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}
)";

const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;
uniform sampler2D ourTexture;

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

GLuint createShaderProgram(const char* vertexSrc, const char* fragmentSrc) {
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexSrc, nullptr);
    glCompileShader(vertexShader);

    GLint success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        SDL_Log("Vertex Shader Compilation Failed: %s", infoLog);
    }

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentSrc, nullptr);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        SDL_Log("Fragment Shader Compilation Failed: %s", infoLog);
    }

    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
        SDL_Log("Shader Program Link Failed: %s", infoLog);
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

GLuint createTexture(const char* path, int* outW, int* outH) {
    SDL_IOStream* io = SDL_IOFromFile(path, "rb");
    if (!io) {
        SDL_Log("Failed to open file stream for %s: %s", path, SDL_GetError());
        return 0;
    }

    SDL_Surface* surface = IMG_Load_IO(io, true);
    if (!surface) {
        SDL_Log("Image loading failed: %s", SDL_GetError());
        return 0;
    }

    SDL_Surface* optimizedSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ABGR8888);
    SDL_DestroySurface(surface);

    if (!optimizedSurface) {
        SDL_Log("Surface conversion failed: %s", SDL_GetError());
        return 0;
    }

    if (outW) *outW = optimizedSurface->w;
    if (outH) *outH = optimizedSurface->h;

    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, optimizedSurface->w, optimizedSurface->h,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, optimizedSurface->pixels);
    glGenerateMipmap(GL_TEXTURE_2D);

    SDL_DestroySurface(optimizedSurface);

    return textureID;
}

struct MSAAFBO {
    GLuint fbo = 0;
    GLuint colorRBO = 0;
    GLuint depthRBO = 0;
    int width = 0;
    int height = 0;
    const int samples = 4;
};

void setupMSAAFramebuffer(MSAAFBO& msaa, int width, int height) {
    if (msaa.width == width && msaa.height == height) return;

    msaa.width = width;
    msaa.height = height;

    if (msaa.fbo == 0) glGenFramebuffers(1, &msaa.fbo);
    if (msaa.colorRBO == 0) glGenRenderbuffers(1, &msaa.colorRBO);
    if (msaa.depthRBO == 0) glGenRenderbuffers(1, &msaa.depthRBO);

    glBindFramebuffer(GL_FRAMEBUFFER, msaa.fbo);

    glBindRenderbuffer(GL_RENDERBUFFER, msaa.colorRBO);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa.samples, GL_RGBA8, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msaa.colorRBO);

    glBindRenderbuffer(GL_RENDERBUFFER, msaa.depthRBO);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa.samples, GL_DEPTH24_STENCIL8, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, msaa.depthRBO);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        SDL_Log("MSAA Framebuffer is incomplete!");
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

int main(int argc, char* argv[]) {
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("SDL Initialization failed: %s", SDL_GetError());
        return -1;
    }

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    SDL_Window* window = SDL_CreateWindow("SDL3 GLM GLAD 3D Textured Cube (Classic MSAA FBO)", 800, 450, SDL_WINDOW_OPENGL);
    if (!window) {
        SDL_Log("Window creation failed: %s", SDL_GetError());
        SDL_Quit();
        return -1;
    }

    SDL_GLContext glContext = SDL_GL_CreateContext(window);
    if (!glContext) {
        SDL_Log("OpenGL context creation failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    SDL_GL_MakeCurrent(window, glContext);
    SDL_GL_SetSwapInterval(1);

    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        SDL_Log("Failed to initialize GLAD");
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    GLuint shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

    float vertices[] = {
        // Front face
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,

        // Back face
        -0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  1.0f, 1.0f,

        // Left face
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        // Right face
         0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  0.0f, 0.0f,

        // Top face
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,

        // Bottom face
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f
    };

    GLuint VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    GLsizei stride = 5 * sizeof(float);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);

    int texW = 0, texH = 0;
    GLuint textureID = createTexture("assets/textures/clear_01.png", &texW, &texH);
    if (textureID == 0) {
        SDL_Log("Failed to load texture.");
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(1, &VBO);
        glDeleteProgram(shaderProgram);
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    GLint mvpLoc = glGetUniformLocation(shaderProgram, "uMVP");
    GLint texLoc = glGetUniformLocation(shaderProgram, "ourTexture");

    MSAAFBO msaa;
    float rotationAngle = 0.0f;
    Uint64 lastTicks = SDL_GetTicksNS();

    bool running = true;
    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT) {
                running = false;
            }
        }

        Uint64 currentTicks = SDL_GetTicksNS();
        float deltaTime = static_cast<float>(currentTicks - lastTicks) / 1e9f;
        lastTicks = currentTicks;

        rotationAngle += 60.0f * deltaTime;
        if (rotationAngle >= 360.0f) {
            rotationAngle -= 360.0f;
        }

        int winW = 800, winH = 450;
        SDL_GetWindowSizeInPixels(window, &winW, &winH);
        if (winH == 0) winH = 1;

        setupMSAAFramebuffer(msaa, winW, winH);

        // Pass 1: Render scene into MSAA FBO
        glBindFramebuffer(GL_FRAMEBUFFER, msaa.fbo);
        glViewport(0, 0, winW, winH);

        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        float aspect = (float)winW / (float)winH;
        glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 1.5f, 3.5f), 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::rotate(model, glm::radians(rotationAngle), glm::vec3(0.5f, 1.0f, 0.0f));

        glm::mat4 mvp = proj * view * model;

        glUseProgram(shaderProgram);
        glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvp));

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureID);
        glUniform1i(texLoc, 0);

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

        // Pass 2: Blit MSAA FBO to Window Framebuffer
        glBindFramebuffer(GL_READ_FRAMEBUFFER, msaa.fbo);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
        glBlitFramebuffer(0, 0, winW, winH, 0, 0, winW, winH, GL_COLOR_BUFFER_BIT, GL_NEAREST);

        SDL_GL_SwapWindow(window);
    }

    if (msaa.fbo) glDeleteFramebuffers(1, &msaa.fbo);
    if (msaa.colorRBO) glDeleteRenderbuffers(1, &msaa.colorRBO);
    if (msaa.depthRBO) glDeleteRenderbuffers(1, &msaa.depthRBO);

    glDeleteTextures(1, &textureID);
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    SDL_GL_DestroyContext(glContext);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

1B. File Contents for main.cpp (SDL3 Callbacks API) 1B. Содержимое файла main.cpp (SDL3 Callbacks API)

#define SDL_MAIN_USE_CALLBACKS 1
#include <glad/glad.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>

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

const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;
uniform mat4 uMVP;

void main() {
    gl_Position = uMVP * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}
)";

const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;
uniform sampler2D ourTexture;

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

GLuint createShaderProgram(const char* vertexSrc, const char* fragmentSrc) {
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexSrc, nullptr);
    glCompileShader(vertexShader);

    GLint success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        SDL_Log("Vertex Shader Compilation Failed: %s", infoLog);
    }

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentSrc, nullptr);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        SDL_Log("Fragment Shader Compilation Failed: %s", infoLog);
    }

    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
        SDL_Log("Shader Program Link Failed: %s", infoLog);
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

GLuint createTexture(const char* path, int* outW, int* outH) {
    SDL_IOStream* io = SDL_IOFromFile(path, "rb");
    if (!io) {
        SDL_Log("Failed to open file stream for %s: %s", path, SDL_GetError());
        return 0;
    }

    SDL_Surface* surface = IMG_Load_IO(io, true);
    if (!surface) {
        SDL_Log("Image loading failed: %s", SDL_GetError());
        return 0;
    }

    SDL_Surface* optimizedSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ABGR8888);
    SDL_DestroySurface(surface);

    if (!optimizedSurface) {
        SDL_Log("Surface conversion failed: %s", SDL_GetError());
        return 0;
    }

    if (outW) *outW = optimizedSurface->w;
    if (outH) *outH = optimizedSurface->h;

    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, optimizedSurface->w, optimizedSurface->h,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, optimizedSurface->pixels);
    glGenerateMipmap(GL_TEXTURE_2D);

    SDL_DestroySurface(optimizedSurface);

    return textureID;
}

struct AppState {
    SDL_Window* window = nullptr;
    SDL_GLContext glContext = nullptr;
    GLuint shaderProgram = 0;
    GLuint VAO = 0;
    GLuint VBO = 0;
    GLuint textureID = 0;
    int texW = 0;
    int texH = 0;
    GLint mvpLoc = -1;
    GLint texLoc = -1;
    float rotationAngle = 0.0f;
    Uint64 lastTicks = 0;

    GLuint msaaFBO = 0;
    GLuint msaaColorRBO = 0;
    GLuint msaaDepthRBO = 0;
    int fboWidth = 0;
    int fboHeight = 0;
    const int msaaSamples = 4;
};

void setupMSAAFramebuffer(AppState* state, int width, int height) {
    if (state->fboWidth == width && state->fboHeight == height) return;

    state->fboWidth = width;
    state->fboHeight = height;

    if (state->msaaFBO == 0) glGenFramebuffers(1, &state->msaaFBO);
    if (state->msaaColorRBO == 0) glGenRenderbuffers(1, &state->msaaColorRBO);
    if (state->msaaDepthRBO == 0) glGenRenderbuffers(1, &state->msaaDepthRBO);

    glBindFramebuffer(GL_FRAMEBUFFER, state->msaaFBO);

    glBindRenderbuffer(GL_RENDERBUFFER, state->msaaColorRBO);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, state->msaaSamples, GL_RGBA8, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, state->msaaColorRBO);

    glBindRenderbuffer(GL_RENDERBUFFER, state->msaaDepthRBO);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, state->msaaSamples, GL_DEPTH24_STENCIL8, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, state->msaaDepthRBO);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        SDL_Log("MSAA Framebuffer is incomplete!");
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("SDL Initialization failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetHint(SDL_HINT_MAIN_CALLBACK_RATE, "60");

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    AppState* state = new AppState();
    *appstate = state;

    state->window = SDL_CreateWindow("SDL3 GLM GLAD 3D Textured Cube (Callbacks MSAA FBO)", 800, 450, SDL_WINDOW_OPENGL);
    if (!state->window) {
        SDL_Log("Window creation failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    state->glContext = SDL_GL_CreateContext(state->window);
    if (!state->glContext) {
        SDL_Log("OpenGL context creation failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_GL_MakeCurrent(state->window, state->glContext);
    SDL_GL_SetSwapInterval(1);

    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        SDL_Log("Failed to initialize GLAD");
        return SDL_APP_FAILURE;
    }

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    state->shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

    float vertices[] = {
        // Front face
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,

        // Back face
        -0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  1.0f, 1.0f,

        // Left face
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        // Right face
         0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  0.0f, 0.0f,

        // Top face
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 0.0f,

        // Bottom face
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f
    };

    glGenVertexArrays(1, &state->VAO);
    glGenBuffers(1, &state->VBO);

    glBindVertexArray(state->VAO);
    glBindBuffer(GL_ARRAY_BUFFER, state->VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    GLsizei stride = 5 * sizeof(float);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);

    state->textureID = createTexture("assets/textures/clear_01.png", &state->texW, &state->texH);
    if (state->textureID == 0) {
        return SDL_APP_FAILURE;
    }

    state->mvpLoc = glGetUniformLocation(state->shaderProgram, "uMVP");
    state->texLoc = glGetUniformLocation(state->shaderProgram, "ourTexture");
    state->lastTicks = SDL_GetTicksNS();

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void* appstate) {
    auto* state = static_cast<AppState*>(appstate);

    Uint64 currentTicks = SDL_GetTicksNS();
    float deltaTime = static_cast<float>(currentTicks - state->lastTicks) / 1e9f;
    state->lastTicks = currentTicks;

    state->rotationAngle += 60.0f * deltaTime;
    if (state->rotationAngle >= 360.0f) {
        state->rotationAngle -= 360.0f;
    }

    int winW = 800, winH = 450;
    SDL_GetWindowSizeInPixels(state->window, &winW, &winH);
    if (winH == 0) winH = 1;

    setupMSAAFramebuffer(state, winW, winH);

    // Pass 1: Render 3D scene into offscreen MSAA Framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, state->msaaFBO);
    glViewport(0, 0, winW, winH);

    glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    float aspect = (float)winW / (float)winH;
    glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
    glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 1.5f, 3.5f), 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::rotate(model, glm::radians(state->rotationAngle), glm::vec3(0.5f, 1.0f, 0.0f));

    glm::mat4 mvp = proj * view * model;

    glUseProgram(state->shaderProgram);
    glUniformMatrix4fv(state->mvpLoc, 1, GL_FALSE, glm::value_ptr(mvp));

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, state->textureID);
    glUniform1i(state->texLoc, 0);

    glBindVertexArray(state->VAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);

    // Pass 2: Blit (resolve) MSAA FBO to Default Window Framebuffer
    glBindFramebuffer(GL_READ_FRAMEBUFFER, state->msaaFBO);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBlitFramebuffer(0, 0, winW, winH, 0, 0, winW, winH, GL_COLOR_BUFFER_BIT, GL_NEAREST);

    SDL_GL_SwapWindow(state->window);

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result) {
    if (appstate) {
        auto* state = static_cast<AppState*>(appstate);

        if (state->msaaFBO) glDeleteFramebuffers(1, &state->msaaFBO);
        if (state->msaaColorRBO) glDeleteRenderbuffers(1, &state->msaaColorRBO);
        if (state->msaaDepthRBO) glDeleteRenderbuffers(1, &state->msaaDepthRBO);

        if (state->textureID) glDeleteTextures(1, &state->textureID);
        if (state->VAO) glDeleteVertexArrays(1, &state->VAO);
        if (state->VBO) glDeleteBuffers(1, &state->VBO);
        if (state->shaderProgram) glDeleteProgram(state->shaderProgram);

        if (state->glContext) {
            SDL_GL_DestroyContext(state->glContext);
        }
        if (state->window) {
            SDL_DestroyWindow(state->window);
        }
        delete state;
    }
    SDL_Quit();
}

2. File Contents for CMakeLists.txt 2. Содержимое файла CMakeLists.txt

cmake_minimum_required(VERSION 3.21)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Debug")
endif()

project(draw-texture-cube-msaa-fbo-sdl3-glm-glad-opengl-desktop CXX C)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# -----------------------------------------------------------------------------
# Locate System OpenGL Package
# -----------------------------------------------------------------------------
find_package(OpenGL REQUIRED)

# -----------------------------------------------------------------------------
# Locate SDL3 and SDL3_image Packages
# -----------------------------------------------------------------------------
if(WIN32)
    set(SDL3_DIR "C:/libs/sdl3-3.4.16-mingw/lib/cmake/SDL3")
    set(SDL3_image_DIR "C:/libs/sdl3_image-3.4.6-mingw/lib/cmake/SDL3_image")
else()
    set(SDL3_DIR "$ENV{HOME}/libs/sdl3-3.4.16-ubuntu/lib/cmake/SDL3")
    set(SDL3_image_DIR "$ENV{HOME}/libs/sdl3_image-3.4.6-ubuntu/lib/cmake/SDL3_image")
endif()

find_package(SDL3 REQUIRED CONFIG)
find_package(SDL3_image REQUIRED CONFIG)

# -----------------------------------------------------------------------------
# Set Include Path for Header-Only GLM Library
# -----------------------------------------------------------------------------
if(WIN32)
    set(GLM_INCLUDE_DIR "C:/libs/glm-1.0.3/glm")
else()
    set(GLM_INCLUDE_DIR "$ENV{HOME}/libs/glm-1.0.3")
endif()

# -----------------------------------------------------------------------------
# Set Path and Source for GLAD Loader
# -----------------------------------------------------------------------------
if(WIN32)
    set(GLAD_DIR "C:/libs/glad-0.1.36/opengl-3.3")
else()
    set(GLAD_DIR "$ENV{HOME}/libs/glad-0.1.36/opengl-3.3")
endif()

set(GLAD_INCLUDE_DIR "${GLAD_DIR}/include")
set(GLAD_SOURCE "${GLAD_DIR}/src/glad.c")

# -----------------------------------------------------------------------------
# Target Definition
# -----------------------------------------------------------------------------
set(PROJECT_SOURCES
    main.cpp
    ${GLAD_SOURCE}
)

add_executable(app ${PROJECT_SOURCES})

target_include_directories(app PRIVATE 
    ${GLM_INCLUDE_DIR}
    ${GLAD_INCLUDE_DIR}
)

target_link_libraries(app PRIVATE 
    SDL3::SDL3-static 
    SDL3_image::SDL3_image-static 
    OpenGL::GL
)

# -----------------------------------------------------------------------------
# Copy Assets Directory to Build Output
# -----------------------------------------------------------------------------
add_custom_command(
    TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CMAKE_CURRENT_SOURCE_DIR}/assets"
            "$<TARGET_FILE_DIR:app>/assets"
)

# -----------------------------------------------------------------------------
# Platform-Specific Compiler & Linker Flags
# -----------------------------------------------------------------------------
if(WIN32)
    target_compile_options(app PRIVATE -Os)
    target_link_options(app PRIVATE -static -s -static-libgcc)

    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
        target_link_options(app PRIVATE -mconsole)
    else()
        target_link_options(app PRIVATE -mwindows)
    endif()
endif()

Step 4: Create Build & Execution Scripts Шаг 4: Создание скриптов сборки и запуска

Windows (MinGW / Batch Scripts)

config-exe.bat

cmake -G Ninja -S . -B dist/win

build-exe.bat

cmake --build dist/win

run-exe.bat

@echo off
.\dist\win\app.exe

Run the scripts sequentially in Command Prompt or PowerShell: Последовательно запустите скрипты в командной строке или PowerShell:

config-exe.bat
build-exe.bat
run-exe.bat

Ubuntu (Bash Scripts)

config-bin.sh

#!/usr/bin/env bash
set -e
cmake -G Ninja -S . -B dist/linux

build-bin.sh

#!/usr/bin/env bash
set -e
cmake --build dist/linux

run-bin.sh

#!/usr/bin/env bash
set -e
./dist/linux/app

Grant execute permissions on Ubuntu before running: Предоставьте права на выполнение в Ubuntu перед запуском:

chmod +x config-bin.sh build-bin.sh run-bin.sh
./config-bin.sh
./build-bin.sh
./run-bin.sh

Step 5: Expected Output Шаг 5: Ожидаемый результат

Upon running the application, a window will open displaying a smoothly rotating 3D wooden crate cube with crisp, hardware-accelerated 4x anti-aliased edges powered by an offscreen MSAA Framebuffer Object (FBO). После запуска приложения откроется окно с плавно вращающимся 3D-деревянным ящиком с гладкими краями с 4x MSAA сглаживанием, работающим через закадровый буфер кадра (FBO).

SDL3 GLM GLAD 3D Textured Cube (Classic MSAA FBO)

or (if using the Callbacks API version): или (если используется версия с Callbacks API):

SDL3 GLM GLAD 3D Textured Cube (Callbacks MSAA FBO)

Support My Work Поддержать проект

If these tutorials helped you, consider buying me a coffee! Если эти туториалы вам помогли, вы можете поддержать автора.

Sberbank (Russia only) Сбербанк (только для РФ)

Sberbank SBP QR Code

Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)

+7 (917) 212-29-59

USDT TRC20

USDT TRC20 QR Code

Support via Cryptocurrency Поддержка криптовалютой

TMtY1YifNf6FKvgeFmqKGQR4NStKr3csGp