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.
- Installing cgltf C glTF Loading Library Установка C-библиотеки загрузки glTF cgltf
- Installing GLAD OpenGL Loader Library Установка библиотеки-загрузчика OpenGL GLAD
- Installing GLM Library Установка библиотеки GLM
- Building SDL3 Static Library for Windows (MinGW & CMake) Сборка статической библиотеки SDL3 для Windows (MinGW и CMake)
- Building SDL3 Static Library for Ubuntu (GCC, CMake & Ninja) Сборка статической библиотеки SDL3 для Ubuntu (GCC, CMake и Ninja)
- Building SDL3_image Static Library for Windows (MinGW & CMake) Сборка статической библиотеки SDL3_image для Windows (MinGW и CMake)
- Building SDL3_image Static Library for Ubuntu (GCC, CMake & Ninja) Сборка статической библиотеки SDL3_image для Ubuntu (GCC, CMake и Ninja)
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
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();
}
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) Сбербанк (только для РФ)
Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)
USDT TRC20
Support via Cryptocurrency Поддержка криптовалютой