Loading 3D GLB Model with WebP Texture, cgltf, GLM, GLAD, and OpenGL 3.3 Загрузка 3D-модели GLB с текстурой WebP, cgltf, GLM, GLAD и OpenGL 3.3

A step-by-step guide to parsing a binary 3D model (.glb) using cgltf, leveraging WebP texture compression via SDL3_image with static WebP libraries for efficient asset size, and rendering a 3D Y-Bot model rotating around its Y-axis using GLM perspective projection on Windows and Ubuntu. Пошаговое руководство по разбору бинарной 3D-модели (.glb) с помощью cgltf, использованию сжатия текстур WebP через SDL3_image со статическими библиотеками WebP для уменьшения размера активов и отрисовке 3D-модели Y-Bot с вращением вокруг оси Y с использованием перспективной проекции GLM под Windows и Ubuntu.

Note on WebP Integration Примечание об интеграции WebP

This guide utilizes WebP texture format support through statically linked WebP libraries (`libwebp`, `libwebpdemux`, `libwebpmux`, and `libsharpyuv`) alongside `SDL3_image`. В этом руководстве используется поддержка формата текстур WebP через статический линкуемые библиотеки WebP (`libwebp`, `libwebpdemux`, `libwebpmux` и `libsharpyuv`) в сочетании с `SDL3_image`.

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

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

Windows (Command Prompt / PowerShell)

mkdir C:\projects\load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop
mkdir C:\projects\load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop\assets\models\y-bot
cd C:\projects\load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop

Ubuntu / WSL (Terminal)

mkdir -p ~/projects/load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop/assets/models/y-bot
cd ~/projects/load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop

Step 2: Download & Place Model & WebP Texture Assets Шаг 2: Скачивание и размещение модели и текстуры WebP

Download the Y-Bot 3D binary glTF model file and its compressed WebP texture: Скачайте бинарный файл 3D-модели glTF Y-Bot и её сжатую текстуру WebP:

Download y-bot.glb Скачать y-bot.glb Download y-bot.webp (WebP Texture) Скачать y-bot.webp (Текстура WebP)

Place both y-bot.glb and y-bot.webp into your project's assets/models/y-bot/ directory. Поместите оба файла y-bot.glb и y-bot.webp в папку вашего проекта assets/models/y-bot/.

Windows Path

C:\projects\load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop\assets\models\y-bot\y-bot.glb
C:\projects\load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop\assets\models\y-bot\y-bot.webp

Ubuntu / WSL Command (Copying from Windows to WSL)

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

cp /mnt/c/projects/load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop/assets/models/y-bot/y-bot.* \
   ~/projects/load-glb-model-webp-cgltf-sdl3-glm-glad-opengl-desktop/assets/models/y-bot/

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>

#define CGLTF_IMPLEMENTATION
#include <cgltf.h>

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) {
    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("WebP 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;
    }

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

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 Y-Bot WebP Model (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);

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

    const char* modelPath = "assets/models/y-bot/y-bot.glb";
    size_t glbSize = 0;
    void* glbData = SDL_LoadFile(modelPath, &glbSize);
    if (!glbData) {
        SDL_Log("Failed to load model file into memory: %s", SDL_GetError());
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    cgltf_options options = {};
    cgltf_data* data = nullptr;
    cgltf_result result = cgltf_parse(&options, glbData, glbSize, &data);

    GLuint VAO = 0, VBO_pos = 0, VBO_uv = 0, EBO = 0;
    GLsizei indexCount = 0;
    GLenum indexType = GL_UNSIGNED_SHORT;

    if (result == cgltf_result_success) {
        cgltf_load_buffers(&options, data, modelPath);

        cgltf_mesh& mesh = data->meshes[0];
        cgltf_primitive& prim = mesh.primitives[0];

        cgltf_accessor* posAccessor = nullptr;
        cgltf_accessor* uvAccessor = nullptr;
        cgltf_accessor* indexAccessor = prim.indices;

        for (cgltf_size i = 0; i < prim.attributes_count; ++i) {
            if (prim.attributes[i].type == cgltf_attribute_type_position) {
                posAccessor = prim.attributes[i].data;
            } else if (prim.attributes[i].type == cgltf_attribute_type_texcoord) {
                uvAccessor = prim.attributes[i].data;
            }
        }

        glGenVertexArrays(1, &VAO);
        glBindVertexArray(VAO);

        if (posAccessor) {
            float* positions = (float*)((uint8_t*)posAccessor->buffer_view->buffer->data +
                                         posAccessor->buffer_view->offset +
                                         posAccessor->offset);
            size_t posSize = posAccessor->count * 3 * sizeof(float);
            float* positionsCopy = (float*)SDL_malloc(posSize);
            memcpy(positionsCopy, positions, posSize);

            glGenBuffers(1, &VBO_pos);
            glBindBuffer(GL_ARRAY_BUFFER, VBO_pos);
            glBufferData(GL_ARRAY_BUFFER, posSize, positionsCopy, GL_STATIC_DRAW);
            SDL_free(positionsCopy);

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

        if (uvAccessor) {
            float* uvs = (float*)((uint8_t*)uvAccessor->buffer_view->buffer->data +
                                   uvAccessor->buffer_view->offset +
                                   uvAccessor->offset);
            size_t uvSize = uvAccessor->count * 2 * sizeof(float);
            float* uvsCopy = (float*)SDL_malloc(uvSize);
            memcpy(uvsCopy, uvs, uvSize);

            glGenBuffers(1, &VBO_uv);
            glBindBuffer(GL_ARRAY_BUFFER, VBO_uv);
            glBufferData(GL_ARRAY_BUFFER, uvSize, uvsCopy, GL_STATIC_DRAW);
            SDL_free(uvsCopy);

            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
        }

        if (indexAccessor) {
            void* indices = (void*)((uint8_t*)indexAccessor->buffer_view->buffer->data +
                                    indexAccessor->buffer_view->offset +
                                    indexAccessor->offset);
            size_t indexSize = indexAccessor->count * indexAccessor->stride;
            void* indicesCopy = SDL_malloc(indexSize);
            memcpy(indicesCopy, indices, indexSize);

            glGenBuffers(1, &EBO);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize, indicesCopy, GL_STATIC_DRAW);
            SDL_free(indicesCopy);

            if (indexAccessor->component_type == cgltf_component_type_r_32u) {
                indexType = GL_UNSIGNED_INT;
            } else if (indexAccessor->component_type == cgltf_component_type_r_16u) {
                indexType = GL_UNSIGNED_SHORT;
            } else if (indexAccessor->component_type == cgltf_component_type_r_8u) {
                indexType = GL_UNSIGNED_BYTE;
            }

            indexCount = (GLsizei)indexAccessor->count;
        }

        glBindVertexArray(0);

        cgltf_free(data);
        SDL_free(glbData);
    } else {
        SDL_Log("Failed to parse GLB model: %s", modelPath);
        SDL_free(glbData);
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    GLuint textureID = createTexture("assets/models/y-bot/y-bot.webp");
    if (textureID == 0) {
        SDL_Log("Failed to load Y-Bot WebP texture.");
        glDeleteVertexArrays(1, &VAO);
        if (VBO_pos) glDeleteBuffers(1, &VBO_pos);
        if (VBO_uv) glDeleteBuffers(1, &VBO_uv);
        if (EBO) glDeleteBuffers(1, &EBO);
        glDeleteProgram(shaderProgram);
        SDL_GL_DestroyContext(glContext);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

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

    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);
        glViewport(0, 0, winW, winH);

        float aspect = (float)winW / (float)(winH == 0 ? 1 : 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.2f, 3.0f), glm::vec3(0.0f, 0.9f, 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.0f, 1.0f, 0.0f));

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

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

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

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

        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, indexCount, indexType, 0);

        SDL_GL_SwapWindow(window);
    }

    glDeleteTextures(1, &textureID);
    glDeleteVertexArrays(1, &VAO);
    if (VBO_pos) glDeleteBuffers(1, &VBO_pos);
    if (VBO_uv) glDeleteBuffers(1, &VBO_uv);
    if (EBO) glDeleteBuffers(1, &EBO);
    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>

#define CGLTF_IMPLEMENTATION
#include <cgltf.h>

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) {
    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("WebP 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;
    }

    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_pos = 0;
    GLuint VBO_uv = 0;
    GLuint EBO = 0;
    GLuint textureID = 0;
    GLsizei indexCount = 0;
    GLenum indexType = GL_UNSIGNED_SHORT;
    GLint mvpLoc = -1;
    GLint texLoc = -1;
    float rotationAngle = 0.0f;
    Uint64 lastTicks = 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 Y-Bot WebP Model (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 (!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);

    const char* modelPath = "assets/models/y-bot/y-bot.glb";
    size_t glbSize = 0;
    void* glbData = SDL_LoadFile(modelPath, &glbSize);

    if (!glbData) {
        SDL_Log("Failed to load Y-Bot model file into memory: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    cgltf_options options = {};
    cgltf_data* data = nullptr;
    cgltf_result result = cgltf_parse(&options, glbData, glbSize, &data);

    if (result == cgltf_result_success) {
        cgltf_load_buffers(&options, data, modelPath);

        cgltf_mesh& mesh = data->meshes[0];
        cgltf_primitive& prim = mesh.primitives[0];

        cgltf_accessor* posAccessor = nullptr;
        cgltf_accessor* uvAccessor = nullptr;
        cgltf_accessor* indexAccessor = prim.indices;

        for (cgltf_size i = 0; i < prim.attributes_count; ++i) {
            if (prim.attributes[i].type == cgltf_attribute_type_position) {
                posAccessor = prim.attributes[i].data;
            } else if (prim.attributes[i].type == cgltf_attribute_type_texcoord) {
                uvAccessor = prim.attributes[i].data;
            }
        }

        glGenVertexArrays(1, &state->VAO);
        glBindVertexArray(state->VAO);

        if (posAccessor) {
            float* positions = (float*)((uint8_t*)posAccessor->buffer_view->buffer->data +
                                         posAccessor->buffer_view->offset +
                                         posAccessor->offset);
            size_t posSize = posAccessor->count * 3 * sizeof(float);
            float* positionsCopy = (float*)SDL_malloc(posSize);
            memcpy(positionsCopy, positions, posSize);

            glGenBuffers(1, &state->VBO_pos);
            glBindBuffer(GL_ARRAY_BUFFER, state->VBO_pos);
            glBufferData(GL_ARRAY_BUFFER, posSize, positionsCopy, GL_STATIC_DRAW);
            SDL_free(positionsCopy);

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

        if (uvAccessor) {
            float* uvs = (float*)((uint8_t*)uvAccessor->buffer_view->buffer->data +
                                   uvAccessor->buffer_view->offset +
                                   uvAccessor->offset);
            size_t uvSize = uvAccessor->count * 2 * sizeof(float);
            float* uvsCopy = (float*)SDL_malloc(uvSize);
            memcpy(uvsCopy, uvs, uvSize);

            glGenBuffers(1, &state->VBO_uv);
            glBindBuffer(GL_ARRAY_BUFFER, state->VBO_uv);
            glBufferData(GL_ARRAY_BUFFER, uvSize, uvsCopy, GL_STATIC_DRAW);
            SDL_free(uvsCopy);

            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
        }

        if (indexAccessor) {
            void* indices = (void*)((uint8_t*)indexAccessor->buffer_view->buffer->data +
                                    indexAccessor->buffer_view->offset +
                                    indexAccessor->offset);
            size_t indexSize = indexAccessor->count * indexAccessor->stride;
            void* indicesCopy = SDL_malloc(indexSize);
            memcpy(indicesCopy, indices, indexSize);

            glGenBuffers(1, &state->EBO);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, state->EBO);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize, indicesCopy, GL_STATIC_DRAW);
            SDL_free(indicesCopy);

            if (indexAccessor->component_type == cgltf_component_type_r_32u) {
                state->indexType = GL_UNSIGNED_INT;
            } else if (indexAccessor->component_type == cgltf_component_type_r_16u) {
                state->indexType = GL_UNSIGNED_SHORT;
            } else if (indexAccessor->component_type == cgltf_component_type_r_8u) {
                state->indexType = GL_UNSIGNED_BYTE;
            }

            state->indexCount = (GLsizei)indexAccessor->count;
        }

        glBindVertexArray(0);

        cgltf_free(data);
        SDL_free(glbData);
    } else {
        SDL_Log("Failed to parse Y-Bot GLB model: %s", modelPath);
        SDL_free(glbData);
        return SDL_APP_FAILURE;
    }

    state->textureID = createTexture("assets/models/y-bot/y-bot.webp");
    if (state->textureID == 0) {
        SDL_Log("Failed to load Y-Bot WebP texture.");
        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);
    glViewport(0, 0, winW, winH);

    float aspect = (float)winW / (float)(winH == 0 ? 1 : 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.2f, 3.0f), glm::vec3(0.0f, 0.9f, 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.0f, 1.0f, 0.0f));

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

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

    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);
    glDrawElements(GL_TRIANGLES, state->indexCount, state->indexType, 0);

    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->textureID) glDeleteTextures(1, &state->textureID);
        if (state->VAO) glDeleteVertexArrays(1, &state->VAO);
        if (state->VBO_pos) glDeleteBuffers(1, &state->VBO_pos);
        if (state->VBO_uv) glDeleteBuffers(1, &state->VBO_uv);
        if (state->EBO) glDeleteBuffers(1, &state->EBO);
        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(load-glb-model-webp-cgltf-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, SDL3_image, and WebP 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-webp-mingw/lib/cmake/SDL3_image")
    set(webp_ROOT "C:/libs/webp-1.6.0-mingw")
    set(webp_INCLUDE_DIR "C:/libs/webp-1.6.0-mingw/include")
    set(webp_LIBRARY "C:/libs/webp-1.6.0-mingw/lib/libwebp.a")
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-webp-ubuntu/lib/cmake/SDL3_image")
    set(webp_ROOT "$ENV{HOME}/libs/webp-1.6.0-ubuntu")
    set(webp_INCLUDE_DIR "$ENV{HOME}/libs/webp-1.6.0-ubuntu/include")
    set(webp_LIBRARY "$ENV{HOME}/libs/webp-1.6.0-ubuntu/lib/libwebp.a")
endif()

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

# -----------------------------------------------------------------------------
# Set Include Paths for Header-Only and Source Libraries
# -----------------------------------------------------------------------------
if(WIN32)
    set(GLM_INCLUDE_DIR "C:/libs/glm-1.0.3/glm")
    set(CGLTF_INCLUDE_DIR "C:/libs/cgltf-1.15")
    set(GLAD_INCLUDE_DIR "C:/libs/glad-0.1.36/opengl-3.3/include")
    set(GLAD_SOURCE "C:/libs/glad-0.1.36/opengl-3.3/src/glad.c")
else()
    set(GLM_INCLUDE_DIR "$ENV{HOME}/libs/glm-1.0.3")
    set(CGLTF_INCLUDE_DIR "$ENV{HOME}/libs/cgltf-1.15")
    set(GLAD_INCLUDE_DIR "$ENV{HOME}/libs/glad-0.1.36/opengl-3.3/include")
    set(GLAD_SOURCE "$ENV{HOME}/libs/glad-0.1.36/opengl-3.3/src/glad.c")
endif()

# -----------------------------------------------------------------------------
# Target Definition & Sources
# -----------------------------------------------------------------------------
add_executable(app
    main.cpp
    ${GLAD_SOURCE}
)

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

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

# -----------------------------------------------------------------------------
# Link Libraries & 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()

    target_link_libraries(app PRIVATE 
        SDL3_image::SDL3_image-static
        SDL3::SDL3-static
        OpenGL::GL
        C:/libs/webp-1.6.0-mingw/lib/libwebpdemux.a
        C:/libs/webp-1.6.0-mingw/lib/libwebpmux.a
        C:/libs/webp-1.6.0-mingw/lib/libwebp.a
        C:/libs/webp-1.6.0-mingw/lib/libsharpyuv.a
    )
else()
    target_link_libraries(app PRIVATE 
        SDL3_image::SDL3_image-static
        SDL3::SDL3-static
        OpenGL::GL
        $ENV{HOME}/libs/webp-1.6.0-ubuntu/lib/libwebpdemux.a
        $ENV{HOME}/libs/webp-1.6.0-ubuntu/lib/libwebpmux.a
        $ENV{HOME}/libs/webp-1.6.0-ubuntu/lib/libwebp.a
        $ENV{HOME}/libs/webp-1.6.0-ubuntu/lib/libsharpyuv.a
    )
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

Test permissions and execute on Ubuntu: Настройте права и запустите под 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 the textured Y-Bot 3D model with WebP texture compression loaded via cgltf, rotating around its vertical Y-axis smoothly using delta time. После запуска приложения откроется окно с 3D-моделью Y-Bot с текстурой WebP, загруженной через cgltf, плавно вращающейся вокруг вертикальной оси Y с использованием дельта-тайма.

SDL3 GLM GLAD Y-Bot WebP Model (Classic)

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

SDL3 GLM GLAD Y-Bot WebP Model (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