diff --git a/examples/Makefile b/examples/Makefile index 6a39a1c5..b0bf4252 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -466,6 +466,7 @@ MODELS = \ models/models_loading \ models/models_loading_vox \ models/models_loading_gltf \ + models/models_loading_m3d \ models/models_orthographic_projection \ models/models_rlgl_solar_system \ models/models_skybox \ diff --git a/examples/models/models_loading.c b/examples/models/models_loading.c index bb3b490f..d4066141 100644 --- a/examples/models/models_loading.c +++ b/examples/models/models_loading.c @@ -12,6 +12,8 @@ * raylib can load .iqm animations. * - VOX > Binary file format. MagikaVoxel mesh format: * https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt +* - M3D > Binary file format. Model 3D format: +* https://bztsrc.gitlab.io/model3d * * Example originally created with raylib 2.0, last time updated with raylib 4.2 * @@ -80,7 +82,8 @@ int main(void) IsFileExtension(droppedFiles.paths[0], ".gltf") || IsFileExtension(droppedFiles.paths[0], ".glb") || IsFileExtension(droppedFiles.paths[0], ".vox") || - IsFileExtension(droppedFiles.paths[0], ".iqm")) // Model file formats supported + IsFileExtension(droppedFiles.paths[0], ".iqm") || + IsFileExtension(droppedFiles.paths[0], ".m3d")) // Model file formats supported { UnloadModel(model); // Unload previous model model = LoadModel(droppedFiles.paths[0]); // Load new model diff --git a/examples/models/models_loading_m3d.c b/examples/models/models_loading_m3d.c new file mode 100644 index 00000000..ca77d360 --- /dev/null +++ b/examples/models/models_loading_m3d.c @@ -0,0 +1,127 @@ +/******************************************************************************************* +* +* raylib [models] example - Load M3D model (with optional animations) and play them +* +* Example static mesh Suzanne from Blender +* Example animated seagull model from Scorched 3D, licensed GPLv2 +* +* Copyright (c) 2019-2022 Culacant (@culacant) and Ramon Santamaria (@raysan5) +* Copyright (c) 2022 bzt (@bztsrc) +* +******************************************************************************************** +* +* NOTE: To export a model from blender, just use https://gitlab.com/bztsrc/model3d/-/tree/master/blender +* and make sure to add "(action)" markers to the timeline if you want multiple animations. +* +********************************************************************************************/ + +#include "raylib.h" + +#include + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(int argc, char **argv) +{ + char *model_fn = argc > 1 ? argv[1] : "resources/models/m3d/suzanne.m3d"; + + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [models] example - M3D model"); + + // 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.projection = CAMERA_PERSPECTIVE; // Camera mode type + Vector3 position = { 0.0f, 0.0f, 0.0f }; // Set model position + + // Load model + Model model = LoadModel(model_fn); // Load the animated model mesh and basic data + + // Load animation data + unsigned int animsCount = 0; + ModelAnimation *anims = LoadModelAnimations(model_fn, &animsCount); + int animFrameCounter = 0, animId = 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 (animsCount) + { + if (IsKeyDown(KEY_SPACE)) + { + animFrameCounter++; + UpdateModelAnimation(model, anims[animId], animFrameCounter); + if (animFrameCounter >= anims[animId].frameCount) animFrameCounter = 0; + } + + // Select animation on mouse click + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + animFrameCounter = 0; + animId++; + if (animId >= animsCount) animId = 0; + UpdateModelAnimation(model, anims[animId], 0); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, WHITE); // Draw 3d model with texture + if(anims) + for (int i = 0; i < model.boneCount; i++) + { + DrawCube(anims[animId].framePoses[animFrameCounter][i].translation, 0.2f, 0.2f, 0.2f, RED); + } + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("PRESS SPACE to PLAY MODEL ANIMATION", 10, GetScreenHeight() - 30, 10, MAROON); + DrawText("MOUSE LEFT BUTTON to CYCLE THROUGH ANIMATIONS", 10, GetScreenHeight() - 20, 10, DARKGRAY); + DrawText("(c) Suzanne 3D model by blender", screenWidth - 200, screenHeight - 20, 10, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + + // Unload model animations data + for (unsigned 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/src/external/m3d.h b/src/external/m3d.h index 0dacb6e0..3b29927d 100644 --- a/src/external/m3d.h +++ b/src/external/m3d.h @@ -1865,6 +1865,9 @@ static void *_m3dstbi__png_load(_m3dstbi__context *s, int *x, int *y, int *comp, #define stbi__png_load _m3dstbi__png_load #define stbi_zlib_decode_malloc_guesssize_headerflag _m3dstbi_zlib_decode_malloc_guesssize_headerflag #endif +#if !defined(M3D_NOIMPORTER) && defined(STBI_INCLUDE_STB_IMAGE_H) && !defined(STB_IMAGE_IMPLEMENTATION) +#error "stb_image.h included without STB_IMAGE_IMPLEMENTATION. Sorry, we need some stuff defined inside the ifguard for proper integration" +#endif #if defined(M3D_EXPORTER) && !defined(INCLUDE_STB_IMAGE_WRITE_H) /* zlib_compressor from @@ -2165,11 +2168,9 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char unsigned int i, len = 0; unsigned char *buff = NULL; char *fn2; -#ifdef STBI__PNG_TYPE unsigned int w, h; stbi__context s; stbi__result_info ri; -#endif /* do we have loaded this texture already? */ for(i = 0; i < model->numtexture; i++) @@ -2212,7 +2213,6 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char model->texture[i].w = model->texture[i].h = 0; model->texture[i].d = NULL; if(buff) { if(buff[0] == 0x89 && buff[1] == 'P' && buff[2] == 'N' && buff[3] == 'G') { -#ifdef STBI__PNG_TYPE s.read_from_callbacks = 0; s.img_buffer = s.img_buffer_original = (unsigned char *) buff; s.img_buffer_end = s.img_buffer_original_end = (unsigned char *) buff+len; @@ -2223,7 +2223,6 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char model->texture[i].w = w; model->texture[i].h = h; model->texture[i].f = (uint8_t)len; -#endif } else { #ifdef M3D_TX_INTERP if((model->errcode = M3D_TX_INTERP(fn, buff, len, &model->texture[i])) != M3D_SUCCESS) { @@ -3225,7 +3224,7 @@ asciiend: /* parse header */ data += sizeof(m3dhdr_t); - M3D_LOG(data); + M3D_LOG((char*)data); model->name = (char*)data; for(; data < end && *data; data++) {}; data++; model->license = (char*)data; @@ -3264,12 +3263,12 @@ asciiend: } if((sizeof(M3D_INDEX) == 2 && (model->vi_s > 2 || model->si_s > 2 || model->ci_s > 2 || model->ti_s > 2 || model->bi_s > 2 || model->sk_s > 2 || model->fc_s > 2 || model->hi_s > 2 || model->fi_s > 2)) || - (sizeof(M3D_VOXEL) == 2 && model->vp_s > 2)) { + (sizeof(M3D_VOXEL) < (size_t)model->vp_s && model->vp_s != 8)) { M3D_LOG("32 bit indices not supported, unable to load model"); M3D_FREE(model); return NULL; } - if(model->vi_s > 4 || model->si_s > 4) { + if(model->vi_s > 4 || model->si_s > 4 || model->vp_s == 4) { M3D_LOG("Invalid index size, unable to load model"); M3D_FREE(model); return NULL; @@ -3346,12 +3345,12 @@ memerr: M3D_LOG("Out of memory"); for(i = 0, data += sizeof(m3dchunk_t); data < chunk; i++) { switch(model->vc_s) { case 1: - model->tmap[i].u = (M3D_FLOAT)(data[0]) / (M3D_FLOAT)255.0; - model->tmap[i].v = (M3D_FLOAT)(data[1]) / (M3D_FLOAT)255.0; + model->tmap[i].u = (M3D_FLOAT)((uint8_t)data[0]) / (M3D_FLOAT)255.0; + model->tmap[i].v = (M3D_FLOAT)((uint8_t)data[1]) / (M3D_FLOAT)255.0; break; case 2: - model->tmap[i].u = (M3D_FLOAT)(*((int16_t*)(data+0))) / (M3D_FLOAT)65535.0; - model->tmap[i].v = (M3D_FLOAT)(*((int16_t*)(data+2))) / (M3D_FLOAT)65535.0; + model->tmap[i].u = (M3D_FLOAT)(*((uint16_t*)(data+0))) / (M3D_FLOAT)65535.0; + model->tmap[i].v = (M3D_FLOAT)(*((uint16_t*)(data+2))) / (M3D_FLOAT)65535.0; break; case 4: model->tmap[i].u = (M3D_FLOAT)(*((float*)(data+0))); @@ -5219,7 +5218,7 @@ memerr: if(vrtxidx) M3D_FREE(vrtxidx); if(model->preview.data && model->preview.length) { sl = _m3d_safestr(sn, 0); if(sl) { - ptr -= (uintptr_t)out; len = (unsigned int)((uintptr_t)ptr + (uintptr_t)20); + ptr -= (uintptr_t)out; len = (unsigned int)((uintptr_t)ptr + (uintptr_t)20 + strlen(sl)); out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uintptr_t)out; if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } ptr += sprintf(ptr, "Preview\r\n%s.png\r\n\r\n", sl); diff --git a/src/rmodels.c b/src/rmodels.c index 37bc95f0..ec9e7adf 100644 --- a/src/rmodels.c +++ b/src/rmodels.c @@ -92,10 +92,6 @@ #define M3D_REALLOC RL_REALLOC #define M3D_FREE RL_FREE - // Let the M3D loader know about stb_image is used in this project, - // to allow it to use on textures loading - #include "external/stb_image.h" - #define M3D_IMPLEMENTATION #include "external/m3d.h" // Model3D file format loading #endif @@ -157,6 +153,7 @@ 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 #endif //---------------------------------------------------------------------------------- @@ -1862,6 +1859,9 @@ ModelAnimation *LoadModelAnimations(const char *fileName, unsigned int *animCoun #if defined(SUPPORT_FILEFORMAT_IQM) if (IsFileExtension(fileName, ".iqm")) animations = LoadModelAnimationsIQM(fileName, animCount); #endif +#if defined(SUPPORT_FILEFORMAT_M3D) + if (IsFileExtension(fileName, ".m3d")) animations = LoadModelAnimationsM3D(fileName, animCount); +#endif #if defined(SUPPORT_FILEFORMAT_GLTF) //if (IsFileExtension(fileName, ".gltf;.glb")) animations = LoadModelAnimationGLTF(fileName, animCount); #endif @@ -5135,19 +5135,29 @@ static Model LoadM3D(const char *fileName) m3dp_t *prop = NULL; unsigned int bytesRead = 0; unsigned char *fileData = LoadFileData(fileName, &bytesRead); - int i, j, k, l, mi = -2; + int i, j, k, l, n, mi = -2; if (fileData != NULL) { m3d = m3d_load(fileData, m3d_loaderhook, m3d_freehook, NULL); - if (!m3d || (m3d->errcode != M3D_SUCCESS)) + if (!m3d || M3D_ERR_ISFATAL(m3d->errcode)) { - TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load M3D data", fileName); + TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load M3D data, error code %d", fileName, m3d ? m3d->errcode : -2); + if (m3d) m3d_free(m3d); + UnloadFileData(fileData); return model; } else TRACELOG(LOG_INFO, "MODEL: [%s] M3D data loaded successfully: %i faces/%i materials", fileName, m3d->numface, m3d->nummaterial); + // no face? this is probably just a material library + if (!m3d->numface) + { + m3d_free(m3d); + UnloadFileData(fileData); + return model; + } + if (m3d->nummaterial > 0) { model.meshCount = model.materialCount = m3d->nummaterial; @@ -5161,17 +5171,25 @@ static Model LoadM3D(const char *fileName) model.meshes = (Mesh *)RL_CALLOC(model.meshCount, sizeof(Mesh)); model.meshMaterial = (int *)RL_CALLOC(model.meshCount, sizeof(int)); - model.materials = (Material *)RL_CALLOC(model.meshCount + 1, sizeof(Material)); + model.materials = (Material *)RL_CALLOC(model.materialCount + 1, sizeof(Material)); // Map no material to index 0 with default shader, everything else materialid + 1 model.materials[0] = LoadMaterialDefault(); - model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = (Texture2D){ rlGetTextureIdDefault(), 1, 1, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 }; for (i = l = 0, k = -1; i < m3d->numface; i++, l++) { // Materials are grouped together if (mi != m3d->face[i].materialid) { + // there should be only one material switch per material kind, but be bulletproof for unoptimal model files + if (k + 1 >= model.meshCount) + { + model.meshCount++; + model.meshes = (Mesh *)RL_REALLOC(model.meshes, model.meshCount*sizeof(Mesh)); + memset(&model.meshes[model.meshCount - 1], 0, sizeof(Mesh)); + model.meshMaterial = (int *)RL_REALLOC(model.meshMaterial, model.meshCount*sizeof(int)); + } + k++; mi = m3d->face[i].materialid; @@ -5182,6 +5200,19 @@ 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)); + model.meshes[k].boneWeights = (float *)RL_CALLOC(model.meshes[k].vertexCount*4, sizeof(float)); + 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; } @@ -5197,14 +5228,25 @@ static Model LoadM3D(const char *fileName) model.meshes[k].vertices[l * 9 + 7] = m3d->vertex[m3d->face[i].vertex[2]].y*m3d->scale; model.meshes[k].vertices[l * 9 + 8] = m3d->vertex[m3d->face[i].vertex[2]].z*m3d->scale; + if (mi == M3D_UNDEF) + { + // without vertex color (full transparency), we use the default color + if (m3d->vertex[m3d->face[i].vertex[0]].color & 0xFF000000) + memcpy(&model.meshes[k].colors[l * 12 + 0], &m3d->vertex[m3d->face[i].vertex[0]].color, 4); + if (m3d->vertex[m3d->face[i].vertex[1]].color & 0xFF000000) + memcpy(&model.meshes[k].colors[l * 12 + 4], &m3d->vertex[m3d->face[i].vertex[1]].color, 4); + if (m3d->vertex[m3d->face[i].vertex[2]].color & 0xFF000000) + memcpy(&model.meshes[k].colors[l * 12 + 8], &m3d->vertex[m3d->face[i].vertex[2]].color, 4); + } + 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] = m3d->tmap[m3d->face[i].texcoord[0]].v; + 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] = m3d->tmap[m3d->face[i].texcoord[1]].v; + 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] = m3d->tmap[m3d->face[i].texcoord[2]].v; + 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) @@ -5219,12 +5261,28 @@ static Model LoadM3D(const char *fileName) 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) + if (m3d->numbone && m3d->numskin) { + for (n = 0; n < 3; n++) { + int skinid = m3d->vertex[m3d->face[i].vertex[n]].skinid; + // check if there's a skin for this mesh, should be, just failsafe + if (skinid != M3D_UNDEF && skinid < m3d->numskin) + { + for (j = 0; j < 4; j++) + { + model.meshes[k].boneIds[l * 12 + n * 4 + j] = m3d->skin[skinid].boneid[j]; + model.meshes[k].boneWeights[l * 12 + n * 4 + j] = m3d->skin[skinid].weight[j]; + } + } + } + } } + // Load materials for (i = 0; i < m3d->nummaterial; i++) { model.materials[i + 1] = LoadMaterialDefault(); - model.materials[i + 1].maps[MATERIAL_MAP_DIFFUSE].texture = (Texture2D){ rlGetTextureIdDefault(), 1, 1, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 }; for (j = 0; j < m3d->material[i].numprop; j++) { @@ -5240,7 +5298,6 @@ static Model LoadM3D(const char *fileName) case m3dp_Ks: { memcpy(&model.materials[i + 1].maps[MATERIAL_MAP_SPECULAR].color, &prop->value.color, 4); - model.materials[i + 1].maps[MATERIAL_MAP_SPECULAR].value = 0.0f; } break; case m3dp_Ns: { @@ -5270,11 +5327,11 @@ static Model LoadM3D(const char *fileName) { Image image = { 0 }; image.data = m3d->texture[prop->value.textureid].d; - image.width = m3d->texture[prop->value.textureid].w; + image.width = m3d->texture[prop->value.textureid].w; image.height = m3d->texture[prop->value.textureid].h; image.mipmaps = 1; - image.format = (m3d->texture[prop->value.textureid].f == 4)? PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 : - ((m3d->texture[prop->value.textureid].f == 3)? PIXELFORMAT_UNCOMPRESSED_R8G8B8 : + image.format = (m3d->texture[prop->value.textureid].f == 4)? PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 : + ((m3d->texture[prop->value.textureid].f == 3)? PIXELFORMAT_UNCOMPRESSED_R8G8B8 : ((m3d->texture[prop->value.textureid].f == 2)? PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA : PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)); switch (prop->type) @@ -5283,6 +5340,8 @@ static Model LoadM3D(const char *fileName) case m3dp_map_Ks: model.materials[i + 1].maps[MATERIAL_MAP_SPECULAR].texture = LoadTextureFromImage(image); break; case m3dp_map_Ke: model.materials[i + 1].maps[MATERIAL_MAP_EMISSION].texture = LoadTextureFromImage(image); break; case m3dp_map_Km: model.materials[i + 1].maps[MATERIAL_MAP_NORMAL].texture = LoadTextureFromImage(image); break; + case m3dp_map_Ka: model.materials[i + 1].maps[MATERIAL_MAP_OCCLUSION].texture = LoadTextureFromImage(image); break; + case m3dp_map_Pm: model.materials[i + 1].maps[MATERIAL_MAP_ROUGHNESS].texture = LoadTextureFromImage(image); break; default: break; } } @@ -5291,12 +5350,130 @@ static Model LoadM3D(const char *fileName) } } + // Load bones + if(m3d->numbone) + { + model.boneCount = m3d->numbone; + model.bones = RL_MALLOC(m3d->numbone*sizeof(BoneInfo)); + model.bindPose = RL_MALLOC(m3d->numbone*sizeof(Transform)); + for (i = 0; i < m3d->numbone; i++) + { + model.bones[i].parent = m3d->bone[i].parent; + strncpy(model.bones[i].name, m3d->bone[i].name, sizeof(model.bones[i].name)); + model.bindPose[i].translation.x = m3d->vertex[m3d->bone[i].pos].x; + model.bindPose[i].translation.y = m3d->vertex[m3d->bone[i].pos].y; + model.bindPose[i].translation.z = m3d->vertex[m3d->bone[i].pos].z; + model.bindPose[i].rotation.x = m3d->vertex[m3d->bone[i].ori].x; + 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; + } + } + + // Load bone-pose default mesh into animation vertices. These will be updated when UpdateModelAnimation gets + // called, but not before, however DrawMesh uses these if they exists (so not good if they are left empty). + if (m3d->numbone && m3d->numskin) + { + for(i = 0; i < model.meshCount; i++) + { + memcpy(model.meshes[i].animVertices, model.meshes[i].vertices, model.meshes[i].vertexCount*3*sizeof(float)); + memcpy(model.meshes[i].animNormals, model.meshes[i].normals, model.meshes[i].vertexCount*3*sizeof(float)); + } + } + m3d_free(m3d); UnloadFileData(fileData); } return model; } + +// 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; + + *animCount = 0; + + if (fileData != NULL) + { + m3d = m3d_load(fileData, m3d_loaderhook, m3d_freehook, NULL); + + if (!m3d || M3D_ERR_ISFATAL(m3d->errcode)) + { + TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load M3D data, error code %d", fileName, m3d ? m3d->errcode : -2); + UnloadFileData(fileData); + return NULL; + } + 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? + if (!m3d->numaction || !m3d->numbone || !m3d->numskin) + { + m3d_free(m3d); + UnloadFileData(fileData); + return NULL; + } + + animations = RL_MALLOC(m3d->numaction*sizeof(ModelAnimation)); + *animCount = m3d->numaction; + + for (unsigned int a = 0; a < m3d->numaction; a++) + { + animations[a].frameCount = m3d->action[a].durationmsec / M3D_ANIMDELAY; + animations[a].boneCount = m3d->numbone; + animations[a].bones = RL_MALLOC(m3d->numbone*sizeof(BoneInfo)); + animations[a].framePoses = RL_MALLOC(animations[a].frameCount*sizeof(Transform *)); + // strncpy(animations[a].name, m3d->action[a].name, sizeof(animations[a].name)); + TRACELOG(LOG_INFO, "MODEL: [%s] animation #%i: %i msec, %i frames", fileName, a, m3d->action[a].durationmsec, animations[a].frameCount); + + for (i = 0; i < m3d->numbone; i++) + { + animations[a].bones[i].parent = m3d->bone[i].parent; + strncpy(animations[a].bones[i].name, m3d->bone[i].name, sizeof(animations[a].bones[i].name)); + } + + // M3D stores frames at arbitrary intervals with sparse skeletons. We need full skeletons at + // regular intervals, so let the M3D SDK do the heavy lifting and calculate interpolated bones + for (i = 0; i < animations[a].frameCount; i++) + { + animations[a].framePoses[i] = RL_MALLOC(m3d->numbone*sizeof(Transform)); + + m3db_t *pose = m3d_pose(m3d, a, i * M3D_ANIMDELAY); + if (pose != NULL) + { + for (j = 0; j < m3d->numbone; j++) + { + animations[a].framePoses[i][j].translation.x = m3d->vertex[pose[j].pos].x; + animations[a].framePoses[i][j].translation.y = m3d->vertex[pose[j].pos].y; + animations[a].framePoses[i][j].translation.z = m3d->vertex[pose[j].pos].z; + animations[a].framePoses[i][j].rotation.x = m3d->vertex[pose[j].ori].x; + animations[a].framePoses[i][j].rotation.y = m3d->vertex[pose[j].ori].y; + animations[a].framePoses[i][j].rotation.z = m3d->vertex[pose[j].ori].z; + animations[a].framePoses[i][j].rotation.w = m3d->vertex[pose[j].ori].w; + animations[a].framePoses[i][j].rotation = QuaternionNormalize(animations[a].framePoses[i][j].rotation); + animations[a].framePoses[i][j].scale.x = animations[a].framePoses[i][j].scale.y = animations[a].framePoses[i][j].scale.z = 1.0f; + } + RL_FREE(pose); + } + } + } + + m3d_free(m3d); + UnloadFileData(fileData); + } + + return animations; +} #endif #endif // SUPPORT_MODULE_RMODELS