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