diff --git a/examples/models/models_gltf_animation.c b/examples/models/models_gltf_animation.c new file mode 100644 index 000000000..e2405832c --- /dev/null +++ b/examples/models/models_gltf_animation.c @@ -0,0 +1,115 @@ +/******************************************************************************************* +* +* raylib [models] example - Load 3d gltf model with animations and play them +* +* This example has been created using raylib 3.5 (www.raylib.com) +* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* +* Example contributed by Hristo Stamenov (@object71) and reviewed by Ramon Santamaria (@raysan5) +* +* Copyright (c) 2021 Hristo Stamenov (@object71) and Ramon Santamaria (@raysan5) +* +******************************************************************************************** +* +* 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 that the scale of your models is set to 0. Scaling can be done from the export menu. +* +********************************************************************************************/ + +#include "raylib.h" + +#include + + +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [models] example - model animation"); + + // 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.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target) + camera.fovy = 45.0f; // Camera field-of-view Y + camera.type = CAMERA_PERSPECTIVE; // Camera mode type + + Model model = LoadModel("resources/models/rigged_figure.glb"); // Load the animated model mesh and + // basic data +// Texture2D texture = LoadTexture("resources/guy/guytex.png"); // Load model texture and set material +// SetMaterialTexture(&model.materials[0], MAP_DIFFUSE, texture); // Set model material map texture + + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position + + // Load animation data + int animsCount = 0; + ModelAnimation *anims = LoadModelAnimations("resources/models/rigged_figure.glb", &animsCount); + int animFrameCounter = 0; + + SetCameraMode(camera, CAMERA_FREE); // Set free camera mode + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera); + + // Play animation when spacebar is held down + if (IsKeyDown(KEY_SPACE)) + { + animFrameCounter++; + UpdateModelAnimation(model, anims[0], animFrameCounter); + if (animFrameCounter >= anims[0].frameCount) animFrameCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + BeginMode3D(camera); + + 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.boneCount; i++) + { + DrawCube(anims[0].framePoses[animFrameCounter][i].translation, 0.2f, 0.2f, 0.2f, RED); + } + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("PRESS SPACE to PLAY MODEL ANIMATION", 10, 10, 20, MAROON); + DrawText("(c) Guy IQM 3D model by @culacant", screenWidth - 200, screenHeight - 20, 10, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- +// UnloadTexture(texture); // Unload texture + + // Unload model animations data + for (int i = 0; i < animsCount; i++) UnloadModelAnimation(anims[i]); + RL_FREE(anims); + + UnloadModel(model); // Unload model + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/examples/models/resources/models/rigged_figure.glb b/examples/models/resources/models/rigged_figure.glb new file mode 100644 index 000000000..c505a33df Binary files /dev/null and b/examples/models/resources/models/rigged_figure.glb differ diff --git a/src/models.c b/src/models.c index b014352df..a9cf2c845 100644 --- a/src/models.c +++ b/src/models.c @@ -114,9 +114,11 @@ static Model LoadOBJ(const char *fileName); // Load OBJ mesh data #endif #if defined(SUPPORT_FILEFORMAT_IQM) static Model LoadIQM(const char *fileName); // Load IQM mesh data +static ModelAnimation *LoadIQMModelAnimations(const char *fileName, int *animCount); // Load IQM animation data #endif #if defined(SUPPORT_FILEFORMAT_GLTF) static Model LoadGLTF(const char *fileName); // Load GLTF mesh data +static ModelAnimation *LoadGLTFModelAnimations(const char *fileName, int *animCount); // Load GLTF animation data #endif //---------------------------------------------------------------------------------- @@ -1008,211 +1010,15 @@ void SetModelMeshMaterial(Model *model, int meshId, int materialId) // Load model animations from file ModelAnimation *LoadModelAnimations(const char *fileName, int *animCount) { - #define IQM_MAGIC "INTERQUAKEMODEL" // IQM file magic number - #define IQM_VERSION 2 // only IQM version 2 supported - - unsigned int fileSize = 0; - unsigned char *fileData = LoadFileData(fileName, &fileSize); - unsigned char *fileDataPtr = fileData; - - typedef struct IQMHeader { - char magic[16]; - unsigned int version; - unsigned int filesize; - unsigned int flags; - unsigned int num_text, ofs_text; - unsigned int num_meshes, ofs_meshes; - unsigned int num_vertexarrays, num_vertexes, ofs_vertexarrays; - unsigned int num_triangles, ofs_triangles, ofs_adjacency; - unsigned int num_joints, ofs_joints; - unsigned int num_poses, ofs_poses; - unsigned int num_anims, ofs_anims; - unsigned int num_frames, num_framechannels, ofs_frames, ofs_bounds; - unsigned int num_comment, ofs_comment; - unsigned int num_extensions, ofs_extensions; - } IQMHeader; - - typedef struct IQMPose { - int parent; - unsigned int mask; - float channeloffset[10]; - float channelscale[10]; - } IQMPose; - - typedef struct IQMAnim { - unsigned int name; - unsigned int first_frame, num_frames; - float framerate; - unsigned int flags; - } IQMAnim; - - // In case file can not be read, return an empty model - if (fileDataPtr == NULL) return NULL; - - // Read IQM header - IQMHeader *iqmHeader = (IQMHeader *)fileDataPtr; - - if (memcmp(iqmHeader->magic, IQM_MAGIC, sizeof(IQM_MAGIC)) != 0) - { - TRACELOG(LOG_WARNING, "MODEL: [%s] IQM file is not a valid model", fileName); - return NULL; - } - - if (iqmHeader->version != IQM_VERSION) - { - TRACELOG(LOG_WARNING, "MODEL: [%s] IQM file version not supported (%i)", fileName, iqmHeader->version); - return NULL; - } - - // Get bones data - IQMPose *poses = RL_MALLOC(iqmHeader->num_poses*sizeof(IQMPose)); - //fseek(iqmFile, iqmHeader->ofs_poses, SEEK_SET); - //fread(poses, iqmHeader->num_poses*sizeof(IQMPose), 1, iqmFile); - memcpy(poses, fileDataPtr + iqmHeader->ofs_poses, iqmHeader->num_poses*sizeof(IQMPose)); - - // Get animations data - *animCount = iqmHeader->num_anims; - IQMAnim *anim = RL_MALLOC(iqmHeader->num_anims*sizeof(IQMAnim)); - //fseek(iqmFile, iqmHeader->ofs_anims, SEEK_SET); - //fread(anim, iqmHeader->num_anims*sizeof(IQMAnim), 1, iqmFile); - memcpy(anim, fileDataPtr + iqmHeader->ofs_anims, iqmHeader->num_anims*sizeof(IQMAnim)); - - ModelAnimation *animations = RL_MALLOC(iqmHeader->num_anims*sizeof(ModelAnimation)); - - // frameposes - unsigned short *framedata = RL_MALLOC(iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); - //fseek(iqmFile, iqmHeader->ofs_frames, SEEK_SET); - //fread(framedata, iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short), 1, iqmFile); - memcpy(framedata, fileDataPtr + iqmHeader->ofs_frames, iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); - - 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 = RL_MALLOC(iqmHeader->num_poses*sizeof(BoneInfo)); - animations[a].framePoses = RL_MALLOC(anim[a].num_frames*sizeof(Transform *)); - //animations[a].framerate = anim.framerate; // TODO: Use framerate? - - for (unsigned int j = 0; j < iqmHeader->num_poses; j++) - { - strcpy(animations[a].bones[j].name, "ANIMJOINTNAME"); - animations[a].bones[j].parent = poses[j].parent; - } - - for (unsigned int j = 0; j < anim[a].num_frames; j++) animations[a].framePoses[j] = RL_MALLOC(iqmHeader->num_poses*sizeof(Transform)); - - int dcounter = anim[a].first_frame*iqmHeader->num_framechannels; - - for (unsigned int frame = 0; frame < anim[a].num_frames; frame++) - { - for (unsigned int i = 0; i < iqmHeader->num_poses; i++) - { - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[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]; - dcounter++; - } - - animations[a].framePoses[frame][i].rotation = QuaternionNormalize(animations[a].framePoses[frame][i].rotation); - } - } - - // Build frameposes - for (unsigned int frame = 0; frame < anim[a].num_frames; frame++) - { - for (int i = 0; i < animations[a].boneCount; i++) - { - if (animations[a].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); - } - } - } - } - - RL_FREE(fileData); - - RL_FREE(framedata); - RL_FREE(poses); - RL_FREE(anim); + ModelAnimation *animations = NULL; +#if defined(SUPPORT_FILEFORMAT_IQM) + if (IsFileExtension(fileName, ".iqm")) animations = LoadIQMModelAnimations(fileName, animCount); +#endif +#if defined(SUPPORT_FILEFORMAT_GLTF) + if (IsFileExtension(fileName, ".gltf;.glb")) animations = LoadGLTFModelAnimations(fileName, animCount); +#endif + return animations; } @@ -3549,6 +3355,218 @@ static Model LoadIQM(const char *fileName) return model; } + +// Load IQM animation data +static ModelAnimation* LoadIQMModelAnimations(const char* fileName, int* animCount) +{ +#define IQM_MAGIC "INTERQUAKEMODEL" // IQM file magic number +#define IQM_VERSION 2 // only IQM version 2 supported + + unsigned int fileSize = 0; + unsigned char *fileData = LoadFileData(fileName, &fileSize); + unsigned char *fileDataPtr = fileData; + + typedef struct IQMHeader { + char magic[16]; + unsigned int version; + unsigned int filesize; + unsigned int flags; + unsigned int num_text, ofs_text; + unsigned int num_meshes, ofs_meshes; + unsigned int num_vertexarrays, num_vertexes, ofs_vertexarrays; + unsigned int num_triangles, ofs_triangles, ofs_adjacency; + unsigned int num_joints, ofs_joints; + unsigned int num_poses, ofs_poses; + unsigned int num_anims, ofs_anims; + unsigned int num_frames, num_framechannels, ofs_frames, ofs_bounds; + unsigned int num_comment, ofs_comment; + unsigned int num_extensions, ofs_extensions; + } IQMHeader; + + typedef struct IQMPose { + int parent; + unsigned int mask; + float channeloffset[10]; + float channelscale[10]; + } IQMPose; + + typedef struct IQMAnim { + unsigned int name; + unsigned int first_frame, num_frames; + float framerate; + unsigned int flags; + } IQMAnim; + + // In case file can not be read, return an empty model + if (fileDataPtr == NULL) return NULL; + + // Read IQM header + IQMHeader *iqmHeader = (IQMHeader *)fileDataPtr; + + if (memcmp(iqmHeader->magic, IQM_MAGIC, sizeof(IQM_MAGIC)) != 0) + { + TRACELOG(LOG_WARNING, "MODEL: [%s] IQM file is not a valid model", fileName); + return NULL; + } + + if (iqmHeader->version != IQM_VERSION) + { + TRACELOG(LOG_WARNING, "MODEL: [%s] IQM file version not supported (%i)", fileName, iqmHeader->version); + return NULL; + } + + // Get bones data + IQMPose *poses = RL_MALLOC(iqmHeader->num_poses*sizeof(IQMPose)); + //fseek(iqmFile, iqmHeader->ofs_poses, SEEK_SET); + //fread(poses, iqmHeader->num_poses*sizeof(IQMPose), 1, iqmFile); + memcpy(poses, fileDataPtr + iqmHeader->ofs_poses, iqmHeader->num_poses*sizeof(IQMPose)); + + // Get animations data + *animCount = iqmHeader->num_anims; + IQMAnim *anim = RL_MALLOC(iqmHeader->num_anims*sizeof(IQMAnim)); + //fseek(iqmFile, iqmHeader->ofs_anims, SEEK_SET); + //fread(anim, iqmHeader->num_anims*sizeof(IQMAnim), 1, iqmFile); + memcpy(anim, fileDataPtr + iqmHeader->ofs_anims, iqmHeader->num_anims*sizeof(IQMAnim)); + + ModelAnimation *animations = RL_MALLOC(iqmHeader->num_anims*sizeof(ModelAnimation)); + + // frameposes + unsigned short *framedata = RL_MALLOC(iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); + //fseek(iqmFile, iqmHeader->ofs_frames, SEEK_SET); + //fread(framedata, iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short), 1, iqmFile); + memcpy(framedata, fileDataPtr + iqmHeader->ofs_frames, iqmHeader->num_frames*iqmHeader->num_framechannels*sizeof(unsigned short)); + + 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 = RL_MALLOC(iqmHeader->num_poses*sizeof(BoneInfo)); + animations[a].framePoses = RL_MALLOC(anim[a].num_frames*sizeof(Transform *)); + //animations[a].framerate = anim.framerate; // TODO: Use framerate? + + for (unsigned int j = 0; j < iqmHeader->num_poses; j++) + { + strcpy(animations[a].bones[j].name, "ANIMJOINTNAME"); + animations[a].bones[j].parent = poses[j].parent; + } + + for (unsigned int j = 0; j < anim[a].num_frames; j++) animations[a].framePoses[j] = RL_MALLOC(iqmHeader->num_poses*sizeof(Transform)); + + int dcounter = anim[a].first_frame*iqmHeader->num_framechannels; + + for (unsigned int frame = 0; frame < anim[a].num_frames; frame++) + { + for (unsigned int i = 0; i < iqmHeader->num_poses; i++) + { + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[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]; + dcounter++; + } + + animations[a].framePoses[frame][i].rotation = QuaternionNormalize(animations[a].framePoses[frame][i].rotation); + } + } + + // Build frameposes + for (unsigned int frame = 0; frame < anim[a].num_frames; frame++) + { + for (int i = 0; i < animations[a].boneCount; i++) + { + if (animations[a].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); + } + } + } + } + + RL_FREE(fileData); + + RL_FREE(framedata); + RL_FREE(poses); + RL_FREE(anim); + + return animations; +} + #endif #if defined(SUPPORT_FILEFORMAT_GLTF) @@ -3921,4 +3939,54 @@ static Model LoadGLTF(const char *fileName) return model; } + +// LoadGLTF loads in animation data from given filename +static ModelAnimation* LoadGLTFModelAnimations(const char *fileName, int *animCount) +{ + /*********************************************************************************** + + Function implemented by Hristo Stamenov (@object71) + + Features: + - Supports .gltf and .glb files + + Some restrictions (not exhaustive): + - ... + + *************************************************************************************/ + + // glTF file loading + unsigned int dataSize = 0; + unsigned char *fileData = LoadFileData(fileName, &dataSize); + + ModelAnimation *animations = NULL; + + if (fileData == NULL) return animations; + + // glTF data loading + cgltf_options options = { 0 }; + cgltf_data *data = NULL; + cgltf_result result = cgltf_parse(&options, fileData, dataSize, &data); + + if (result == cgltf_result_success) + { + TRACELOG(LOG_INFO, "MODEL: [%s] glTF animations (%s) count: %i", fileName, (data->file_type == 2)? "glb" : + "gltf", data->animations_count); + + animations = RL_MALLOC(data->animations_count*sizeof(ModelAnimation)); + + for (unsigned int a = 0; a < data->animations_count; a++) + { + cgltf_animation *animation = data->animations + a; + } + + cgltf_free(data); + } + else TRACELOG(LOG_WARNING, ": [%s] Failed to load glTF data", fileName); + + RL_FREE(fileData); + + return animations; +} + #endif diff --git a/src/raylib.h b/src/raylib.h index a28711174..df0e5e431 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -1350,6 +1350,7 @@ RLAPI void SetModelMeshMaterial(Model *model, int meshId, int materialId); // Model animations loading/unloading functions RLAPI ModelAnimation *LoadModelAnimations(const char *fileName, int *animsCount); // Load model animations from file +RLAPI ModelAnimation *LoadModelAnimations(const char *fileName, int *animsCount); // Load model animations from file RLAPI void UpdateModelAnimation(Model model, ModelAnimation anim, int frame); // Update model animation pose RLAPI void UnloadModelAnimation(ModelAnimation anim); // Unload animation data RLAPI bool IsModelAnimationValid(Model model, ModelAnimation anim); // Check model animation skeleton match