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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 T4aXT67#KWV{an^LB{Ts5l#2th 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++)