From d3c1a04983ee8fdc5dc3bbad5f77c572dd20c36c Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 2 Jan 2023 20:46:33 +0100 Subject: [PATCH] REVIEWED: GLTF animations support #2844 --- examples/models/models_loading_gltf.c | 41 ++++--- examples/models/models_loading_gltf.png | Bin 21286 -> 25896 bytes src/rmodels.c | 135 ++++++++++++++---------- 3 files changed, 97 insertions(+), 79 deletions(-) diff --git a/examples/models/models_loading_gltf.c b/examples/models/models_loading_gltf.c index 879715906..c19df3f5b 100644 --- a/examples/models/models_loading_gltf.c +++ b/examples/models/models_loading_gltf.c @@ -27,46 +27,43 @@ int main(void) // 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.position = (Vector3){ 5.0f, 5.0f, 5.0f }; // Camera position + camera.target = (Vector3){ 0.0f, 2.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.projection = CAMERA_PERSPECTIVE; // Camera mode type - // Loaf gltf model + // Load gltf model Model model = LoadModel("resources/models/gltf/robot.glb"); + + // Load gltf model animations unsigned int animsCount = 0; - ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/robot.glb", &animsCount); - unsigned int animIndex = 0; + unsigned int animCurrentFrame = 0; + ModelAnimation *modelAnimations = LoadModelAnimations("resources/models/gltf/robot.glb", &animsCount); Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position - SetCameraMode(camera, CAMERA_FREE); // Set free camera mode + SetCameraMode(camera, CAMERA_FREE); // Set free camera mode - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + 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 + 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_UP)) animIndex = (animIndex + 1)%animsCount; + else if (IsKeyPressed(KEY_DOWN)) animIndex = (animIndex + animsCount - 1)%animsCount; - if (IsKeyPressed(KEY_DOWN)) - { - animIndex = (animIndex + animsCount - 1) % animsCount; - } - - currentFrame = (currentFrame + 1) % anim.frameCount; - UpdateModelAnimation(model, anim, currentFrame); + animCurrentFrame = (animCurrentFrame + 1)%anim.frameCount; + UpdateModelAnimation(model, anim, animCurrentFrame); + UpdateCamera(&camera); //---------------------------------------------------------------------------------- @@ -74,7 +71,7 @@ int main(void) //---------------------------------------------------------------------------------- BeginDrawing(); - ClearBackground(SKYBLUE); + ClearBackground(RAYWHITE); BeginMode3D(camera); @@ -83,7 +80,7 @@ int main(void) EndMode3D(); - DrawText("Use the up/down arrow keys to switch animation.", 10, 10, 20, WHITE); + DrawText("Use the UP/DOWN arrow keys to switch animation", 10, 10, 20, GRAY); EndDrawing(); //---------------------------------------------------------------------------------- diff --git a/examples/models/models_loading_gltf.png b/examples/models/models_loading_gltf.png index 654444b848342eff35d72b5243671f1a1243cbf3..34748a22e587a86dc297238c939fbbe148fb5153 100644 GIT binary patch literal 25896 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 literal 21286 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 diff --git a/src/rmodels.c b/src/rmodels.c index ed32751c3..786e5ac79 100644 --- a/src/rmodels.c +++ b/src/rmodels.c @@ -142,18 +142,18 @@ 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 *LoadModelAnimationsIQM(const char *fileName, unsigned int *animCount); // Load IQM animation data +static ModelAnimation *LoadModelAnimationsIQM(const char *fileName, unsigned int *animCount); // Load IQM animation data #endif #if defined(SUPPORT_FILEFORMAT_GLTF) static Model LoadGLTF(const char *fileName); // Load GLTF mesh data -static ModelAnimation *LoadModelAnimationsGLTF(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 #endif #if defined(SUPPORT_FILEFORMAT_M3D) static Model LoadM3D(const char *filename); // Load M3D mesh data -static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, unsigned int *animCount); // Load M3D animation data +static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, unsigned int *animCount); // Load M3D animation data #endif //---------------------------------------------------------------------------------- @@ -3829,6 +3829,12 @@ RayCollision GetRayCollisionQuad(Ray ray, Vector3 p1, Vector3 p2, Vector3 p3, Ve return collision; } +//---------------------------------------------------------------------------------- +// Module specific Functions Definition +//---------------------------------------------------------------------------------- +#if defined(SUPPORT_FILEFORMAT_IQM) || defined(SUPPORT_FILEFORMAT_GLTF) +// Build pose from parent joints +// NOTE: Required for animations loading (required by IQM and GLTF) static void BuildPoseFromParentJoints(BoneInfo *bones, int boneCount, Transform *transforms) { for (int i = 0; i < boneCount; i++) @@ -3847,10 +3853,8 @@ static void BuildPoseFromParentJoints(BoneInfo *bones, int boneCount, Transform } } } +#endif -//---------------------------------------------------------------------------------- -// Module specific Functions Definition -//---------------------------------------------------------------------------------- #if defined(SUPPORT_FILEFORMAT_OBJ) // Load OBJ mesh data // @@ -4690,7 +4694,8 @@ static Image LoadImageFromCgltfImage(cgltf_image *cgltfImage, const char *texPat return image; } -static BoneInfo *LoadGLTFBoneInfo(cgltf_skin skin, int *boneCount) +// Load bone info from GLTF skin data +static BoneInfo *LoadBoneInfoGLTF(cgltf_skin skin, int *boneCount) { *boneCount = skin.joints_count; BoneInfo *bones = RL_MALLOC(skin.joints_count*sizeof(BoneInfo)); @@ -4700,8 +4705,9 @@ static BoneInfo *LoadGLTFBoneInfo(cgltf_skin skin, int *boneCount) cgltf_node node = *skin.joints[i]; strncpy(bones[i].name, node.name, sizeof(bones[i].name)); - // find parent bone index + // Find parent bone index unsigned int parentIndex = -1; + for (unsigned int j = 0; j < skin.joints_count; j++) { if (skin.joints[j] == node.parent) @@ -4731,7 +4737,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 + - Supports basic animations RESTRICTIONS: - Only triangle meshes supported @@ -5079,12 +5085,16 @@ static Model LoadGLTF(const char *fileName) // 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 + // + // LIMITATIONS: + // - Only supports 1 armature per file, and skips loading it if there are multiple armatures + // - Only supports linear interpolation (default method in Blender when checked "Always Sample Animations" when exporting a GLTF file) + // - Only supports translation/rotation/scale animation channel.path, weights not considered (i.e. morph targets) //---------------------------------------------------------------------------------------------------- - if (data->skins_count == 1) { cgltf_skin skin = data->skins[0]; - model.bones = LoadGLTFBoneInfo(skin, &model.boneCount); + model.bones = LoadBoneInfoGLTF(skin, &model.boneCount); model.bindPose = RL_MALLOC(model.boneCount*sizeof(Transform)); for (unsigned int i = 0; i < model.boneCount; i++) @@ -5181,19 +5191,19 @@ static Model LoadGLTF(const char *fileName) } // 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) +static bool GetPoseAtTimeGLTF(cgltf_accessor *input, cgltf_accessor *output, float time, void *data) { - // input and output should have the same count - + // Input and output should have the same count float tstart = 0.0f; float tend = 0.0f; - - int keyframe = 0; // defaults to first pose + 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); + + cgltf_bool r2 = cgltf_accessor_read_float(input, i + 1, &tend, 1); if (!r2) return false; if ((tstart <= time) && (time < tend)) @@ -5227,13 +5237,16 @@ static bool GetGLTFPoseAtTime(cgltf_accessor* input, cgltf_accessor *output, flo 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. + + // 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) +#define GLTF_ANIMDELAY 17 // Animation frames delay, (~1000 ms/60 FPS = 16.666666* ms) + static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned int *animCount) { // glTF file loading @@ -5241,10 +5254,12 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in 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); @@ -5262,9 +5277,10 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in 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); + animations[i].bones = LoadBoneInfoGLTF(skin, &animations[i].boneCount); cgltf_animation animData = data->animations[i]; @@ -5276,10 +5292,12 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in 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]) @@ -5291,7 +5309,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in if (boneIndex == -1) { - // animation channel for a node not in the armature. + // Animation channel for a node not in the armature continue; } @@ -5313,10 +5331,12 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in { 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); + } + 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); @@ -5333,17 +5353,16 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in { 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)) + if (!GetPoseAtTimeGLTF(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); } @@ -5351,10 +5370,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in if (boneChannels[k].rotate) { - if (!GetGLTFPoseAtTime(boneChannels[k].rotate->sampler->input, - boneChannels[k].rotate->sampler->output, - time, - &rotation)) + if (!GetPoseAtTimeGLTF(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); } @@ -5362,10 +5378,7 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in if (boneChannels[k].scale) { - if (!GetGLTFPoseAtTime(boneChannels[k].scale->sampler->input, - boneChannels[k].scale->sampler->output, - time, - &scale)) + if (!GetPoseAtTimeGLTF(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); } @@ -5374,7 +5387,8 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in animations[i].framePoses[j][k] = (Transform){ .translation = translation, .rotation = rotation, - .scale = scale}; + .scale = scale + }; } BuildPoseFromParentJoints(animations[i].bones, animations[i].boneCount, animations[i].framePoses[j]); @@ -5383,7 +5397,8 @@ static ModelAnimation *LoadModelAnimationsGLTF(const char *fileName, unsigned in 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); + } + 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); } @@ -5576,12 +5591,14 @@ static Model LoadM3D(const char *fileName) model.meshes[k].vertices = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); model.meshes[k].texcoords = (float *)RL_CALLOC(model.meshes[k].vertexCount*2, sizeof(float)); model.meshes[k].normals = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); + // without material, we rely on vertex colors if (mi == M3D_UNDEF && model.meshes[k].colors == NULL) { model.meshes[k].colors = RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(unsigned char)); for (j = 0; j < model.meshes[k].vertexCount*4; j += 4) memcpy(&model.meshes[k].colors[j], &WHITE, 4); } + if (m3d->numbone && m3d->numskin) { model.meshes[k].boneIds = (unsigned char *)RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(unsigned char)); @@ -5589,6 +5606,7 @@ static Model LoadM3D(const char *fileName) model.meshes[k].animVertices = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); model.meshes[k].animNormals = (float *)RL_CALLOC(model.meshes[k].vertexCount*3, sizeof(float)); } + model.meshMaterial[k] = mi + 1; l = 0; } @@ -5617,25 +5635,25 @@ static Model LoadM3D(const char *fileName) if (m3d->face[i].texcoord[0] != M3D_UNDEF) { - model.meshes[k].texcoords[l * 6 + 0] = m3d->tmap[m3d->face[i].texcoord[0]].u; - model.meshes[k].texcoords[l * 6 + 1] = 1.0 - m3d->tmap[m3d->face[i].texcoord[0]].v; - model.meshes[k].texcoords[l * 6 + 2] = m3d->tmap[m3d->face[i].texcoord[1]].u; - model.meshes[k].texcoords[l * 6 + 3] = 1.0 - m3d->tmap[m3d->face[i].texcoord[1]].v; - model.meshes[k].texcoords[l * 6 + 4] = m3d->tmap[m3d->face[i].texcoord[2]].u; - model.meshes[k].texcoords[l * 6 + 5] = 1.0 - m3d->tmap[m3d->face[i].texcoord[2]].v; + model.meshes[k].texcoords[l*6 + 0] = m3d->tmap[m3d->face[i].texcoord[0]].u; + model.meshes[k].texcoords[l*6 + 1] = 1.0 - m3d->tmap[m3d->face[i].texcoord[0]].v; + model.meshes[k].texcoords[l*6 + 2] = m3d->tmap[m3d->face[i].texcoord[1]].u; + model.meshes[k].texcoords[l*6 + 3] = 1.0 - m3d->tmap[m3d->face[i].texcoord[1]].v; + model.meshes[k].texcoords[l*6 + 4] = m3d->tmap[m3d->face[i].texcoord[2]].u; + model.meshes[k].texcoords[l*6 + 5] = 1.0 - m3d->tmap[m3d->face[i].texcoord[2]].v; } if (m3d->face[i].normal[0] != M3D_UNDEF) { - model.meshes[k].normals[l * 9 + 0] = m3d->vertex[m3d->face[i].normal[0]].x; - model.meshes[k].normals[l * 9 + 1] = m3d->vertex[m3d->face[i].normal[0]].y; - model.meshes[k].normals[l * 9 + 2] = m3d->vertex[m3d->face[i].normal[0]].z; - model.meshes[k].normals[l * 9 + 3] = m3d->vertex[m3d->face[i].normal[1]].x; - model.meshes[k].normals[l * 9 + 4] = m3d->vertex[m3d->face[i].normal[1]].y; - model.meshes[k].normals[l * 9 + 5] = m3d->vertex[m3d->face[i].normal[1]].z; - model.meshes[k].normals[l * 9 + 6] = m3d->vertex[m3d->face[i].normal[2]].x; - model.meshes[k].normals[l * 9 + 7] = m3d->vertex[m3d->face[i].normal[2]].y; - model.meshes[k].normals[l * 9 + 8] = m3d->vertex[m3d->face[i].normal[2]].z; + model.meshes[k].normals[l*9 + 0] = m3d->vertex[m3d->face[i].normal[0]].x; + model.meshes[k].normals[l*9 + 1] = m3d->vertex[m3d->face[i].normal[0]].y; + model.meshes[k].normals[l*9 + 2] = m3d->vertex[m3d->face[i].normal[0]].z; + model.meshes[k].normals[l*9 + 3] = m3d->vertex[m3d->face[i].normal[1]].x; + model.meshes[k].normals[l*9 + 4] = m3d->vertex[m3d->face[i].normal[1]].y; + model.meshes[k].normals[l*9 + 5] = m3d->vertex[m3d->face[i].normal[1]].z; + model.meshes[k].normals[l*9 + 6] = m3d->vertex[m3d->face[i].normal[2]].x; + model.meshes[k].normals[l*9 + 7] = m3d->vertex[m3d->face[i].normal[2]].y; + model.meshes[k].normals[l*9 + 8] = m3d->vertex[m3d->face[i].normal[2]].z; } // Add skin (vertex / bone weight pairs) @@ -5658,8 +5676,8 @@ static Model LoadM3D(const char *fileName) { // raylib does not handle boneless meshes with skeletal animations, so // we put all vertices without a bone into a special "no bone" bone - model.meshes[k].boneIds[l * 12 + n * 4] = m3d->numbone; - model.meshes[k].boneWeights[l * 12 + n * 4] = 1.0f; + model.meshes[k].boneIds[l*12 + n*4] = m3d->numbone; + model.meshes[k].boneWeights[l*12 + n*4] = 1.0f; } } } @@ -5754,6 +5772,7 @@ static Model LoadM3D(const char *fileName) model.bindPose[i].rotation.y = m3d->vertex[m3d->bone[i].ori].y; model.bindPose[i].rotation.z = m3d->vertex[m3d->bone[i].ori].z; model.bindPose[i].rotation.w = m3d->vertex[m3d->bone[i].ori].w; + // TODO: if the orientation quaternion not normalized, then that's encoding scaling model.bindPose[i].rotation = QuaternionNormalize(model.bindPose[i].rotation); model.bindPose[i].scale.x = model.bindPose[i].scale.y = model.bindPose[i].scale.z = 1.0f; @@ -5799,15 +5818,16 @@ static Model LoadM3D(const char *fileName) return model; } +#define M3D_ANIMDELAY 17 // Animation frames delay, (~1000 ms/60 FPS = 16.666666* ms) + // Load M3D animation data -#define M3D_ANIMDELAY 17 // that's roughly ~1000 msec / 60 FPS (16.666666* msec) static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, unsigned int *animCount) { m3d_t *m3d = NULL; unsigned int bytesRead = 0; unsigned char *fileData = LoadFileData(fileName, &bytesRead); ModelAnimation *animations = NULL; - int i, j; + int i = 0, j = 0; *animCount = 0; @@ -5824,7 +5844,7 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, unsigned int else TRACELOG(LOG_INFO, "MODEL: [%s] M3D data loaded successfully: %i animations, %i bones, %i skins", fileName, m3d->numaction, m3d->numbone, m3d->numskin); - // no animation or bone+skin? + // No animation or bone+skin? if (!m3d->numaction || !m3d->numbone || !m3d->numskin) { m3d_free(m3d); @@ -5860,7 +5880,8 @@ static ModelAnimation *LoadModelAnimationsM3D(const char *fileName, unsigned int { animations[a].framePoses[i] = RL_MALLOC((m3d->numbone + 1)*sizeof(Transform)); - m3db_t *pose = m3d_pose(m3d, a, i * M3D_ANIMDELAY); + m3db_t *pose = m3d_pose(m3d, a, i*M3D_ANIMDELAY); + if (pose != NULL) { for (j = 0; j < m3d->numbone; j++)