Drawing a Triangle with SDL3, GLM, and OpenGL 3.3 (Without GLAD) Отрисовка треугольника с помощью SDL3, GLM и OpenGL 3.3 (Без GLAD)

A step-by-step guide to drawing a colored 2D triangle using modern OpenGL 3.3 Core profile, GLM matrix transformations, and SDL3 via dynamic function pointer loading on Windows and Ubuntu. Пошаговое руководство по отрисовке цветного 2D-треугольника с использованием профиля OpenGL 3.3 Core, матричных трансформаций GLM и SDL3 через динамическую загрузку указателей на функции под Windows и Ubuntu.

Before proceeding, make sure you have built and installed the static libraries on your system using the appropriate guides: Перед продолжением убедитесь, что вы собрали и установили статические библиотеки в вашей системе, следуя соответствующим инструкциям:

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

Create a workspace folder and a new directory for your SDL3 GLM OpenGL triangle project: Создайте рабочую папку для проектов и новую директорию для вашего тестового проекта треугольника SDL3 GLM OpenGL:

Windows (Command Prompt / PowerShell)

mkdir C:\projects\triangle-sdl3-glm-opengl-cpp
cd C:\projects\triangle-sdl3-glm-opengl-cpp

Ubuntu (Terminal)

mkdir -p ~/projects/triangle-sdl3-glm-opengl-cpp
cd ~/projects/triangle-sdl3-glm-opengl-cpp

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

You can create these files using Notepad / Sublime Text on Windows, or nano in Ubuntu terminal: Вы можете создать эти файлы через Блокнот / Sublime Text в Windows или с помощью nano в терминале Ubuntu:

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 для выхода.
Under the Hood: Modern OpenGL Function LoadingПод капотом: Загрузка функций Modern OpenGL
On Windows, standard system headers only declare legacy OpenGL 1.1 function prototypes. Modern Core profile features (VAOs, VBOs, Shaders, Program Objects) require dynamically fetching function pointers via SDL_GL_GetProcAddress. This tutorial demonstrates how to manually load these pointers before moving on to helper libraries like GLAD. В Windows стандартные системные заголовки объявляют только прототипы функций унаследованного OpenGL 1.1. Возможности современного профиля Core (VAO, VBO, шейдеры, программные объекты) требуют динамического получения указателей на функции через SDL_GL_GetProcAddress. Этот туториал демонстрирует ручную загрузку этих указателей перед переходом к библиотекам-загрузчикам вроде GLAD.

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

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define GL_GLEXT_PROTOTYPES
#include <SDL3/SDL_opengl.h>
#include <SDL3/SDL_opengl_glext.h>

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

// Vertex Shader source code
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;
uniform mat4 uMVP;

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

// Fragment Shader source code
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
in vec3 ourColor;

void main() {
    FragColor = vec4(ourColor, 1.0);
}
)";

// Function pointer pointers for OpenGL 3.3 Core dynamic loading
PFNGLCREATESHADERPROC glCreateShader_ptr = nullptr;
PFNGLSHADERSOURCEPROC glShaderSource_ptr = nullptr;
PFNGLCOMPILESHADERPROC glCompileShader_ptr = nullptr;
PFNGLGETSHADERIVPROC glGetShaderiv_ptr = nullptr;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog_ptr = nullptr;
PFNGLCREATEPROGRAMPROC glCreateProgram_ptr = nullptr;
PFNGLATTACHSHADERPROC glAttachShader_ptr = nullptr;
PFNGLLINKPROGRAMPROC glLinkProgram_ptr = nullptr;
PFNGLGETPROGRAMIVPROC glGetProgramiv_ptr = nullptr;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog_ptr = nullptr;
PFNGLDELETESHADERPROC glDeleteShader_ptr = nullptr;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays_ptr = nullptr;
PFNGLGENBUFFERSPROC glGenBuffers_ptr = nullptr;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray_ptr = nullptr;
PFNGLBINDBUFFERPROC glBindBuffer_ptr = nullptr;
PFNGLBUFFERDATAPROC glBufferData_ptr = nullptr;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer_ptr = nullptr;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray_ptr = nullptr;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation_ptr = nullptr;
PFNGLUSEPROGRAMPROC glUseProgram_ptr = nullptr;
PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv_ptr = nullptr;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_ptr = nullptr;
PFNGLDELETEBUFFERSPROC glDeleteBuffers_ptr = nullptr;
PFNGLDELETEPROGRAMPROC glDeleteProgram_ptr = nullptr;

// Redefining symbol calls to redirected function pointers
#define glCreateShader glCreateShader_ptr
#define glShaderSource glShaderSource_ptr
#define glCompileShader glCompileShader_ptr
#define glGetShaderiv glGetShaderiv_ptr
#define glGetShaderInfoLog glGetShaderInfoLog_ptr
#define glCreateProgram glCreateProgram_ptr
#define glAttachShader glAttachShader_ptr
#define glLinkProgram glLinkProgram_ptr
#define glGetProgramiv glGetProgramiv_ptr
#define glGetProgramInfoLog glGetProgramInfoLog_ptr
#define glDeleteShader glDeleteShader_ptr
#define glGenVertexArrays glGenVertexArrays_ptr
#define glGenBuffers glGenBuffers_ptr
#define glBindVertexArray glBindVertexArray_ptr
#define glBindBuffer glBindBuffer_ptr
#define glBufferData glBufferData_ptr
#define glVertexAttribPointer glVertexAttribPointer_ptr
#define glEnableVertexAttribArray glEnableVertexAttribArray_ptr
#define glGetUniformLocation glGetUniformLocation_ptr
#define glUseProgram glUseProgram_ptr
#define glUniformMatrix4fv glUniformMatrix4fv_ptr
#define glDeleteVertexArrays glDeleteVertexArrays_ptr
#define glDeleteBuffers glDeleteBuffers_ptr
#define glDeleteProgram glDeleteProgram_ptr

bool loadOpenGLExtensions() {
    #define LOAD_PROC(type, name) \
        name##_ptr = (type)SDL_GL_GetProcAddress(#name); \
        if (!name##_ptr) { SDL_Log("Failed to load: %s", #name); return false; }

    LOAD_PROC(PFNGLCREATESHADERPROC, glCreateShader);
    LOAD_PROC(PFNGLSHADERSOURCEPROC, glShaderSource);
    LOAD_PROC(PFNGLCOMPILESHADERPROC, glCompileShader);
    LOAD_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv);
    LOAD_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog);
    LOAD_PROC(PFNGLCREATEPROGRAMPROC, glCreateProgram);
    LOAD_PROC(PFNGLATTACHSHADERPROC, glAttachShader);
    LOAD_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram);
    LOAD_PROC(PFNGLGETPROGRAMIVPROC, glGetProgramiv);
    LOAD_PROC(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog);
    LOAD_PROC(PFNGLDELETESHADERPROC, glDeleteShader);
    LOAD_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays);
    LOAD_PROC(PFNGLGENBUFFERSPROC, glGenBuffers);
    LOAD_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray);
    LOAD_PROC(PFNGLBINDBUFFERPROC, glBindBuffer);
    LOAD_PROC(PFNGLBUFFERDATAPROC, glBufferData);
    LOAD_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer);
    LOAD_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
    LOAD_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation);
    LOAD_PROC(PFNGLUSEPROGRAMPROC, glUseProgram);
    LOAD_PROC(PFNGLUNIFORMMATRIX4FVPROC, glUniformMatrix4fv);
    LOAD_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays);
    LOAD_PROC(PFNGLDELETEBUFFERSPROC, glDeleteBuffers);
    LOAD_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram);

    #undef LOAD_PROC
    return true;
}

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;
}

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

    // Configure OpenGL 3.3 Core Profile attributes
    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_Window* window = SDL_CreateWindow("SDL3 GLM OpenGL 3.3 Triangle (Classic)", 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); // Enable VSync

    // Load OpenGL function pointers via SDL
    if (!loadOpenGLExtensions()) {
        SDL_Log("Failed to load required OpenGL procedures.");
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    GLuint shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

    // Vertex data: Position (X, Y, Z) and Color (R, G, B)
    float vertices[] = {
        // Positions         // Colors
         0.0f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, // Top (Red)
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, // Bottom Left (Green)
         0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f  // Bottom Right (Blue)
    };

    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);

    // Position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // Color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // Setup GLM Matrices
    glm::vec3 clearColorVec(0.1f, 0.12f, 0.15f);
    glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 450.0f, 0.1f, 100.0f);
    glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    glm::mat4 model = glm::mat4(1.0f);
    glm::mat4 mvp = projection * view * model;

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

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

        // Clear screen
        glClearColor(clearColorVec.r, clearColorVec.g, clearColorVec.b, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

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

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

        SDL_GL_SwapWindow(window);
    }

    // Cleanup resources
    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 <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define GL_GLEXT_PROTOTYPES
#include <SDL3/SDL_opengl.h>
#include <SDL3/SDL_opengl_glext.h>

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

// Vertex Shader source code
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;
uniform mat4 uMVP;

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

// Fragment Shader source code
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
in vec3 ourColor;

void main() {
    FragColor = vec4(ourColor, 1.0);
}
)";

// Function pointer pointers for OpenGL 3.3 Core dynamic loading
PFNGLCREATESHADERPROC glCreateShader_ptr = nullptr;
PFNGLSHADERSOURCEPROC glShaderSource_ptr = nullptr;
PFNGLCOMPILESHADERPROC glCompileShader_ptr = nullptr;
PFNGLGETSHADERIVPROC glGetShaderiv_ptr = nullptr;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog_ptr = nullptr;
PFNGLCREATEPROGRAMPROC glCreateProgram_ptr = nullptr;
PFNGLATTACHSHADERPROC glAttachShader_ptr = nullptr;
PFNGLLINKPROGRAMPROC glLinkProgram_ptr = nullptr;
PFNGLGETPROGRAMIVPROC glGetProgramiv_ptr = nullptr;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog_ptr = nullptr;
PFNGLDELETESHADERPROC glDeleteShader_ptr = nullptr;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays_ptr = nullptr;
PFNGLGENBUFFERSPROC glGenBuffers_ptr = nullptr;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray_ptr = nullptr;
PFNGLBINDBUFFERPROC glBindBuffer_ptr = nullptr;
PFNGLBUFFERDATAPROC glBufferData_ptr = nullptr;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer_ptr = nullptr;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray_ptr = nullptr;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation_ptr = nullptr;
PFNGLUSEPROGRAMPROC glUseProgram_ptr = nullptr;
PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv_ptr = nullptr;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_ptr = nullptr;
PFNGLDELETEBUFFERSPROC glDeleteBuffers_ptr = nullptr;
PFNGLDELETEPROGRAMPROC glDeleteProgram_ptr = nullptr;

#define glCreateShader glCreateShader_ptr
#define glShaderSource glShaderSource_ptr
#define glCompileShader glCompileShader_ptr
#define glGetShaderiv glGetShaderiv_ptr
#define glGetShaderInfoLog glGetShaderInfoLog_ptr
#define glCreateProgram glCreateProgram_ptr
#define glAttachShader glAttachShader_ptr
#define glLinkProgram glLinkProgram_ptr
#define glGetProgramiv glGetProgramiv_ptr
#define glGetProgramInfoLog glGetProgramInfoLog_ptr
#define glDeleteShader glDeleteShader_ptr
#define glGenVertexArrays glGenVertexArrays_ptr
#define glGenBuffers glGenBuffers_ptr
#define glBindVertexArray glBindVertexArray_ptr
#define glBindBuffer glBindBuffer_ptr
#define glBufferData glBufferData_ptr
#define glVertexAttribPointer glVertexAttribPointer_ptr
#define glEnableVertexAttribArray glEnableVertexAttribArray_ptr
#define glGetUniformLocation glGetUniformLocation_ptr
#define glUseProgram glUseProgram_ptr
#define glUniformMatrix4fv glUniformMatrix4fv_ptr
#define glDeleteVertexArrays glDeleteVertexArrays_ptr
#define glDeleteBuffers glDeleteBuffers_ptr
#define glDeleteProgram glDeleteProgram_ptr

bool loadOpenGLExtensions() {
    #define LOAD_PROC(type, name) \
        name##_ptr = (type)SDL_GL_GetProcAddress(#name); \
        if (!name##_ptr) { SDL_Log("Failed to load: %s", #name); return false; }

    LOAD_PROC(PFNGLCREATESHADERPROC, glCreateShader);
    LOAD_PROC(PFNGLSHADERSOURCEPROC, glShaderSource);
    LOAD_PROC(PFNGLCOMPILESHADERPROC, glCompileShader);
    LOAD_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv);
    LOAD_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog);
    LOAD_PROC(PFNGLCREATEPROGRAMPROC, glCreateProgram);
    LOAD_PROC(PFNGLATTACHSHADERPROC, glAttachShader);
    LOAD_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram);
    LOAD_PROC(PFNGLGETPROGRAMIVPROC, glGetProgramiv);
    LOAD_PROC(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog);
    LOAD_PROC(PFNGLDELETESHADERPROC, glDeleteShader);
    LOAD_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays);
    LOAD_PROC(PFNGLGENBUFFERSPROC, glGenBuffers);
    LOAD_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray);
    LOAD_PROC(PFNGLBINDBUFFERPROC, glBindBuffer);
    LOAD_PROC(PFNGLBUFFERDATAPROC, glBufferData);
    LOAD_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer);
    LOAD_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
    LOAD_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation);
    LOAD_PROC(PFNGLUSEPROGRAMPROC, glUseProgram);
    LOAD_PROC(PFNGLUNIFORMMATRIX4FVPROC, glUniformMatrix4fv);
    LOAD_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays);
    LOAD_PROC(PFNGLDELETEBUFFERSPROC, glDeleteBuffers);
    LOAD_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram);

    #undef LOAD_PROC
    return true;
}

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;
}

struct AppState {
    SDL_Window* window = nullptr;
    SDL_GLContext glContext = nullptr;
    GLuint shaderProgram = 0;
    GLuint VAO = 0;
    GLuint VBO = 0;
    GLint mvpLoc = -1;
    glm::mat4 mvp = glm::mat4(1.0f);
    glm::vec3 clearColorVec = glm::vec3(0.1f, 0.12f, 0.15f);
};

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_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);

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

    state->window = SDL_CreateWindow("SDL3 GLM OpenGL 3.3 Triangle (Callbacks)", 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 (!loadOpenGLExtensions()) {
        SDL_Log("Failed to load required OpenGL procedures.");
        return SDL_APP_FAILURE;
    }

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

    float vertices[] = {
        // Positions         // Colors
         0.0f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f,
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,
         0.5f, -0.5f, 0.0f,  0.0f, 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);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

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

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 450.0f, 0.1f, 100.0f);
    glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    glm::mat4 model = glm::mat4(1.0f);
    state->mvp = projection * view * model;

    state->mvpLoc = glGetUniformLocation(state->shaderProgram, "uMVP");

    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);

    glClearColor(state->clearColorVec.r, state->clearColorVec.g, state->clearColorVec.b, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

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

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

    SDL_GL_SwapWindow(state->window);

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result) {
    if (appstate) {
        auto* state = static_cast(appstate);
        
        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();
}
SDL3 Note: Classic Loop vs. Callbacks API Заметка SDL3: Классический цикл vs. Callbacks API

Option 1A uses the traditional main() event loop. Option 1B uses the new SDL3 recommended Callbacks API (via SDL_MAIN_USE_CALLBACKS). The Callbacks API hands main loop control over to the OS, preventing window rendering freezes during drag/resize operations and enabling seamless cross-platform support across WebAssembly, iOS, and Android. Вариант 1A использует традиционный цикл событий main(). Вариант 1B использует новый рекомендуемый в SDL3 Callbacks API (через SDL_MAIN_USE_CALLBACKS). Callbacks API передаёт управление основным циклом операционной системе, что предотвращает зависание рендеринга при перетаскивании или изменении размера окна и обеспечивает нативную кроссплатформенность для WebAssembly, iOS и Android.

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

cmake_minimum_required(VERSION 3.21)

# Default to Debug if CMAKE_BUILD_TYPE is not set via command line
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Debug")
endif()

project(triangle-sdl3-glm-opengl-cpp CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

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

# -----------------------------------------------------------------------------
# Locate SDL3 Package via Custom Paths
# -----------------------------------------------------------------------------
if(WIN32)
    # Points to the directory containing SDL3Config.cmake
    set(SDL3_DIR "C:/libs/sdl3-3.4.16-mingw/lib/cmake/SDL3")
else()
    set(SDL3_DIR "$ENV{HOME}/libs/sdl3-3.4.16-ubuntu/lib/cmake/SDL3")
endif()

find_package(SDL3 REQUIRED CONFIG)

# -----------------------------------------------------------------------------
# Set Include Path for Header-Only GLM Library
# -----------------------------------------------------------------------------
if(WIN32)
    # Points to C:/libs/glm-1.0.3/glm where the inner 'glm' directory is located
    set(GLM_INCLUDE_DIR "C:/libs/glm-1.0.3/glm")
else()
    set(GLM_INCLUDE_DIR "$ENV{HOME}/libs/glm-1.0.3)
endif()

# -----------------------------------------------------------------------------
# Target Definition
# -----------------------------------------------------------------------------
set(PROJECT_SOURCES
    main.cpp
)

add_executable(app ${PROJECT_SOURCES})

# Include GLM headers directory
target_include_directories(app PRIVATE ${GLM_INCLUDE_DIR})

# Link SDL3 statically and system OpenGL library
target_link_libraries(app PRIVATE SDL3::SDL3-static OpenGL::GL)

# -----------------------------------------------------------------------------
# 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 3: Create Build & Execution Scripts Шаг 3: Создание скриптов сборки и запуска

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 4: Expected Output Шаг 4: Ожидаемый результат

After running the build and execution scripts, a window showing a smoothly interpolated RGB triangle on a dark gray background will appear. После запуска скриптов сборки и выполнения откроется окно с интерполированным RGB-треугольником на темно-сером фоне.

The window title will display: Заголовок окна будет отображать:

SDL3 GLM OpenGL 3.3 Triangle (Classic)

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

SDL3 GLM OpenGL 3.3 Triangle (Callbacks)

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