diff --git a/examples/models/models_gltf_animation.c b/examples/models/models_gltf_animation.c new file mode 100644 index 000000000..e31205002 --- /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/gltf/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/gltf/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++) + { + DrawSphere(anims[0].framePoses[animFrameCounter][i].translation, 0.01f, RED); + } + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("PRESS SPACE to PLAY MODEL ANIMATION", 10, 10, 20, MAROON); + DrawText("(cc4) Rigged Figure by @Cesium", 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/models_gltf_model.c b/examples/models/models_gltf_model.c new file mode 100644 index 000000000..c064e34f8 --- /dev/null +++ b/examples/models/models_gltf_model.c @@ -0,0 +1,87 @@ +/******************************************************************************************* +* +* raylib [models] example - Load 3d gltf model +* +* 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/gltf/Avocado.glb"); // Load the animated model mesh and + + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position + + 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); + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + BeginMode3D(camera); + + DrawModelEx(model, position, (Vector3){ 0.0f, 1.0f, 0.0f }, 180.0f, (Vector3){ 15.0f, 15.0f, 15.0f }, WHITE); + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("(cc0) Avocado by @Microsoft", screenWidth - 200, screenHeight - 20, 10, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + + UnloadModel(model); // Unload model + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/examples/models/resources/gltf/Avocado.glb b/examples/models/resources/gltf/Avocado.glb new file mode 100644 index 000000000..006b79c39 Binary files /dev/null and b/examples/models/resources/gltf/Avocado.glb differ diff --git a/examples/models/resources/gltf/LICENSE b/examples/models/resources/gltf/LICENSE new file mode 100644 index 000000000..77c73c62e --- /dev/null +++ b/examples/models/resources/gltf/LICENSE @@ -0,0 +1,11 @@ +Rigged Figure model has been created by Cesium (https://cesium.com/cesiumjs/), +and licensed as Creative Commons Attribution 4.0 International License. + +Check for details: http://creativecommons.org/licenses/by/4.0/ + +Avocado model is provided by Microsoft +and licensed as CC0 Universal Public Domain + +Check for details: https://creativecommons.org/publicdomain/zero/1.0/ + +GLTF sample models for testing are taken from: https://github.com/KhronosGroup/glTF-Sample-Models/ diff --git a/examples/models/resources/gltf/rigged_figure.glb b/examples/models/resources/gltf/rigged_figure.glb new file mode 100644 index 000000000..c505a33df Binary files /dev/null and b/examples/models/resources/gltf/rigged_figure.glb differ diff --git a/src/models.c b/src/models.c index 98aa2eff2..b3b784317 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 //---------------------------------------------------------------------------------- @@ -852,7 +854,7 @@ Mesh *LoadMeshes(const char *fileName, int *meshCount) // Upload mesh vertex data to GPU void UploadMesh(Mesh *mesh) { - rlLoadMesh(&mesh, false); // Static mesh by default + rlLoadMesh(mesh, false); // Static mesh by default } // Unload mesh from memory (RAM and/or VRAM) @@ -1014,211 +1016,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; } @@ -3554,6 +3360,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) @@ -3763,7 +3781,8 @@ static Model LoadGLTF(const char *fileName) int primitivesCount = 0; - for (unsigned int i = 0; i < data->meshes_count; i++) primitivesCount += (int)data->meshes[i].primitives_count; + for (unsigned int i = 0; i < data->meshes_count; i++) + primitivesCount += (int)data->meshes[i].primitives_count; // Process glTF data and map to model model.meshCount = primitivesCount; @@ -3771,9 +3790,65 @@ static Model LoadGLTF(const char *fileName) model.materialCount = (int)data->materials_count + 1; model.materials = RL_MALLOC(model.materialCount*sizeof(Material)); model.meshMaterial = RL_MALLOC(model.meshCount*sizeof(int)); + model.boneCount = data->nodes_count; + model.bones = RL_CALLOC(model.boneCount, sizeof(BoneInfo)); + model.bindPose = RL_CALLOC(model.boneCount, sizeof(Transform)); - for (int i = 0; i < model.meshCount; i++) model.meshes[i].vboId = (unsigned int *)RL_CALLOC(DEFAULT_MESH_VERTEX_BUFFERS, sizeof(unsigned int)); - + for (int i = 0; i < model.meshCount; i++) + model.meshes[i].vboId = (unsigned int *)RL_CALLOC(DEFAULT_MESH_VERTEX_BUFFERS, sizeof(unsigned int)); + + for (unsigned int j = 0; j < data->nodes_count; j++) + { + strcpy(model.bones[j].name, data->nodes[j].name == 0 ? "ANIMJOINT" : data->nodes[j].name); + model.bones[j].parent = j != 0 ? data->nodes[j].parent - data->nodes : 0; + } + + for (unsigned int i = 0; i < data->nodes_count; i++) + { + if(data->nodes[i].has_translation) + { + memcpy(&model.bindPose[i].translation, data->nodes[i].translation, 3 * sizeof(float)); + } + else + { + model.bindPose[i].translation = Vector3Zero(); + } + + if(data->nodes[i].has_rotation) + { + memcpy(&model.bindPose[i].rotation, data->nodes[i].rotation, 4 * sizeof(float)); + } + else + { + model.bindPose[i].rotation = QuaternionIdentity(); + } + model.bindPose[i].rotation = QuaternionNormalize(model.bindPose[i].rotation); + + if(data->nodes[i].has_scale) + { + memcpy(&model.bindPose[i].scale, data->nodes[i].scale, 3 * sizeof(float)); + } + else + { + model.bindPose[i].scale = Vector3One(); + } + } + + for (int i = 0; i < model.boneCount; i++) + { + Transform* currentTransform = model.bindPose + i; + BoneInfo* currentBone = model.bones + i; + Transform* parentTransform = model.bindPose + currentBone->parent; + + if (currentBone->parent >= 0) + { + currentTransform->rotation = QuaternionMultiply(parentTransform->rotation, currentTransform->rotation); + currentTransform->translation = Vector3RotateByQuaternion(currentTransform->translation, parentTransform->rotation); + currentTransform->translation = Vector3Add(currentTransform->translation, parentTransform->translation); + currentTransform->scale = Vector3Multiply(parentTransform->scale, parentTransform->scale); + } + } + for (int i = 0; i < model.materialCount - 1; i++) { model.materials[i] = LoadMaterialDefault(); @@ -3854,16 +3929,22 @@ static Model LoadGLTF(const char *fileName) { cgltf_accessor *acc = data->meshes[i].primitives[p].attributes[j].data; model.meshes[primitiveIndex].vertexCount = (int)acc->count; - model.meshes[primitiveIndex].vertices = RL_MALLOC(model.meshes[primitiveIndex].vertexCount*3*sizeof(float)); + int bufferSize = model.meshes[primitiveIndex].vertexCount * 3 * sizeof(float); + model.meshes[primitiveIndex].vertices = RL_MALLOC(bufferSize); + model.meshes[primitiveIndex].animVertices = RL_MALLOC(bufferSize); - LOAD_ACCESSOR(float, 3, acc, model.meshes[primitiveIndex].vertices) + LOAD_ACCESSOR(float, 3, acc, model.meshes[primitiveIndex].vertices); + memcpy(model.meshes[primitiveIndex].animVertices, model.meshes[primitiveIndex].vertices, bufferSize); } else if (data->meshes[i].primitives[p].attributes[j].type == cgltf_attribute_type_normal) { cgltf_accessor *acc = data->meshes[i].primitives[p].attributes[j].data; - model.meshes[primitiveIndex].normals = RL_MALLOC(acc->count*3*sizeof(float)); + int bufferSize = acc->count*3*sizeof(float); + model.meshes[primitiveIndex].normals = RL_MALLOC(bufferSize); + model.meshes[primitiveIndex].animNormals = RL_MALLOC(bufferSize); - LOAD_ACCESSOR(float, 3, acc, model.meshes[primitiveIndex].normals) + LOAD_ACCESSOR(float, 3, acc, model.meshes[primitiveIndex].normals); + memcpy(model.meshes[primitiveIndex].animNormals, model.meshes[primitiveIndex].normals, bufferSize); } else if (data->meshes[i].primitives[p].attributes[j].type == cgltf_attribute_type_texcoord) { @@ -3880,6 +3961,45 @@ static Model LoadGLTF(const char *fileName) TRACELOG(LOG_WARNING, "MODEL: [%s] glTF texture coordinates must be float", fileName); } } + else if (data->meshes[i].primitives[p].attributes[j].type == cgltf_attribute_type_joints) + { + cgltf_accessor *acc = data->meshes[i].primitives[p].attributes[j].data; + + if(acc->component_type == cgltf_component_type_r_16u) + { + model.meshes[primitiveIndex].boneIds = RL_MALLOC(sizeof(int) * acc->count * 4); + short* bones = RL_MALLOC(sizeof(short) * acc->count * 4); + + LOAD_ACCESSOR(short, 4, acc, bones); + for(int a = 0; a < acc->count * 4; a ++) + { + cgltf_node* skinJoint = data->skins->joints[bones[a]]; + + for(int k = 0; k < data->nodes_count; k++) + { + if(&(data->nodes[k]) == skinJoint) + { + model.meshes[primitiveIndex].boneIds[a] = k; + break; + } + } + } + RL_FREE(bones); + } + else + { + // TODO: Support other size of bone index? + TRACELOG(LOG_WARNING, "MODEL: [%s] glTF bones in unexpected format", fileName); + } + + } + else if (data->meshes[i].primitives[p].attributes[j].type == cgltf_attribute_type_weights) + { + cgltf_accessor *acc = data->meshes[i].primitives[p].attributes[j].data; + + model.meshes[primitiveIndex].boneWeights = RL_MALLOC(acc->count*4*sizeof(float)); + LOAD_ACCESSOR(float, 4, acc, model.meshes[primitiveIndex].boneWeights) + } } cgltf_accessor *acc = data->meshes[i].primitives[p].indices; @@ -3913,9 +4033,12 @@ static Model LoadGLTF(const char *fileName) { model.meshMaterial[primitiveIndex] = model.materialCount - 1;; } - + +// if(data->meshes[i].) + primitiveIndex++; } + } cgltf_free(data); @@ -3926,4 +4049,146 @@ 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; + + ModelAnimation *output = animations + a; + + output->frameCount = animation->channels->sampler->input->count; + output->boneCount = data->nodes_count; + output->bones = RL_MALLOC(output->boneCount*sizeof(BoneInfo)); + output->framePoses = RL_MALLOC(output->frameCount*sizeof(Transform *)); + + for (unsigned int j = 0; j < data->nodes_count; j++) + { + strcpy(output->bones[j].name, data->nodes[j].name == 0 ? "ANIMJOINT" : data->nodes[j].name); + output->bones[j].parent = j != 0 ? (int)(data->nodes[j].parent - data->nodes) : 0; + } + + for (unsigned int j = 0; j < output->frameCount; j++) + output->framePoses[j] = RL_MALLOC(output->frameCount*data->nodes_count*sizeof(Transform)); + + for (unsigned int frame = 0; frame < output->frameCount; frame++) + { + for (unsigned int i = 0; i < data->nodes_count; i++) + { + output->framePoses[frame][i].translation = Vector3Zero(); + output->framePoses[frame][i].rotation = QuaternionIdentity(); + output->framePoses[frame][i].rotation = QuaternionNormalize(output->framePoses[frame][i].rotation); + output->framePoses[frame][i].scale = Vector3One(); + } + } + + for(int channelId = 0; channelId < animation->channels_count; channelId++) + { + cgltf_animation_channel* channel = animation->channels + channelId; + cgltf_animation_sampler* sampler = channel->sampler; + + int boneId = channel->target_node - data->nodes; + + for(int frame = 0; frame < output->frameCount; frame++) + { + if(channel->target_path == cgltf_animation_path_type_translation) { + Vector3 translation; + if(cgltf_accessor_read_float(sampler->output, frame, (float*)&translation, 3)) + { + output->framePoses[frame][boneId].translation = translation; + } + else if (output->frameCount == 2) + { + memcpy(&translation, frame == 0 ? &(sampler->output->min) : &(sampler->output->max), 3 * sizeof(float)); + output->framePoses[frame][boneId].translation = translation; + } + } + if(channel->target_path == cgltf_animation_path_type_rotation) { + Quaternion rotation; + if(cgltf_accessor_read_float(sampler->output, frame, (float*)&rotation, 4)) + { + output->framePoses[frame][boneId].rotation = rotation; + output->framePoses[frame][boneId].rotation = QuaternionNormalize(output->framePoses[frame][boneId].rotation); + } + else if (output->frameCount == 2) + { + memcpy(&rotation, frame == 0 ? &(sampler->output->min) : &(sampler->output->max), 4 * sizeof(float)); + output->framePoses[frame][boneId].rotation = rotation; + output->framePoses[frame][boneId].rotation = QuaternionNormalize(output->framePoses[frame][boneId].rotation); + } + } + if(channel->target_path == cgltf_animation_path_type_scale) { + Vector3 scale; + if(cgltf_accessor_read_float(sampler->output, frame, (float*)&scale, 4)) + { + output->framePoses[frame][boneId].scale = scale; + } + else if (output->frameCount == 2) + { + memcpy(&scale, frame == 0 ? &(sampler->output->min) : &(sampler->output->max), 3 * sizeof(float)); + output->framePoses[frame][boneId].scale = scale; + } + } + } + } + + // Build frameposes + for (unsigned int frame = 0; frame < output->frameCount; frame++) + { + for (int i = 0; i < output->boneCount; i++) + { + if (output->bones[i].parent >= 0) + { + output->framePoses[frame][i].rotation = QuaternionMultiply(output->framePoses[frame][output->bones[i].parent].rotation, output->framePoses[frame][i].rotation); + output->framePoses[frame][i].translation = Vector3RotateByQuaternion(output->framePoses[frame][i].translation, output->framePoses[frame][output->bones[i].parent].rotation); + output->framePoses[frame][i].translation = Vector3Add(output->framePoses[frame][i].translation, output->framePoses[frame][output->bones[i].parent].translation); + output->framePoses[frame][i].scale = Vector3Multiply(output->framePoses[frame][i].scale, output->framePoses[frame][output->bones[i].parent].scale); + } + } + } + + } + + cgltf_free(data); + } + else TRACELOG(LOG_WARNING, ": [%s] Failed to load glTF data", fileName); + + RL_FREE(fileData); + + return animations; +} + #endif