summaryrefslogtreecommitdiffhomepage
path: root/src/rmodels.c
diff options
context:
space:
mode:
authorCharles <[email protected]>2023-01-02 14:23:48 -0500
committerGitHub <[email protected]>2023-01-02 20:23:48 +0100
commitf2e3d6eca724ee88794b20934aa9bf9d818280a9 (patch)
tree7be26e5e7f752f9739758725df6fca447e118258 /src/rmodels.c
parentfabedf76367d238a18437190e1a984b468a94e60 (diff)
downloadraylib-f2e3d6eca724ee88794b20934aa9bf9d818280a9.tar.gz
raylib-f2e3d6eca724ee88794b20934aa9bf9d818280a9.zip
[models] Add GLTF animation support (#2844)
* add GLTF animation support * use correct index when allocating animVertices and animNormals * early exit LoadModelAnimationsGLTF if the gtlf file fails to parse * update models/models_loading_gltf.c to play gltf animation Updated the .blend file to use weights rather than bone parents so it fits into the framework. Exported with weights to the .glb file. * fix order of operations for bone scale in UpdateModelAnimation * minor doc cleanup and improvements * fix formatting * fix float formatting * fix brace alignment and replace asserts with log messages
Diffstat (limited to 'src/rmodels.c')
-rw-r--r--src/rmodels.c320
1 files changed, 302 insertions, 18 deletions
diff --git a/src/rmodels.c b/src/rmodels.c
index 9bb54961..ed32751c 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)