diff --git a/examples/examples_list.txt b/examples/examples_list.txt index 042c1aed3..dc3312893 100644 --- a/examples/examples_list.txt +++ b/examples/examples_list.txt @@ -173,7 +173,7 @@ models;models_decals;★★★★;5.6-dev;5.6-dev;2025;2025;"JP Mortiboys";@them models;models_directional_billboard;★★☆☆;5.6-dev;5.6;2025;2025;"Robin";@RobinsAviary models;models_animation_blend_custom;★★★★;5.5;5.5;2026;2026;"dmitrii-brand";@dmitrii-brand models;models_animation_blending;☆☆☆☆;5.5;5.6-dev;2024;2024;"Kirandeep";@Kirandeep-Singh-Khehra -models;models_animation_timming;★★☆☆;5.6;5.6;2026;2026;"Ramon Santamaria";@raysan5 +models;models_animation_timing;★★☆☆;5.6;5.6;2026;2026;"Ramon Santamaria";@raysan5 shaders;shaders_ascii_rendering;★★☆☆;5.5;5.6;2025;2025;"Maicon Santana";@maiconpintoabreu shaders;shaders_basic_lighting;★★★★;3.0;4.2;2019;2025;"Chris Camacho";@chriscamacho shaders;shaders_model_shader;★★☆☆;1.3;3.7;2014;2025;"Ramon Santamaria";@raysan5 diff --git a/examples/models/models_animation_blend_custom.c b/examples/models/models_animation_blend_custom.c index 76dba6a69..f846a125b 100644 --- a/examples/models/models_animation_blend_custom.c +++ b/examples/models/models_animation_blend_custom.c @@ -6,7 +6,7 @@ * * Example originally created with raylib 5.5, last time updated with raylib 5.5 * -* This example demonstrates per-bone animation blending, allowing smooth transitions +* DETAILS: Example demonstrates per-bone animation blending, allowing smooth transitions * between two animations by interpolating bone transforms. This is useful for: * - Blending movement animations (walk/run) with action animations (jump/attack) * - Creating smooth animation transitions @@ -27,8 +27,10 @@ #include "raymath.h" -#include // Required for: memcpy() -#include // Required for: NULL +#include "rlgl.h" // Requried for: rlUpdateVertexBuffer() (CPU-skinning) + +#include // Required for: memcpy() +#include // Required for: NULL #if defined(PLATFORM_DESKTOP) #define GLSL_VERSION 330 @@ -40,8 +42,8 @@ // Module Functions Declaration //------------------------------------------------------------------------------------ static bool IsUpperBodyBone(const char *boneName); -static void BlendModelAnimationsBones(Model *model, ModelAnimation *anim1, int frame1, - ModelAnimation *anim2, int frame2, float blendFactor, bool upperBodyBlend); +static void UpdateModelAnimationBones(Model *model, ModelAnimation *anim1, int frame1, + ModelAnimation *anim2, int frame2, float blend, bool upperBodyBlend); //------------------------------------------------------------------------------------ // Program main entry point @@ -57,51 +59,40 @@ int main(void) // Define the camera to look into our 3d world Camera camera = { 0 }; - camera.position = (Vector3){ 5.0f, 5.0f, 5.0f }; // Camera position - camera.target = (Vector3){ 0.0f, 2.0f, 0.0f }; // Camera looking at point + camera.position = (Vector3){ 4.0f, 4.0f, 4.0f }; // Camera position + camera.target = (Vector3){ 0.0f, 1.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 gltf model - Model characterModel = LoadModel("resources/models/gltf/greenman.glb"); + Model model = LoadModel("resources/models/gltf/greenman.glb"); + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position // Load skinning shader + // WARNING: GPU skinning must be enabled in raylib with a compilation flag, + // if not enabled, CPU skinning will be used instead Shader skinningShader = LoadShader(TextFormat("resources/shaders/glsl%i/skinning.vs", GLSL_VERSION), TextFormat("resources/shaders/glsl%i/skinning.fs", GLSL_VERSION)); - - characterModel.materials[1].shader = skinningShader; + model.materials[1].shader = skinningShader; // Load gltf model animations - int animsCount = 0; - ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/greenman.glb", &animsCount); + int animCount = 0; + ModelAnimation *anims = LoadModelAnimations("resources/models/gltf/greenman.glb", &animCount); - // Log all available animations for debugging - TraceLog(LOG_INFO, "Found %d animations:", animsCount); - for (int i = 0; i < animsCount; i++) - { - TraceLog(LOG_INFO, " Animation %d: %s (%d frames)", i, modelAnimations[i].name, modelAnimations[i].keyframeCount); - } - - // Use specific indices: walk/move = 2, attack = 3 - unsigned int animIndex1 = 2; // Walk/Move animation (index 2) - unsigned int animIndex2 = 3; // Attack animation (index 3) + // Use specific animation indices: 2-walk/move, 3-attack + unsigned int animIndex0 = 2; // Walk/Move animation (index 2) + unsigned int animIndex1 = 3; // Attack animation (index 3) + unsigned int animCurrentFrame0 = 0; unsigned int animCurrentFrame1 = 0; - unsigned int animCurrentFrame2 = 0; // Validate indices - if (animIndex1 >= animsCount) animIndex1 = 0; - if (animIndex2 >= animsCount) animIndex2 = (animsCount > 1) ? 1 : 0; - - TraceLog(LOG_INFO, "Using Walk (index %d): %s", animIndex1, modelAnimations[animIndex1].name); - TraceLog(LOG_INFO, "Using Attack (index %d): %s", animIndex2, modelAnimations[animIndex2].name); + if (animIndex0 >= animCount) animIndex0 = 0; + if (animIndex1 >= animCount) animIndex1 = (animCount > 1) ? 1 : 0; - Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position - bool upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50) + bool upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50) - DisableCursor(); // Limit cursor to relative movement inside the window - - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- // Main game loop @@ -109,24 +100,28 @@ int main(void) { // Update //---------------------------------------------------------------------------------- - UpdateCamera(&camera, CAMERA_THIRD_PERSON); + UpdateCamera(&camera, CAMERA_ORBITAL); // Toggle upper/lower body blending mode (SPACE key) if (IsKeyPressed(KEY_SPACE)) upperBodyBlend = !upperBodyBlend; // Update animation frames - ModelAnimation anim1 = modelAnimations[animIndex1]; - ModelAnimation anim2 = modelAnimations[animIndex2]; + ModelAnimation anim0 = anims[animIndex0]; + ModelAnimation anim1 = anims[animIndex1]; + animCurrentFrame0 = (animCurrentFrame0 + 1)%anim0.keyframeCount; animCurrentFrame1 = (animCurrentFrame1 + 1)%anim1.keyframeCount; - animCurrentFrame2 = (animCurrentFrame2 + 1)%anim2.keyframeCount; // Blend the two animations - characterModel.transform = MatrixTranslate(position.x, position.y, position.z); // When upperBodyBlend is ON: upper body = attack (1.0), lower body = walk (0.0) // When upperBodyBlend is OFF: uniform blend at 0.5 (50% walk, 50% attack) - float blendFactor = upperBodyBlend ? 1.0f : 0.5f; - BlendModelAnimationsBones(&characterModel, &anim1, animCurrentFrame1, &anim2, animCurrentFrame2, blendFactor, upperBodyBlend); + float blendFactor = (upperBodyBlend? 1.0f : 0.5f); + UpdateModelAnimationBones(&model, &anim0, animCurrentFrame0, + &anim1, animCurrentFrame1, blendFactor, upperBodyBlend); + + // raylib provided animation blending function + //UpdateModelAnimationEx(model, anim0, (float)animCurrentFrame0, + // anim1, (float)animCurrentFrame1, blendFactor); //---------------------------------------------------------------------------------- // Draw @@ -137,19 +132,18 @@ int main(void) BeginMode3D(camera); - // Draw character mesh, pose calculation is done in shader (GPU skinning) - DrawMesh(characterModel.meshes[0], characterModel.materials[1], characterModel.transform); + DrawModel(model, position, 1.0f, WHITE); DrawGrid(10, 1.0f); EndMode3D(); // Draw UI - DrawText("BONE BLENDING EXAMPLE", 10, 10, 20, DARKGRAY); - DrawText(TextFormat("Walk (Animation 2): %s", anim1.name), 10, 35, 10, GRAY); - DrawText(TextFormat("Attack (Animation 3): %s", anim2.name), 10, 50, 10, GRAY); - DrawText(TextFormat("Mode: %s", upperBodyBlend ? "Upper/Lower Body Blending" : "Uniform Blending"), 10, 65, 10, GRAY); - DrawText("SPACE - Toggle blending mode", 10, GetScreenHeight() - 20, 10, DARKGRAY); + DrawText(TextFormat("ANIM 0: %s", anim0.name), 10, 10, 20, GRAY); + DrawText(TextFormat("ANIM 1: %s", anim1.name), 10, 40, 20, GRAY); + DrawText(TextFormat("[SPACE] Toggle blending mode: %s", + upperBodyBlend? "Upper/Lower Body Blending" : "Uniform Blending"), + 10, GetScreenHeight() - 30, 20, DARKGRAY); EndDrawing(); //---------------------------------------------------------------------------------- @@ -157,8 +151,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 @@ -199,74 +193,138 @@ static bool IsUpperBodyBone(const char *boneName) } // Blend two animations per-bone with selective upper/lower body blending -static void BlendModelAnimationsBones(Model *model, ModelAnimation *anim1, int frame1, - ModelAnimation *anim2, int frame2, float blendFactor, bool upperBodyBlend) +static void UpdateModelAnimationBones(Model *model, ModelAnimation *anim0, int frame0, + ModelAnimation *anim1, int frame1, float blend, bool upperBodyBlend) { // Validate inputs - if (anim1->boneCount == 0 || anim1->keyframePoses == NULL || - anim2->boneCount == 0 || anim2->keyframePoses == NULL || - model->skeleton.boneCount == 0 || model->skeleton.bindPose == NULL) + if ((anim0->boneCount != 0) && (anim0->keyframePoses != NULL) && + (anim1->boneCount != 0) && (anim1->keyframePoses != NULL) && + (model->skeleton.boneCount != 0) && (model->skeleton.bindPose != NULL)) { - return; - } - - // Clamp blend factor to [0, 1] - blendFactor = fminf(1.0f, fmaxf(0.0f, blendFactor)); - - // Ensure frame indices are valid - if (frame1 >= anim1->keyframeCount) frame1 = anim1->keyframeCount - 1; - if (frame2 >= anim2->keyframeCount) frame2 = anim2->keyframeCount - 1; - if (frame1 < 0) frame1 = 0; - if (frame2 < 0) frame2 = 0; - - // Get bone count (use minimum of all to be safe) - int boneCount = model->skeleton.boneCount; - if (anim1->boneCount < boneCount) boneCount = anim1->boneCount; - if (anim2->boneCount < boneCount) boneCount = anim2->boneCount; - - // Blend each bone - for (int boneId = 0; boneId < boneCount; boneId++) - { - // Determine blend factor for this bone - float boneBlendFactor = blendFactor; + // Clamp blend factor to [0, 1] + blend = fminf(1.0f, fmaxf(0.0f, blend)); - // If upper body blending is enabled, use different blend factors for upper vs lower body - if (upperBodyBlend) + // Ensure frame indices are valid + if (frame0 >= anim0->keyframeCount) frame0 = anim0->keyframeCount - 1; + if (frame1 >= anim1->keyframeCount) frame1 = anim1->keyframeCount - 1; + if (frame0 < 0) frame0 = 0; + if (frame1 < 0) frame1 = 0; + + // Get bone count (use minimum of all to be safe) + int boneCount = model->skeleton.boneCount; + if (anim0->boneCount < boneCount) boneCount = anim0->boneCount; + if (anim1->boneCount < boneCount) boneCount = anim1->boneCount; + + // Blend each bone + for (int boneIndex = 0; boneIndex < boneCount; boneIndex++) { - const char *boneName = model->skeleton.bones[boneId].name; - bool isUpperBody = IsUpperBodyBone(boneName); + // Determine blend factor for this bone + float boneBlendFactor = blend; - // Upper body: use anim2 (attack), Lower body: use anim1 (walk) - // blendFactor = 0.0 means full anim1 (walk), 1.0 means full anim2 (attack) - if (isUpperBody) boneBlendFactor = blendFactor; // Upper body: blend towards anim2 (attack) - else boneBlendFactor = 1.0f - blendFactor; // Lower body: blend towards anim1 (walk) - invert the blend + // If upper body blending is enabled, use different blend factors for upper vs lower body + if (upperBodyBlend) + { + const char *boneName = model->skeleton.bones[boneIndex].name; + bool isUpperBody = IsUpperBodyBone(boneName); + + // Upper body: use anim1 (attack), Lower body: use anim0 (walk) + // blend = 0.0 means full anim0 (walk), 1.0 means full anim1 (attack) + if (isUpperBody) boneBlendFactor = blend; // Upper body: blend towards anim1 (attack) + else boneBlendFactor = 1.0f - blend; // Lower body: blend towards anim0 (walk) - invert the blend + } + + // Get transforms from both animations + Transform *bindTransform = &model->skeleton.bindPose[boneIndex]; + Transform *animTransform0 = &anim0->keyframePoses[frame0][boneIndex]; + Transform *animTransform1 = &anim1->keyframePoses[frame1][boneIndex]; + + // Blend the transforms + Transform blended = { 0 }; + blended.translation = Vector3Lerp(animTransform0->translation, animTransform1->translation, boneBlendFactor); + blended.rotation = QuaternionSlerp(animTransform0->rotation, animTransform1->rotation, boneBlendFactor); + blended.scale = Vector3Lerp(animTransform0->scale, animTransform1->scale, boneBlendFactor); + + // Convert bind pose to matrix + Matrix bindMatrix = MatrixMultiply(MatrixMultiply( + MatrixScale(bindTransform->scale.x, bindTransform->scale.y, bindTransform->scale.z), + QuaternionToMatrix(bindTransform->rotation)), + MatrixTranslate(bindTransform->translation.x, bindTransform->translation.y, bindTransform->translation.z)); + + // Convert blended transform to matrix + Matrix blendedMatrix = MatrixMultiply(MatrixMultiply( + MatrixScale(blended.scale.x, blended.scale.y, blended.scale.z), + QuaternionToMatrix(blended.rotation)), + MatrixTranslate(blended.translation.x, blended.translation.y, blended.translation.z)); + + // Calculate final bone matrix (similar to UpdateModelAnimationBones) + model->boneMatrices[boneIndex] = MatrixMultiply(MatrixInvert(bindMatrix), blendedMatrix); + } + + // CPU skinning, updates CPU buffers and uploads them to GPU (if available) + // NOTE: Fallback in case GPU skinning is not supported or enabled + for (int m = 0; m < model->meshCount; m++) + { + Mesh mesh = model->meshes[m]; + Vector3 animVertex = { 0 }; + Vector3 animNormal = { 0 }; + const int vertexValuesCount = mesh.vertexCount*3; + + int boneIndex = 0; + int boneCounter = 0; + float boneWeight = 0.0f; + bool bufferUpdateRequired = false; // Flag to check when anim vertex information is updated + + // Skip if missing bone data or missing anim buffers initialization + if ((mesh.boneWeights == NULL) || (mesh.boneIndices == NULL) || + (mesh.animVertices == NULL) || (mesh.animNormals == NULL)) continue; + + for (int vCounter = 0; vCounter < vertexValuesCount; vCounter += 3) + { + mesh.animVertices[vCounter] = 0; + mesh.animVertices[vCounter + 1] = 0; + mesh.animVertices[vCounter + 2] = 0; + if (mesh.animNormals != NULL) + { + mesh.animNormals[vCounter] = 0; + mesh.animNormals[vCounter + 1] = 0; + mesh.animNormals[vCounter + 2] = 0; + } + + // Iterates over 4 bones per vertex + for (int j = 0; j < 4; j++, boneCounter++) + { + boneWeight = mesh.boneWeights[boneCounter]; + boneIndex = mesh.boneIndices[boneCounter]; + + // Early stop when no transformation will be applied + if (boneWeight == 0.0f) continue; + animVertex = (Vector3){ mesh.vertices[vCounter], mesh.vertices[vCounter + 1], mesh.vertices[vCounter + 2] }; + animVertex = Vector3Transform(animVertex, model->boneMatrices[boneIndex]); + mesh.animVertices[vCounter] += animVertex.x*boneWeight; + mesh.animVertices[vCounter + 1] += animVertex.y*boneWeight; + mesh.animVertices[vCounter + 2] += animVertex.z*boneWeight; + bufferUpdateRequired = true; + + // Normals processing + // NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals) + if ((mesh.normals != NULL) && (mesh.animNormals != NULL )) + { + animNormal = (Vector3){ mesh.normals[vCounter], mesh.normals[vCounter + 1], mesh.normals[vCounter + 2] }; + animNormal = Vector3Transform(animNormal, MatrixTranspose(MatrixInvert(model->boneMatrices[boneIndex]))); + mesh.animNormals[vCounter] += animNormal.x*boneWeight; + mesh.animNormals[vCounter + 1] += animNormal.y*boneWeight; + mesh.animNormals[vCounter + 2] += animNormal.z*boneWeight; + } + } + } + + if (bufferUpdateRequired) + { + // Update GPU vertex buffers with updated data (position + normals) + rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_POSITION], mesh.animVertices, mesh.vertexCount*3*sizeof(float), 0); + if (mesh.normals != NULL) rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_NORMAL], mesh.animNormals, mesh.vertexCount*3*sizeof(float), 0); + } } - - // Get transforms from both animations - Transform *bindTransform = &model->skeleton.bindPose[boneId]; - Transform *anim1Transform = &anim1->keyframePoses[frame1][boneId]; - Transform *anim2Transform = &anim2->keyframePoses[frame2][boneId]; - - // Blend the transforms - Transform blended = { 0 }; - blended.translation = Vector3Lerp(anim1Transform->translation, anim2Transform->translation, boneBlendFactor); - blended.rotation = QuaternionSlerp(anim1Transform->rotation, anim2Transform->rotation, boneBlendFactor); - blended.scale = Vector3Lerp(anim1Transform->scale, anim2Transform->scale, boneBlendFactor); - - // Convert bind pose to matrix - Matrix bindMatrix = MatrixMultiply(MatrixMultiply( - MatrixScale(bindTransform->scale.x, bindTransform->scale.y, bindTransform->scale.z), - QuaternionToMatrix(bindTransform->rotation)), - MatrixTranslate(bindTransform->translation.x, bindTransform->translation.y, bindTransform->translation.z)); - - // Convert blended transform to matrix - Matrix blendedMatrix = MatrixMultiply(MatrixMultiply( - MatrixScale(blended.scale.x, blended.scale.y, blended.scale.z), - QuaternionToMatrix(blended.rotation)), - MatrixTranslate(blended.translation.x, blended.translation.y, blended.translation.z)); - - // Calculate final bone matrix (similar to UpdateModelAnimationBones) - model->boneMatrices[boneId] = MatrixMultiply(MatrixInvert(bindMatrix), blendedMatrix); } } diff --git a/examples/models/models_animation_blend_custom.png b/examples/models/models_animation_blend_custom.png index 0f6dbca87..95ea1dad2 100644 Binary files a/examples/models/models_animation_blend_custom.png and b/examples/models/models_animation_blend_custom.png differ diff --git a/examples/models/models_animation_blending.c b/examples/models/models_animation_blending.c index 39623cd28..a5d3d4b1d 100644 --- a/examples/models/models_animation_blending.c +++ b/examples/models/models_animation_blending.c @@ -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 diff --git a/examples/models/models_animation_blending.png b/examples/models/models_animation_blending.png index 0d70c1a88..d1ca4a51b 100644 Binary files a/examples/models/models_animation_blending.png and b/examples/models/models_animation_blending.png differ diff --git a/examples/models/models_animation_gpu_skinning.c b/examples/models/models_animation_gpu_skinning.c index 0cd890449..b50dd86a3 100644 --- a/examples/models/models_animation_gpu_skinning.c +++ b/examples/models/models_animation_gpu_skinning.c @@ -10,7 +10,6 @@ * * WARNING: GPU skinning must be enabled in raylib with a compilation flag, * if not enabled, CPU skinning will be used instead -* NOTE: Due to limitations in the Apple OpenGL driver, this feature does not work on MacOS * * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, * BSD-like license that allows static linking with closed source software diff --git a/examples/models/models_animation_gpu_skinning.png b/examples/models/models_animation_gpu_skinning.png index 8003c16ef..a8d102408 100644 Binary files a/examples/models/models_animation_gpu_skinning.png and b/examples/models/models_animation_gpu_skinning.png differ diff --git a/examples/models/models_animation_timming.c b/examples/models/models_animation_timing.c similarity index 57% rename from examples/models/models_animation_timming.c rename to examples/models/models_animation_timing.c index 8069537ce..d845e3a9c 100644 --- a/examples/models/models_animation_timming.c +++ b/examples/models/models_animation_timing.c @@ -1,6 +1,6 @@ /******************************************************************************************* * -* raylib [models] example - animation timming +* raylib [models] example - animation timing * * Example complexity rating: [★★☆☆] 2/4 * @@ -16,7 +16,7 @@ #include "raylib.h" #define RAYGUI_IMPLEMENTATION -#include "raygui.h" // Required for: UI controls +#include "raygui.h" // Required for: UI controls //------------------------------------------------------------------------------------ // Program main entry point @@ -28,7 +28,7 @@ int main(void) const int screenWidth = 800; const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [models] example - animation timming"); + InitWindow(screenWidth, screenHeight, "raylib [models] example - animation timing"); // Define the camera to look into our 3d world Camera camera = { 0 }; @@ -43,13 +43,21 @@ int main(void) Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model world position // 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); // Animation playing variables - unsigned int animIndex = 0; // Current animation playing + int animIndex = 10; // Current animation playing float animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames) - float animFrameSpeed = 0.1f; // Animation play speed + float animFrameSpeed = 0.5f; // Animation play speed + bool animPause = false; // Pause animation + + // UI required variables + char *animNames[64] = { 0 }; + for (int i = 0; i < animCount; i++) animNames[i] = anims[i].name; + + bool dropdownEditMode = false; + float animFrameProgress = 0.0f; SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -61,17 +69,20 @@ int main(void) //---------------------------------------------------------------------------------- UpdateCamera(&camera, CAMERA_ORBITAL); - // Select current animation - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) animIndex = (animIndex + 1)%animsCount; - else if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) animIndex = (animIndex + animsCount - 1)%animsCount; + if (IsKeyPressed(KEY_P)) animPause = !animPause; - // Select animation playing speed - if (IsKeyPressed(KEY_RIGHT)) animFrameSpeed += 0.1f; - else if (IsKeyPressed(KEY_LEFT)) animFrameSpeed -= 0.1f; + if (!animPause && (animIndex < animCount)) + { + // Update model animation + animCurrentFrame += animFrameSpeed; + if (animCurrentFrame >= anims[animIndex].keyframeCount) animCurrentFrame = 0.0f; + UpdateModelAnimation(model, anims[animIndex], animCurrentFrame); + } - // Update model animation - animCurrentFrame += animFrameSpeed; - UpdateModelAnimation(model, modelAnimations[animIndex], animCurrentFrame); + // NOTE: Animation and playing speed selected through UI + + // Update progressbar value with current frame + animFrameProgress = animCurrentFrame; //---------------------------------------------------------------------------------- // Draw @@ -88,13 +99,22 @@ int main(void) EndMode3D(); - // Draw UI - //GuiDropdownBox((Rectangle){ 10, 20, 240, 30 }, "text", &animIndex, editMode); + // Draw UI, select anim and playing speed + GuiSetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING, 1); + if (GuiDropdownBox((Rectangle){ 10, 10, 140, 24 }, TextJoin(animNames, animCount, ";"), + &animIndex, dropdownEditMode)) dropdownEditMode = !dropdownEditMode; - DrawText(TextFormat("FRAME SPEED: x%.1f", animFrameSpeed), 10, 40, 20, RED); + GuiSlider((Rectangle){ 260, 10, 500, 24 }, "FRAME SPEED: ", TextFormat("x%.1f", animFrameSpeed), + &animFrameSpeed, 0.1f, 2.0f); - DrawText("Use the LEFT/RIGHT mouse buttons to switch animation", 10, 10, 20, GRAY); - DrawText(TextFormat("Animation: %s", modelAnimations[animIndex].name), 10, GetScreenHeight() - 20, 10, DARKGRAY); + // Draw playing timeline with keyframes + GuiLabel((Rectangle){ 10, GetScreenHeight() - 64, GetScreenWidth() - 20, 24 }, + TextFormat("CURRENT FRAME: %.2f / %i", animFrameProgress, anims[animIndex].keyframeCount)); + GuiProgressBar((Rectangle){ 10, GetScreenHeight() - 40, GetScreenWidth() - 20, 24 }, NULL, NULL, + &animFrameProgress, 0.0f, (float)anims[animIndex].keyframeCount); + for (int i = 0; i < anims[animIndex].keyframeCount; i++) + DrawRectangle(10 + ((float)(GetScreenWidth() - 20)/(float)anims[animIndex].keyframeCount)*(float)i, + GetScreenHeight() - 40, 1, 24, BLUE); EndDrawing(); //---------------------------------------------------------------------------------- @@ -102,6 +122,7 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- + UnloadModelAnimations(anims, animCount); // Unload model animation UnloadModel(model); // Unload model and meshes/material CloseWindow(); // Close window and OpenGL context @@ -110,5 +131,3 @@ int main(void) return 0; } - - diff --git a/examples/models/models_animation_timing.png b/examples/models/models_animation_timing.png new file mode 100644 index 000000000..4448fec67 Binary files /dev/null and b/examples/models/models_animation_timing.png differ diff --git a/examples/models/models_animation_timming.png b/examples/models/models_animation_timming.png deleted file mode 100644 index 673994b7c..000000000 Binary files a/examples/models/models_animation_timming.png and /dev/null differ diff --git a/examples/models/models_loading.c b/examples/models/models_loading.c index 650b3126f..0672da047 100644 --- a/examples/models/models_loading.c +++ b/examples/models/models_loading.c @@ -7,11 +7,11 @@ * NOTE: raylib supports multiple models file formats: * * - OBJ > Text file format. Must include vertex position-texcoords-normals information, -* if files references some .mtl materials file, it will be loaded (or try to) -* - GLTF > Text/binary file format. Includes lot of information and it could -* also reference external files, raylib will try loading mesh and materials data +* if .obj references some .mtl materials file, it will be tried to be loaded +* - GLTF/GLB > Text/binary file formats. Includes lot of information and it could +* also reference external files, mesh and materials data will be tried to be loaded * - IQM > Binary file format. Includes mesh vertex data but also animation data, -* raylib can load .iqm animations +* meshes and animation data can be loaded * - VOX > Binary file format. MagikaVoxel mesh format: * https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt * - M3D > Binary file format. Model 3D format: @@ -43,10 +43,10 @@ int main(void) // Define the camera to look into our 3d world Camera camera = { 0 }; camera.position = (Vector3){ 50.0f, 50.0f, 50.0f }; // Camera position - camera.target = (Vector3){ 0.0f, 10.0f, 0.0f }; // Camera looking at point + camera.target = (Vector3){ 0.0f, 12.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 mode type + camera.projection = CAMERA_PERSPECTIVE; // Camera mode type Model model = LoadModel("resources/models/obj/castle.obj"); // Load model Texture2D texture = LoadTexture("resources/models/obj/castle_diffuse.png"); // Load model texture @@ -61,8 +61,6 @@ int main(void) bool selected = false; // Selected object flag - DisableCursor(); // Limit cursor to relative movement inside the window - SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -71,7 +69,7 @@ int main(void) { // Update //---------------------------------------------------------------------------------- - UpdateCamera(&camera, CAMERA_FIRST_PERSON); + UpdateCamera(&camera, CAMERA_ORBITAL); // Load new models/textures on drag&drop if (IsFileDropped()) @@ -93,7 +91,10 @@ int main(void) bounds = GetMeshBoundingBox(model.meshes[0]); - // TODO: Move camera position from target enough distance to visualize model properly + // Move camera position from target enough distance to visualize model properly + camera.position.x = bounds.max.x + 10.0f; + camera.position.y = bounds.max.y + 10.0f; + camera.position.z = bounds.max.z + 10.0f; } else if (IsFileExtension(droppedFiles.paths[0], ".png")) // Texture file formats supported { diff --git a/examples/models/models_loading_gltf.c b/examples/models/models_loading_gltf.c index 7729da13f..f21207ba1 100644 --- a/examples/models/models_loading_gltf.c +++ b/examples/models/models_loading_gltf.c @@ -42,15 +42,17 @@ int main(void) camera.fovy = 45.0f; // Camera field-of-view Y camera.projection = CAMERA_PERSPECTIVE; // Camera projection type - // Load gltf model + // Load model Model model = LoadModel("resources/models/gltf/robot.glb"); - Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model world position - // Load gltf model animations - int animsCount = 0; - unsigned int animIndex = 0; - unsigned int animCurrentFrame = 0; - ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/robot.glb", &animsCount); + // Load model animations + int animCount = 0; + ModelAnimation *anims = LoadModelAnimations("resources/models/gltf/robot.glb", &animCount); + + // Animation playing variables + unsigned int animIndex = 0; // Current animation playing + unsigned int animCurrentFrame = 0; // Current animation frame SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -63,13 +65,12 @@ int main(void) UpdateCamera(&camera, CAMERA_ORBITAL); // Select current animation - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) animIndex = (animIndex + 1)%animsCount; - else if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) animIndex = (animIndex + animsCount - 1)%animsCount; + if (IsKeyPressed(KEY_RIGHT)) animIndex = (animIndex + 1)%animCount; + else if (IsKeyPressed(KEY_LEFT)) animIndex = (animIndex + animCount - 1)%animCount; // Update model animation - ModelAnimation anim = modelAnimations[animIndex]; - animCurrentFrame = (animCurrentFrame + 1)%anim.frameCount; - UpdateModelAnimation(model, anim, animCurrentFrame); + animCurrentFrame = (animCurrentFrame + 1)%anims[animIndex].keyframeCount; + UpdateModelAnimation(model, anims[animIndex], (float)animCurrentFrame); //---------------------------------------------------------------------------------- // Draw @@ -79,12 +80,15 @@ int main(void) ClearBackground(RAYWHITE); BeginMode3D(camera); - DrawModel(model, position, 1.0f, WHITE); // Draw animated model + + DrawModel(model, position, 1.0f, WHITE); + DrawGrid(10, 1.0f); + EndMode3D(); - DrawText("Use the LEFT/RIGHT mouse buttons to switch animation", 10, 10, 20, GRAY); - DrawText(TextFormat("Animation: %s", anim.name), 10, GetScreenHeight() - 20, 10, DARKGRAY); + DrawText(TextFormat("Current animation: %s", anims[animIndex].name), 10, 40, 20, MAROON); + DrawText("Use the LEFT/RIGHT keys to switch animation", 10, 10, 20, GRAY); EndDrawing(); //---------------------------------------------------------------------------------- @@ -92,7 +96,8 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(model); // Unload model and meshes/material + UnloadModelAnimations(anims, animCount); // Unload model animations data + UnloadModel(model); // Unload model CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/models/models_loading_gltf.png b/examples/models/models_loading_gltf.png index ce2a85284..9aa3e2350 100644 Binary files a/examples/models/models_loading_gltf.png and b/examples/models/models_loading_gltf.png differ diff --git a/examples/models/models_loading_iqm.c b/examples/models/models_loading_iqm.c index 97d97ad40..0eab898a4 100644 --- a/examples/models/models_loading_iqm.c +++ b/examples/models/models_loading_iqm.c @@ -8,17 +8,15 @@ * * Example contributed by Culacant (@culacant) and reviewed by Ramon Santamaria (@raysan5) * +* NOTES: To export an IQM model from blender, make sure it is not posed, the vertices need +* to be in the same position as they would be in edit mode and the scale of the models is +* set to 0; scaling can be set from the export menu +* * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, * BSD-like license that allows static linking with closed source software * * Copyright (c) 2019-2025 Culacant (@culacant) and Ramon Santamaria (@raysan5) * -******************************************************************************************** -* -* NOTE: To export a model from blender, make sure it is not posed, the vertices need to be -* in the same position as they would be in edit mode and the scale of your models is -* set to 0. Scaling can be done from the export menu -* ********************************************************************************************/ #include "raylib.h" @@ -38,7 +36,7 @@ int main(void) // Define the camera to look into our 3d world Camera camera = { 0 }; camera.position = (Vector3){ 10.0f, 10.0f, 10.0f }; // Camera position - camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Camera looking at point + camera.target = (Vector3){ 0.0f, 4.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 mode type @@ -46,15 +44,16 @@ int main(void) Model model = LoadModel("resources/models/iqm/guy.iqm"); // Load the animated model mesh and basic data Texture2D texture = LoadTexture("resources/models/iqm/guytex.png"); // Load model texture and set material SetMaterialTexture(&model.materials[0], MATERIAL_MAP_DIFFUSE, texture); // Set model material map texture - - Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position // Load animation data - int animsCount = 0; - ModelAnimation *anims = LoadModelAnimations("resources/models/iqm/guyanim.iqm", &animsCount); - float animFrameCounter = 0; + int animCount = 0; + ModelAnimation *anims = LoadModelAnimations("resources/models/iqm/guyanim.iqm", &animCount); + + // Animation playing variables + unsigned int animIndex = 0; // Current animation playing + float animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames) - DisableCursor(); // Catch cursor SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -66,9 +65,9 @@ int main(void) UpdateCamera(&camera, CAMERA_ORBITAL); // Play animation when spacebar is held down - animFrameCounter += 1.0f; - UpdateModelAnimation(model, anims[0], animFrameCounter); - if (animFrameCounter >= anims[0].keyframeCount) animFrameCounter = 0; + animCurrentFrame += 1.0f; + UpdateModelAnimation(model, anims[0], animCurrentFrame); + if (animCurrentFrame >= anims[0].keyframeCount) animCurrentFrame = 0; //---------------------------------------------------------------------------------- // Draw @@ -81,16 +80,11 @@ int main(void) DrawModelEx(model, position, (Vector3){ 1.0f, 0.0f, 0.0f }, -90.0f, (Vector3){ 1.0f, 1.0f, 1.0f }, WHITE); - for (int i = 0; i < model.skeleton.boneCount; i++) - { - //DrawCube(anims[0].keyframePoses[animFrameCounter][i].translation, 0.2f, 0.2f, 0.2f, RED); - } - - DrawGrid(10, 1.0f); // Draw a grid + DrawGrid(10, 1.0f); EndMode3D(); - DrawText("PRESS SPACE to PLAY MODEL ANIMATION", 10, 10, 20, MAROON); + DrawText(TextFormat("Current animation: %s", anims[animIndex].name), 10, 10, 20, MAROON); DrawText("(c) Guy IQM 3D model by @culacant", screenWidth - 200, screenHeight - 20, 10, GRAY); EndDrawing(); @@ -99,9 +93,9 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); // Unload texture - UnloadModelAnimations(anims, animsCount); // Unload model animations data - UnloadModel(model); // Unload model + UnloadTexture(texture); // Unload texture + UnloadModelAnimations(anims, animCount); // Unload model animations data + UnloadModel(model); // Unload model CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/models/models_loading_iqm.png b/examples/models/models_loading_iqm.png index 57e39dd3e..560cb3580 100644 Binary files a/examples/models/models_loading_iqm.png and b/examples/models/models_loading_iqm.png differ diff --git a/examples/models/models_loading_m3d.c b/examples/models/models_loading_m3d.c index 452a21610..06911bf53 100644 --- a/examples/models/models_loading_m3d.c +++ b/examples/models/models_loading_m3d.c @@ -21,6 +21,8 @@ #include "raylib.h" +static void DrawModelSkeleton(ModelSkeleton skeleton, ModelAnimPose pose, float scale, Color color); + //------------------------------------------------------------------------------------ // Program main entry point //------------------------------------------------------------------------------------ @@ -41,22 +43,17 @@ int main(void) camera.fovy = 45.0f; // Camera field-of-view Y camera.projection = CAMERA_PERSPECTIVE; // Camera projection type - Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position - - char modelFileName[128] = "resources/models/m3d/cesium_man.m3d"; - bool drawMesh = 1; - bool drawSkeleton = 1; - bool animPlaying = false; // Store anim state, what to draw - // Load model - Model model = LoadModel(modelFileName); // Load the bind-pose model mesh and basic data + Model model = LoadModel("resources/models/m3d/cesium_man.m3d"); // Load the animated model mesh and basic data + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position - // Load animations - int animsCount = 0; - int animFrameCounter = 0, animId = 0; - ModelAnimation *anims = LoadModelAnimations(modelFileName, &animsCount); // Load skeletal animation data + // Load animation data + int animCount = 0; + ModelAnimation *anims = LoadModelAnimations("resources/models/m3d/cesium_man.m3d", &animCount); - DisableCursor(); // Limit cursor to relative movement inside the window + // Animation playing variables + unsigned int animIndex = 0; // Current animation playing + float animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames) SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -66,38 +63,16 @@ int main(void) { // Update //---------------------------------------------------------------------------------- - UpdateCamera(&camera, CAMERA_FIRST_PERSON); + UpdateCamera(&camera, CAMERA_ORBITAL); - if (animsCount) - { - // Play animation when spacebar is held down (or step one frame with N) - if (IsKeyDown(KEY_SPACE) || IsKeyPressed(KEY_N)) - { - animFrameCounter++; + // Select current animation + if (IsKeyPressed(KEY_RIGHT)) animIndex = (animIndex + 1)%animCount; + else if (IsKeyPressed(KEY_LEFT)) animIndex = (animIndex + animCount - 1)%animCount; - if (animFrameCounter >= anims[animId].frameCount) animFrameCounter = 0; - - UpdateModelAnimation(model, anims[animId], animFrameCounter); - animPlaying = true; - } - - // Select animation by pressing C - if (IsKeyPressed(KEY_C)) - { - animFrameCounter = 0; - animId++; - - if (animId >= (int)animsCount) animId = 0; - UpdateModelAnimation(model, anims[animId], 0); - animPlaying = true; - } - } - - // Toggle skeleton drawing - if (IsKeyPressed(KEY_B)) drawSkeleton ^= 1; - - // Toggle mesh drawing - if (IsKeyPressed(KEY_M)) drawMesh ^= 1; + // Update model animation + animCurrentFrame += 1.0f; + if (animCurrentFrame >= anims[animIndex].keyframeCount) animCurrentFrame = 0.0f; + UpdateModelAnimation(model, anims[animIndex], animCurrentFrame); //---------------------------------------------------------------------------------- // Draw @@ -109,52 +84,19 @@ int main(void) BeginMode3D(camera); // Draw 3d model with texture - if (drawMesh) DrawModel(model, position, 1.0f, WHITE); - - // Draw the animated skeleton - if (drawSkeleton) + if (!IsKeyDown(KEY_SPACE)) DrawModel(model, position, 1.0f, WHITE); + else { - // Loop to (boneCount - 1) because the last one is a special "no bone" bone, - // needed to workaround buggy models - // without a -1, we would always draw a cube at the origin - for (int i = 0; i < model.boneCount - 1; i++) - { - // By default the model is loaded in bind-pose by LoadModel() - // But if UpdateModelAnimation() has been called at least once - // then the model is already in animation pose, so we need the animated skeleton - if (!animPlaying || !animsCount) - { - // Display the bind-pose skeleton - DrawCube(model.bindPose[i].translation, 0.04f, 0.04f, 0.04f, RED); - - if (model.bones[i].parent >= 0) - { - DrawLine3D(model.bindPose[i].translation, - model.bindPose[model.bones[i].parent].translation, RED); - } - } - else - { - // Display the frame-pose skeleton - DrawCube(anims[animId].framePoses[animFrameCounter][i].translation, 0.05f, 0.05f, 0.05f, RED); - - if (anims[animId].bones[i].parent >= 0) - { - DrawLine3D(anims[animId].framePoses[animFrameCounter][i].translation, - anims[animId].framePoses[animFrameCounter][anims[animId].bones[i].parent].translation, RED); - } - } - } + // Draw the animated skeleton + DrawModelSkeleton(model.skeleton, anims[animIndex].keyframePoses[(int)animCurrentFrame], 1.0f, RED); } - DrawGrid(10, 1.0f); // Draw a grid + DrawGrid(10, 1.0f); EndMode3D(); - DrawText("PRESS SPACE to PLAY MODEL ANIMATION", 10, GetScreenHeight() - 80, 10, MAROON); - DrawText("PRESS N to STEP ONE ANIMATION FRAME", 10, GetScreenHeight() - 60, 10, DARKGRAY); - DrawText("PRESS C to CYCLE THROUGH ANIMATIONS", 10, GetScreenHeight() - 40, 10, DARKGRAY); - DrawText("PRESS M to toggle MESH, B to toggle SKELETON DRAWING", 10, GetScreenHeight() - 20, 10, DARKGRAY); + DrawText(TextFormat("Current animation: %s", anims[animIndex].name), 10, 10, 20, LIGHTGRAY); + DrawText("Press SPACE to draw skeleton", 10, 40, 20, MAROON); DrawText("(c) CesiumMan model by KhronosGroup", GetScreenWidth() - 210, GetScreenHeight() - 20, 10, GRAY); EndDrawing(); @@ -163,14 +105,28 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - - // Unload model animations data - UnloadModelAnimations(anims, animsCount); - - UnloadModel(model); // Unload model + UnloadModelAnimations(anims, animCount); // Unload model animations data + UnloadModel(model); // Unload model CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } + +// Draw model skeleton +static void DrawModelSkeleton(ModelSkeleton skeleton, ModelAnimPose pose, float scale, Color color) +{ + // Loop to (boneCount - 1) because the last one is a special "no bone" bone, + // needed to workaround buggy models without a -1, a cube is always drawn at the origin + for (int i = 0; i < skeleton.boneCount - 1; i++) + { + // Display the frame-pose skeleton + DrawCube(pose[i].translation, scale*0.05f, scale*0.05f, scale*0.05f, color); + + if (skeleton.bones[i].parent >= 0) + { + DrawLine3D(pose[i].translation, pose[skeleton.bones[i].parent].translation, color); + } + } +} \ No newline at end of file diff --git a/examples/models/models_loading_m3d.png b/examples/models/models_loading_m3d.png index 9220419ad..101831c60 100644 Binary files a/examples/models/models_loading_m3d.png and b/examples/models/models_loading_m3d.png differ diff --git a/examples/models/models_loading_vox.c b/examples/models/models_loading_vox.c index 06dc651d7..04b203ad9 100644 --- a/examples/models/models_loading_vox.c +++ b/examples/models/models_loading_vox.c @@ -25,9 +25,9 @@ #include "rlights.h" #if defined(PLATFORM_DESKTOP) -#define GLSL_VERSION 330 + #define GLSL_VERSION 330 #else // PLATFORM_ANDROID, PLATFORM_WEB -#define GLSL_VERSION 100 + #define GLSL_VERSION 100 #endif //------------------------------------------------------------------------------------ @@ -130,6 +130,7 @@ int main(void) camerarot.y = 0; } + // Update camere movement, custom controls UpdateCameraPro(&camera, (Vector3){ (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))*0.1f - (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))*0.1f, // Move forward-backward (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))*0.1f - (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))*0.1f, // Move right-left @@ -173,7 +174,7 @@ int main(void) DrawText("- MOUSE LEFT BUTTON: CYCLE VOX MODELS", 20, 50, 10, BLUE); DrawText("- MOUSE MIDDLE BUTTON: ZOOM OR ROTATE CAMERA", 20, 70, 10, BLUE); DrawText("- UP-DOWN-LEFT-RIGHT KEYS: MOVE CAMERA", 20, 90, 10, BLUE); - DrawText(TextFormat("Model file: %s", GetFileName(voxFileNames[currentModel])), 10, 10, 20, GRAY); + DrawText(TextFormat("VOX model file: %s", GetFileName(voxFileNames[currentModel])), 10, 10, 20, GRAY); EndDrawing(); //---------------------------------------------------------------------------------- diff --git a/examples/models/models_loading_vox.png b/examples/models/models_loading_vox.png index 417d887e4..2bae0ab3e 100644 Binary files a/examples/models/models_loading_vox.png and b/examples/models/models_loading_vox.png differ diff --git a/projects/VS2022/examples/models_animation_timming.vcxproj b/projects/VS2022/examples/models_animation_timing.vcxproj similarity index 99% rename from projects/VS2022/examples/models_animation_timming.vcxproj rename to projects/VS2022/examples/models_animation_timing.vcxproj index a18123bbc..9a1034925 100644 --- a/projects/VS2022/examples/models_animation_timming.vcxproj +++ b/projects/VS2022/examples/models_animation_timing.vcxproj @@ -55,7 +55,7 @@ Win32Proj models_animation_timming 10.0 - models_animation_timming + models_animation_timing @@ -553,7 +553,7 @@ - + diff --git a/projects/VS2022/raylib.sln b/projects/VS2022/raylib.sln index 51268e037..02fe4b197 100644 --- a/projects/VS2022/raylib.sln +++ b/projects/VS2022/raylib.sln @@ -435,7 +435,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "models_animation_blending", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "core_window_web", "examples\core_window_web.vcxproj", "{4E7157E0-6CDB-47AE-A19A-FEC3876FA8A3}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "models_animation_timming", "examples\models_animation_timming.vcxproj", "{89D5A0E9-683C-465C-BF85-A880865175C8}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "models_animation_timing", "examples\models_animation_timing.vcxproj", "{89D5A0E9-683C-465C-BF85-A880865175C8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/config.h b/src/config.h index 3beaeeceb..5fb4f5545 100644 --- a/src/config.h +++ b/src/config.h @@ -131,13 +131,13 @@ // Module: rlgl - Configuration values //------------------------------------------------------------------------------------ #if !defined(EXTERNAL_CONFIG_FLAGS) +//#define SUPPORT_GPU_SKINNING 1 // GPU skinning, comment if your GPU does not support more than 8 VBOs + // Enable OpenGL Debug Context (only available on OpenGL 4.3) -//#define RLGL_ENABLE_OPENGL_DEBUG_CONTEXT 1 +//#define RLGL_ENABLE_OPENGL_DEBUG_CONTEXT 1 // OpenGL debug context requested // Show OpenGL extensions and capabilities detailed logs on init -//#define RLGL_SHOW_GL_DETAILS_INFO 1 - -#define RL_SUPPORT_MESH_GPU_SKINNING 1 // GPU skinning, comment if your GPU does not support more than 8 VBOs +//#define RLGL_SHOW_GL_DETAILS_INFO 1 // Show OpenGL detailed info on initialization (limits and extensions) #endif //#define RL_DEFAULT_BATCH_BUFFER_ELEMENTS 4096 // Default internal render batch elements limits @@ -149,8 +149,8 @@ #define RL_MAX_SHADER_LOCATIONS 32 // Maximum number of shader locations supported -#define RL_CULL_DISTANCE_NEAR 0.05 // Default projection matrix near cull distance -#define RL_CULL_DISTANCE_FAR 4000.0 // Default projection matrix far cull distance +#define RL_CULL_DISTANCE_NEAR 0.05 // Default projection matrix near cull distance +#define RL_CULL_DISTANCE_FAR 4000.0 // Default projection matrix far cull distance // Default shader vertex attribute locations #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_POSITION 0 @@ -160,27 +160,31 @@ #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT 4 #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2 5 #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INDICES 6 -#if defined(RL_SUPPORT_MESH_GPU_SKINNING) - #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS 7 +#if defined(SUPPORT_GPU_SKINNING) + #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES 7 #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS 8 #endif -#define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCE_TX 9 +#define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCETRANSFORMS 9 -// Default shader vertex attribute names to set location points -// NOTE: When a new shader is loaded, the following locations are tried to be set for convenience +// Default shader vertex attribute/uniform names to set location points +// NOTE: When a new shader is loaded, locations are tried to be set for convenience, +// if the following names are found in the shader, if not, it's up to the user to set locations #define RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION "vertexPosition" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_POSITION #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD "vertexTexCoord" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD #define RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL "vertexNormal" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_NORMAL #define RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR "vertexColor" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_COLOR #define RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT "vertexTangent" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 "vertexTexCoord2" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2 +#define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES "vertexBoneIndices" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES +#define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS "vertexBoneWeights" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS #define RL_DEFAULT_SHADER_UNIFORM_NAME_MVP "mvp" // model-view-projection matrix #define RL_DEFAULT_SHADER_UNIFORM_NAME_VIEW "matView" // view matrix #define RL_DEFAULT_SHADER_UNIFORM_NAME_PROJECTION "matProjection" // projection matrix #define RL_DEFAULT_SHADER_UNIFORM_NAME_MODEL "matModel" // model matrix #define RL_DEFAULT_SHADER_UNIFORM_NAME_NORMAL "matNormal" // normal matrix (transpose(inverse(matModelView)) -#define RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR "colDiffuse" // color diffuse (base tint color, multiplied by texture color) +#define RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR "colDiffuse" // color diffuse (tint color, multiplied by texture color) +#define RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES "boneMatrices" // bone matrices #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE0 "texture0" // texture0 (texture slot active 0) #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE1 "texture1" // texture1 (texture slot active 1) #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE2 "texture2" // texture2 (texture slot active 2) @@ -278,10 +282,11 @@ //------------------------------------------------------------------------------------ #define MAX_MATERIAL_MAPS 12 // Maximum number of shader maps supported -#ifdef RL_SUPPORT_MESH_GPU_SKINNING -#define MAX_MESH_VERTEX_BUFFERS 9 // Maximum vertex buffers (VBO) per mesh +#ifdef SUPPORT_GPU_SKINNING + // NOTE: Two additional vertex buffers required to store bone indices and bone weights + #define MAX_MESH_VERTEX_BUFFERS 9 // Maximum vertex buffers (VBO) per mesh #else -#define MAX_MESH_VERTEX_BUFFERS 7 // Maximum vertex buffers (VBO) per mesh + #define MAX_MESH_VERTEX_BUFFERS 7 // Maximum vertex buffers (VBO) per mesh #endif //------------------------------------------------------------------------------------ diff --git a/src/raylib.h b/src/raylib.h index 06138ceca..bd64bbe6f 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -353,13 +353,15 @@ typedef struct Mesh { unsigned char *colors; // Vertex colors (RGBA - 4 components per vertex) (shader-location = 3) unsigned short *indices; // Vertex indices (in case vertex data comes indexed) - // Animation vertex data + // Skin data for animation + int boneCount; // Number of bones (MAX: 256 bones) + unsigned char *boneIndices; // Vertex bone indices, up to 4 bones influence by vertex (skinning) (shader-location = 6) + float *boneWeights; // Vertex bone weight, up to 4 bones influence by vertex (skinning) (shader-location = 7) + + // Runtime animation vertex data (CPU skinning) + // NOTE: In case of GPU skinning, not used, pointers are NULL float *animVertices; // Animated vertex positions (after bones transformations) float *animNormals; // Animated normals (after bones transformations) - unsigned char *boneIds; // Vertex bone ids, max 255 bone ids, up to 4 bones influence by vertex (skinning) (shader-location = 6) - float *boneWeights; // Vertex bone weight, up to 4 bones influence by vertex (skinning) (shader-location = 7) - Matrix *boneMatrices; // Bones animated transformation matrices - int boneCount; // Number of bones // OpenGL identifiers unsigned int vaoId; // OpenGL Vertex Array Object id @@ -393,12 +395,22 @@ typedef struct Transform { Vector3 scale; // Scale } Transform; +// Anim pose, an array of Transform[] +typedef Transform *ModelAnimPose; + // Bone, skeletal animation bone typedef struct BoneInfo { char name[32]; // Bone name int parent; // Bone parent } BoneInfo; +// Skeleton, animation bones hierarchy +typedef struct ModelSkeleton { + int boneCount; // Number of bones + BoneInfo *bones; // Bones information (skeleton) + ModelAnimPose bindPose; // Bones base transformation (Transform[]) +} ModelSkeleton; + // Model, meshes, materials and animation data typedef struct Model { Matrix transform; // Local transform matrix @@ -410,18 +422,20 @@ typedef struct Model { int *meshMaterial; // Mesh material number // Animation data - int boneCount; // Number of bones - BoneInfo *bones; // Bones information (skeleton) - Transform *bindPose; // Bones base transformation (pose) + ModelSkeleton skeleton; // Skeleton for animation + + // Runtime animation data (CPU/GPU skinning) + ModelAnimPose currentPose; // Current animation pose (Transform[]) + Matrix *boneMatrices; // Bones animated transformation matrices } Model; -// ModelAnimation +// ModelAnimation, contains a full animation sequence typedef struct ModelAnimation { char name[32]; // Animation name - int boneCount; // Number of bones - int frameCount; // Number of animation frames - BoneInfo *bones; // Bones information (skeleton) - Transform **framePoses; // Poses array by frame + + int boneCount; // Number of bones (per pose) + int keyframeCount; // Number of animation key frames + ModelAnimPose *keyframePoses; // Animation sequence keyframe poses [keyframe][pose] } ModelAnimation; // Ray, ray for raycasting @@ -768,6 +782,8 @@ typedef enum { #define MATERIAL_MAP_SPECULAR MATERIAL_MAP_METALNESS // Shader location index +// NOTE: Some locations are tried to be set automatically on shader loading, +// but only if default attributes/uniforms names are found, check config.h for names typedef enum { SHADER_LOC_VERTEX_POSITION = 0, // Shader location: vertex attribute: position SHADER_LOC_VERTEX_TEXCOORD01, // Shader location: vertex attribute: texcoord01 @@ -790,15 +806,15 @@ typedef enum { SHADER_LOC_MAP_ROUGHNESS, // Shader location: sampler2d texture: roughness SHADER_LOC_MAP_OCCLUSION, // Shader location: sampler2d texture: occlusion SHADER_LOC_MAP_EMISSION, // Shader location: sampler2d texture: emission - SHADER_LOC_MAP_HEIGHT, // Shader location: sampler2d texture: height + SHADER_LOC_MAP_HEIGHT, // Shader location: sampler2d texture: heightmap SHADER_LOC_MAP_CUBEMAP, // Shader location: samplerCube texture: cubemap SHADER_LOC_MAP_IRRADIANCE, // Shader location: samplerCube texture: irradiance SHADER_LOC_MAP_PREFILTER, // Shader location: samplerCube texture: prefilter SHADER_LOC_MAP_BRDF, // Shader location: sampler2d texture: brdf - SHADER_LOC_VERTEX_BONEIDS, // Shader location: vertex attribute: boneIds - SHADER_LOC_VERTEX_BONEWEIGHTS, // Shader location: vertex attribute: boneWeights - SHADER_LOC_BONE_MATRICES, // Shader location: array of matrices uniform: boneMatrices - SHADER_LOC_VERTEX_INSTANCE_TX // Shader location: vertex attribute: instanceTransform + SHADER_LOC_VERTEX_BONEIDS, // Shader location: vertex attribute: bone indices + SHADER_LOC_VERTEX_BONEWEIGHTS, // Shader location: vertex attribute: bone weights + SHADER_LOC_MATRIX_BONETRANSFORMS, // Shader location: matrix attribute: bone transforms (animation) + SHADER_LOC_VERTEX_INSTANCETRANSFORMS // Shader location: vertex attribute: instance transforms } ShaderLocationIndex; #define SHADER_LOC_MAP_DIFFUSE SHADER_LOC_MAP_ALBEDO @@ -1619,11 +1635,8 @@ RLAPI void SetModelMeshMaterial(Model *model, int meshId, int materialId); // Model animations loading/unloading functions RLAPI ModelAnimation *LoadModelAnimations(const char *fileName, int *animCount); // Load model animations from file -RLAPI void UpdateModelAnimation(Model model, ModelAnimation anim, int frame); // Update model animation pose (CPU) -RLAPI void UpdateModelAnimationBones(Model model, ModelAnimation anim, int frame); // Update model animation mesh bone matrices (GPU skinning) -RLAPI void UpdateModelAnimationBonesLerp(Model model, ModelAnimation animA, int frameA, ModelAnimation animB, int frameB, float value); // Update model animation mesh bone matrices with interpolation between two poses(GPU skinning) -RLAPI void UpdateModelVertsToCurrentBones(Model model); // Update model vertices according to mesh bone matrices (CPU) -RLAPI void UnloadModelAnimation(ModelAnimation anim); // Unload animation data +RLAPI void UpdateModelAnimation(Model model, ModelAnimation anim, float frame); // Update model animation pose (vertex buffers and bone matrices) +RLAPI void UpdateModelAnimationEx(Model model, ModelAnimation animA, float frameA, ModelAnimation animB, float frameB, float blend); // Update model animation pose, blending two animations RLAPI void UnloadModelAnimations(ModelAnimation *animations, int animCount); // Unload animation array data RLAPI bool IsModelAnimationValid(Model model, ModelAnimation anim); // Check model animation skeleton match diff --git a/src/rcore.c b/src/rcore.c index c29fb1e19..f1481890b 100644 --- a/src/rcore.c +++ b/src/rcore.c @@ -1286,7 +1286,7 @@ Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode) // - vertex color location = 3 // - vertex tangent location = 4 // - vertex texcoord2 location = 5 - // - vertex boneIds location = 6 + // - vertex boneIndices location = 6 // - vertex boneWeights location = 7 // NOTE: If any location is not found, loc point becomes -1 @@ -1303,9 +1303,9 @@ Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode) shader.locs[SHADER_LOC_VERTEX_NORMAL] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL); shader.locs[SHADER_LOC_VERTEX_TANGENT] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT); shader.locs[SHADER_LOC_VERTEX_COLOR] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR); - shader.locs[SHADER_LOC_VERTEX_BONEIDS] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS); + shader.locs[SHADER_LOC_VERTEX_BONEIDS] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES); shader.locs[SHADER_LOC_VERTEX_BONEWEIGHTS] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS); - shader.locs[SHADER_LOC_VERTEX_INSTANCE_TX] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCE_TX); + shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORMS] = rlGetLocationAttrib(shader.id, RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCETRANSFORMS); // Get handles to GLSL uniform locations (vertex shader) shader.locs[SHADER_LOC_MATRIX_MVP] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_MVP); @@ -1313,7 +1313,7 @@ Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode) shader.locs[SHADER_LOC_MATRIX_PROJECTION] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_PROJECTION); shader.locs[SHADER_LOC_MATRIX_MODEL] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_MODEL); shader.locs[SHADER_LOC_MATRIX_NORMAL] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_NORMAL); - shader.locs[SHADER_LOC_BONE_MATRICES] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_BONE_MATRICES); + shader.locs[SHADER_LOC_MATRIX_BONETRANSFORMS] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES); // Get handles to GLSL uniform locations (fragment shader) shader.locs[SHADER_LOC_COLOR_DIFFUSE] = rlGetLocationUniform(shader.id, RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR); diff --git a/src/rlgl.h b/src/rlgl.h index 0cde3e6eb..67817a3c9 100644 --- a/src/rlgl.h +++ b/src/rlgl.h @@ -65,7 +65,7 @@ * #define RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR "vertexColor" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_COLOR * #define RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT "vertexTangent" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT * #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 "vertexTexCoord2" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2 -* #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS "vertexBoneIds" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS +* #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES "vertexBoneIndices" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES * #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS "vertexBoneWeights" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS * #define RL_DEFAULT_SHADER_UNIFORM_NAME_MVP "mvp" // model-view-projection matrix * #define RL_DEFAULT_SHADER_UNIFORM_NAME_VIEW "matView" // view matrix @@ -73,7 +73,7 @@ * #define RL_DEFAULT_SHADER_UNIFORM_NAME_MODEL "matModel" // model matrix * #define RL_DEFAULT_SHADER_UNIFORM_NAME_NORMAL "matNormal" // normal matrix (transpose(inverse(matModelView))) * #define RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR "colDiffuse" // color diffuse (base tint color, multiplied by texture color) -* #define RL_DEFAULT_SHADER_UNIFORM_NAME_BONE_MATRICES "boneMatrices" // bone matrices +* #define RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES "boneMatrices" // bone matrices * #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE0 "texture0" // texture0 (texture slot active 0) * #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE1 "texture1" // texture1 (texture slot active 1) * #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE2 "texture2" // texture2 (texture slot active 2) @@ -345,16 +345,16 @@ #ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_INDICES #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INDICES 6 #endif -#ifdef RL_SUPPORT_MESH_GPU_SKINNING -#ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS - #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS 7 +#ifdef SUPPORT_GPU_SKINNING + #ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES + #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES 7 + #endif + #ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS + #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS 8 + #endif #endif -#ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS - #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS 8 -#endif -#endif -#ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCE_TX - #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCE_TX 9 +#ifndef RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCETRANSFORMS + #define RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCETRANSFORMS 9 #endif //---------------------------------------------------------------------------------- @@ -992,31 +992,34 @@ RLAPI void rlLoadDrawQuad(void); // Load and draw a quad // Default shader vertex attribute names to set location points #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION - #define RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION "vertexPosition" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION + #define RL_DEFAULT_SHADER_ATTRIB_NAME_POSITION "vertexPosition" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_POSITION #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD - #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD "vertexTexCoord" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD + #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD "vertexTexCoord" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL - #define RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL "vertexNormal" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL + #define RL_DEFAULT_SHADER_ATTRIB_NAME_NORMAL "vertexNormal" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_NORMAL #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR - #define RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR "vertexColor" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR + #define RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR "vertexColor" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_COLOR #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT - #define RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT "vertexTangent" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT + #define RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT "vertexTangent" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 - #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 "vertexTexCoord2" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 + #define RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2 "vertexTexCoord2" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2 #endif -#ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS - #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS "vertexBoneIds" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS +#ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES + #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES "vertexBoneIndices" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES #endif #ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS - #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS "vertexBoneWeights" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS + #define RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS "vertexBoneWeights" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS #endif -#ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCE_TX - #define RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCE_TX "instanceTransform" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCE_TX +#ifndef RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES + #define RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES "boneMatrices" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEMATRICES +#endif +#ifndef RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCETRANSFORMS + #define RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCETRANSFORMS "instanceTransform" // Bound by default to shader location: RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCETRANSFORMS #endif #ifndef RL_DEFAULT_SHADER_UNIFORM_NAME_MVP @@ -1037,9 +1040,6 @@ RLAPI void rlLoadDrawQuad(void); // Load and draw a quad #ifndef RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR #define RL_DEFAULT_SHADER_UNIFORM_NAME_COLOR "colDiffuse" // color diffuse (base tint color, multiplied by texture color) #endif -#ifndef RL_DEFAULT_SHADER_UNIFORM_NAME_BONE_MATRICES - #define RL_DEFAULT_SHADER_UNIFORM_NAME_BONE_MATRICES "boneMatrices" // bone matrices -#endif #ifndef RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE0 #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE0 "texture0" // texture0 (texture slot active 0) #endif @@ -1049,6 +1049,9 @@ RLAPI void rlLoadDrawQuad(void); // Load and draw a quad #ifndef RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE2 #define RL_DEFAULT_SHADER_SAMPLER2D_NAME_TEXTURE2 "texture2" // texture2 (texture slot active 2) #endif +#ifndef RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES + #define RL_DEFAULT_SHADER_UNIFORM_NAME_BONEMATRICES "boneMatrices" // bone matrices (required for GPU skinning) +#endif //---------------------------------------------------------------------------------- // Module Types and Structures Definition @@ -4339,10 +4342,9 @@ unsigned int rlLoadShaderProgram(unsigned int vShaderId, unsigned int fShaderId) glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_COLOR, RL_DEFAULT_SHADER_ATTRIB_NAME_COLOR); glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT, RL_DEFAULT_SHADER_ATTRIB_NAME_TANGENT); glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2, RL_DEFAULT_SHADER_ATTRIB_NAME_TEXCOORD2); - glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCE_TX, RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCE_TX); - -#ifdef RL_SUPPORT_MESH_GPU_SKINNING - glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEIDS); + glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_INSTANCETRANSFORMS, RL_DEFAULT_SHADER_ATTRIB_NAME_INSTANCETRANSFORMS); +#ifdef SUPPORT_GPU_SKINNING + glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEINDICES); glBindAttribLocation(programId, RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS, RL_DEFAULT_SHADER_ATTRIB_NAME_BONEWEIGHTS); #endif diff --git a/src/rmodels.c b/src/rmodels.c index b90b6a53f..1649fb8f3 100644 --- a/src/rmodels.c +++ b/src/rmodels.c @@ -167,6 +167,9 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, int *animCou static void ProcessMaterialsOBJ(Material *rayMaterials, tinyobj_material_t *materials, int materialCount); // Process obj materials #endif +// Update model vertex data (positions and normals) +static void UpdateModelAnimationVertexBuffers(Model model); + //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- @@ -1182,7 +1185,7 @@ bool IsModelValid(Model model) if ((model.meshes[i].tangents != NULL) && (model.meshes[i].vboId[4] == 0)) { result = false; break; } // Vertex tangents buffer not uploaded to GPU if ((model.meshes[i].texcoords2 != NULL) && (model.meshes[i].vboId[5] == 0)) { result = false; break; } // Vertex texcoords2 buffer not uploaded to GPU if ((model.meshes[i].indices != NULL) && (model.meshes[i].vboId[6] == 0)) { result = false; break; } // Vertex indices buffer not uploaded to GPU - if ((model.meshes[i].boneIds != NULL) && (model.meshes[i].vboId[7] == 0)) { result = false; break; } // Vertex boneIds buffer not uploaded to GPU + if ((model.meshes[i].boneIndices != NULL) && (model.meshes[i].vboId[7] == 0)) { result = false; break; } // Vertex boneIndices buffer not uploaded to GPU if ((model.meshes[i].boneWeights != NULL) && (model.meshes[i].vboId[8] == 0)) { result = false; break; } // Vertex boneWeights buffer not uploaded to GPU // NOTE: Some OpenGL versions do not support VAO, so we don't check it @@ -1212,8 +1215,8 @@ void UnloadModel(Model model) RL_FREE(model.meshMaterial); // Unload animation data - RL_FREE(model.bones); - RL_FREE(model.bindPose); + RL_FREE(model.skeleton.bones); + RL_FREE(model.skeleton.bindPose); TRACELOG(LOG_INFO, "MODEL: Unloaded model (and meshes) from RAM and VRAM"); } @@ -1273,9 +1276,8 @@ void UploadMesh(Mesh *mesh, bool dynamic) mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_TANGENT] = 0; // Vertex buffer: tangents mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2] = 0; // Vertex buffer: texcoords2 mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_INDICES] = 0; // Vertex buffer: indices - -#ifdef RL_SUPPORT_MESH_GPU_SKINNING - mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS] = 0; // Vertex buffer: boneIds +#ifdef SUPPORT_GPU_SKINNING + mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES] = 0; // Vertex buffer: boneIndices mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEWEIGHTS] = 0; // Vertex buffer: boneWeights #endif @@ -1375,21 +1377,21 @@ void UploadMesh(Mesh *mesh, bool dynamic) rlDisableVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_TEXCOORD2); } -#ifdef RL_SUPPORT_MESH_GPU_SKINNING - if (mesh->boneIds != NULL) +#ifdef SUPPORT_GPU_SKINNING + if (mesh->boneIndices != NULL) { - // Enable vertex attribute: boneIds (shader-location = 7) - mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS] = rlLoadVertexBuffer(mesh->boneIds, mesh->vertexCount*4*sizeof(unsigned char), dynamic); - rlSetVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS, 4, RL_UNSIGNED_BYTE, 0, 0, 0); - rlEnableVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS); + // Enable vertex attribute: boneIndices (shader-location = 7) + mesh->vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES] = rlLoadVertexBuffer(mesh->boneIndices, mesh->vertexCount*4*sizeof(unsigned char), dynamic); + rlSetVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES, 4, RL_UNSIGNED_BYTE, 0, 0, 0); + rlEnableVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES); } else { - // Default vertex attribute: boneIds + // Default vertex attribute: boneIndices // WARNING: Default value provided to shader if location available float value[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - rlSetVertexAttributeDefault(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS, value, SHADER_ATTRIB_VEC4, 4); - rlDisableVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS); + rlSetVertexAttributeDefault(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES, value, SHADER_ATTRIB_VEC4, 4); + rlDisableVertexAttribute(RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES); } if (mesh->boneWeights != NULL) @@ -1436,17 +1438,17 @@ void DrawMesh(Mesh mesh, Material material, Matrix transform) #define GL_COLOR_ARRAY 0x8076 #define GL_TEXTURE_COORD_ARRAY 0x8078 - if (mesh.texcoords && material.maps[MATERIAL_MAP_DIFFUSE].texture.id > 0) rlEnableTexture(material.maps[MATERIAL_MAP_DIFFUSE].texture.id); + if ((mesh.texcoords != NULL) && (material.maps[MATERIAL_MAP_DIFFUSE].texture.id > 0)) rlEnableTexture(material.maps[MATERIAL_MAP_DIFFUSE].texture.id); - if (mesh.animVertices) rlEnableStatePointer(GL_VERTEX_ARRAY, mesh.animVertices); + if (mesh.animVertices != NULL) rlEnableStatePointer(GL_VERTEX_ARRAY, mesh.animVertices); else rlEnableStatePointer(GL_VERTEX_ARRAY, mesh.vertices); if (mesh.texcoords) rlEnableStatePointer(GL_TEXTURE_COORD_ARRAY, mesh.texcoords); - if (mesh.animNormals) rlEnableStatePointer(GL_NORMAL_ARRAY, mesh.animNormals); - else if (mesh.normals) rlEnableStatePointer(GL_NORMAL_ARRAY, mesh.normals); + if (mesh.animNormals != NULL) rlEnableStatePointer(GL_NORMAL_ARRAY, mesh.animNormals); + else if (mesh.normals != NULL) rlEnableStatePointer(GL_NORMAL_ARRAY, mesh.normals); - if (mesh.colors) rlEnableStatePointer(GL_COLOR_ARRAY, mesh.colors); + if (mesh.colors != NULL) rlEnableStatePointer(GL_COLOR_ARRAY, mesh.colors); rlPushMatrix(); rlMultMatrixf(MatrixToFloat(transform)); @@ -1528,14 +1530,6 @@ void DrawMesh(Mesh mesh, Material material, Matrix transform) // Upload model normal matrix (if locations available) if (material.shader.locs[SHADER_LOC_MATRIX_NORMAL] != -1) rlSetUniformMatrix(material.shader.locs[SHADER_LOC_MATRIX_NORMAL], MatrixTranspose(MatrixInvert(matModel))); - -#ifdef RL_SUPPORT_MESH_GPU_SKINNING - // Upload Bone Transforms - if ((material.shader.locs[SHADER_LOC_BONE_MATRICES] != -1) && mesh.boneMatrices) - { - rlSetUniformMatrices(material.shader.locs[SHADER_LOC_BONE_MATRICES], mesh.boneMatrices, mesh.boneCount); - } -#endif //----------------------------------------------------- // Bind active texture maps (if available) @@ -1615,11 +1609,11 @@ void DrawMesh(Mesh mesh, Material material, Matrix transform) rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_TEXCOORD02]); } -#ifdef RL_SUPPORT_MESH_GPU_SKINNING +#ifdef SUPPORT_GPU_SKINNING // Bind mesh VBO data: vertex bone ids (shader-location = 6, if available) if (material.shader.locs[SHADER_LOC_VERTEX_BONEIDS] != -1) { - rlEnableVertexBuffer(mesh.vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS]); + rlEnableVertexBuffer(mesh.vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES]); rlSetVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_BONEIDS], 4, RL_UNSIGNED_BYTE, 0, 0, 0); rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_BONEIDS]); } @@ -1743,7 +1737,7 @@ void DrawMeshInstanced(Mesh mesh, Material material, const Matrix *transforms, i if (material.shader.locs[SHADER_LOC_MATRIX_PROJECTION] != -1) rlSetUniformMatrix(material.shader.locs[SHADER_LOC_MATRIX_PROJECTION], matProjection); // Create instances buffer - instanceTransforms = (float16 *)RL_MALLOC(instances*sizeof(float16)); + instanceTransforms = (float16 *)RL_CALLOC(instances, sizeof(float16)); // Fill buffer with instances transformations as float16 arrays for (int i = 0; i < instances; i++) instanceTransforms[i] = MatrixToFloatV(transforms[i]); @@ -1757,14 +1751,14 @@ void DrawMeshInstanced(Mesh mesh, Material material, const Matrix *transforms, i // no faster, since all the transform matrices are transferred anyway instancesVboId = rlLoadVertexBuffer(instanceTransforms, instances*sizeof(float16), false); - // Instances transformation matrices are sent to shader attribute location: SHADER_LOC_VERTEX_INSTANCE_TX - if (material.shader.locs[SHADER_LOC_VERTEX_INSTANCE_TX] != -1) + // Instances transformation matrices are sent to shader attribute location: SHADER_LOC_VERTEX_INSTANCETRANSFORMS + if (material.shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORMS] != -1) { for (unsigned int i = 0; i < 4; i++) { - rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_INSTANCE_TX] + i); - rlSetVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_INSTANCE_TX] + i, 4, RL_FLOAT, 0, sizeof(Matrix), i*sizeof(Vector4)); - rlSetVertexAttributeDivisor(material.shader.locs[SHADER_LOC_VERTEX_INSTANCE_TX] + i, 1); + rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORMS] + i); + rlSetVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORMS] + i, 4, RL_FLOAT, 0, sizeof(Matrix), i*sizeof(Vector4)); + rlSetVertexAttributeDivisor(material.shader.locs[SHADER_LOC_VERTEX_INSTANCETRANSFORMS] + i, 1); } } @@ -1777,15 +1771,6 @@ void DrawMeshInstanced(Mesh mesh, Material material, const Matrix *transforms, i // Upload model normal matrix (if locations available) if (material.shader.locs[SHADER_LOC_MATRIX_NORMAL] != -1) rlSetUniformMatrix(material.shader.locs[SHADER_LOC_MATRIX_NORMAL], MatrixTranspose(MatrixInvert(matModel))); - -#ifdef RL_SUPPORT_MESH_GPU_SKINNING - // Upload Bone Transforms - if ((material.shader.locs[SHADER_LOC_BONE_MATRICES] != -1) && mesh.boneMatrices) - { - rlSetUniformMatrices(material.shader.locs[SHADER_LOC_BONE_MATRICES], mesh.boneMatrices, mesh.boneCount); - } -#endif - //----------------------------------------------------- // Bind active texture maps (if available) @@ -1863,11 +1848,11 @@ void DrawMeshInstanced(Mesh mesh, Material material, const Matrix *transforms, i rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_TEXCOORD02]); } -#ifdef RL_SUPPORT_MESH_GPU_SKINNING +#ifdef SUPPORT_GPU_SKINNING // Bind mesh VBO data: vertex bone ids (shader-location = 6, if available) if (material.shader.locs[SHADER_LOC_VERTEX_BONEIDS] != -1) { - rlEnableVertexBuffer(mesh.vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEIDS]); + rlEnableVertexBuffer(mesh.vboId[RL_DEFAULT_SHADER_ATTRIB_LOCATION_BONEINDICES]); rlSetVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_BONEIDS], 4, RL_UNSIGNED_BYTE, 0, 0, 0); rlEnableVertexAttribute(material.shader.locs[SHADER_LOC_VERTEX_BONEIDS]); } @@ -1946,6 +1931,7 @@ void UnloadMesh(Mesh mesh) if (mesh.vboId != NULL) for (int i = 0; i < MAX_MESH_VERTEX_BUFFERS; i++) rlUnloadVertexBuffer(mesh.vboId[i]); RL_FREE(mesh.vboId); + // Unload mesh vertex buffers RL_FREE(mesh.vertices); RL_FREE(mesh.texcoords); RL_FREE(mesh.normals); @@ -1954,11 +1940,13 @@ void UnloadMesh(Mesh mesh) RL_FREE(mesh.texcoords2); RL_FREE(mesh.indices); + // Unload mesh skin animation data + RL_FREE(mesh.boneWeights); + RL_FREE(mesh.boneIndices); + + // Unload mesh runtime CPU skinning data RL_FREE(mesh.animVertices); RL_FREE(mesh.animNormals); - RL_FREE(mesh.boneWeights); - RL_FREE(mesh.boneIds); - RL_FREE(mesh.boneMatrices); } // Export mesh data to file @@ -2189,7 +2177,7 @@ Material *LoadMaterials(const char *fileName, int *materialCount) int result = tinyobj_parse_mtl_file(&mats, &count, fileName); if (result != TINYOBJ_SUCCESS) TRACELOG(LOG_WARNING, "MATERIAL: [%s] Failed to parse materials file", fileName); - materials = (Material *)RL_MALLOC(count*sizeof(Material)); + materials = (Material *)RL_CALLOC(count, sizeof(Material)); ProcessMaterialsOBJ(materials, mats, count); tinyobj_materials_free(mats, count); @@ -2228,10 +2216,11 @@ bool IsMaterialValid(Material material) { bool result = false; - if ((material.maps != NULL) && // Validate material contain some map + if ((material.maps != NULL) && // Validate material contain some map (material.shader.id > 0)) result = true; // Validate material shader is valid - // TODO: Check if available maps contain loaded textures + // NOTE: Checking if available maps contain loaded textures does not determine if + // a material is valid, there can be maps without a texture assigned, only properties return result; } @@ -2287,146 +2276,205 @@ ModelAnimation *LoadModelAnimations(const char *fileName, int *animCount) return animations; } -// Update model animated bones transform matrices for a given frame -// NOTE: Updated data is not uploaded to GPU but kept at model.meshes[i].boneMatrices[boneId], -// to be uploaded to shader at drawing, in case GPU skinning is enabled -void UpdateModelAnimationBones(Model model, ModelAnimation anim, int frame) +// Update model animation data (vertex buffers / bone matrices) for a specific pose +// NOTE 1: Request frame could be fractional, using a lerp interpolation between two frames +// NOTE 2: Updated vertex animation data is uploaded to GPU in case of CPU skinning, +// for GPU skinning, bone matrices are uploaded to shader on DrawModelEx() +void UpdateModelAnimation(Model model, ModelAnimation anim, float frame) { - if ((anim.frameCount > 0) && (anim.bones != NULL) && (anim.framePoses != NULL)) + if (model.boneMatrices == NULL) return; + + //UpdateModelAnimationEx(model, anim, frame, anim, frame, 0.0f); + + // Update model animated bones transform matrices for a given frame + if ((anim.keyframeCount > 0) && (model.skeleton.bones != NULL) && (anim.keyframePoses != NULL)) { - if (frame >= anim.frameCount) frame = frame%anim.frameCount; + // Get frame and blending from frame factor required + int currentFrame = (int)frame; + int nextFrame = currentFrame + 1; + float blend = frame - currentFrame; + blend = Clamp(blend, 0.0f, 1.0f); + if (currentFrame >= anim.keyframeCount) currentFrame = currentFrame%anim.keyframeCount; + if (nextFrame >= anim.keyframeCount) nextFrame = nextFrame%anim.keyframeCount; - // Get first mesh which have bones - int firstMeshWithBones = -1; + Matrix bindPoseMatrix = { 0 }; + Matrix currentPoseMatrix = { 0 }; - for (int i = 0; i < model.meshCount; i++) + // Update all bones and bone matrices of model + for (int boneIndex = 0; boneIndex < model.skeleton.boneCount; boneIndex++) { - if (model.meshes[i].boneMatrices) - { - if (firstMeshWithBones == -1) - { - firstMeshWithBones = i; - break; - } - } + // Compute interpolated pose between current and next frame + // NOTE: Storing animation frame data in model.currentPose + model.currentPose[boneIndex].translation = Vector3Lerp( + anim.keyframePoses[currentFrame][boneIndex].translation, + anim.keyframePoses[nextFrame][boneIndex].translation, blend); + model.currentPose[boneIndex].rotation = QuaternionSlerp( + anim.keyframePoses[currentFrame][boneIndex].rotation, + anim.keyframePoses[nextFrame][boneIndex].rotation, blend); + model.currentPose[boneIndex].scale = Vector3Lerp( + anim.keyframePoses[currentFrame][boneIndex].scale, + anim.keyframePoses[nextFrame][boneIndex].scale, blend); + + // Compute runtime bone matrix from model current pose + //----------------------------------------------------------------------------------- + Transform *bindPoseTransform = &model.skeleton.bindPose[boneIndex]; + bindPoseMatrix = MatrixMultiply( + MatrixMultiply(MatrixScale(bindPoseTransform->scale.x, bindPoseTransform->scale.y, bindPoseTransform->scale.z), + QuaternionToMatrix(bindPoseTransform->rotation)), + MatrixTranslate(bindPoseTransform->translation.x, bindPoseTransform->translation.y, bindPoseTransform->translation.z)); + + Transform *currentPoseTransform = &model.currentPose[boneIndex]; + currentPoseMatrix = MatrixMultiply( + MatrixMultiply(MatrixScale(currentPoseTransform->scale.x, currentPoseTransform->scale.y, currentPoseTransform->scale.z), + QuaternionToMatrix(currentPoseTransform->rotation)), + MatrixTranslate(currentPoseTransform->translation.x, currentPoseTransform->translation.y, currentPoseTransform->translation.z)); + + model.boneMatrices[boneIndex] = MatrixMultiply(MatrixInvert(bindPoseMatrix), currentPoseMatrix); + //----------------------------------------------------------------------------------- } - if (firstMeshWithBones != -1) - { - // Update all bones and boneMatrices of first mesh with bones - for (int boneId = 0; boneId < anim.boneCount; boneId++) - { - Transform *bindTransform = &model.bindPose[boneId]; - Matrix bindMatrix = MatrixMultiply(MatrixMultiply( - MatrixScale(bindTransform->scale.x, bindTransform->scale.y, bindTransform->scale.z), - QuaternionToMatrix(bindTransform->rotation)), - MatrixTranslate(bindTransform->translation.x, bindTransform->translation.y, bindTransform->translation.z)); - - Transform *targetTransform = &anim.framePoses[frame][boneId]; - Matrix targetMatrix = MatrixMultiply(MatrixMultiply( - MatrixScale(targetTransform->scale.x, targetTransform->scale.y, targetTransform->scale.z), - QuaternionToMatrix(targetTransform->rotation)), - MatrixTranslate(targetTransform->translation.x, targetTransform->translation.y, targetTransform->translation.z)); - - model.meshes[firstMeshWithBones].boneMatrices[boneId] = MatrixMultiply(MatrixInvert(bindMatrix), targetMatrix); - } - - // Update remaining meshes with bones - // NOTE: Using deep copy because shallow copy results in double free with 'UnloadModel()' - for (int i = firstMeshWithBones + 1; i < model.meshCount; i++) - { - if (model.meshes[i].boneMatrices) - { - memcpy(model.meshes[i].boneMatrices, - model.meshes[firstMeshWithBones].boneMatrices, - model.meshes[i].boneCount*sizeof(model.meshes[i].boneMatrices[0])); - } - } - } + // CPU skinning, updates CPU buffers and uploads them to GPU + // NOTE: On GPU skinning not supported, use CPU skinning + UpdateModelAnimationVertexBuffers(model); } } -// Update model animated bones transform matrices by interpolating between two different given frames of different ModelAnimation(could be same too) -// NOTE: Updated data is not uploaded to GPU but kept at model.meshes[i].boneMatrices[boneId], -// to be uploaded to shader at drawing, in case GPU skinning is enabled -void UpdateModelAnimationBonesLerp(Model model, ModelAnimation animA, int frameA, ModelAnimation animB, int frameB, float value) +// Update model animation data (vertex buffers / bone matrices) for a specific pose, +// defined by two different animations at specific frames blended together +// NOTE 1: Request frames could be fractional, using a lerp interpolation between two frames +// NOTE 2: Updated vertex animation data is uploaded to GPU in case of CPU skinning, +// for GPU skinning, bone matrices are uploaded to shader on DrawModelEx() +void UpdateModelAnimationEx(Model model, ModelAnimation animA, float frameA, ModelAnimation animB, float frameB, float blend) { - if ((animA.frameCount > 0) && (animA.bones != NULL) && (animA.framePoses != NULL) && - (animB.frameCount > 0) && (animB.bones != NULL) && (animB.framePoses != NULL) && - (value >= 0.0f) && (value <= 1.0f)) + if (model.boneMatrices == NULL) return; + + if ((animA.keyframeCount > 0) && (animA.keyframePoses != NULL) && + (animB.keyframeCount > 0) && (animB.keyframePoses != NULL) && + (blend >= 0.0f) && (blend <= 1.0f)) { - frameA = frameA % animA.frameCount; - frameB = frameB % animB.frameCount; + // Inter-frame interpolation values for first animation + int currentFrameA = (int)frameA%animA.keyframeCount; + int nextFrameA = currentFrameA + 1; + float blendA = frameA - currentFrameA; + blendA = Clamp(blendA, 0.0f, 1.0f); + if (currentFrameA >= animA.keyframeCount) currentFrameA = currentFrameA%animA.keyframeCount; + if (nextFrameA >= animA.keyframeCount) nextFrameA = nextFrameA%animA.keyframeCount; - for (int i = 0; i < model.meshCount; i++) + // Inter-frame interpolation values for second animation + int currentFrameB = (int)frameB%animB.keyframeCount; + int nextFrameB = currentFrameB + 1; + float blendB = frameB - currentFrameB; + blendB = Clamp(blendB, 0.0f, 1.0f); + if (currentFrameB >= animB.keyframeCount) currentFrameB = currentFrameB%animB.keyframeCount; + if (nextFrameB >= animB.keyframeCount) nextFrameB = nextFrameB%animB.keyframeCount; + + Matrix bindPoseMatrix = { 0 }; + Matrix currentPoseMatrix = { 0 }; + + for (int boneIndex = 0; boneIndex < model.skeleton.boneCount; boneIndex++) { - if (model.meshes[i].boneMatrices) - { - assert(model.meshes[i].boneCount == animA.boneCount); - assert(model.meshes[i].boneCount == animB.boneCount); + // Get frame-interpolation for first animation + Vector3 frameATranslation = Vector3Lerp( + animA.keyframePoses[currentFrameA][boneIndex].translation, + animA.keyframePoses[nextFrameA][boneIndex].translation, blendA); + Quaternion frameARotation = QuaternionSlerp( + animA.keyframePoses[currentFrameA][boneIndex].rotation, + animA.keyframePoses[nextFrameA][boneIndex].rotation, blendA); + Vector3 frameAScale = Vector3Lerp( + animA.keyframePoses[currentFrameA][boneIndex].scale, + animA.keyframePoses[nextFrameA][boneIndex].scale, blendA); - for (int boneId = 0; boneId < model.meshes[i].boneCount; boneId++) - { - Vector3 inTranslation = model.bindPose[boneId].translation; - Quaternion inRotation = model.bindPose[boneId].rotation; - Vector3 inScale = model.bindPose[boneId].scale; + // Get frame-interpolation for second animation + Vector3 frameBTranslation = Vector3Lerp( + animB.keyframePoses[currentFrameB][boneIndex].translation, + animB.keyframePoses[nextFrameB][boneIndex].translation, blendB); + Quaternion frameBRotation = QuaternionSlerp( + animB.keyframePoses[currentFrameB][boneIndex].rotation, + animB.keyframePoses[nextFrameB][boneIndex].rotation, blendB); + Vector3 frameBScale = Vector3Lerp( + animB.keyframePoses[currentFrameB][boneIndex].scale, + animB.keyframePoses[nextFrameB][boneIndex].scale, blendB); + + // Compute interpolated pose between both animations frames + // NOTE: Storing animation frame data in model.currentPose + model.currentPose[boneIndex].translation = Vector3Lerp(frameATranslation, frameBTranslation, blend); + model.currentPose[boneIndex].rotation = QuaternionSlerp(frameARotation, frameBRotation, blend); + model.currentPose[boneIndex].scale = Vector3Lerp(frameAScale, frameBScale, blend); - Vector3 outATranslation = animA.framePoses[frameA][boneId].translation; - Quaternion outARotation = animA.framePoses[frameA][boneId].rotation; - Vector3 outAScale = animA.framePoses[frameA][boneId].scale; + // Compute runtime bone matrix from model current pose + //----------------------------------------------------------------------------------- + Transform *bindPoseTransform = &model.skeleton.bindPose[boneIndex]; + bindPoseMatrix = MatrixMultiply( + MatrixMultiply(MatrixScale(bindPoseTransform->scale.x, bindPoseTransform->scale.y, bindPoseTransform->scale.z), + QuaternionToMatrix(bindPoseTransform->rotation)), + MatrixTranslate(bindPoseTransform->translation.x, bindPoseTransform->translation.y, bindPoseTransform->translation.z)); - Vector3 outBTranslation = animB.framePoses[frameB][boneId].translation; - Quaternion outBRotation = animB.framePoses[frameB][boneId].rotation; - Vector3 outBScale = animB.framePoses[frameB][boneId].scale; + Transform *currentPoseTransform = &model.currentPose[boneIndex]; + currentPoseMatrix = MatrixMultiply( + MatrixMultiply(MatrixScale(currentPoseTransform->scale.x, currentPoseTransform->scale.y, currentPoseTransform->scale.z), + QuaternionToMatrix(currentPoseTransform->rotation)), + MatrixTranslate(currentPoseTransform->translation.x, currentPoseTransform->translation.y, currentPoseTransform->translation.z)); - Vector3 outTranslation = Vector3Lerp(outATranslation, outBTranslation, value); - Quaternion outRotation = QuaternionSlerp(outARotation, outBRotation, value); - Vector3 outScale = Vector3Lerp(outAScale, outBScale, value); + model.boneMatrices[boneIndex] = MatrixMultiply(MatrixInvert(bindPoseMatrix), currentPoseMatrix); + //----------------------------------------------------------------------------------- - Vector3 invTranslation = Vector3RotateByQuaternion(Vector3Negate(inTranslation), QuaternionInvert(inRotation)); - Quaternion invRotation = QuaternionInvert(inRotation); - Vector3 invScale = Vector3Divide((Vector3){ 1.0f, 1.0f, 1.0f }, inScale); + /* + Vector3 outATranslation = animA.keyframePoses[currentFrameA][boneIndex].translation; + Quaternion outARotation = animA.keyframePoses[currentFrameA][boneIndex].rotation; + Vector3 outAScale = animA.keyframePoses[currentFrameA][boneIndex].scale; - Vector3 boneTranslation = Vector3Add( - Vector3RotateByQuaternion(Vector3Multiply(outScale, invTranslation), - outRotation), outTranslation); - Quaternion boneRotation = QuaternionMultiply(outRotation, invRotation); - Vector3 boneScale = Vector3Multiply(outScale, invScale); + Vector3 outBTranslation = animB.keyframePoses[currentFrameB][boneIndex].translation; + Quaternion outBRotation = animB.keyframePoses[currentFrameB][boneIndex].rotation; + Vector3 outBScale = animB.keyframePoses[currentFrameB][boneIndex].scale; - Matrix boneMatrix = MatrixMultiply(MatrixMultiply( - QuaternionToMatrix(boneRotation), - MatrixTranslate(boneTranslation.x, boneTranslation.y, boneTranslation.z)), - MatrixScale(boneScale.x, boneScale.y, boneScale.z)); + // Invert bind pose transformation + Vector3 invBindTranslation = Vector3RotateByQuaternion( + Vector3Negate(model.skeleton.bindPose[boneIndex].translation), + QuaternionInvert(model.skeleton.bindPose[boneIndex].rotation)); + Quaternion invBindRotation = QuaternionInvert(model.skeleton.bindPose[boneIndex].rotation); + Vector3 invBindScale = Vector3Divide((Vector3){ 1.0f, 1.0f, 1.0f }, model.skeleton.bindPose[boneIndex].scale); - model.meshes[i].boneMatrices[boneId] = boneMatrix; - } - } + Vector3 boneTranslation = Vector3Add(Vector3RotateByQuaternion( + Vector3Multiply(model.currentPose[boneIndex].scale, invBindTranslation), + model.currentPose[boneIndex].rotation), + model.currentPose[boneIndex].translation); + Quaternion boneRotation = QuaternionMultiply(model.currentPose[boneIndex].rotation, invBindRotation); + Vector3 boneScale = Vector3Multiply(model.currentPose[boneIndex].scale, invBindScale); + + model.boneMatrices[boneIndex] = MatrixMultiply( + MatrixMultiply(QuaternionToMatrix(boneRotation), + MatrixTranslate(boneTranslation.x, boneTranslation.y, boneTranslation.z)), + MatrixScale(boneScale.x, boneScale.y, boneScale.z)); + */ } + + // CPU skinning, updates CPU buffers and uploads them to GPU (if available) + // NOTE: Fallback in case GPU skinning is not supported or enabled + UpdateModelAnimationVertexBuffers(model); } } -// Update model vertex data (positions and normals) from mesh bone data -// NOTE: Updated data is uploaded to GPU -void UpdateModelVertsToCurrentBones(Model model) +// Update model vertex animation buffers (positions and normals) +// NOTE: Required for CPU skinning, uploads animated vertex buffers to GPU +static void UpdateModelAnimationVertexBuffers(Model model) { - //UpdateModelAnimationBones(model, anim, frame); // TODO: Review - for (int m = 0; m < model.meshCount; m++) { Mesh mesh = model.meshes[m]; Vector3 animVertex = { 0 }; Vector3 animNormal = { 0 }; - const int vValues = mesh.vertexCount*3; + const int vertexValuesCount = mesh.vertexCount*3; - int boneId = 0; + int boneIndex = 0; int boneCounter = 0; float boneWeight = 0.0f; - bool updated = false; // Flag to check when anim vertex information is updated + bool bufferUpdateRequired = false; // Flag to check when anim vertex information is updated - // Skip if missing bone data, causes segfault without on some models - if ((mesh.boneWeights == NULL) || (mesh.boneIds == NULL)) continue; + // Skip if missing bone data or missing anim buffers initialization + if ((mesh.boneWeights == NULL) || (mesh.boneIndices == NULL) || + (mesh.animVertices == NULL) || (mesh.animNormals == NULL)) continue; - for (int vCounter = 0; vCounter < vValues; vCounter += 3) + for (int vCounter = 0; vCounter < vertexValuesCount; vCounter += 3) { mesh.animVertices[vCounter] = 0; mesh.animVertices[vCounter + 1] = 0; @@ -2442,23 +2490,23 @@ void UpdateModelVertsToCurrentBones(Model model) for (int j = 0; j < 4; j++, boneCounter++) { boneWeight = mesh.boneWeights[boneCounter]; - boneId = mesh.boneIds[boneCounter]; + boneIndex = mesh.boneIndices[boneCounter]; // Early stop when no transformation will be applied if (boneWeight == 0.0f) continue; animVertex = (Vector3){ mesh.vertices[vCounter], mesh.vertices[vCounter + 1], mesh.vertices[vCounter + 2] }; - animVertex = Vector3Transform(animVertex, model.meshes[m].boneMatrices[boneId]); + animVertex = Vector3Transform(animVertex, model.boneMatrices[boneIndex]); mesh.animVertices[vCounter] += animVertex.x*boneWeight; - mesh.animVertices[vCounter+1] += animVertex.y*boneWeight; - mesh.animVertices[vCounter+2] += animVertex.z*boneWeight; - updated = true; + mesh.animVertices[vCounter + 1] += animVertex.y*boneWeight; + mesh.animVertices[vCounter + 2] += animVertex.z*boneWeight; + bufferUpdateRequired = true; // Normals processing // NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals) if ((mesh.normals != NULL) && (mesh.animNormals != NULL )) { animNormal = (Vector3){ mesh.normals[vCounter], mesh.normals[vCounter + 1], mesh.normals[vCounter + 2] }; - animNormal = Vector3Transform(animNormal, MatrixTranspose(MatrixInvert(model.meshes[m].boneMatrices[boneId]))); + animNormal = Vector3Transform(animNormal, MatrixTranspose(MatrixInvert(model.boneMatrices[boneIndex]))); mesh.animNormals[vCounter] += animNormal.x*boneWeight; mesh.animNormals[vCounter + 1] += animNormal.y*boneWeight; mesh.animNormals[vCounter + 2] += animNormal.z*boneWeight; @@ -2466,53 +2514,36 @@ void UpdateModelVertsToCurrentBones(Model model) } } - if (updated) + if (bufferUpdateRequired) { - rlUpdateVertexBuffer(mesh.vboId[0], mesh.animVertices, mesh.vertexCount*3*sizeof(float), 0); // Update vertex position - if (mesh.normals != NULL) rlUpdateVertexBuffer(mesh.vboId[2], mesh.animNormals, mesh.vertexCount*3*sizeof(float), 0); // Update vertex normals + // Update GPU vertex buffers with updated data (position + normals) + rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_POSITION], mesh.animVertices, mesh.vertexCount*3*sizeof(float), 0); + if (mesh.normals != NULL) rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_NORMAL], mesh.animNormals, mesh.vertexCount*3*sizeof(float), 0); } } } -// at least 2x speed up vs the old method -// Update model animated vertex data (positions and normals) for a given frame -// NOTE: Updated data is uploaded to GPU -void UpdateModelAnimation(Model model, ModelAnimation anim, int frame) -{ - UpdateModelAnimationBones(model,anim,frame); - UpdateModelVertsToCurrentBones(model); -} - // Unload animation array data void UnloadModelAnimations(ModelAnimation *animations, int animCount) { - for (int i = 0; i < animCount; i++) UnloadModelAnimation(animations[i]); + for (int a = 0; a < animCount; a++) + { + for (int i = 0; i < animations[a].keyframeCount; i++) + RL_FREE(animations[a].keyframePoses[i]); + + RL_FREE(animations[a].keyframePoses); + } + RL_FREE(animations); } -// Unload animation data -void UnloadModelAnimation(ModelAnimation anim) -{ - for (int i = 0; i < anim.frameCount; i++) RL_FREE(anim.framePoses[i]); - - RL_FREE(anim.bones); - RL_FREE(anim.framePoses); -} - // Check model animation skeleton match // NOTE: Only number of bones and parent connections are checked bool IsModelAnimationValid(Model model, ModelAnimation anim) { int result = true; - if (model.boneCount != anim.boneCount) result = false; - else - { - for (int i = 0; i < model.boneCount; i++) - { - if (model.bones[i].parent != anim.bones[i].parent) { result = false; break; } - } - } + if (model.skeleton.boneCount != anim.boneCount) result = false; return result; } @@ -3883,17 +3914,31 @@ void DrawModelEx(Model model, Vector3 position, Vector3 rotationAxis, float rota for (int i = 0; i < model.meshCount; i++) { - Color color = model.materials[model.meshMaterial[i]].maps[MATERIAL_MAP_DIFFUSE].color; + Material mat = model.materials[model.meshMaterial[i]]; + Color colDiffuse = mat.maps[MATERIAL_MAP_DIFFUSE].color; - Color colorTint = WHITE; - colorTint.r = (unsigned char)(((int)color.r*(int)tint.r)/255); - colorTint.g = (unsigned char)(((int)color.g*(int)tint.g)/255); - colorTint.b = (unsigned char)(((int)color.b*(int)tint.b)/255); - colorTint.a = (unsigned char)(((int)color.a*(int)tint.a)/255); + // Applying color tint directly to material diffuse map, + // because is comes as an input paramter to the function + Color colTinted = { 0 }; + colTinted.r = (unsigned char)(((int)colDiffuse.r*(int)tint.r)/255); + colTinted.g = (unsigned char)(((int)colDiffuse.g*(int)tint.g)/255); + colTinted.b = (unsigned char)(((int)colDiffuse.b*(int)tint.b)/255); + colTinted.a = (unsigned char)(((int)colDiffuse.a*(int)tint.a)/255); - model.materials[model.meshMaterial[i]].maps[MATERIAL_MAP_DIFFUSE].color = colorTint; - DrawMesh(model.meshes[i], model.materials[model.meshMaterial[i]], model.transform); - model.materials[model.meshMaterial[i]].maps[MATERIAL_MAP_DIFFUSE].color = color; + mat.maps[MATERIAL_MAP_DIFFUSE].color = colTinted; + + // Upload runtime bone transforms matrices, to compute skinning on the shader (GPU-skinning) + // NOTE: Required location must be found and Mesh bones indices and weights must be also uploaded to shader + if ((mat.shader.locs[SHADER_LOC_MATRIX_BONETRANSFORMS] != -1) && (model.boneMatrices != NULL)) + { + rlEnableShader(mat.shader.id); // Enable shader to set bone transform matrices + rlSetUniformMatrices(mat.shader.locs[SHADER_LOC_MATRIX_BONETRANSFORMS], model.boneMatrices, model.skeleton.boneCount); + } + + DrawMesh(model.meshes[i], mat, model.transform); + + // Restore material diffuse map color (before tint applied) + mat.maps[MATERIAL_MAP_DIFFUSE].color = colDiffuse; } } @@ -4344,7 +4389,7 @@ static void BuildPoseFromParentJoints(BoneInfo *bones, int boneCount, Transform { if (bones[i].parent > i) { - TRACELOG(LOG_WARNING, "Assumes bones are toplogically sorted, but bone %d has parent %d. Skipping.", i, bones[i].parent); + TRACELOG(LOG_WARNING, "Skipping bone not topologically sorted: Bone %d has parent %d", i, bones[i].parent); continue; } transforms[i].rotation = QuaternionMultiply(transforms[bones[i].parent].rotation, transforms[i].rotation); @@ -4762,11 +4807,11 @@ static Model LoadIQM(const char *fileName) for (int i = 0; i < model.meshCount; i++) { - //fseek(iqmFile, iqmHeader->ofs_text + imesh[i].name, SEEK_SET); + //fseek(iqmFile, iqmHeader->ofs_text + imesh[a].name, SEEK_SET); //fread(name, sizeof(char), MESH_NAME_LENGTH, iqmFile); memcpy(name, fileDataPtr + iqmHeader->ofs_text + imesh[i].name, MESH_NAME_LENGTH*sizeof(char)); - //fseek(iqmFile, iqmHeader->ofs_text + imesh[i].material, SEEK_SET); + //fseek(iqmFile, iqmHeader->ofs_text + imesh[a].material, SEEK_SET); //fread(material, sizeof(char), MATERIAL_NAME_LENGTH, iqmFile); memcpy(material, fileDataPtr + iqmHeader->ofs_text + imesh[i].material, MATERIAL_NAME_LENGTH*sizeof(char)); @@ -4783,16 +4828,18 @@ static Model LoadIQM(const char *fileName) model.meshes[i].normals = (float *)RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); // Default vertex normals model.meshes[i].texcoords = (float *)RL_CALLOC(model.meshes[i].vertexCount*2, sizeof(float)); // Default vertex texcoords - model.meshes[i].boneIds = (unsigned char *)RL_CALLOC(model.meshes[i].vertexCount*4, sizeof(unsigned char)); // Up-to 4 bones supported! + model.meshes[i].boneIndices = (unsigned char *)RL_CALLOC(model.meshes[i].vertexCount*4, sizeof(unsigned char)); // Up-to 4 bones supported! model.meshes[i].boneWeights = (float *)RL_CALLOC(model.meshes[i].vertexCount*4, sizeof(float)); // Up-to 4 bones supported! model.meshes[i].triangleCount = imesh[i].num_triangles; model.meshes[i].indices = (unsigned short *)RL_CALLOC(model.meshes[i].triangleCount*3, sizeof(unsigned short)); +#if !defined(SUPPORT_GPU_SKINNING) // Animated vertex data, what we actually process for rendering // NOTE: Animated vertex should be re-uploaded to GPU (if not using GPU skinning) model.meshes[i].animVertices = (float *)RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); model.meshes[i].animNormals = (float *)RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); +#endif } // Triangles data processing @@ -4830,7 +4877,7 @@ static Model LoadIQM(const char *fileName) case IQM_POSITION: { vertex = (float *)RL_MALLOC(iqmHeader->num_vertexes*3*sizeof(float)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(vertex, iqmHeader->num_vertexes*3*sizeof(float), 1, iqmFile); memcpy(vertex, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*3*sizeof(float)); @@ -4840,7 +4887,7 @@ static Model LoadIQM(const char *fileName) for (unsigned int i = imesh[m].first_vertex*3; i < (imesh[m].first_vertex + imesh[m].num_vertexes)*3; i++) { model.meshes[m].vertices[vCounter] = vertex[i]; - model.meshes[m].animVertices[vCounter] = vertex[i]; + if (model.meshes[m].animVertices != NULL) model.meshes[m].animVertices[vCounter] = vertex[i]; vCounter++; } } @@ -4848,7 +4895,7 @@ static Model LoadIQM(const char *fileName) case IQM_NORMAL: { normal = (float *)RL_MALLOC(iqmHeader->num_vertexes*3*sizeof(float)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(normal, iqmHeader->num_vertexes*3*sizeof(float), 1, iqmFile); memcpy(normal, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*3*sizeof(float)); @@ -4858,7 +4905,7 @@ static Model LoadIQM(const char *fileName) for (unsigned int i = imesh[m].first_vertex*3; i < (imesh[m].first_vertex + imesh[m].num_vertexes)*3; i++) { model.meshes[m].normals[vCounter] = normal[i]; - model.meshes[m].animNormals[vCounter] = normal[i]; + if (model.meshes[m].animNormals != NULL) model.meshes[m].animNormals[vCounter] = normal[i]; vCounter++; } } @@ -4866,7 +4913,7 @@ static Model LoadIQM(const char *fileName) case IQM_TEXCOORD: { text = (float *)RL_MALLOC(iqmHeader->num_vertexes*2*sizeof(float)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(text, iqmHeader->num_vertexes*2*sizeof(float), 1, iqmFile); memcpy(text, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*2*sizeof(float)); @@ -4883,7 +4930,7 @@ static Model LoadIQM(const char *fileName) case IQM_BLENDINDEXES: { blendi = (char *)RL_MALLOC(iqmHeader->num_vertexes*4*sizeof(char)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(blendi, iqmHeader->num_vertexes*4*sizeof(char), 1, iqmFile); memcpy(blendi, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*4*sizeof(char)); @@ -4892,7 +4939,7 @@ static Model LoadIQM(const char *fileName) int boneCounter = 0; for (unsigned int i = imesh[m].first_vertex*4; i < (imesh[m].first_vertex + imesh[m].num_vertexes)*4; i++) { - model.meshes[m].boneIds[boneCounter] = blendi[i]; + model.meshes[m].boneIndices[boneCounter] = blendi[i]; boneCounter++; } } @@ -4900,7 +4947,7 @@ static Model LoadIQM(const char *fileName) case IQM_BLENDWEIGHTS: { blendw = (unsigned char *)RL_MALLOC(iqmHeader->num_vertexes*4*sizeof(unsigned char)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(blendw, iqmHeader->num_vertexes*4*sizeof(unsigned char), 1, iqmFile); memcpy(blendw, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*4*sizeof(unsigned char)); @@ -4917,7 +4964,7 @@ static Model LoadIQM(const char *fileName) case IQM_COLOR: { color = (unsigned char *)RL_MALLOC(iqmHeader->num_vertexes*4*sizeof(unsigned char)); - //fseek(iqmFile, va[i].offset, SEEK_SET); + //fseek(iqmFile, va[a].offset, SEEK_SET); //fread(blendw, iqmHeader->num_vertexes*4*sizeof(unsigned char), 1, iqmFile); memcpy(color, fileDataPtr + va[i].offset, iqmHeader->num_vertexes*4*sizeof(unsigned char)); @@ -4942,45 +4989,39 @@ static Model LoadIQM(const char *fileName) //fread(ijoint, sizeof(IQMJoint), iqmHeader->num_joints, iqmFile); memcpy(ijoint, fileDataPtr + iqmHeader->ofs_joints, iqmHeader->num_joints*sizeof(IQMJoint)); - model.boneCount = iqmHeader->num_joints; - model.bones = (BoneInfo *)RL_MALLOC(iqmHeader->num_joints*sizeof(BoneInfo)); - model.bindPose = (Transform *)RL_MALLOC(iqmHeader->num_joints*sizeof(Transform)); + model.skeleton.boneCount = iqmHeader->num_joints; + model.skeleton.bones = (BoneInfo *)RL_CALLOC(iqmHeader->num_joints, sizeof(BoneInfo)); + model.skeleton.bindPose = (Transform *)RL_CALLOC(iqmHeader->num_joints, sizeof(Transform)); for (unsigned int i = 0; i < iqmHeader->num_joints; i++) { // Bones - model.bones[i].parent = ijoint[i].parent; - //fseek(iqmFile, iqmHeader->ofs_text + ijoint[i].name, SEEK_SET); - //fread(model.bones[i].name, sizeof(char), BONE_NAME_LENGTH, iqmFile); - memcpy(model.bones[i].name, fileDataPtr + iqmHeader->ofs_text + ijoint[i].name, BONE_NAME_LENGTH*sizeof(char)); + model.skeleton.bones[i].parent = ijoint[i].parent; + //fseek(iqmFile, iqmHeader->ofs_text + ijoint[a].name, SEEK_SET); + //fread(model.bones[a].name, sizeof(char), BONE_NAME_LENGTH, iqmFile); + memcpy(model.skeleton.bones[i].name, fileDataPtr + iqmHeader->ofs_text + ijoint[i].name, BONE_NAME_LENGTH*sizeof(char)); // Bind pose (base pose) - model.bindPose[i].translation.x = ijoint[i].translate[0]; - model.bindPose[i].translation.y = ijoint[i].translate[1]; - model.bindPose[i].translation.z = ijoint[i].translate[2]; + model.skeleton.bindPose[i].translation.x = ijoint[i].translate[0]; + model.skeleton.bindPose[i].translation.y = ijoint[i].translate[1]; + model.skeleton.bindPose[i].translation.z = ijoint[i].translate[2]; - model.bindPose[i].rotation.x = ijoint[i].rotate[0]; - model.bindPose[i].rotation.y = ijoint[i].rotate[1]; - model.bindPose[i].rotation.z = ijoint[i].rotate[2]; - model.bindPose[i].rotation.w = ijoint[i].rotate[3]; + model.skeleton.bindPose[i].rotation.x = ijoint[i].rotate[0]; + model.skeleton.bindPose[i].rotation.y = ijoint[i].rotate[1]; + model.skeleton.bindPose[i].rotation.z = ijoint[i].rotate[2]; + model.skeleton.bindPose[i].rotation.w = ijoint[i].rotate[3]; - model.bindPose[i].scale.x = ijoint[i].scale[0]; - model.bindPose[i].scale.y = ijoint[i].scale[1]; - model.bindPose[i].scale.z = ijoint[i].scale[2]; + model.skeleton.bindPose[i].scale.x = ijoint[i].scale[0]; + model.skeleton.bindPose[i].scale.y = ijoint[i].scale[1]; + model.skeleton.bindPose[i].scale.z = ijoint[i].scale[2]; } - BuildPoseFromParentJoints(model.bones, model.boneCount, model.bindPose); + BuildPoseFromParentJoints(model.skeleton.bones, model.skeleton.boneCount, model.skeleton.bindPose); - for (int i = 0; i < model.meshCount; i++) - { - model.meshes[i].boneCount = model.boneCount; - model.meshes[i].boneMatrices = (Matrix *)RL_CALLOC(model.meshes[i].boneCount, sizeof(Matrix)); - - for (int j = 0; j < model.meshes[i].boneCount; j++) - { - model.meshes[i].boneMatrices[j] = MatrixIdentity(); - } - } + // Initialize runtime animation data: current pose and bone matrices + model.currentPose = (Transform *)RL_CALLOC(model.skeleton.boneCount, sizeof(Transform)); + model.boneMatrices = (Matrix *)RL_CALLOC(model.skeleton.boneCount, sizeof(Matrix)); + for (int j = 0; j < model.skeleton.boneCount; j++) model.boneMatrices[j] = MatrixIdentity(); UnloadFileData(fileData); @@ -5078,37 +5119,42 @@ static ModelAnimation *LoadModelAnimationsIQM(const char *fileName, int *animCou //fread(anim, sizeof(IQMAnim), iqmHeader->num_anims, iqmFile); memcpy(anim, fileDataPtr + iqmHeader->ofs_anims, iqmHeader->num_anims*sizeof(IQMAnim)); - ModelAnimation *animations = (ModelAnimation *)RL_MALLOC(iqmHeader->num_anims*sizeof(ModelAnimation)); + ModelAnimation *animations = (ModelAnimation *)RL_CALLOC(iqmHeader->num_anims, sizeof(ModelAnimation)); // frameposes - unsigned short *framedata = (unsigned short *)RL_MALLOC(iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); + unsigned short *framedata = (unsigned short *)RL_CALLOC(iqmHeader->num_frames*iqmHeader->num_framechannels, sizeof(unsigned short)); //fseek(iqmFile, iqmHeader->ofs_frames, SEEK_SET); //fread(framedata, sizeof(unsigned short), iqmHeader->num_frames*iqmHeader->num_framechannels, iqmFile); memcpy(framedata, fileDataPtr + iqmHeader->ofs_frames, iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); // joints - IQMJoint *joints = (IQMJoint *)RL_MALLOC(iqmHeader->num_joints*sizeof(IQMJoint)); + IQMJoint *joints = (IQMJoint *)RL_CALLOC(iqmHeader->num_joints, sizeof(IQMJoint)); memcpy(joints, fileDataPtr + iqmHeader->ofs_joints, iqmHeader->num_joints*sizeof(IQMJoint)); for (unsigned int a = 0; a < iqmHeader->num_anims; a++) { - animations[a].frameCount = anim[a].num_frames; animations[a].boneCount = iqmHeader->num_poses; - animations[a].bones = (BoneInfo *)RL_MALLOC(iqmHeader->num_poses*sizeof(BoneInfo)); - animations[a].framePoses = (Transform **)RL_MALLOC(anim[a].num_frames*sizeof(Transform *)); + BoneInfo *bones = (BoneInfo *)RL_CALLOC(iqmHeader->num_poses, sizeof(BoneInfo)); + + animations[a].keyframeCount = anim[a].num_frames; + animations[a].keyframePoses = (Transform **)RL_CALLOC(anim[a].num_frames, sizeof(Transform *)); memcpy(animations[a].name, fileDataPtr + iqmHeader->ofs_text + anim[a].name, 32); - TRACELOG(LOG_INFO, "IQM Anim %s", animations[a].name); - //animations[a].framerate = anim.framerate; // TODO: Use animation framerate data? + // TODO: Use animation framerate data? + //animations[a].framerate = anim.framerate; + + TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s | Frames: %d | Framerate: %f", fileName, animations[a].name, animations[a].keyframeCount, anim[a].framerate); for (unsigned int j = 0; j < iqmHeader->num_poses; j++) { - // If animations and skeleton are in the same file, copy bone names to anim - if (iqmHeader->num_joints > 0) memcpy(animations[a].bones[j].name, fileDataPtr + iqmHeader->ofs_text + joints[j].name, BONE_NAME_LENGTH*sizeof(char)); - else memcpy(animations[a].bones[j].name, "ANIMJOINTNAME", 13); // Default bone name otherwise - animations[a].bones[j].parent = poses[j].parent; + bones[j].parent = poses[j].parent; + + // NOTE: No need to store bones names, bones only required to generate keyframe poses + //if (iqmHeader->num_joints > 0) memcpy(bones[j].name, fileDataPtr + iqmHeader->ofs_text + joints[j].name, BONE_NAME_LENGTH*sizeof(char)); + //else memcpy(bones[j].name, "ANIMJOINTNAME", 13); // Default bone name otherwise } - for (unsigned int j = 0; j < anim[a].num_frames; j++) animations[a].framePoses[j] = (Transform *)RL_MALLOC(iqmHeader->num_poses*sizeof(Transform)); + for (unsigned int j = 0; j < anim[a].num_frames; j++) + animations[a].keyframePoses[j] = (Transform *)RL_MALLOC(iqmHeader->num_poses*sizeof(Transform)); int dcounter = anim[a].first_frame*iqmHeader->num_framechannels; @@ -5116,87 +5162,87 @@ static ModelAnimation *LoadModelAnimationsIQM(const char *fileName, int *animCou { for (unsigned int i = 0; i < iqmHeader->num_poses; i++) { - animations[a].framePoses[frame][i].translation.x = poses[i].channeloffset[0]; + animations[a].keyframePoses[frame][i].translation.x = poses[i].channeloffset[0]; if (poses[i].mask & 0x01) { - animations[a].framePoses[frame][i].translation.x += framedata[dcounter]*poses[i].channelscale[0]; + animations[a].keyframePoses[frame][i].translation.x += framedata[dcounter]*poses[i].channelscale[0]; dcounter++; } - animations[a].framePoses[frame][i].translation.y = poses[i].channeloffset[1]; + animations[a].keyframePoses[frame][i].translation.y = poses[i].channeloffset[1]; if (poses[i].mask & 0x02) { - animations[a].framePoses[frame][i].translation.y += framedata[dcounter]*poses[i].channelscale[1]; + animations[a].keyframePoses[frame][i].translation.y += framedata[dcounter]*poses[i].channelscale[1]; dcounter++; } - animations[a].framePoses[frame][i].translation.z = poses[i].channeloffset[2]; + animations[a].keyframePoses[frame][i].translation.z = poses[i].channeloffset[2]; if (poses[i].mask & 0x04) { - animations[a].framePoses[frame][i].translation.z += framedata[dcounter]*poses[i].channelscale[2]; + animations[a].keyframePoses[frame][i].translation.z += framedata[dcounter]*poses[i].channelscale[2]; dcounter++; } - animations[a].framePoses[frame][i].rotation.x = poses[i].channeloffset[3]; + animations[a].keyframePoses[frame][i].rotation.x = poses[i].channeloffset[3]; if (poses[i].mask & 0x08) { - animations[a].framePoses[frame][i].rotation.x += framedata[dcounter]*poses[i].channelscale[3]; + animations[a].keyframePoses[frame][i].rotation.x += framedata[dcounter]*poses[i].channelscale[3]; dcounter++; } - animations[a].framePoses[frame][i].rotation.y = poses[i].channeloffset[4]; + animations[a].keyframePoses[frame][i].rotation.y = poses[i].channeloffset[4]; if (poses[i].mask & 0x10) { - animations[a].framePoses[frame][i].rotation.y += framedata[dcounter]*poses[i].channelscale[4]; + animations[a].keyframePoses[frame][i].rotation.y += framedata[dcounter]*poses[i].channelscale[4]; dcounter++; } - animations[a].framePoses[frame][i].rotation.z = poses[i].channeloffset[5]; + animations[a].keyframePoses[frame][i].rotation.z = poses[i].channeloffset[5]; if (poses[i].mask & 0x20) { - animations[a].framePoses[frame][i].rotation.z += framedata[dcounter]*poses[i].channelscale[5]; + animations[a].keyframePoses[frame][i].rotation.z += framedata[dcounter]*poses[i].channelscale[5]; dcounter++; } - animations[a].framePoses[frame][i].rotation.w = poses[i].channeloffset[6]; + animations[a].keyframePoses[frame][i].rotation.w = poses[i].channeloffset[6]; if (poses[i].mask & 0x40) { - animations[a].framePoses[frame][i].rotation.w += framedata[dcounter]*poses[i].channelscale[6]; + animations[a].keyframePoses[frame][i].rotation.w += framedata[dcounter]*poses[i].channelscale[6]; dcounter++; } - animations[a].framePoses[frame][i].scale.x = poses[i].channeloffset[7]; + animations[a].keyframePoses[frame][i].scale.x = poses[i].channeloffset[7]; if (poses[i].mask & 0x80) { - animations[a].framePoses[frame][i].scale.x += framedata[dcounter]*poses[i].channelscale[7]; + animations[a].keyframePoses[frame][i].scale.x += framedata[dcounter]*poses[i].channelscale[7]; dcounter++; } - animations[a].framePoses[frame][i].scale.y = poses[i].channeloffset[8]; + animations[a].keyframePoses[frame][i].scale.y = poses[i].channeloffset[8]; if (poses[i].mask & 0x100) { - animations[a].framePoses[frame][i].scale.y += framedata[dcounter]*poses[i].channelscale[8]; + animations[a].keyframePoses[frame][i].scale.y += framedata[dcounter]*poses[i].channelscale[8]; dcounter++; } - animations[a].framePoses[frame][i].scale.z = poses[i].channeloffset[9]; + animations[a].keyframePoses[frame][i].scale.z = poses[i].channeloffset[9]; if (poses[i].mask & 0x200) { - animations[a].framePoses[frame][i].scale.z += framedata[dcounter]*poses[i].channelscale[9]; + animations[a].keyframePoses[frame][i].scale.z += framedata[dcounter]*poses[i].channelscale[9]; dcounter++; } - animations[a].framePoses[frame][i].rotation = QuaternionNormalize(animations[a].framePoses[frame][i].rotation); + animations[a].keyframePoses[frame][i].rotation = QuaternionNormalize(animations[a].keyframePoses[frame][i].rotation); } } @@ -5205,15 +5251,17 @@ static ModelAnimation *LoadModelAnimationsIQM(const char *fileName, int *animCou { for (int i = 0; i < animations[a].boneCount; i++) { - if (animations[a].bones[i].parent >= 0) + if (bones[i].parent >= 0) { - animations[a].framePoses[frame][i].rotation = QuaternionMultiply(animations[a].framePoses[frame][animations[a].bones[i].parent].rotation, animations[a].framePoses[frame][i].rotation); - animations[a].framePoses[frame][i].translation = Vector3RotateByQuaternion(animations[a].framePoses[frame][i].translation, animations[a].framePoses[frame][animations[a].bones[i].parent].rotation); - animations[a].framePoses[frame][i].translation = Vector3Add(animations[a].framePoses[frame][i].translation, animations[a].framePoses[frame][animations[a].bones[i].parent].translation); - animations[a].framePoses[frame][i].scale = Vector3Multiply(animations[a].framePoses[frame][i].scale, animations[a].framePoses[frame][animations[a].bones[i].parent].scale); + animations[a].keyframePoses[frame][i].rotation = QuaternionMultiply(animations[a].keyframePoses[frame][bones[i].parent].rotation, animations[a].keyframePoses[frame][i].rotation); + animations[a].keyframePoses[frame][i].translation = Vector3RotateByQuaternion(animations[a].keyframePoses[frame][i].translation, animations[a].keyframePoses[frame][bones[i].parent].rotation); + animations[a].keyframePoses[frame][i].translation = Vector3Add(animations[a].keyframePoses[frame][i].translation, animations[a].keyframePoses[frame][bones[i].parent].translation); + animations[a].keyframePoses[frame][i].scale = Vector3Multiply(animations[a].keyframePoses[frame][i].scale, animations[a].keyframePoses[frame][bones[i].parent].scale); } } } + + RL_FREE(bones); } UnloadFileData(fileData); @@ -5375,7 +5423,7 @@ static Model LoadGLTF(const char *fileName) - Supports basic animations - Transforms, including parent-child relations, are applied on the mesh data, but the hierarchy is not kept (as it can't be represented) - - Mesh instances in the glTF file (i.e. same mesh linked from multiple nodes) + - Mesh instances in the glTF file (a.e. same mesh linked from multiple nodes) are turned into separate raylib Meshes RESTRICTIONS: @@ -5604,7 +5652,7 @@ static Model LoadGLTF(const char *fileName) // NOTE: Visit each node in the hierarchy and process any mesh linked from it // - Each primitive within a glTF node becomes a raylib Mesh // - The local-to-world transform of each node is used to transform the points/normals/tangents of the created Mesh(es) - // - Any glTF mesh linked from more than one Node (i.e. instancing) is turned into multiple Mesh's, as each Node will have its own transform applied + // - Any glTF mesh linked from more than one Node (a.e. instancing) is turned into multiple Mesh's, as each Node will have its own transform applied // // WARNING: The code below disregards the scenes defined in the file, all nodes are used //---------------------------------------------------------------------------------------------------- @@ -6092,15 +6140,15 @@ static Model LoadGLTF(const char *fileName) // LIMITATIONS: // - Only supports 1 armature per file, and skips loading it if there are multiple armatures // - Only supports linear interpolation (default method in Blender when checked "Always Sample Animations" when exporting a GLTF file) - // - Only supports translation/rotation/scale animation channel.path, weights not considered (i.e. morph targets) + // - Only supports translation/rotation/scale animation channel.path, weights not considered (a.e. morph targets) //---------------------------------------------------------------------------------------------------- if (data->skins_count > 0) { cgltf_skin skin = data->skins[0]; - model.bones = LoadBoneInfoGLTF(skin, &model.boneCount); - model.bindPose = (Transform *)RL_MALLOC(model.boneCount*sizeof(Transform)); + model.skeleton.bones = LoadBoneInfoGLTF(skin, &model.skeleton.boneCount); + model.skeleton.bindPose = (Transform *)RL_CALLOC(model.skeleton.boneCount, sizeof(Transform)); - for (int i = 0; i < model.boneCount; i++) + for (int i = 0; i < model.skeleton.boneCount; i++) { cgltf_node *node = skin.joints[i]; cgltf_float worldTransform[16]; @@ -6111,7 +6159,11 @@ static Model LoadGLTF(const char *fileName) worldTransform[2], worldTransform[6], worldTransform[10], worldTransform[14], worldTransform[3], worldTransform[7], worldTransform[11], worldTransform[15] }; - MatrixDecompose(worldMatrix, &(model.bindPose[i].translation), &(model.bindPose[i].rotation), &(model.bindPose[i].scale)); + + MatrixDecompose(worldMatrix, + &(model.skeleton.bindPose[i].translation), + &(model.skeleton.bindPose[i].rotation), + &(model.skeleton.bindPose[i].scale)); } if (data->skins_count > 1) TRACELOG(LOG_WARNING, "MODEL: [%s] can only load one skin (armature) per model, but gltf skins_count == %i", fileName, data->skins_count); @@ -6143,7 +6195,7 @@ static Model LoadGLTF(const char *fileName) // NOTE: JOINTS_n can only be vec4 and u8/u16 // SPECS: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview - // WARNING: raylib only supports model.meshes[].boneIds as u8 (unsigned char), + // WARNING: raylib only supports model.meshes[].boneIndices as u8 (unsigned char), // if data is provided in any other format, it is converted to supported format but // it could imply data loss (a warning message is issued in that case) @@ -6151,16 +6203,16 @@ static Model LoadGLTF(const char *fileName) { if (attribute->component_type == cgltf_component_type_r_8u) { - // Init raylib mesh boneIds to copy glTF attribute data - model.meshes[meshIndex].boneIds = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); + // Init raylib mesh boneIndices to copy glTF attribute data + model.meshes[meshIndex].boneIndices = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); // Load attribute: vec4, u8 (unsigned char) - LOAD_ATTRIBUTE(attribute, 4, unsigned char, model.meshes[meshIndex].boneIds) + LOAD_ATTRIBUTE(attribute, 4, unsigned char, model.meshes[meshIndex].boneIndices) } else if (attribute->component_type == cgltf_component_type_r_16u) { - // Init raylib mesh boneIds to copy glTF attribute data - model.meshes[meshIndex].boneIds = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); + // Init raylib mesh boneIndices to copy glTF attribute data + model.meshes[meshIndex].boneIndices = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); // Load data into a temp buffer to be converted to raylib data type unsigned short *temp = (unsigned short *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned short)); @@ -6177,7 +6229,7 @@ static Model LoadGLTF(const char *fileName) } // Despite the possible overflow, we convert data to unsigned char - model.meshes[meshIndex].boneIds[b] = (unsigned char)temp[b]; + model.meshes[meshIndex].boneIndices[b] = (unsigned char)temp[b]; } RL_FREE(temp); @@ -6243,7 +6295,7 @@ static Model LoadGLTF(const char *fileName) if (data->skins_count > 0 && !hasJoints && node->parent != NULL && node->parent->mesh == NULL) { int parentBoneId = -1; - for (int joint = 0; joint < model.boneCount; joint++) + for (int joint = 0; joint < model.skeleton.boneCount; joint++) { if (data->skins[0].joints[joint] == node->parent) { @@ -6254,38 +6306,34 @@ static Model LoadGLTF(const char *fileName) if (parentBoneId >= 0) { - model.meshes[meshIndex].boneIds = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); + model.meshes[meshIndex].boneIndices = (unsigned char *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); model.meshes[meshIndex].boneWeights = (float *)RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(float)); for (int vertexIndex = 0; vertexIndex < model.meshes[meshIndex].vertexCount*4; vertexIndex += 4) { - model.meshes[meshIndex].boneIds[vertexIndex] = (unsigned char)parentBoneId; + model.meshes[meshIndex].boneIndices[vertexIndex] = (unsigned char)parentBoneId; model.meshes[meshIndex].boneWeights[vertexIndex] = 1.0f; } } } - // Animated vertex data +#if !defined(SUPPORT_GPU_SKINNING) + // Animated vertex data (CPU skinning) model.meshes[meshIndex].animVertices = (float *)RL_CALLOC(model.meshes[meshIndex].vertexCount*3, sizeof(float)); memcpy(model.meshes[meshIndex].animVertices, model.meshes[meshIndex].vertices, model.meshes[meshIndex].vertexCount*3*sizeof(float)); model.meshes[meshIndex].animNormals = (float *)RL_CALLOC(model.meshes[meshIndex].vertexCount*3, sizeof(float)); - if (model.meshes[meshIndex].normals != NULL) - { - memcpy(model.meshes[meshIndex].animNormals, model.meshes[meshIndex].normals, model.meshes[meshIndex].vertexCount*3*sizeof(float)); - } - - // Bone Transform Matrices - model.meshes[meshIndex].boneCount = model.boneCount; - model.meshes[meshIndex].boneMatrices = (Matrix *)RL_CALLOC(model.meshes[meshIndex].boneCount, sizeof(Matrix)); - - for (int j = 0; j < model.meshes[meshIndex].boneCount; j++) - { - model.meshes[meshIndex].boneMatrices[j] = MatrixIdentity(); - } + if (model.meshes[meshIndex].normals != NULL) memcpy(model.meshes[meshIndex].animNormals, model.meshes[meshIndex].normals, model.meshes[meshIndex].vertexCount*3*sizeof(float)); +#endif + model.meshes[meshIndex].boneCount = model.skeleton.boneCount; meshIndex++; // Move to next mesh } } + + // Initialize runtime animation data: current pose and bone matrices + model.currentPose = (Transform *)RL_CALLOC(model.skeleton.boneCount, sizeof(Transform)); + model.boneMatrices = (Matrix *)RL_CALLOC(model.skeleton.boneCount, sizeof(Matrix)); + for (int j = 0; j < model.skeleton.boneCount; j++) model.boneMatrices[j] = MatrixIdentity(); //---------------------------------------------------------------------------------------------------- // Free all cgltf loaded data @@ -6484,11 +6532,11 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo }; MatrixDecompose(worldMatrix, &worldTransform.translation, &worldTransform.rotation, &worldTransform.scale); - for (unsigned int i = 0; i < data->animations_count; i++) + for (unsigned int a = 0; a < data->animations_count; a++) { - animations[i].bones = LoadBoneInfoGLTF(skin, &animations[i].boneCount); + BoneInfo *bones = LoadBoneInfoGLTF(skin, &animations[a].boneCount); - cgltf_animation animData = data->animations[i]; + cgltf_animation animData = data->animations[a]; struct Channels { cgltf_animation_channel *translate; @@ -6497,7 +6545,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo cgltf_interpolation_type interpolationType; }; - struct Channels *boneChannels = (struct Channels *)RL_CALLOC(animations[i].boneCount, sizeof(struct Channels)); + struct Channels *boneChannels = (struct Channels *)RL_CALLOC(animations[a].boneCount, sizeof(struct Channels)); float animDuration = 0.0f; for (unsigned int j = 0; j < animData.channels_count; j++) @@ -6514,11 +6562,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo } } - if (boneIndex == -1) - { - // Animation channel for a node not in the armature - continue; - } + if (boneIndex == -1) continue; // Animation channel for a node not in the skeleton boneChannels[boneIndex].interpolationType = animData.channels[j].sampler->interpolation; @@ -6538,34 +6582,34 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo } else { - TRACELOG(LOG_WARNING, "MODEL: [%s] Unsupported target_path on channel %d's sampler for animation %d. Skipping.", fileName, j, i); + TRACELOG(LOG_WARNING, "MODEL: [%s] Unsupported target_path on channel %d's sampler for animation %d. Skipping.", fileName, j, a); } } else TRACELOG(LOG_WARNING, "MODEL: [%s] Invalid interpolation curve encountered for GLTF animation.", fileName); - float t = 0.0f; - cgltf_bool r = cgltf_accessor_read_float(channel.sampler->input, channel.sampler->input->count - 1, &t, 1); + float time = 0.0f; + cgltf_bool result = cgltf_accessor_read_float(channel.sampler->input, channel.sampler->input->count - 1, &time, 1); - if (!r) + if (!result) { TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load input time", fileName); continue; } - animDuration = (t > animDuration)? t : animDuration; + animDuration = (time > animDuration)? time : animDuration; } - if (animData.name != NULL) strncpy(animations[i].name, animData.name, sizeof(animations[i].name) - 1); + if (animData.name != NULL) strncpy(animations[a].name, animData.name, sizeof(animations[a].name) - 1); - animations[i].frameCount = (int)(animDuration*GLTF_FRAMERATE) + 1; - animations[i].framePoses = (Transform **)RL_MALLOC(animations[i].frameCount*sizeof(Transform *)); + animations[a].keyframeCount = (int)(animDuration*GLTF_FRAMERATE) + 1; + animations[a].keyframePoses = (Transform **)RL_CALLOC(animations[a].keyframeCount, sizeof(Transform *)); - for (int j = 0; j < animations[i].frameCount; j++) + for (int j = 0; j < animations[a].keyframeCount; j++) { - animations[i].framePoses[j] = (Transform *)RL_MALLOC(animations[i].boneCount*sizeof(Transform)); + animations[a].keyframePoses[j] = (Transform *)RL_CALLOC(animations[a].boneCount, sizeof(Transform)); float time = (float)j / GLTF_FRAMERATE; - for (int k = 0; k < animations[i].boneCount; k++) + for (int k = 0; k < animations[a].boneCount; k++) { Vector3 translation = {skin.joints[k]->translation[0], skin.joints[k]->translation[1], skin.joints[k]->translation[2]}; Quaternion rotation = {skin.joints[k]->rotation[0], skin.joints[k]->rotation[1], skin.joints[k]->rotation[2], skin.joints[k]->rotation[3]}; @@ -6575,7 +6619,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo { if (!GetPoseAtTimeGLTF(boneChannels[k].interpolationType, boneChannels[k].translate->sampler->input, boneChannels[k].translate->sampler->output, time, &translation)) { - TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load translate pose data for bone %s", fileName, animations[i].bones[k].name); + TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load translate pose data for bone %s", fileName, bones[k].name); } } @@ -6583,7 +6627,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo { if (!GetPoseAtTimeGLTF(boneChannels[k].interpolationType, boneChannels[k].rotate->sampler->input, boneChannels[k].rotate->sampler->output, time, &rotation)) { - TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load rotate pose data for bone %s", fileName, animations[i].bones[k].name); + TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load rotate pose data for bone %s", fileName, bones[k].name); } } @@ -6591,40 +6635,43 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, int *animCo { if (!GetPoseAtTimeGLTF(boneChannels[k].interpolationType, boneChannels[k].scale->sampler->input, boneChannels[k].scale->sampler->output, time, &scale)) { - TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load scale pose data for bone %s", fileName, animations[i].bones[k].name); + TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load scale pose data for bone %s", fileName, bones[k].name); } } - animations[i].framePoses[j][k] = (Transform){ + animations[a].keyframePoses[j][k] = (Transform){ .translation = translation, .rotation = rotation, .scale = scale }; } - Transform *root = &animations[i].framePoses[j][0]; + Transform *root = &animations[a].keyframePoses[j][0]; root->rotation = QuaternionMultiply(worldTransform.rotation, root->rotation); root->scale = Vector3Multiply(root->scale, worldTransform.scale); root->translation = Vector3Multiply(root->translation, worldTransform.scale); root->translation = Vector3RotateByQuaternion(root->translation, worldTransform.rotation); root->translation = Vector3Add(root->translation, worldTransform.translation); - BuildPoseFromParentJoints(animations[i].bones, animations[i].boneCount, animations[i].framePoses[j]); + BuildPoseFromParentJoints(bones, animations[a].boneCount, animations[a].keyframePoses[j]); } - TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s (%d frames, %fs)", fileName, (animData.name != NULL)? animData.name : "NULL", animations[i].frameCount, animDuration); + TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s | Frames: %d | Duration: %fs", fileName, (animData.name != NULL)? animData.name : "NULL", animations[a].keyframeCount, animDuration); RL_FREE(boneChannels); + RL_FREE(bones); } } if (data->skins_count > 1) { - TRACELOG(LOG_WARNING, "MODEL: [%s] expected exactly one skin to load animation data from, but found %i", fileName, data->skins_count); + TRACELOG(LOG_WARNING, "MODEL: [%s] Expected one unique skin to load animation data from, but found %i", fileName, data->skins_count); } cgltf_free(data); } + UnloadFileData(fileData); + return animations; } #endif @@ -6795,13 +6842,13 @@ static Model LoadM3D(const char *fileName) // WARNING: Sorting is not needed, valid M3D model files should already be sorted // Just keeping the sorting function for reference (Check PR #3363 #3385) /* - for (i = 1; i < m3d->numface; i++) + for (a = 1; a < m3d->numface; a++) { - if (m3d->face[i-1].materialid <= m3d->face[i].materialid) continue; + if (m3d->face[a-1].materialid <= m3d->face[a].materialid) continue; - // face[i-1] > face[i]. slide face[i] lower - m3df_t slider = m3d->face[i]; - j = i-1; + // face[a-1] > face[a]. slide face[a] lower + m3df_t slider = m3d->face[a]; + j = a-1; do { // face[j] > slider, face[j+1] is svailable vacant gap @@ -6875,10 +6922,12 @@ static Model LoadM3D(const char *fileName) if (m3d->numbone && m3d->numskin) { - model.meshes[k].boneIds = (unsigned char *)RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(unsigned char)); + model.meshes[k].boneIndices = (unsigned char *)RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(unsigned char)); model.meshes[k].boneWeights = (float *)RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(float)); +#if !defined(SUPPORT_GPU_SKINNING) model.meshes[k].animVertices = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); model.meshes[k].animNormals = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); +#endif } model.meshMaterial[k] = mi + 1; @@ -6942,7 +6991,7 @@ static Model LoadM3D(const char *fileName) { for (j = 0; j < 4; j++) { - model.meshes[k].boneIds[l*12 + n*4 + j] = m3d->skin[skinid].boneid[j]; + model.meshes[k].boneIndices[l*12 + n*4 + j] = m3d->skin[skinid].boneid[j]; model.meshes[k].boneWeights[l*12 + n*4 + j] = m3d->skin[skinid].weight[j]; } } @@ -6950,7 +6999,7 @@ static Model LoadM3D(const char *fileName) { // raylib does not handle boneless meshes with skeletal animations, so // we put all vertices without a bone into a special "no bone" bone - model.meshes[k].boneIds[l*12 + n*4] = m3d->numbone; + model.meshes[k].boneIndices[l*12 + n*4] = m3d->numbone; model.meshes[k].boneWeights[l*12 + n*4] = 1.0f; } } @@ -7031,47 +7080,47 @@ static Model LoadM3D(const char *fileName) // Load bones if (m3d->numbone) { - model.boneCount = m3d->numbone + 1; - model.bones = (BoneInfo *)RL_CALLOC(model.boneCount, sizeof(BoneInfo)); - model.bindPose = (Transform *)RL_CALLOC(model.boneCount, sizeof(Transform)); + model.skeleton.boneCount = m3d->numbone + 1; + model.skeleton.bones = (BoneInfo *)RL_CALLOC(model.skeleton.boneCount, sizeof(BoneInfo)); + model.skeleton.bindPose = (Transform *)RL_CALLOC(model.skeleton.boneCount, sizeof(Transform)); for (i = 0; i < (int)m3d->numbone; i++) { - model.bones[i].parent = m3d->bone[i].parent; - strncpy(model.bones[i].name, m3d->bone[i].name, sizeof(model.bones[i].name) - 1); - model.bindPose[i].translation.x = m3d->vertex[m3d->bone[i].pos].x*m3d->scale; - model.bindPose[i].translation.y = m3d->vertex[m3d->bone[i].pos].y*m3d->scale; - model.bindPose[i].translation.z = m3d->vertex[m3d->bone[i].pos].z*m3d->scale; - model.bindPose[i].rotation.x = m3d->vertex[m3d->bone[i].ori].x; - model.bindPose[i].rotation.y = m3d->vertex[m3d->bone[i].ori].y; - model.bindPose[i].rotation.z = m3d->vertex[m3d->bone[i].ori].z; - model.bindPose[i].rotation.w = m3d->vertex[m3d->bone[i].ori].w; + model.skeleton.bones[i].parent = m3d->bone[i].parent; + strncpy(model.skeleton.bones[i].name, m3d->bone[i].name, sizeof(model.skeleton.bones[i].name) - 1); + model.skeleton.bindPose[i].translation.x = m3d->vertex[m3d->bone[i].pos].x*m3d->scale; + model.skeleton.bindPose[i].translation.y = m3d->vertex[m3d->bone[i].pos].y*m3d->scale; + model.skeleton.bindPose[i].translation.z = m3d->vertex[m3d->bone[i].pos].z*m3d->scale; + model.skeleton.bindPose[i].rotation.x = m3d->vertex[m3d->bone[i].ori].x; + model.skeleton.bindPose[i].rotation.y = m3d->vertex[m3d->bone[i].ori].y; + model.skeleton.bindPose[i].rotation.z = m3d->vertex[m3d->bone[i].ori].z; + model.skeleton.bindPose[i].rotation.w = m3d->vertex[m3d->bone[i].ori].w; - // TODO: If the orientation quaternion is not normalized, then that's encoding scaling - model.bindPose[i].rotation = QuaternionNormalize(model.bindPose[i].rotation); - model.bindPose[i].scale.x = model.bindPose[i].scale.y = model.bindPose[i].scale.z = 1.0f; + // NOTE: If the orientation quaternion is not normalized, then that's encoding scaling + model.skeleton.bindPose[i].rotation = QuaternionNormalize(model.skeleton.bindPose[i].rotation); + model.skeleton.bindPose[i].scale.x = model.skeleton.bindPose[i].scale.y = model.skeleton.bindPose[i].scale.z = 1.0f; // Child bones are stored in parent bone relative space, convert that into model space - if (model.bones[i].parent >= 0) + if (model.skeleton.bones[i].parent >= 0) { - model.bindPose[i].rotation = QuaternionMultiply(model.bindPose[model.bones[i].parent].rotation, model.bindPose[i].rotation); - model.bindPose[i].translation = Vector3RotateByQuaternion(model.bindPose[i].translation, model.bindPose[model.bones[i].parent].rotation); - model.bindPose[i].translation = Vector3Add(model.bindPose[i].translation, model.bindPose[model.bones[i].parent].translation); - model.bindPose[i].scale = Vector3Multiply(model.bindPose[i].scale, model.bindPose[model.bones[i].parent].scale); + model.skeleton.bindPose[i].rotation = QuaternionMultiply(model.skeleton.bindPose[model.skeleton.bones[i].parent].rotation, model.skeleton.bindPose[i].rotation); + model.skeleton.bindPose[i].translation = Vector3RotateByQuaternion(model.skeleton.bindPose[i].translation, model.skeleton.bindPose[model.skeleton.bones[i].parent].rotation); + model.skeleton.bindPose[i].translation = Vector3Add(model.skeleton.bindPose[i].translation, model.skeleton.bindPose[model.skeleton.bones[i].parent].translation); + model.skeleton.bindPose[i].scale = Vector3Multiply(model.skeleton.bindPose[i].scale, model.skeleton.bindPose[model.skeleton.bones[i].parent].scale); } } // Add a special "no bone" bone - model.bones[i].parent = -1; - memcpy(model.bones[i].name, "NO BONE", 7); - model.bindPose[i].translation.x = 0.0f; - model.bindPose[i].translation.y = 0.0f; - model.bindPose[i].translation.z = 0.0f; - model.bindPose[i].rotation.x = 0.0f; - model.bindPose[i].rotation.y = 0.0f; - model.bindPose[i].rotation.z = 0.0f; - model.bindPose[i].rotation.w = 1.0f; - model.bindPose[i].scale.x = model.bindPose[i].scale.y = model.bindPose[i].scale.z = 1.0f; + model.skeleton.bones[i].parent = -1; + memcpy(model.skeleton.bones[i].name, "NO BONE", 7); + model.skeleton.bindPose[i].translation.x = 0.0f; + model.skeleton.bindPose[i].translation.y = 0.0f; + model.skeleton.bindPose[i].translation.z = 0.0f; + model.skeleton.bindPose[i].rotation.x = 0.0f; + model.skeleton.bindPose[i].rotation.y = 0.0f; + model.skeleton.bindPose[i].rotation.z = 0.0f; + model.skeleton.bindPose[i].rotation.w = 1.0f; + model.skeleton.bindPose[i].scale.x = model.skeleton.bindPose[i].scale.y = model.skeleton.bindPose[i].scale.z = 1.0f; } // Load bone-pose default mesh into animation vertices. These will be updated when UpdateModelAnimation gets @@ -7080,16 +7129,19 @@ static Model LoadM3D(const char *fileName) { for (i = 0; i < model.meshCount; i++) { + model.meshes[i].boneCount = model.skeleton.boneCount; + +#if !defined(SUPPORT_GPU_SKINNING) + // Initialize vertex buffers for CPU skinning memcpy(model.meshes[i].animVertices, model.meshes[i].vertices, model.meshes[i].vertexCount*3*sizeof(float)); memcpy(model.meshes[i].animNormals, model.meshes[i].normals, model.meshes[i].vertexCount*3*sizeof(float)); - - model.meshes[i].boneCount = model.boneCount; - model.meshes[i].boneMatrices = (Matrix *)RL_CALLOC(model.meshes[i].boneCount, sizeof(Matrix)); - for (j = 0; j < model.meshes[i].boneCount; j++) - { - model.meshes[i].boneMatrices[j] = MatrixIdentity(); - } +#endif } + + // Initialize runtime animation data: current pose and bone matrices + model.currentPose = (Transform *)RL_CALLOC(model.skeleton.boneCount, sizeof(Transform)); + model.boneMatrices = (Matrix *)RL_CALLOC(model.skeleton.boneCount, sizeof(Matrix)); + for (int j = 0; j < model.skeleton.boneCount; j++) model.boneMatrices[j] = MatrixIdentity(); } m3d_free(m3d); @@ -7123,8 +7175,7 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, int *animCou UnloadFileData(fileData); return NULL; } - else TRACELOG(LOG_INFO, "MODEL: [%s] M3D data loaded successfully: %i animations, %i bones, %i skins", fileName, - m3d->numaction, m3d->numbone, m3d->numskin); + else TRACELOG(LOG_INFO, "MODEL: [%s] M3D data loaded successfully: %i animations, %i bones, %i skins", fileName, m3d->numaction, m3d->numbone, m3d->numskin); // No animation or bones, exit out. skins are not required because some people use one animation for N models if (!m3d->numaction || !m3d->numbone) @@ -7139,29 +7190,30 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, int *animCou for (unsigned int a = 0; a < m3d->numaction; a++) { - animations[a].frameCount = m3d->action[a].durationmsec/M3D_ANIMDELAY; animations[a].boneCount = m3d->numbone + 1; - animations[a].bones = (BoneInfo *)RL_MALLOC((m3d->numbone + 1)*sizeof(BoneInfo)); - animations[a].framePoses = (Transform **)RL_MALLOC(animations[a].frameCount*sizeof(Transform *)); + BoneInfo *bones = (BoneInfo *)RL_CALLOC((m3d->numbone + 1), sizeof(BoneInfo)); + + animations[a].keyframeCount = m3d->action[a].durationmsec/M3D_ANIMDELAY; + animations[a].keyframePoses = (Transform **)RL_CALLOC(animations[a].keyframeCount, sizeof(Transform *)); strncpy(animations[a].name, m3d->action[a].name, sizeof(animations[a].name) - 1); - TRACELOG(LOG_INFO, "MODEL: [%s] animation #%i: %i msec, %i frames", fileName, a, m3d->action[a].durationmsec, animations[a].frameCount); + TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s | Frames: %d | Duration: %fs", fileName, animations[a].name, animations[a].keyframeCount, m3d->action[a].durationmsec); for (i = 0; i < (int)m3d->numbone; i++) { - animations[a].bones[i].parent = m3d->bone[i].parent; - strncpy(animations[a].bones[i].name, m3d->bone[i].name, sizeof(animations[a].bones[i].name) - 1); + bones[i].parent = m3d->bone[i].parent; + strncpy(bones[i].name, m3d->bone[i].name, sizeof(bones[i].name) - 1); } // A special, never transformed "no bone" bone, used for boneless vertices - animations[a].bones[i].parent = -1; - memcpy(animations[a].bones[i].name, "NO BONE", 7); + bones[i].parent = -1; + memcpy(bones[i].name, "NO BONE", 7); // M3D stores frames at arbitrary intervals with sparse skeletons. We need full skeletons at // regular intervals, so let the M3D SDK do the heavy lifting and calculate interpolated bones - for (i = 0; i < animations[a].frameCount; i++) + for (i = 0; i < animations[a].keyframeCount; i++) { - animations[a].framePoses[i] = (Transform *)RL_MALLOC((m3d->numbone + 1)*sizeof(Transform)); + animations[a].keyframePoses[i] = (Transform *)RL_CALLOC((m3d->numbone + 1), sizeof(Transform)); m3db_t *pose = m3d_pose(m3d, a, i*M3D_ANIMDELAY); @@ -7169,38 +7221,43 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, int *animCou { for (j = 0; j < (int)m3d->numbone; j++) { - animations[a].framePoses[i][j].translation.x = m3d->vertex[pose[j].pos].x*m3d->scale; - animations[a].framePoses[i][j].translation.y = m3d->vertex[pose[j].pos].y*m3d->scale; - animations[a].framePoses[i][j].translation.z = m3d->vertex[pose[j].pos].z*m3d->scale; - animations[a].framePoses[i][j].rotation.x = m3d->vertex[pose[j].ori].x; - animations[a].framePoses[i][j].rotation.y = m3d->vertex[pose[j].ori].y; - animations[a].framePoses[i][j].rotation.z = m3d->vertex[pose[j].ori].z; - animations[a].framePoses[i][j].rotation.w = m3d->vertex[pose[j].ori].w; - animations[a].framePoses[i][j].rotation = QuaternionNormalize(animations[a].framePoses[i][j].rotation); - animations[a].framePoses[i][j].scale.x = animations[a].framePoses[i][j].scale.y = animations[a].framePoses[i][j].scale.z = 1.0f; + animations[a].keyframePoses[i][j].translation.x = m3d->vertex[pose[j].pos].x*m3d->scale; + animations[a].keyframePoses[i][j].translation.y = m3d->vertex[pose[j].pos].y*m3d->scale; + animations[a].keyframePoses[i][j].translation.z = m3d->vertex[pose[j].pos].z*m3d->scale; + animations[a].keyframePoses[i][j].rotation.x = m3d->vertex[pose[j].ori].x; + animations[a].keyframePoses[i][j].rotation.y = m3d->vertex[pose[j].ori].y; + animations[a].keyframePoses[i][j].rotation.z = m3d->vertex[pose[j].ori].z; + animations[a].keyframePoses[i][j].rotation.w = m3d->vertex[pose[j].ori].w; + animations[a].keyframePoses[i][j].rotation = QuaternionNormalize(animations[a].keyframePoses[i][j].rotation); + animations[a].keyframePoses[i][j].scale.x = animations[a].keyframePoses[i][j].scale.y = animations[a].keyframePoses[i][j].scale.z = 1.0f; // Child bones are stored in parent bone relative space, convert that into model space - if (animations[a].bones[j].parent >= 0) + if (bones[j].parent >= 0) { - animations[a].framePoses[i][j].rotation = QuaternionMultiply(animations[a].framePoses[i][animations[a].bones[j].parent].rotation, animations[a].framePoses[i][j].rotation); - animations[a].framePoses[i][j].translation = Vector3RotateByQuaternion(animations[a].framePoses[i][j].translation, animations[a].framePoses[i][animations[a].bones[j].parent].rotation); - animations[a].framePoses[i][j].translation = Vector3Add(animations[a].framePoses[i][j].translation, animations[a].framePoses[i][animations[a].bones[j].parent].translation); - animations[a].framePoses[i][j].scale = Vector3Multiply(animations[a].framePoses[i][j].scale, animations[a].framePoses[i][animations[a].bones[j].parent].scale); + animations[a].keyframePoses[i][j].rotation = QuaternionMultiply(animations[a].keyframePoses[i][bones[j].parent].rotation, animations[a].keyframePoses[i][j].rotation); + animations[a].keyframePoses[i][j].translation = Vector3RotateByQuaternion(animations[a].keyframePoses[i][j].translation, animations[a].keyframePoses[i][bones[j].parent].rotation); + animations[a].keyframePoses[i][j].translation = Vector3Add(animations[a].keyframePoses[i][j].translation, animations[a].keyframePoses[i][bones[j].parent].translation); + animations[a].keyframePoses[i][j].scale = Vector3Multiply(animations[a].keyframePoses[i][j].scale, animations[a].keyframePoses[i][bones[j].parent].scale); } } // Default transform for the "no bone" bone - animations[a].framePoses[i][j].translation.x = 0.0f; - animations[a].framePoses[i][j].translation.y = 0.0f; - animations[a].framePoses[i][j].translation.z = 0.0f; - animations[a].framePoses[i][j].rotation.x = 0.0f; - animations[a].framePoses[i][j].rotation.y = 0.0f; - animations[a].framePoses[i][j].rotation.z = 0.0f; - animations[a].framePoses[i][j].rotation.w = 1.0f; - animations[a].framePoses[i][j].scale.x = animations[a].framePoses[i][j].scale.y = animations[a].framePoses[i][j].scale.z = 1.0f; + animations[a].keyframePoses[i][j].translation.x = 0.0f; + animations[a].keyframePoses[i][j].translation.y = 0.0f; + animations[a].keyframePoses[i][j].translation.z = 0.0f; + animations[a].keyframePoses[i][j].rotation.x = 0.0f; + animations[a].keyframePoses[i][j].rotation.y = 0.0f; + animations[a].keyframePoses[i][j].rotation.z = 0.0f; + animations[a].keyframePoses[i][j].rotation.w = 1.0f; + animations[a].keyframePoses[i][j].scale.x = 1.0f; + animations[a].keyframePoses[i][j].scale.y = 1.0f; + animations[a].keyframePoses[i][j].scale.z = 1.0f; + RL_FREE(pose); } } + + RL_FREE(bones); } m3d_free(m3d);