Playing Chicken Dance Animation for Y-Bot using ozz-animation, SDL3 Callbacks, cgltf, and OpenGL 3.3 Воспроизведение анимации танец цыпленка для Y-Bot с использованием ozz-animation, коллбэков SDL3, cgltf и OpenGL 3.3

A step-by-step guide to configuring and integrating ozz-animation, cgltf, GLM, GLAD, and SDL3 libraries to render an animated Y-Bot model via SDL3 Main Callbacks on Windows and Ubuntu. Пошаговое руководство по настройке и интеграции библиотек ozz-animation, cgltf, GLM, GLAD и SDL3 для рендеринга анимированной модели Y-Bot через главные коллбэки SDL3 под Windows и Ubuntu.

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

Create a workspace folder and a new directory for your ozz-animation project: Создайте рабочую папку для проектов и новую директорию для вашего проекта ozz-animation:

Windows (Command Prompt / PowerShell)

mkdir C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp
mkdir C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp\assets\models\y-bot
cd C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp

Ubuntu (Terminal)

mkdir -p ~/projects/y-bot-ozz-animation-opengl-sdl3-cpp/assets/models/y-bot
cd ~/projects/y-bot-ozz-animation-opengl-sdl3-cpp

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

Download the Y-Bot 3D binary glTF model file, its JPEG texture, skeleton data, and chicken dance animation files: Скачайте бинарный файл 3D-модели glTF Y-Bot, её текстуру JPEG, данные скелета и файлы анимации танца цыпленка:

Download y-bot.glb Скачать y-bot.glb Download y-bot.jpg Скачать y-bot.jpg Download skeleton.ozz Скачать skeleton.ozz Download animation.ozz Скачать animation.ozz

Place y-bot.glb, y-bot.jpg, skeleton.ozz, and animation.ozz into your project's assets/models/y-bot/ directory. Поместите файлы y-bot.glb, y-bot.jpg, skeleton.ozz и animation.ozz в папку вашего проекта assets/models/y-bot/.

Windows Path

C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp\assets\models\y-bot\y-bot.glb
C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp\assets\models\y-bot\y-bot.jpg
C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp\assets\models\y-bot\skeleton.ozz
C:\projects\y-bot-ozz-animation-opengl-sdl3-cpp\assets\models\y-bot\animation.ozz

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/y-bot-ozz-animation-opengl-sdl3-cpp/assets/models/y-bot/* \
    ~/projects/y-bot-ozz-animation-opengl-sdl3-cpp/assets/models/y-bot/

Step 3: Create Source Files (main.cpp & CMakeLists.txt) Шаг 3: Создание исходных файлов (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 для выхода.

File Contents for main.cpp (SDL3 Callbacks API) Содержимое файла 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>

// Ozz-animation headers (math headers must precede containers using ozz types)
#include <ozz/base/maths/simd_math.h>
#include <ozz/base/maths/soa_transform.h>
#include <ozz/animation/runtime/animation.h>
#include <ozz/animation/runtime/local_to_model_job.h>
#include <ozz/animation/runtime/sampling_job.h>
#include <ozz/animation/runtime/skeleton.h>
#include <ozz/base/containers/vector.h>
#include <ozz/base/io/archive.h>
#include <ozz/base/io/stream.h>

// Custom SDL3 stream wrapper for ozz-animation archives
class OzzSDLStream : public ozz::io::Stream
{
public:
    OzzSDLStream(SDL_IOStream *io)
        : io_(io)
        , opened_(io != nullptr)
    {
        if (io_)
            SDL_SeekIO(io_, 0, SDL_IO_SEEK_SET);
    }

    ~OzzSDLStream()
    {
        if (io_)
            SDL_CloseIO(io_);
    }

    bool opened() const override { return opened_; }

    size_t Read(void *buffer, size_t size) override
    {
        size_t total = 0;
        while (total < size)
        {
            size_t read = SDL_ReadIO(io_, (uint8_t *)buffer + total, size - total);
            if (read == 0)
                break;
            total += read;
        }
        return total;
    }

    size_t Write(const void *, size_t) override { return 0; }

    int Seek(int offset, Origin origin) override
    {
        SDL_IOWhence whence = SDL_IO_SEEK_SET;
        if (origin == kCurrent)
            whence = SDL_IO_SEEK_CUR;
        else if (origin == kEnd)
            whence = SDL_IO_SEEK_END;

        Sint64 pos = SDL_SeekIO(io_, offset, whence);
        return pos >= 0 ? 0 : -1;
    }

    int Tell() const override
    {
        Sint64 pos = SDL_TellIO(io_);
        return pos >= 0 ? (int)pos : -1;
    }

    size_t Size() const override
    {
        Sint64 size = SDL_GetIOSize(io_);
        return size >= 0 ? (size_t)size : 0;
    }

private:
    SDL_IOStream *io_;
    bool opened_;
};

const char* skinningVertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec4 aJoints;  // Up to 4 influencing joints per vertex
layout (location = 3) in vec4 aWeights; // Weights corresponding to each joint

out vec2 vTexCoord;

uniform mat4 uMvpMatrix;

const int MAX_JOINTS = 100;
uniform mat4 uJointMatrices[MAX_JOINTS];

void main()
{
    mat4 skinMatrix = 
          aWeights.x * uJointMatrices[int(aJoints.x)]
        + aWeights.y * uJointMatrices[int(aJoints.y)]
        + aWeights.z * uJointMatrices[int(aJoints.z)]
        + aWeights.w * uJointMatrices[int(aJoints.w)];

    vec4 skinnedPos = skinMatrix * vec4(aPosition, 1.0);

    vTexCoord = aTexCoord;
    gl_Position = uMvpMatrix * skinnedPos;
}
)";

const char* skinningFragmentShaderSource = R"(
#version 330 core
in vec2 vTexCoord;
uniform sampler2D ourTexture;
out vec4 fragColor;

void main()
{
    fragColor = texture(ourTexture, vTexCoord);
}
)";

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("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 InverseBindMatrix {
    glm::mat4 m;
};

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 VBO_joints = 0;
    GLuint VBO_weights = 0;
    GLuint EBO = 0;
    GLuint textureID = 0;
    GLsizei indexCount = 0;
    GLenum indexType = GL_UNSIGNED_SHORT;
    GLint mvpLoc = -1;
    GLint texLoc = -1;
    GLint jointMatricesLoc = -1;

    // Ozz-animation variables
    ozz::animation::Skeleton ozzSkeleton;
    ozz::animation::Animation ozzAnimation;
    ozz::animation::SamplingJob::Context ozzSamplingContext;
    float ozzAnimationTime = 0.0f;
    ozz::vector<ozz::math::Float4x4> ozzModelMatrices;
    std::vector<InverseBindMatrix> ybotInverseBindMatrices;

    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 ozz-animation Y-Bot Model", 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);

    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(skinningVertexShaderSource, skinningFragmentShaderSource);

    // Load Ozz Skeleton
    const char *skeletonPath = "assets/models/y-bot/skeleton.ozz";
    SDL_IOStream *skeletonIo = SDL_IOFromFile(skeletonPath, "rb");
    if (skeletonIo) {
        OzzSDLStream stream(skeletonIo);
        ozz::io::IArchive archive(&stream);
        if (archive.TestTag<ozz::animation::Skeleton>()) {
            ozz::animation::Skeleton tempSkeleton;
            archive >> tempSkeleton;
            state->ozzSkeleton = std::move(tempSkeleton);
        } else {
            SDL_Log("CRITICAL: Archive tag mismatch for Skeleton!");
        }
    } else {
        SDL_Log("Failed to open ozz skeleton: %s", skeletonPath);
        return SDL_APP_FAILURE;
    }

    // Load Ozz Animation
    const char *animationPath = "assets/models/y-bot/animation.ozz";
    SDL_IOStream *animIo = SDL_IOFromFile(animationPath, "rb");
    if (animIo) {
        OzzSDLStream stream(animIo);
        ozz::io::IArchive archive(&stream);
        archive >> state->ozzAnimation;
    } else {
        SDL_Log("Failed to open ozz animation: %s", animationPath);
        return SDL_APP_FAILURE;
    }

    state->ozzSamplingContext.Resize(state->ozzSkeleton.num_joints());
    state->ozzAnimationTime = 0.0f;
    state->ozzModelMatrices.resize(state->ozzSkeleton.num_joints());

    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* jointsAccessor = nullptr;
        cgltf_accessor* weightsAccessor = 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;
            } else if (prim.attributes[i].type == cgltf_attribute_type_joints) {
                jointsAccessor = prim.attributes[i].data;
            } else if (prim.attributes[i].type == cgltf_attribute_type_weights) {
                weightsAccessor = 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 (jointsAccessor) {
            uint8_t* jointsData = (uint8_t*)jointsAccessor->buffer_view->buffer->data +
                                  jointsAccessor->buffer_view->offset +
                                  jointsAccessor->offset;
            size_t jointsSize = jointsAccessor->count * jointsAccessor->stride;
            void* jointsCopy = SDL_malloc(jointsSize);
            memcpy(jointsCopy, jointsData, jointsSize);

            glGenBuffers(1, &state->VBO_joints);
            glBindBuffer(GL_ARRAY_BUFFER, state->VBO_joints);
            glBufferData(GL_ARRAY_BUFFER, jointsSize, jointsCopy, GL_STATIC_DRAW);
            SDL_free(jointsCopy);

            GLenum jointGlType = GL_UNSIGNED_SHORT;
            size_t componentSize = sizeof(unsigned short);
            if (jointsAccessor->component_type == cgltf_component_type_r_8u) {
                jointGlType = GL_UNSIGNED_BYTE;
                componentSize = sizeof(unsigned char);
            }

            glEnableVertexAttribArray(2);
            glVertexAttribPointer(2, 4, jointGlType, GL_FALSE, 4 * componentSize, (void*)0);
        }

        if (weightsAccessor) {
            float* weightsData = (float*)((uint8_t*)weightsAccessor->buffer_view->buffer->data +
                                           weightsAccessor->buffer_view->offset +
                                           weightsAccessor->offset);
            size_t weightsSize = weightsAccessor->count * 4 * sizeof(float);
            float* weightsCopy = (float*)SDL_malloc(weightsSize);
            memcpy(weightsCopy, weightsData, weightsSize);

            glGenBuffers(1, &state->VBO_weights);
            glBindBuffer(GL_ARRAY_BUFFER, state->VBO_weights);
            glBufferData(GL_ARRAY_BUFFER, weightsSize, weightsCopy, GL_STATIC_DRAW);
            SDL_free(weightsCopy);

            glEnableVertexAttribArray(3);
            glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * 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);

        if (data->skins_count > 0) {
            cgltf_skin& skin = data->skins[0];
            if (skin.inverse_bind_matrices) {
                cgltf_accessor* ibmAccessor = skin.inverse_bind_matrices;
                size_t numJointsInSkin = skin.joints_count;
                state->ybotInverseBindMatrices.resize(numJointsInSkin);
                float* ibmData = (float*)((uint8_t*)ibmAccessor->buffer_view->buffer->data +
                                           ibmAccessor->buffer_view->offset +
                                           ibmAccessor->offset);
                for (size_t i = 0; i < numJointsInSkin; ++i) {
                    memcpy(&state->ybotInverseBindMatrices[i].m[0][0], &ibmData[i * 16], 16 * sizeof(float));
                }
            }
        }

        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.jpg");
    if (state->textureID == 0) {
        SDL_Log("Failed to load Y-Bot JPEG texture.");
        return SDL_APP_FAILURE;
    }

    state->mvpLoc = glGetUniformLocation(state->shaderProgram, "uMvpMatrix");
    state->texLoc = glGetUniformLocation(state->shaderProgram, "ourTexture");
    state->jointMatricesLoc = glGetUniformLocation(state->shaderProgram, "uJointMatrices");
    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;

    // Advance and loop animation time
    if (state->ozzAnimation.duration() > 0.0f) {
        state->ozzAnimationTime += deltaTime;
        if (state->ozzAnimationTime > state->ozzAnimation.duration()) {
            state->ozzAnimationTime = fmod(state->ozzAnimationTime, state->ozzAnimation.duration());
        }

        // Sampling job
        ozz::animation::SamplingJob sampling_job;
        sampling_job.animation = &state->ozzAnimation;
        sampling_job.ratio = state->ozzAnimationTime / state->ozzAnimation.duration();
        sampling_job.context = &state->ozzSamplingContext;

        ozz::vector<ozz::math::SoaTransform> locals;
        locals.resize(state->ozzSkeleton.num_soa_joints());
        sampling_job.output = make_span(locals);

        if (sampling_job.Run()) {
            ozz::animation::LocalToModelJob conv_job;
            conv_job.skeleton = &state->ozzSkeleton;
            conv_job.input = make_span(locals);
            conv_job.output = make_span(state->ozzModelMatrices);
            conv_job.Run();
        }
    }

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

    // Prepare active joint matrix palette for skinning
    const int activeJointIndices[] = {
        2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 
        22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 
        40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 
        58, 59, 60, 61, 62, 63, 64, 65, 66
    };
    const int numActiveJoints = 65;
    std::vector<float> jointMatricesData(numActiveJoints * 16);

    for (int idx = 0; idx < numActiveJoints; ++idx) {
        int i = activeJointIndices[idx];
        const ozz::math::Float4x4 &m = state->ozzModelMatrices[i];

        glm::mat4 ozzMat;
        memcpy(&ozzMat[0][0], &m.cols[0], 16 * sizeof(float));
        glm::mat4 finalJointMat;

        if (idx < (int)state->ybotInverseBindMatrices.size()) {
            finalJointMat = ozzMat * state->ybotInverseBindMatrices[idx].m;
        } else {
            finalJointMat = ozzMat;
        }

        memcpy(&jointMatricesData[idx * 16], &finalJointMat[0][0], 16 * sizeof(float));
    }

    glUniformMatrix4fv(state->jointMatricesLoc, numActiveJoints, GL_FALSE, jointMatricesData.data());

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

    glBindVertexArray(state->VAO);
    glDrawElements(GL_TRIANGLES, state->indexCount, state->indexType, 0);
    glBindVertexArray(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->VBO_joints) glDeleteBuffers(1, &state->VBO_joints);
        if (state->VBO_weights) glDeleteBuffers(1, &state->VBO_weights);
        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();
}
SDL3 Note: Callbacks API Заметка SDL3: Callbacks API

This implementation uses the 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. Эта реализация использует рекомендуемый в 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)

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

project(y-bot-ozz-animation-opengl-sdl3-cpp CXX C)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(OpenGL REQUIRED)

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)

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")
    set(OZZ_INCLUDE_DIR "C:/libs/ozz-animation-0.17.0-mingw/include")
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")
    set(OZZ_INCLUDE_DIR "$ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/include")
endif()

add_executable(app
    main.cpp
    ${GLAD_SOURCE}
)

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

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

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/ozz-animation-0.17.0-mingw/lib/libozz_animation_offline_r.a
        C:/libs/ozz-animation-0.17.0-mingw/lib/libozz_animation_r.a
        C:/libs/ozz-animation-0.17.0-mingw/lib/libozz_geometry_r.a
        C:/libs/ozz-animation-0.17.0-mingw/lib/libozz_options_r.a
        C:/libs/ozz-animation-0.17.0-mingw/lib/libozz_base_r.a
    )
else()
    target_link_libraries(app PRIVATE 
        SDL3_image::SDL3_image-static
        SDL3::SDL3-static
        OpenGL::GL
        $ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/lib/libozz_animation_offline_r.a
        $ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/lib/libozz_animation_r.a
        $ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/lib/libozz_geometry_r.a
        $ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/lib/libozz_options_r.a
        $ENV{HOME}/libs/ozz-animation-0.17.0-ubuntu/lib/libozz_base_r.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

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

After running the build and execution scripts, a window should appear displaying the animated Y-Bot character performing the chicken dance: После запуска скриптов сборки и выполнения появится окно с отображением анимированного персонажа Y-Bot, исполняющего танец цыпленка:

SDL3 ozz-animation Y-Bot Model

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