WARNING: BREAKING: REDESIGNED: **Animation System** #4606

REVIEWED: Reorganized structures for a clearer distinction between "skeleton", "skin" and "skinning" data
ADDED: New structures: `ModelSkeleton`, `ModelAnimPose` (alias `Transform*`)
ADDED: Runtime data `currentPose` and `boneMatrices` to `Model` structure
ADDED: Support animation frames-blending, for timing control
ADDED: Support animations blending, between two animations
REVIEWED: All models animation loading functions
ADDED: `UpdateModelAnimationEx()` for two animations blending
REMOVED: `UpdateModelAnimationBones*()`, simplified API
REVIEWED: Shader attributes/uniforms names for animations, for consistency
REVIEWED: Multiple tweaks on animations loading for consistency between formats
ADDED: example: `models_animation_timing`
ADDED: example: `models_animation_blending`
REVIEWED: example: `models_animation_gpu_skinning`
REVIEWED: example: `models_animation_blend_custom`
REVIEWED: All animated models loading examples
This commit is contained in:
Ray
2026-02-24 01:18:57 +01:00
parent bee3dc6673
commit d4dc038e2e
26 changed files with 1052 additions and 807 deletions

View File

@ -21,7 +21,8 @@
#include "raylib.h"
#define clamp(x,a,b) ((x < a)? a : (x > b)? b : x)
#define RAYGUI_IMPLEMENTATION
#include "raygui.h" // Required for: UI controls
#if defined(PLATFORM_DESKTOP)
#define GLSL_VERSION 330
@ -43,31 +44,58 @@ int main(void)
// Define the camera to look into our 3d world
Camera camera = { 0 };
camera.position = (Vector3){ 8.0f, 8.0f, 8.0f }; // Camera position
camera.position = (Vector3){ 6.0f, 6.0f, 6.0f }; // Camera position
camera.target = (Vector3){ 0.0f, 2.0f, 0.0f }; // Camera looking at point
camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target)
camera.fovy = 45.0f; // Camera field-of-view Y
camera.projection = CAMERA_PERSPECTIVE; // Camera projection type
// Load model
Model characterModel = LoadModel("resources/models/gltf/robot.glb"); // Load character model
Model model = LoadModel("resources/models/gltf/robot.glb"); // Load character model
Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model world position
// Load skinning shader
// WARNING: It requires SUPPORT_GPU_SKINNING enabled on raylib (disabled by default)
Shader skinningShader = LoadShader(TextFormat("resources/shaders/glsl%i/skinning.vs", GLSL_VERSION),
TextFormat("resources/shaders/glsl%i/skinning.fs", GLSL_VERSION));
// Assign skinning shader to all materials shaders
for (int i = 0; i < characterModel.materialCount; i++) characterModel.materials[i].shader = skinningShader;
//for (int i = 0; i < model.materialCount; i++) model.materials[i].shader = skinningShader;
// Load model animations
int animsCount = 0;
ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/robot.glb", &animsCount);
int animCount = 0;
ModelAnimation *anims = LoadModelAnimations("resources/models/gltf/robot.glb", &animCount);
// Define animation variables
unsigned int animIndex0 = 0;
unsigned int animIndex1 = 0;
float animCurrentFrame = 0;
float blendFactor = 0.5f;
// Animation playing variables
// NOTE: Two animations are played with a smooth transition between them
int currentAnimPlaying = 0; // Current animation playing (0 o 1)
int nextAnimToPlay = 1; // Next animation to play (to transition)
bool animTransition = false; // Flag to register anim transition state
int animIndex0 = 10; // Current animation playing (walking)
float animCurrentFrame0 = 0.0f; // Current animation frame (supporting interpolated frames)
float animFrameSpeed0 = 0.5f; // Current animation play speed
int animIndex1 = 6; // Next animation to play (running)
float animCurrentFrame1 = 0.0f; // Next animation frame (supporting interpolated frames)
float animFrameSpeed1 = 0.5f; // Next animation play speed
float animBlendFactor = 0.0f; // Blend factor from anim0[frame0] --> anim1[frame1], [0.0f..1.0f]
// NOTE: 0.0f results in full anim0[] and 1.0f in full anim1[]
float animBlendTime = 2.0f; // Time to blend from one playing animation to another (in seconds)
float animBlendTimeCounter = 0.0f; // Time counter (delta time)
bool animPause = false; // Pause animation
// UI required variables
char *animNames[64] = { 0 }; // Pointers to animation names for dropdown box
for (int i = 0; i < animCount; i++) animNames[i] = anims[i].name;
bool dropdownEditMode0 = false;
bool dropdownEditMode1 = false;
float animFrameProgress0 = 0.0f;
float animFrameProgress1 = 0.0f;
float animBlendProgress = 0.0f;
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
@ -79,23 +107,100 @@ int main(void)
//----------------------------------------------------------------------------------
UpdateCamera(&camera, CAMERA_ORBITAL);
// Select current animation
if (IsKeyPressed(KEY_T)) animIndex0 = (animIndex0 + 1)%animsCount;
else if (IsKeyPressed(KEY_G)) animIndex0 = (animIndex0 + animsCount - 1)%animsCount;
if (IsKeyPressed(KEY_Y)) animIndex1 = (animIndex1 + 1)%animsCount;
else if (IsKeyPressed(KEY_H)) animIndex1 = (animIndex1 + animsCount - 1)%animsCount;
// Select blend factor
if (IsKeyPressed(KEY_U)) blendFactor = clamp(blendFactor - 0.1, 0.0f, 1.0f);
else if (IsKeyPressed(KEY_J)) blendFactor = clamp(blendFactor + 0.1, 0.0f, 1.0f);
if (IsKeyPressed(KEY_P)) animPause = !animPause;
// Update animation
animCurrentFrame += 0.2f;
if (!animPause)
{
// Start transition from anim0[] to anim1[]
if (IsKeyPressed(KEY_SPACE) && !animTransition)
{
if (currentAnimPlaying == 0)
{
// Transition anim0 --> anim1
nextAnimToPlay = 1;
animCurrentFrame1 = 0.0f;
}
else
{
// Transition anim1 --> anim0
nextAnimToPlay = 0;
animCurrentFrame0 = 0.0f;
}
// Update bones
// Note: Same animation frame index is used below. By default it loops both animations
UpdateModelAnimationEx(characterModel, modelAnimations[animIndex0], animCurrentFrame,
modelAnimations[animIndex1], animCurrentFrame, blendFactor);
// Set animation transition
animTransition = true;
animBlendTimeCounter = 0.0f;
animBlendFactor = 0.0f;
}
if (animTransition)
{
// Playing anim0 and anim1 at the same time
animCurrentFrame0 += animFrameSpeed0;
if (animCurrentFrame0 >= anims[animIndex0].keyframeCount) animCurrentFrame0 = 0.0f;
animCurrentFrame1 += animFrameSpeed1;
if (animCurrentFrame1 >= anims[animIndex1].keyframeCount) animCurrentFrame1 = 0.0f;
// Increment blend factor over time to transition from anim0 --> anim1 over time
// NOTE: Time blending could be other than linear, using some easing
animBlendFactor = animBlendTimeCounter/animBlendTime;
animBlendTimeCounter += GetFrameTime();
animBlendProgress = animBlendFactor;
// Update model with animations blending
if (nextAnimToPlay == 1)
{
// Blend anim0 --> anim1
UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
anims[animIndex1], animCurrentFrame1, animBlendFactor);
}
else
{
// Blend anim1 --> anim0
UpdateModelAnimationEx(model, anims[animIndex1], animCurrentFrame1,
anims[animIndex0], animCurrentFrame0, animBlendFactor);
}
// Check if transition completed
if (animBlendFactor > 1.0f)
{
// Reset frame states
if (currentAnimPlaying == 0) animCurrentFrame0 = 0.0f;
else if (currentAnimPlaying == 1) animCurrentFrame1 = 0.0f;
currentAnimPlaying = nextAnimToPlay; // Update current animation playing
animBlendFactor = 0.0f; // Reset blend factor
animTransition = false; // Exit transition mode
animBlendTimeCounter = 0.0f;
}
}
else
{
// Play only one anim, the current one
if (currentAnimPlaying == 0)
{
// Playing anim0 at defined speed
animCurrentFrame0 += animFrameSpeed0;
if (animCurrentFrame0 >= anims[animIndex0].keyframeCount) animCurrentFrame0 = 0.0f;
UpdateModelAnimation(model, anims[animIndex0], animCurrentFrame0);
//UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
// anims[animIndex1], animCurrentFrame1, 0.0f);
}
else if (currentAnimPlaying == 1)
{
// Playing anim1 at defined speed
animCurrentFrame1 += animFrameSpeed1;
if (animCurrentFrame1 >= anims[animIndex1].keyframeCount) animCurrentFrame1 = 0.0f;
UpdateModelAnimation(model, anims[animIndex1], animCurrentFrame1);
//UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
// anims[animIndex1], animCurrentFrame1, 1.0f);
}
}
}
// Update progress bars values with current frame for each animation
float animFrameProgress0 = animCurrentFrame0;
float animFrameProgress1 = animCurrentFrame1;
//----------------------------------------------------------------------------------
// Draw
@ -106,16 +211,47 @@ int main(void)
BeginMode3D(camera);
DrawModel(characterModel, (Vector3){0.0f, 0.0f, 0.0f}, 1.0f, WHITE);
DrawModel(model, position, 1.0f, WHITE); // Draw animated model
DrawGrid(10, 1.0f);
EndMode3D();
DrawText("Use the U/J to adjust blend factor", 10, 10, 20, GRAY);
DrawText("Use the T/G to switch first animation", 10, 30, 20, GRAY);
DrawText("Use the Y/H to switch second animation", 10, 50, 20, GRAY);
DrawText(TextFormat("Animations: %s, %s", modelAnimations[animIndex0].name, modelAnimations[animIndex1].name), 10, 70, 20, BLACK);
DrawText(TextFormat("Blend Factor: %f", blendFactor), 10, 86, 20, BLACK);
if (animTransition) DrawText("ANIM TRANSITION BLENDING!", 170, 50, 30, BLUE);
// Draw UI elements
//---------------------------------------------------------------------------------------------
// Draw animation selectors for blending transition
// NOTE: Transition does not start until requested
GuiSetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING, 1);
if (GuiDropdownBox((Rectangle){ 10, 10, 160, 24 }, TextJoin(animNames, animCount, ";"),
&animIndex0, dropdownEditMode0)) dropdownEditMode0 = !dropdownEditMode0;
// Blending process progress bar
if (nextAnimToPlay == 1) GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 0); // Left-->Right
else GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 1); // Right-->Left
GuiProgressBar((Rectangle){ 180, 14, 440, 16 }, NULL, NULL, &animBlendProgress, 0.0f, 1.0f);
GuiSetStyle(PROGRESSBAR, PROGRESS_SIDE, 0); // Reset to Left-->Right
if (GuiDropdownBox((Rectangle){ GetScreenWidth() - 170, 10, 160, 24 }, TextJoin(animNames, animCount, ";"),
&animIndex1, dropdownEditMode1)) dropdownEditMode1 = !dropdownEditMode1;
// Draw playing timeline with keyframes for anim0[]
GuiProgressBar((Rectangle){ 60, GetScreenHeight() - 60, GetScreenWidth() - 180, 20 }, "ANIM 0",
TextFormat("FRAME: %.2f / %i", animFrameProgress0, anims[animIndex0].keyframeCount),
&animFrameProgress0, 0.0f, (float)anims[animIndex0].keyframeCount);
for (int i = 0; i < anims[animIndex0].keyframeCount; i++)
DrawRectangle(60 + ((float)(GetScreenWidth() - 180)/(float)anims[animIndex0].keyframeCount)*(float)i,
GetScreenHeight() - 60, 1, 20, BLUE);
// Draw playing timeline with keyframes for anim1[]
GuiProgressBar((Rectangle){ 60, GetScreenHeight() - 30, GetScreenWidth() - 180, 20 }, "ANIM 1",
TextFormat("FRAME: %.2f / %i", animFrameProgress1, anims[animIndex1].keyframeCount),
&animFrameProgress1, 0.0f, (float)anims[animIndex1].keyframeCount);
for (int i = 0; i < anims[animIndex1].keyframeCount; i++)
DrawRectangle(60 + ((float)(GetScreenWidth() - 180)/(float)anims[animIndex1].keyframeCount)*(float)i,
GetScreenHeight() - 30, 1, 20, BLUE);
//---------------------------------------------------------------------------------------------
EndDrawing();
//----------------------------------------------------------------------------------
@ -123,9 +259,8 @@ int main(void)
// De-Initialization
//--------------------------------------------------------------------------------------
UnloadModelAnimations(modelAnimations, animsCount); // Unload model animation
UnloadModel(characterModel); // Unload model and meshes/material
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
UnloadShader(skinningShader); // Unload GPU skinning shader
CloseWindow(); // Close window and OpenGL context