Skip to content

Commit 6278b7c

Browse files
committed
Add bone blending animation example
- Demonstrates per-bone animation blending for smooth transitions - Supports upper/lower body selective blending (walk + attack) - Includes uniform blending mode for comparison - Uses GPU skinning for performance - Follows raylib example conventions
1 parent a6fa8b9 commit 6278b7c

2 files changed

Lines changed: 292 additions & 0 deletions

File tree

examples/examples_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ models;models_heightmap_rendering;★☆☆☆;1.8;3.5;2015;2025;"Ramon Santamar
163163
models;models_skybox_rendering;★★☆☆;1.8;4.0;2017;2025;"Ramon Santamaria";@raysan5
164164
models;models_textured_cube;★★☆☆;4.5;4.5;2022;2025;"Ramon Santamaria";@raysan5
165165
models;models_animation_gpu_skinning;★★★☆;4.5;4.5;2024;2025;"Daniel Holden";@orangeduck
166+
models;models_animation_bone_blending;★★★★;5.5;5.5;2025;2025;"[Your Name]";@[your_github]
166167
models;models_bone_socket;★★★★;4.5;4.5;2024;2025;"iP";@ipzaur
167168
models;models_tesseract_view;★★☆☆;5.6-dev;5.6-dev;2024;2025;"Timothy van der Valk";@arceryz
168169
models;models_basic_voxel;★★☆☆;5.5;5.5;2025;2025;"Tim Little";@timlittle
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*******************************************************************************************
2+
*
3+
* raylib [models] example - animation bone blending
4+
*
5+
* Example complexity rating: [★★★★] 4/4
6+
*
7+
* Example originally created with raylib 5.5, last time updated with raylib 5.5
8+
*
9+
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
10+
* BSD-like license that allows static linking with closed source software
11+
*
12+
* This example demonstrates per-bone animation blending, allowing smooth transitions
13+
* between two animations by interpolating bone transforms. This is useful for:
14+
* - Blending movement animations (walk/run) with action animations (jump/attack)
15+
* - Creating smooth animation transitions
16+
* - Layering animations (e.g., upper body attack while lower body walks)
17+
*
18+
* Note: Due to limitations in the Apple OpenGL driver, GPU skinning does not work on MacOS
19+
*
20+
********************************************************************************************/
21+
22+
#include "raylib.h"
23+
#include "raymath.h"
24+
#include <string.h> // For memcpy
25+
#include <stddef.h> // For NULL
26+
27+
#if defined(PLATFORM_DESKTOP)
28+
#define GLSL_VERSION 330
29+
#else // PLATFORM_ANDROID, PLATFORM_WEB
30+
#define GLSL_VERSION 100
31+
#endif
32+
33+
//------------------------------------------------------------------------------------
34+
// Check if a bone is part of upper body (for selective blending)
35+
//------------------------------------------------------------------------------------
36+
bool IsUpperBodyBone(const char *boneName)
37+
{
38+
// Common upper body bone names (adjust based on your model)
39+
if (TextIsEqual(boneName, "spine") || TextIsEqual(boneName, "spine1") || TextIsEqual(boneName, "spine2") ||
40+
TextIsEqual(boneName, "chest") || TextIsEqual(boneName, "upperChest") ||
41+
TextIsEqual(boneName, "neck") || TextIsEqual(boneName, "head") ||
42+
TextIsEqual(boneName, "shoulder") || TextIsEqual(boneName, "shoulder_L") || TextIsEqual(boneName, "shoulder_R") ||
43+
TextIsEqual(boneName, "upperArm") || TextIsEqual(boneName, "upperArm_L") || TextIsEqual(boneName, "upperArm_R") ||
44+
TextIsEqual(boneName, "lowerArm") || TextIsEqual(boneName, "lowerArm_L") || TextIsEqual(boneName, "lowerArm_R") ||
45+
TextIsEqual(boneName, "hand") || TextIsEqual(boneName, "hand_L") || TextIsEqual(boneName, "hand_R") ||
46+
TextIsEqual(boneName, "clavicle") || TextIsEqual(boneName, "clavicle_L") || TextIsEqual(boneName, "clavicle_R"))
47+
{
48+
return true;
49+
}
50+
51+
// Check if bone name contains upper body keywords
52+
if (strstr(boneName, "spine") != NULL || strstr(boneName, "chest") != NULL ||
53+
strstr(boneName, "neck") != NULL || strstr(boneName, "head") != NULL ||
54+
strstr(boneName, "shoulder") != NULL || strstr(boneName, "arm") != NULL ||
55+
strstr(boneName, "hand") != NULL || strstr(boneName, "clavicle") != NULL)
56+
{
57+
return true;
58+
}
59+
60+
return false;
61+
}
62+
63+
//------------------------------------------------------------------------------------
64+
// Blend two animations per-bone with selective upper/lower body blending
65+
//------------------------------------------------------------------------------------
66+
void BlendModelAnimationsBones(Model *model, ModelAnimation *anim1, int frame1,
67+
ModelAnimation *anim2, int frame2, float blendFactor, bool upperBodyBlend)
68+
{
69+
// Clamp blend factor to [0, 1]
70+
blendFactor = fminf(1.0f, fmaxf(0.0f, blendFactor));
71+
72+
// Validate inputs
73+
if (anim1->boneCount == 0 || anim1->framePoses == NULL ||
74+
anim2->boneCount == 0 || anim2->framePoses == NULL ||
75+
model->boneCount == 0 || model->bindPose == NULL)
76+
{
77+
return;
78+
}
79+
80+
// Ensure frame indices are valid
81+
if (frame1 >= anim1->frameCount) frame1 = anim1->frameCount - 1;
82+
if (frame2 >= anim2->frameCount) frame2 = anim2->frameCount - 1;
83+
if (frame1 < 0) frame1 = 0;
84+
if (frame2 < 0) frame2 = 0;
85+
86+
// Find first mesh with bones
87+
int firstMeshWithBones = -1;
88+
for (int i = 0; i < model->meshCount; i++)
89+
{
90+
if (model->meshes[i].boneMatrices)
91+
{
92+
firstMeshWithBones = i;
93+
break;
94+
}
95+
}
96+
97+
if (firstMeshWithBones == -1) return;
98+
99+
// Get bone count (use minimum of all to be safe)
100+
int boneCount = model->boneCount;
101+
if (anim1->boneCount < boneCount) boneCount = anim1->boneCount;
102+
if (anim2->boneCount < boneCount) boneCount = anim2->boneCount;
103+
104+
// Blend each bone
105+
for (int boneId = 0; boneId < boneCount; boneId++)
106+
{
107+
// Determine blend factor for this bone
108+
float boneBlendFactor = blendFactor;
109+
110+
// If upper body blending is enabled, use different blend factors for upper vs lower body
111+
if (upperBodyBlend)
112+
{
113+
const char *boneName = model->bones[boneId].name;
114+
bool isUpperBody = IsUpperBodyBone(boneName);
115+
116+
// Upper body: use anim2 (attack), Lower body: use anim1 (walk)
117+
// blendFactor = 0.0 means full anim1 (walk), 1.0 means full anim2 (attack)
118+
if (isUpperBody)
119+
{
120+
// Upper body: blend towards anim2 (attack)
121+
boneBlendFactor = blendFactor;
122+
}
123+
else
124+
{
125+
// Lower body: blend towards anim1 (walk) - invert the blend
126+
boneBlendFactor = 1.0f - blendFactor;
127+
}
128+
}
129+
130+
// Get transforms from both animations
131+
Transform *bindTransform = &model->bindPose[boneId];
132+
Transform *anim1Transform = &anim1->framePoses[frame1][boneId];
133+
Transform *anim2Transform = &anim2->framePoses[frame2][boneId];
134+
135+
// Blend the transforms
136+
Transform blended;
137+
blended.translation = Vector3Lerp(anim1Transform->translation, anim2Transform->translation, boneBlendFactor);
138+
blended.rotation = QuaternionSlerp(anim1Transform->rotation, anim2Transform->rotation, boneBlendFactor);
139+
blended.scale = Vector3Lerp(anim1Transform->scale, anim2Transform->scale, boneBlendFactor);
140+
141+
// Convert bind pose to matrix
142+
Matrix bindMatrix = MatrixMultiply(MatrixMultiply(
143+
MatrixScale(bindTransform->scale.x, bindTransform->scale.y, bindTransform->scale.z),
144+
QuaternionToMatrix(bindTransform->rotation)),
145+
MatrixTranslate(bindTransform->translation.x, bindTransform->translation.y, bindTransform->translation.z));
146+
147+
// Convert blended transform to matrix
148+
Matrix blendedMatrix = MatrixMultiply(MatrixMultiply(
149+
MatrixScale(blended.scale.x, blended.scale.y, blended.scale.z),
150+
QuaternionToMatrix(blended.rotation)),
151+
MatrixTranslate(blended.translation.x, blended.translation.y, blended.translation.z));
152+
153+
// Calculate final bone matrix (similar to UpdateModelAnimationBones)
154+
model->meshes[firstMeshWithBones].boneMatrices[boneId] = MatrixMultiply(MatrixInvert(bindMatrix), blendedMatrix);
155+
}
156+
157+
// Copy bone matrices to remaining meshes
158+
for (int i = firstMeshWithBones + 1; i < model->meshCount; i++)
159+
{
160+
if (model->meshes[i].boneMatrices)
161+
{
162+
memcpy(model->meshes[i].boneMatrices,
163+
model->meshes[firstMeshWithBones].boneMatrices,
164+
model->meshes[i].boneCount * sizeof(model->meshes[i].boneMatrices[0]));
165+
}
166+
}
167+
}
168+
169+
//------------------------------------------------------------------------------------
170+
// Program main entry point
171+
//------------------------------------------------------------------------------------
172+
int main(void)
173+
{
174+
// Initialization
175+
//--------------------------------------------------------------------------------------
176+
const int screenWidth = 800;
177+
const int screenHeight = 450;
178+
179+
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation bone blending");
180+
181+
// Define the camera to look into our 3d world
182+
Camera camera = { 0 };
183+
camera.position = (Vector3){ 5.0f, 5.0f, 5.0f }; // Camera position
184+
camera.target = (Vector3){ 0.0f, 2.0f, 0.0f }; // Camera looking at point
185+
camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target)
186+
camera.fovy = 45.0f; // Camera field-of-view Y
187+
camera.projection = CAMERA_PERSPECTIVE; // Camera projection type
188+
189+
// Load gltf model
190+
Model characterModel = LoadModel("resources/models/gltf/greenman.glb");
191+
192+
// Load skinning shader
193+
Shader skinningShader = LoadShader(TextFormat("resources/shaders/glsl%i/skinning.vs", GLSL_VERSION),
194+
TextFormat("resources/shaders/glsl%i/skinning.fs", GLSL_VERSION));
195+
196+
characterModel.materials[1].shader = skinningShader;
197+
198+
// Load gltf model animations
199+
int animsCount = 0;
200+
ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/greenman.glb", &animsCount);
201+
202+
// Log all available animations for debugging
203+
TraceLog(LOG_INFO, "Found %d animations:", animsCount);
204+
for (int i = 0; i < animsCount; i++)
205+
{
206+
TraceLog(LOG_INFO, " Animation %d: %s (%d frames)", i, modelAnimations[i].name, modelAnimations[i].frameCount);
207+
}
208+
209+
// Use specific indices: walk/move = 2, attack = 3
210+
unsigned int animIndex1 = 2; // Walk/Move animation (index 2)
211+
unsigned int animIndex2 = 3; // Attack animation (index 3)
212+
unsigned int animCurrentFrame1 = 0;
213+
unsigned int animCurrentFrame2 = 0;
214+
215+
// Validate indices
216+
if (animIndex1 >= animsCount) animIndex1 = 0;
217+
if (animIndex2 >= animsCount) animIndex2 = (animsCount > 1) ? 1 : 0;
218+
219+
TraceLog(LOG_INFO, "Using Walk (index %d): %s", animIndex1, modelAnimations[animIndex1].name);
220+
TraceLog(LOG_INFO, "Using Attack (index %d): %s", animIndex2, modelAnimations[animIndex2].name);
221+
222+
Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position
223+
bool upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50)
224+
225+
DisableCursor(); // Limit cursor to relative movement inside the window
226+
227+
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
228+
//--------------------------------------------------------------------------------------
229+
230+
// Main game loop
231+
while (!WindowShouldClose()) // Detect window close button or ESC key
232+
{
233+
// Update
234+
//----------------------------------------------------------------------------------
235+
UpdateCamera(&camera, CAMERA_THIRD_PERSON);
236+
237+
// Toggle upper/lower body blending mode (SPACE key)
238+
if (IsKeyPressed(KEY_SPACE)) upperBodyBlend = !upperBodyBlend;
239+
240+
// Update animation frames
241+
ModelAnimation anim1 = modelAnimations[animIndex1];
242+
ModelAnimation anim2 = modelAnimations[animIndex2];
243+
244+
animCurrentFrame1 = (animCurrentFrame1 + 1) % anim1.frameCount;
245+
animCurrentFrame2 = (animCurrentFrame2 + 1) % anim2.frameCount;
246+
247+
// Blend the two animations
248+
characterModel.transform = MatrixTranslate(position.x, position.y, position.z);
249+
// When upperBodyBlend is ON: upper body = attack (1.0), lower body = walk (0.0)
250+
// When upperBodyBlend is OFF: uniform blend at 0.5 (50% walk, 50% attack)
251+
float blendFactor = upperBodyBlend ? 1.0f : 0.5f;
252+
BlendModelAnimationsBones(&characterModel, &anim1, animCurrentFrame1, &anim2, animCurrentFrame2, blendFactor, upperBodyBlend);
253+
//----------------------------------------------------------------------------------
254+
255+
// Draw
256+
//----------------------------------------------------------------------------------
257+
BeginDrawing();
258+
259+
ClearBackground(RAYWHITE);
260+
261+
BeginMode3D(camera);
262+
263+
// Draw character mesh, pose calculation is done in shader (GPU skinning)
264+
DrawMesh(characterModel.meshes[0], characterModel.materials[1], characterModel.transform);
265+
266+
DrawGrid(10, 1.0f);
267+
268+
EndMode3D();
269+
270+
// Draw UI
271+
DrawText("BONE BLENDING EXAMPLE", 10, 10, 20, DARKGRAY);
272+
DrawText(TextFormat("Walk (Animation 2): %s", anim1.name), 10, 35, 10, GRAY);
273+
DrawText(TextFormat("Attack (Animation 3): %s", anim2.name), 10, 50, 10, GRAY);
274+
DrawText(TextFormat("Mode: %s", upperBodyBlend ? "Upper/Lower Body Blending" : "Uniform Blending"), 10, 65, 10, GRAY);
275+
DrawText("SPACE - Toggle blending mode", 10, GetScreenHeight() - 20, 10, DARKGRAY);
276+
277+
EndDrawing();
278+
//----------------------------------------------------------------------------------
279+
}
280+
281+
// De-Initialization
282+
//--------------------------------------------------------------------------------------
283+
UnloadModelAnimations(modelAnimations, animsCount); // Unload model animation
284+
UnloadModel(characterModel); // Unload model and meshes/material
285+
UnloadShader(skinningShader); // Unload GPU skinning shader
286+
287+
CloseWindow(); // Close window and OpenGL context
288+
//--------------------------------------------------------------------------------------
289+
290+
return 0;
291+
}

0 commit comments

Comments
 (0)