diff --git a/examples/models/models_loading_gltf.c b/examples/models/models_loading_gltf.c index 68228a3f0..879715906 100644 --- a/examples/models/models_loading_gltf.c +++ b/examples/models/models_loading_gltf.c @@ -35,6 +35,10 @@ int main(void) // Loaf gltf model Model model = LoadModel("resources/models/gltf/robot.glb"); + unsigned int animsCount = 0; + ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/robot.glb", &animsCount); + + unsigned int animIndex = 0; Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position @@ -43,11 +47,26 @@ int main(void) SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + unsigned int currentFrame = 0; + // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { // Update //---------------------------------------------------------------------------------- + ModelAnimation anim = modelAnimations[animIndex]; + if (IsKeyPressed(KEY_UP)) + { + animIndex = (animIndex + 1) % animsCount; + } + + if (IsKeyPressed(KEY_DOWN)) + { + animIndex = (animIndex + animsCount - 1) % animsCount; + } + + currentFrame = (currentFrame + 1) % anim.frameCount; + UpdateModelAnimation(model, anim, currentFrame); UpdateCamera(&camera); //---------------------------------------------------------------------------------- @@ -64,6 +83,8 @@ int main(void) EndMode3D(); + DrawText("Use the up/down arrow keys to switch animation.", 10, 10, 20, WHITE); + EndDrawing(); //---------------------------------------------------------------------------------- } @@ -71,7 +92,7 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- UnloadModel(model); // Unload model and meshes/material - + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/models/resources/models/gltf/robot.blend b/examples/models/resources/models/gltf/robot.blend index d3bdac237..efe43c5e5 100644 Binary files a/examples/models/resources/models/gltf/robot.blend and b/examples/models/resources/models/gltf/robot.blend differ diff --git a/examples/models/resources/models/gltf/robot.glb b/examples/models/resources/models/gltf/robot.glb index 73f5bf44e..549011e75 100644 Binary files a/examples/models/resources/models/gltf/robot.glb and b/examples/models/resources/models/gltf/robot.glb differ diff --git a/src/rmodels.c b/src/rmodels.c index 9bb54961a..ed32751c3 100644 --- a/src/rmodels.c +++ b/src/rmodels.c @@ -146,7 +146,7 @@ static ModelAnimation *LoadModelAnimationsIQM(const char *fileName, unsigned int #endif #if defined(SUPPORT_FILEFORMAT_GLTF) static Model LoadGLTF(const char *fileName); // Load GLTF mesh data -//static ModelAnimation *LoadModelAnimationGLTF(const char *fileName, unsigned int *animCount); // Load GLTF animation data +static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned int *animCount); // Load GLTF animation data #endif #if defined(SUPPORT_FILEFORMAT_VOX) static Model LoadVOX(const char *filename); // Load VOX mesh data @@ -1955,7 +1955,7 @@ ModelAnimation *LoadModelAnimations(const char *fileName, unsigned int *animCoun if (IsFileExtension(fileName, ".m3d")) animations = LoadModelAnimationsM3D(fileName, animCount); #endif #if defined(SUPPORT_FILEFORMAT_GLTF) - //if (IsFileExtension(fileName, ".gltf;.glb")) animations = LoadModelAnimationGLTF(fileName, animCount); + if (IsFileExtension(fileName, ".gltf;.glb")) animations = LoadModelAnimationsGLTF(fileName, animCount); #endif return animations; @@ -2029,8 +2029,8 @@ void UpdateModelAnimation(Model model, ModelAnimation anim, int frame) // Vertices processing // NOTE: We use meshes.vertices (default vertex position) to calculate meshes.animVertices (animated vertex position) animVertex = (Vector3){ mesh.vertices[vCounter], mesh.vertices[vCounter + 1], mesh.vertices[vCounter + 2] }; - animVertex = Vector3Multiply(animVertex, outScale); animVertex = Vector3Subtract(animVertex, inTranslation); + animVertex = Vector3Multiply(animVertex, outScale); animVertex = Vector3RotateByQuaternion(animVertex, QuaternionMultiply(outRotation, QuaternionInvert(inRotation))); animVertex = Vector3Add(animVertex, outTranslation); //animVertex = Vector3Transform(animVertex, model.transform); @@ -3829,6 +3829,25 @@ RayCollision GetRayCollisionQuad(Ray ray, Vector3 p1, Vector3 p2, Vector3 p3, Ve return collision; } +static void BuildPoseFromParentJoints(BoneInfo *bones, int boneCount, Transform *transforms) +{ + for (int i = 0; i < boneCount; i++) + { + if (bones[i].parent >= 0) + { + if (bones[i].parent > i) + { + TRACELOG(LOG_WARNING, "Assumes bones are toplogically sorted, but bone %d has parent %d. Skipping.", i, bones[i].parent); + continue; + } + transforms[i].rotation = QuaternionMultiply(transforms[bones[i].parent].rotation, transforms[i].rotation); + transforms[i].translation = Vector3RotateByQuaternion(transforms[i].translation, transforms[bones[i].parent].rotation); + transforms[i].translation = Vector3Add(transforms[i].translation, transforms[bones[i].parent].translation); + transforms[i].scale = Vector3Multiply(transforms[i].scale, transforms[bones[i].parent].scale); + } + } +} + //---------------------------------------------------------------------------------- // Module specific Functions Definition //---------------------------------------------------------------------------------- @@ -4370,17 +4389,7 @@ static Model LoadIQM(const char *fileName) model.bindPose[i].scale.z = ijoint[i].scale[2]; } - // Build bind pose from parent joints - for (int i = 0; i < model.boneCount; i++) - { - if (model.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); - } - } + BuildPoseFromParentJoints(model.bones, model.boneCount, model.bindPose); RL_FREE(fileData); @@ -4681,6 +4690,33 @@ static Image LoadImageFromCgltfImage(cgltf_image *cgltfImage, const char *texPat return image; } +static BoneInfo *LoadGLTFBoneInfo(cgltf_skin skin, int *boneCount) +{ + *boneCount = skin.joints_count; + BoneInfo *bones = RL_MALLOC(skin.joints_count*sizeof(BoneInfo)); + + for (unsigned int i = 0; i < skin.joints_count; i++) + { + cgltf_node node = *skin.joints[i]; + strncpy(bones[i].name, node.name, sizeof(bones[i].name)); + + // find parent bone index + unsigned int parentIndex = -1; + for (unsigned int j = 0; j < skin.joints_count; j++) + { + if (skin.joints[j] == node.parent) + { + parentIndex = j; + break; + } + } + + bones[i].parent = parentIndex; + } + + return bones; +} + // Load glTF file into model struct, .gltf and .glb supported static Model LoadGLTF(const char *fileName) { @@ -4695,6 +4731,7 @@ static Model LoadGLTF(const char *fileName) - Supports PBR metallic/roughness flow, loads material textures, values and colors PBR specular/glossiness flow and extended texture flows not supported - Supports multiple meshes per model (every primitives is loaded as a separate mesh) + - Supports basic animation RESTRICTIONS: - Only triangle meshes supported @@ -5039,11 +5076,41 @@ static Model LoadGLTF(const char *fileName) } } -/* - // TODO: Load glTF meshes animation data + // Load glTF meshes animation data // REF: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#skins // REF: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#skinned-mesh-attributes //---------------------------------------------------------------------------------------------------- + + if (data->skins_count == 1) + { + cgltf_skin skin = data->skins[0]; + model.bones = LoadGLTFBoneInfo(skin, &model.boneCount); + model.bindPose = RL_MALLOC(model.boneCount*sizeof(Transform)); + + for (unsigned int i = 0; i < model.boneCount; i++) + { + cgltf_node node = *skin.joints[i]; + model.bindPose[i].translation.x = node.translation[0]; + model.bindPose[i].translation.y = node.translation[1]; + model.bindPose[i].translation.z = node.translation[2]; + + model.bindPose[i].rotation.x = node.rotation[0]; + model.bindPose[i].rotation.y = node.rotation[1]; + model.bindPose[i].rotation.z = node.rotation[2]; + model.bindPose[i].rotation.w = node.rotation[3]; + + model.bindPose[i].scale.x = node.scale[0]; + model.bindPose[i].scale.y = node.scale[1]; + model.bindPose[i].scale.z = node.scale[2]; + } + + BuildPoseFromParentJoints(model.bones, model.boneCount, model.bindPose); + } + else if (data->skins_count > 1) + { + TRACELOG(LOG_ERROR, "MODEL: [%s] can only load one skin (armature) per model, but gltf skins_count == %i", fileName, data->skins_count); + } + for (unsigned int i = 0, meshIndex = 0; i < data->meshes_count; i++) { for (unsigned int p = 0; p < data->meshes[i].primitives_count; p++) @@ -5065,7 +5132,6 @@ static Model LoadGLTF(const char *fileName) model.meshes[meshIndex].boneIds = RL_CALLOC(model.meshes[meshIndex].vertexCount*4, sizeof(unsigned char)); // Load 4 components of unsigned char data type into mesh.boneIds - // TODO: It seems LOAD_ATTRIBUTE() macro does not work as expected in some cases, // for cgltf_attribute_type_joints we have: // - data.meshes[0] (256 vertices) // - 256 values, provided as cgltf_type_vec4 of bytes (4 byte per joint, stride 4) @@ -5092,10 +5158,17 @@ static Model LoadGLTF(const char *fileName) } } + // Animated vertex data + model.meshes[meshIndex].animVertices = 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 = RL_CALLOC(model.meshes[meshIndex].vertexCount*3, sizeof(float)); + memcpy(model.meshes[meshIndex].animNormals, model.meshes[meshIndex].normals, model.meshes[meshIndex].vertexCount*3*sizeof(float)); + meshIndex++; // Move to next mesh } + } -*/ + // Free all cgltf loaded data cgltf_free(data); } @@ -5106,6 +5179,217 @@ static Model LoadGLTF(const char *fileName) return model; } + +// Get interpolated pose for bone sampler at a specific time. Returns true on success. +static bool GetGLTFPoseAtTime(cgltf_accessor* input, cgltf_accessor *output, float time, void *data) +{ + // input and output should have the same count + + float tstart = 0.0f; + float tend = 0.0f; + + int keyframe = 0; // defaults to first pose + for (int i = 0; i < input->count - 1; i++) + { + cgltf_bool r1 = cgltf_accessor_read_float(input, i, &tstart, 1); + if (!r1) return false; + cgltf_bool r2 = cgltf_accessor_read_float(input, i+1, &tend, 1); + if (!r2) return false; + + if ((tstart <= time) && (time < tend)) + { + keyframe = i; + break; + } + } + + float t = (time - tstart)/(tend - tstart); + t = (t < 0.0f)? 0.0f : t; + t = (t > 1.0f)? 1.0f : t; + + if (output->component_type != cgltf_component_type_r_32f) return false; + + if (output->type == cgltf_type_vec3) + { + float tmp[3] = { 0.0f }; + cgltf_accessor_read_float(output, keyframe, tmp, 3); + Vector3 v1 = {tmp[0], tmp[1], tmp[2]}; + cgltf_accessor_read_float(output, keyframe+1, tmp, 3); + Vector3 v2 = {tmp[0], tmp[1], tmp[2]}; + Vector3 *r = data; + *r = Vector3Lerp(v1, v2, t); + } + else if (output->type == cgltf_type_vec4) + { + float tmp[4] = { 0.0f }; + cgltf_accessor_read_float(output, keyframe, tmp, 4); + Vector4 v1 = {tmp[0], tmp[1], tmp[2], tmp[3]}; + cgltf_accessor_read_float(output, keyframe+1, tmp, 4); + Vector4 v2 = {tmp[0], tmp[1], tmp[2], tmp[3]}; + Vector4 *r = data; + // only v4 is for rotations, so we know it's a quat. + *r = QuaternionSlerp(v1, v2, t); + } + return true; +} + +#define GLTF_ANIMDELAY 17 // that's roughly ~1000 msec / 60 FPS (16.666666* msec) +static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned int *animCount) +{ + // glTF file loading + unsigned int dataSize = 0; + unsigned char *fileData = LoadFileData(fileName, &dataSize); + + ModelAnimation *animations = NULL; + // 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_WARNING, "MODEL: [%s] Failed to load glTF data", fileName); + *animCount = 0; + return NULL; + } + + result = cgltf_load_buffers(&options, data, fileName); + if (result != cgltf_result_success) TRACELOG(LOG_INFO, "MODEL: [%s] Failed to load animation buffers", fileName); + + if (result == cgltf_result_success) + { + if (data->skins_count == 1) + { + cgltf_skin skin = data->skins[0]; + *animCount = data->animations_count; + animations = RL_MALLOC(data->animations_count*sizeof(ModelAnimation)); + for (unsigned int i = 0; i < data->animations_count; i++) + { + animations[i].bones = LoadGLTFBoneInfo(skin, &animations[i].boneCount); + + cgltf_animation animData = data->animations[i]; + + struct Channels { + cgltf_animation_channel *translate; + cgltf_animation_channel *rotate; + cgltf_animation_channel *scale; + }; + + struct Channels *boneChannels = RL_CALLOC(animations[i].boneCount, sizeof(struct Channels)); + float animDuration = 0.0f; + for (unsigned int j = 0; j < animData.channels_count; j++) + { + cgltf_animation_channel channel = animData.channels[j]; + int boneIndex = -1; + for (unsigned int k = 0; k < skin.joints_count; k++) + { + if (animData.channels[j].target_node == skin.joints[k]) + { + boneIndex = k; + break; + } + } + + if (boneIndex == -1) + { + // animation channel for a node not in the armature. + continue; + } + + if (animData.channels[j].sampler->interpolation == cgltf_interpolation_type_linear) + { + if (channel.target_path == cgltf_animation_path_type_translation) + { + boneChannels[boneIndex].translate = &animData.channels[j]; + } + else if (channel.target_path == cgltf_animation_path_type_rotation) + { + boneChannels[boneIndex].rotate = &animData.channels[j]; + } + else if (channel.target_path == cgltf_animation_path_type_scale) + { + boneChannels[boneIndex].scale = &animData.channels[j]; + } + else + { + TRACELOG(LOG_WARNING, "MODEL: [%s] Unsupported target_path on channel %d's sampler for animation %d. Skipping.", fileName, j, i); + } + } else TRACELOG(LOG_WARNING, "MODEL: [%s] Only linear interpolation curves are supported 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); + if (!r) + { + TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load input time", fileName); + continue; + } + + animDuration = (t > animDuration)? t : animDuration; + } + + animations[i].frameCount = (int)(animDuration*1000.0f/GLTF_ANIMDELAY); + animations[i].framePoses = RL_MALLOC(animations[i].frameCount*sizeof(Transform *)); + + for (unsigned int j = 0; j < animations[i].frameCount; j++) + { + animations[i].framePoses[j] = RL_MALLOC(animations[i].boneCount*sizeof(Transform)); + float time = ((float) j*GLTF_ANIMDELAY)/1000.0f; + for (unsigned int k = 0; k < animations[i].boneCount; k++) + { + Vector3 translation = {0, 0, 0}; + Quaternion rotation = {0, 0, 0, 1}; + Vector3 scale = {1, 1, 1}; + if (boneChannels[k].translate) + { + if (!GetGLTFPoseAtTime(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); + } + } + + if (boneChannels[k].rotate) + { + if (!GetGLTFPoseAtTime(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); + } + } + + if (boneChannels[k].scale) + { + if (!GetGLTFPoseAtTime(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); + } + } + + animations[i].framePoses[j][k] = (Transform){ + .translation = translation, + .rotation = rotation, + .scale = scale}; + } + + BuildPoseFromParentJoints(animations[i].bones, animations[i].boneCount, animations[i].framePoses[j]); + } + + TRACELOG(LOG_INFO, "MODEL: [%s] Loaded animation: %s (%d frames, %fs)", fileName, animData.name, animations[i].frameCount, animDuration); + RL_FREE(boneChannels); + } + } else TRACELOG(LOG_ERROR, "MODEL: [%s] expected exactly one skin to load animation data from, but found %i", fileName, data->skins_count); + + cgltf_free(data); + } + + return animations; +} #endif #if defined(SUPPORT_FILEFORMAT_VOX)