Skip to content

Instantly share code, notes, and snippets.

@Kirandeep-Singh-Khehra
Created December 19, 2024 14:35
Show Gist options
  • Save Kirandeep-Singh-Khehra/8c0af6079f41efcc3162a9e9c9badc83 to your computer and use it in GitHub Desktop.
Save Kirandeep-Singh-Khehra/8c0af6079f41efcc3162a9e9c9badc83 to your computer and use it in GitHub Desktop.
Simple implementation of multiple animation blend and helper function for Raylib 3d ModelAnimation.
/*
Simple functon to blend n different RayLib 3d ModelAnimations. This function just update bones. So, skinning shader is required.
*/
#ifndef _KIRAN_RAY_MODELANIMATION_ADV_
#define _KIRAN_RAY_MODELANIMATION_ADV_
#include "raylib.h"
#include "rlgl.h"
#include "raymath.h"
#include <stdlib.h>
#include <assert.h>
// DECLARATIONS //
#define ROLL(x, min, max) ((x) < (min) ? (max) - ((min) - (x)) % ((max) - (min)) : (min) + ((x) - (min)) % ((max) - (min)))
void UpdateModelAnimationBonesPro(Model model, int N, ...);
void UpdateModelVertsToCurrentBones(Model model);
// IMPLEMENTATIONS //
void UpdateModelAnimationBonesPro(Model model, int N, ...)
{
// Early stop
if (N < 1) {
return;
}
// Store args in readable format
ModelAnimation anims[N];
int frame[N];
float weight[N]; // `float` as second argument to `va_arg(arg, float)` is of promotable type `float` and will be promoted to `double`
va_list args;
va_start(args, N);
for (int animIndex = 0; animIndex < N; animIndex ++) {
anims[animIndex] = va_arg(args, ModelAnimation);
frame[animIndex] = va_arg(args, int);
weight[animIndex] = (float)va_arg(args, double);
}
va_end(args);
for (int animIndex = 0; animIndex < N; animIndex ++) {
if ((anims[animIndex].frameCount > 0) && (anims[animIndex].bones != NULL) && (anims[animIndex].framePoses != NULL)) {
frame[animIndex] = ROLL(frame[animIndex], 0, anims[animIndex].frameCount);
} else {
return;
}
}
for (int i = 0; i < model.meshCount; i++)
{
if (model.meshes[i].boneMatrices)
{
for (int animIndex = 0; animIndex < N; animIndex ++) {
assert(model.meshes[i].boneCount == anims[animIndex].boneCount);
}
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;
Vector3 outTranslation = anims[0].framePoses[frame[0]][boneId].translation;
Quaternion outRotation = anims[0].framePoses[frame[0]][boneId].rotation;
Vector3 outScale = anims[0].framePoses[frame[0]][boneId].scale;
Vector3 outNTranslation;
Quaternion outNRotation;
Vector3 outNScale;
float blendFactor = weight[0];
for (int animIndex = 1; animIndex < N; animIndex ++) {
if (weight[animIndex] == 0) continue; // otherwise if first 2 weights are 0 then new blendFactor second time will be nan
outNTranslation = anims[animIndex].framePoses[frame[animIndex]][boneId].translation;
outNRotation = anims[animIndex].framePoses[frame[animIndex]][boneId].rotation;
outNScale = anims[animIndex].framePoses[frame[animIndex]][boneId].scale;
blendFactor = weight[animIndex] / (weight[animIndex] + blendFactor);
outTranslation = Vector3Lerp(outTranslation, outNTranslation, blendFactor);
outRotation = QuaternionSlerp(outRotation, outNRotation, blendFactor);
outScale = Vector3Lerp(outScale, outNScale, blendFactor);
}
Vector3 invTranslation = Vector3RotateByQuaternion(Vector3Negate(inTranslation), QuaternionInvert(inRotation));
Quaternion invRotation = QuaternionInvert(inRotation);
Vector3 invScale = Vector3Divide((Vector3){ 1.0f, 1.0f, 1.0f }, inScale);
Vector3 boneTranslation = Vector3Add(
Vector3RotateByQuaternion(Vector3Multiply(outScale, invTranslation),
outRotation), outTranslation);
Quaternion boneRotation = QuaternionMultiply(outRotation, invRotation);
Vector3 boneScale = Vector3Multiply(outScale, invScale);
Matrix boneMatrix = MatrixMultiply(MatrixMultiply(
QuaternionToMatrix(boneRotation),
MatrixTranslate(boneTranslation.x, boneTranslation.y, boneTranslation.z)),
MatrixScale(boneScale.x, boneScale.y, boneScale.z));
model.meshes[i].boneMatrices[boneId] = boneMatrix;
}
}
}
}
// Update model vertex data (positions and normals) from mesh bone data
// NOTE: Updated data is uploaded to GPU
void UpdateModelVertsToCurrentBones(Model model)
{
for (int m = 0; m < model.meshCount; m++)
{
Mesh mesh = model.meshes[m];
Vector3 animVertex = { 0 };
Vector3 animNormal = { 0 };
int boneId = 0;
int boneCounter = 0;
float boneWeight = 0.0;
bool updated = false; // Flag to check when anim vertex information is updated
const int vValues = mesh.vertexCount*3;
for (int vCounter = 0; vCounter < vValues; 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];
boneId = mesh.boneIds[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]);
mesh.animVertices[vCounter] += animVertex.x*boneWeight;
mesh.animVertices[vCounter+1] += animVertex.y*boneWeight;
mesh.animVertices[vCounter+2] += animVertex.z*boneWeight;
updated = true;
// Normals processing
// NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals)
if (mesh.normals != NULL)
{
animNormal = (Vector3){ mesh.normals[vCounter], mesh.normals[vCounter + 1], mesh.normals[vCounter + 2] };
animNormal = Vector3Transform(animNormal,model.meshes[m].boneMatrices[boneId]);
mesh.animNormals[vCounter] += animNormal.x*boneWeight;
mesh.animNormals[vCounter + 1] += animNormal.y*boneWeight;
mesh.animNormals[vCounter + 2] += animNormal.z*boneWeight;
}
}
}
if (updated)
{
rlUpdateVertexBuffer(mesh.vboId[0], mesh.animVertices, mesh.vertexCount*3*sizeof(float), 0); // Update vertex position
rlUpdateVertexBuffer(mesh.vboId[2], mesh.animNormals, mesh.vertexCount*3*sizeof(float), 0); // Update vertex normals
}
}
}
#endif
@Kirandeep-Singh-Khehra
Copy link
Author

Kirandeep-Singh-Khehra commented Dec 19, 2024

Sample usage

UpdateModelAnimationBonesPro(model, 3/*number of animations passed*/,
                               /* Animation  |   Frame number      | Weight of animation */
                                anims[Idle]  , idleAnimFrameCounter, 1.0f - Vector2Length(velocity),
                                anims[indexX], animFrameCounter    , weightMotionX,
                                anims[indexY], animFrameCounter    , weightMotionY
                              );

Try to use GPU skinning. Or call the following method to update model vertices(CPU side).

UpdateModelVertsToCurrentBones(model);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment