raysan5 / raylib

A simple and easy-to-use library to enjoy videogames programming
http://www.raylib.com
zlib License
22.55k stars 2.26k forks source link

[models] Support Model3D (.m3d) file format #2648

Closed bztsrc closed 2 years ago

bztsrc commented 2 years ago

Hi,

Could you please add Model 3D format to the supported models? It has an stb-style single header SDK written in ANSI C, looks simple to integrate.

I've already started the necessary work, see below (note this is just a minimal implementation, only loads meshes, but does not handle all the M3D features like skeletal animations etc.) Here are the steps I made:

  1. place m3d.h under src/external
  2. added #define SUPPORT_FILEFORMAT_M3D 1 to src/config.h
  3. modified src/rmodels.c, and added LoadM3D: rmodels.c.gz

But the diff is pretty small, here it is in its entirety for easier review:

diff --git a/src/config.h b/src/config.h
index ce7d9b04..a7ec2523 100644
--- a/src/config.h
+++ b/src/config.h
@@ -192,6 +192,7 @@
 #define SUPPORT_FILEFORMAT_IQM      1
 #define SUPPORT_FILEFORMAT_GLTF     1
 #define SUPPORT_FILEFORMAT_VOX      1
+#define SUPPORT_FILEFORMAT_M3D      1
 // Support procedural mesh generation functions, uses external par_shapes.h library
 // NOTE: Some generated meshes DO NOT include generated texture coordinates
 #define SUPPORT_MESH_GENERATION     1
diff --git a/src/rmodels.c b/src/rmodels.c
index ee8b2117..5361f1a5 100644
--- a/src/rmodels.c
+++ b/src/rmodels.c
@@ -12,6 +12,7 @@
 *   #define SUPPORT_FILEFORMAT_IQM
 *   #define SUPPORT_FILEFORMAT_GLTF
 *   #define SUPPORT_FILEFORMAT_VOX
+*   #define SUPPORT_FILEFORMAT_M3D
 *       Selected desired fileformats to be supported for model data loading.
 *
 *   #define SUPPORT_MESH_GENERATION
@@ -86,6 +87,17 @@
     #include "external/vox_loader.h"    // VOX file format loading (MagikaVoxel)
 #endif

+#if defined(SUPPORT_FILEFORMAT_M3D)
+    #define M3D_MALLOC RL_MALLOC
+    #define M3D_REALLOC RL_REALLOC
+    #define M3D_FREE RL_FREE
+    // let the M3D SDK know about stb_image is used in this project, so it'll use it too
+    #include "external/stb_image.h"
+
+    #define M3D_IMPLEMENTATION
+    #include "external/m3d.h"    // Model3D file format loading
+#endif
+
 #if defined(SUPPORT_MESH_GENERATION)
     #define PAR_MALLOC(T, N) ((T*)RL_MALLOC(N*sizeof(T)))
     #define PAR_CALLOC(T, N) ((T*)RL_CALLOC(N*sizeof(T), 1))
@@ -141,6 +153,9 @@ static Model LoadGLTF(const char *fileName);    // Load GLTF mesh data
 #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
+#endif

 //----------------------------------------------------------------------------------
 // Module Functions Definition
@@ -927,6 +942,9 @@ Model LoadModel(const char *fileName)
 #if defined(SUPPORT_FILEFORMAT_VOX)
     if (IsFileExtension(fileName, ".vox")) model = LoadVOX(fileName);
 #endif
+#if defined(SUPPORT_FILEFORMAT_M3D)
+    if (IsFileExtension(fileName, ".m3d")) model = LoadM3D(fileName);
+#endif

     // Make sure model transform is set to identity matrix!
     model.transform = MatrixIdentity();
@@ -5101,4 +5119,129 @@ static Model LoadVOX(const char *fileName)
 }
 #endif

+#if defined(SUPPORT_FILEFORMAT_M3D)
+// the LoadFileData / UnloadFileData API almost the same, but prototypes differ a bit
+unsigned char *m3d_loaderhook(char *fn, unsigned int *len) { return LoadFileData((const char*)fn, len); }
+void m3d_freehook(void *data) { UnloadFileData((unsigned char*)data); }
+// Load M3D mesh data
+static Model LoadM3D(const char *fileName)
+{
+    Model model = { 0 };
+    m3d_t *m3d;
+    m3dp_t *prop;
+    unsigned int bytesRead = 0;
+    unsigned char *fileData = LoadFileData(fileName, &bytesRead);
+    int i, j, k, l, mi = -2;
+
+    if (fileData != NULL)
+    {
+        m3d = m3d_load(fileData, m3d_loaderhook, m3d_freehook, NULL);
+
+        if(!m3d || m3d->errcode != M3D_SUCCESS) {
+            TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load M3D data", fileName);
+            return model;
+        } else {
+            TRACELOG(LOG_INFO, "MODEL: [%s] M3D data loaded successfully: %i faces/%i materials", fileName, m3d->numface, m3d->nummaterial);
+        }
+
+        if(m3d->nummaterial > 0) {
+            model.meshCount = model.materialCount = m3d->nummaterial;
+            TraceLog(LOG_INFO, "MODEL: model has %i material meshes", model.materialCount);
+        } else {
+            model.meshCount = model.materialCount = 1;
+            TraceLog(LOG_INFO, "MODEL: No materials, putting all meshes in a default material");
+        }
+
+        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));
+        /* we 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) {
+                k++;
+                mi = m3d->face[i].materialid;
+                for(j = i, l = 0; j < m3d->numface && mi == m3d->face[j].materialid; j++, l++);
+                model.meshes[k].vertexCount = l*3;
+                model.meshes[k].triangleCount = l;
+                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));
+                model.meshMaterial[k] = mi + 1;
+                l = 0;
+            }
+            /* now we have meshes per material, add triangles */
+            model.meshes[k].vertices[l * 9 + 0] = m3d->vertex[m3d->face[i].vertex[0]].x;
+            model.meshes[k].vertices[l * 9 + 1] = m3d->vertex[m3d->face[i].vertex[0]].y;
+            model.meshes[k].vertices[l * 9 + 2] = m3d->vertex[m3d->face[i].vertex[0]].z;
+            model.meshes[k].vertices[l * 9 + 3] = m3d->vertex[m3d->face[i].vertex[1]].x;
+            model.meshes[k].vertices[l * 9 + 4] = m3d->vertex[m3d->face[i].vertex[1]].y;
+            model.meshes[k].vertices[l * 9 + 5] = m3d->vertex[m3d->face[i].vertex[1]].z;
+            model.meshes[k].vertices[l * 9 + 6] = m3d->vertex[m3d->face[i].vertex[2]].x;
+            model.meshes[k].vertices[l * 9 + 7] = m3d->vertex[m3d->face[i].vertex[2]].y;
+            model.meshes[k].vertices[l * 9 + 8] = m3d->vertex[m3d->face[i].vertex[2]].z;
+            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 + 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 + 4] = m3d->tmap[m3d->face[i].texcoord[2]].u;
+                model.meshes[k].texcoords[l * 6 + 5] = 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;
+            }
+        }
+
+        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++) {
+                prop = &m3d->material[i].prop[j];
+                switch(prop->type) {
+                    case m3dp_Kd:
+                        memcpy(&model.materials[i + 1].maps[MATERIAL_MAP_DIFFUSE].color, &prop->value.color, 4);
+                        model.materials[i + 1].maps[MATERIAL_MAP_DIFFUSE].value = 0.0f;
+                    break;
+                    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_Ke:
+                        memcpy(&model.materials[i + 1].maps[MATERIAL_MAP_EMISSION].color, &prop->value.color, 4);
+                        model.materials[i + 1].maps[MATERIAL_MAP_EMISSION].value = 0.0f;
+                    break;
+                    case m3dp_Pm:
+                        model.materials[i + 1].maps[MATERIAL_MAP_METALNESS].value = prop->value.fnum;
+                    break;
+                    case m3dp_Pr:
+                        model.materials[i + 1].maps[MATERIAL_MAP_ROUGHNESS].value = prop->value.fnum;
+                    break;
+                    case m3dp_Ps:
+                        model.materials[i + 1].maps[MATERIAL_MAP_NORMAL].color = WHITE;
+                        model.materials[i + 1].maps[MATERIAL_MAP_NORMAL].value = prop->value.fnum;
+                    break;
+                }
+            }
+        }
+
+        m3d_free(m3d);
+        UnloadFileData(fileData);
+    }
+
+    return model;
+}
+#endif
+
 #endif      // SUPPORT_MODULE_RMODELS

NOTE: this just loads the mesh and some material properties, but does not provide full support (yet). This is just the first step.

Please let me know what you think.

Thanks, bzt

raysan5 commented 2 years ago

Hi @bztsrc! Your Model3D format looks great! I like it a lot! Congratulations!

Feel free to send a PR with this amazing addition! :D

bztsrc commented 2 years ago

Hi @bztsrc! Your Model3D format looks great! I like it a lot! Congratulations!

Thanks!

Feel free to send a PR with this amazing addition! :D

Sadly since the recent changes in github, I cannot create PRs any more (I have no probs on gitlab, so this must be a github configuration issue).

The Goxel2's maintainer likes this format too, and he has chosen it as Goxel2's main format. I have the same PR creation problems there too :-( (BTW, Model3D can store voxel images, but you won't notice that because on load voxels are transparently converted into a face-culled triangle mesh by the SDK).

Truly sorry about sending a diff, but this is the best I can do in lack of PRs.

Cheers, bzt

raysan5 commented 2 years ago

@bztsrc Ok, no worries, I will implement the changes you provided.

Are you planning to add animations support? It would be great!

bztsrc commented 2 years ago

Ok, no worries, I will implement the changes you provided.

Thank you very much! It's great if raylib can handle .m3d files out-of-the-box, because I'm getting to like this lib more and more. I've just recently started experimenting with it, but so far it's pretty awesome!

Are you planning to add animations support? It would be great!

Yes! I'm studying the IQM code now on how to fill up the variables with a skeleton based animation. But it's not simple, because IQM uses joints, while M3D is using a bone skeleton, with keyframes stored as incremental bone changes (each bone is technically a local coordinate system relative to its parent bone). If everything else fails, then I'll use m3d_pose to generate the skeleton for each frame and I'll load those, that will surely work.

Another issue I have to solve is the textures. As I see all the other loaders use the LoadTexture function which needs a filename as input, but M3D returns decoded width, height, texture pixel data, because it supports inlined and generated textures too. Doesn't seem to be a big problem, but I'll have to figure out how to integrate these, haven't got time to dig deeper what LoadTexture returns.

Cheers, bzt

raysan5 commented 2 years ago

raylib animation system was designed following most tipical conventions, actually it should be equivalent to a standard bone system, independent of the IQM file format.

First format supported with animations was IQM but actually, glTF animations was also partially supported in the past. Current structutes should be able to accomodate any skeletal bones-based system, as far as I know.

// Transform, vectex transformation data
typedef struct Transform {
    Vector3 translation;    // Translation
    Quaternion rotation;    // Rotation
    Vector3 scale;          // Scale
} Transform;

// Bone, skeletal animation bone
typedef struct BoneInfo {
    char name[32];          // Bone name
    int parent;             // Bone parent
} BoneInfo;

// Model, meshes, materials and animation data
typedef struct Model {
    Matrix transform;       // Local transform matrix

    int meshCount;          // Number of meshes
    int materialCount;      // Number of materials
    Mesh *meshes;           // Meshes array
    Material *materials;    // Materials array
    int *meshMaterial;      // Mesh material number

    // Animation data
    int boneCount;          // Number of bones
    BoneInfo *bones;        // Bones information (skeleton)
    Transform *bindPose;    // Bones base transformation (pose)
} Model;

// ModelAnimation
typedef struct ModelAnimation {
    int boneCount;          // Number of bones
    int frameCount;         // Number of animation frames
    BoneInfo *bones;        // Bones information (skeleton)
    Transform **framePoses; // Poses array by frame
} ModelAnimation;

BoneInfo keeps a reference of every bone to its parent bone and the skeleton is just defined as an array of bones. Every bone also defines a base transform that all together define a bindPose.

ModelAnimation is just a sequence of poses.

About the textures, raylib also has the Image structure that exposes width, height and pixel data directly. Texture2D just contains the OpenGL id reference for the uploaded Image in GPU. You can just fill the Image struct manually with retrieved pixel data and then LoadtextureFromImage() to upload that data to GPU and get the texture id.

bztsrc commented 2 years ago

Thank you for the information!

raylib animation system was designed following most tipical conventions

Looks like I can load the animations almost as-is. :-) One question remains, M3D files can store multiple animations (so it should return multiple ModelAnimation records). For example, there can be an animation for walking, one for attack, one for jumping etc.)

About the textures, raylib also has the Image structure

Thank you, in the meantime I've figured it out myself, here's an updated version with pretty useful material support. I've modified the examples/models/models_loading.c example a bit, to accept command line arguments, and added some .m3d example models too. It works great, but looks like models with vertex colors only (no material) needs some fixing, they are loaded as black.

model3d.zip

And if you don't mind, I've added Model3D attribution and link to raylib.h.

Cheers, bzt

bztsrc commented 2 years ago

One question remains, M3D files can store multiple animations

What I mean by that, to handle that, this prototype would need to change:

ModelAnimation *LoadModelAnimations(const char *fileName, unsigned int *animCount);

to

ModelAnimation *LoadModelAnimations(const char *fileName, const char *action, unsigned int *animCount);

or

ModelAnimation **LoadModelAnimations(const char *fileName, unsigned int *animCount);

but obviously these would break the raylib API, so I'm not sure how to proceed.

One solution could be to use animCount as input for the action id (and keep it for frame count as output), but that feels extremely hackish. (This is not a problem for IQM only because the mesh and animation are separated, so people can use different animation files. But here you might have multiple animations in a single file. Actions could be referenced by action id or by action name too.)

Any ideas? Maybe adding a new function?

Cheers, bzt

raysan5 commented 2 years ago

@bztsrc I'm afraid I don't understand the problem, current LoadModelAnimations() should be able to load all the included animations (walk, run, attack...), why action parameter is required? what is the action id?

Could it be that actions are actually the separate animations? As per my understanding, each action could be loaded into one ModelAnimation.

bztsrc commented 2 years ago

why action parameter is required? what is the action id?

The action parameter is the name of the animation, and action id is just the index. For example, a typical game model can have one animation for "walking", one for "attacking", one for "jumping", then you can use the action name to find the action id for that animation.

Could it be that actions are actually the separate animations?

Yes, exactly. The model has a bind pose (stored in the bones chunk), and then several animations (so called actions in M3D parlance), which are just keyframes with only the modified bones. For every animation, the first frame is a difference to the bind pose, and for the other subsequent frames it's just the difference to the previous frame. Simply put, M3D does not store the entire skeleton for each frame. This results in a very small file size, but complicates loading a bit. That's why I have m3d_pose(), which takes an action index and a frame index and returns the entire skeleton with all the bones, modified for that frame.

As per my understanding, each action could be loaded into one ModelAnimation.

Well yes. But how should the user know which one? LoadModelAnimations() requires a filename as input, so how should people know which animation is which? (As I have said, for other formats typically only one animation is stored in a separate file, so this isn't an issue because the filename is different).

Maybe adding a simple char name[32] to the ModelAnimation struct and letting the people iterate through it would be enough?

Or maybe I'm just overthinking, this isn't an issue at all, because people are using the same animations within a game for all models therefore it's okay to just rely on the action id (which is the index to the array returned by LoadModelAnimations())?

Anyway, for now I'll just implement this without action names, and please consider and tell me if you think adding char name[32] to ModelAnimation struct sounds reasonable to you.

Thanks for your answer and all the help you provided, bzt

raysan5 commented 2 years ago

@bztsrc Personally I would keep it simple and just skip the animations naming for now and just use the index to refer to any animation. User is responsible to define the animation index for their models on user code. Actually, same approach is planned for glTF models that can also contain multiple animations.

bztsrc commented 2 years ago

Personally I would keep it simple and just skip the animations naming for now and just use the index to refer to any animation. User is responsible to define the animation index for their models on user code.

Ok, understood! I was also assuming that I'm overthinking this.

Good news, here's a modified version! This loads now vertex colors correctly (and fallbacks to a clay-like color if there are no materials not vertex colors either), and I've implemented bones, skin and animations too. I've also added examples/models/models_loading_m3d.c, and two example models, suzanne.m3d (the well-known blender mascot) and a seagull.m3d (which is an animated seagull converted from Scorch3D's milkshape model).

model3d.tgz

Unfortunately it isn't finished just yet. It works great and plays most of the animated models I've tested with, but not all. Sometimes bones are off position, like the seagull's wings. And loading inlined textures isn't perfect yet either, have to debug that too. BTW, in what space does UpdateModelAnimation() expect the bone vertices? I'm providing them in model space, and not bone local space (these only are identical for the root bone, but not for the child bones), that might be the source of the positioning issue?

Anyway I wanted to send this to you, because it is major milestone on adding animated M3D models!

Cheers, bzt

bztsrc commented 2 years ago

Hi,

Here's the newest version:

model3d.tgz

Please merge this as soon as possible, because it contains memory leak fixes too.

  1. I did lots and lots of tests and found some potential memory leaks fuzzing with invalid model files. These has been fixed.
  2. Otherwise I've fixed the texture issue (I've modified the m3d.h too). Long story short, it can only rely on stb_image.h if it's implemented in the same file as m3d.h, because stb_image defines some stuff needed as static. I've checked that with ifdef guards, but as a result it silently compiled without an inlined texture decoder. Now it throws a warning if stb_image.h is included without implementation, and otherwise relies on its own inlined texture decoder.
  3. I've also fixed the UV coordinates, turns out you flip V, so I had to flip it too to counteract.
  4. I've changed the default color to WHITE, because the clay-like color messed with the texture, tinted it.
  5. I've checked and double-checked the bone and skin loader code, I'm sure it is correct.

As a result, this version supports static meshes 100% perfectly.

The one and only issue that remains is with the UpdateModelAnimation call. I'm now comparing that to my implementation here, trying to figure out what's the difference. So far both seems to expect the mesh in bind-pose in model-space, and they both convert that into bone-local space as a first step. Yet, for some reason the results are different.

Cheers, bzt

raysan5 commented 2 years ago

@bztsrc Hi! Excuse the late response! Just implemented the latest changes provided. Thanks!

Just note I'm not implementing the argv inputs for the models_loading example.

bztsrc commented 2 years ago

Hi! Excuse the late response! Just implemented the latest changes provided. Thanks!

No worries, and I'm the one saying thanks! :-)

Just note I'm not implementing the argv inputs for the models_loading example.

That's okay! I needed that because drag'n'drop is not working for me at all (and I'm more of a CLI guy, I don't have any resource-eating shiny eye-candy heavy file manager installed, just mc). It's perfectly fine if you not merge that part! (But if you don't mind me asking, I'm curious, any particular reason why to refuse CLI arguments? You don't have to use them if you don't want to, and I bet I'm not the only one who prefers them.)

But back on the topic, any idea on the animation issue? Any suggestion where should I look? Right now I'm dumping transformation matrices and comparing them, but it's going pretty slowly. My guess is, the problem originates from using model-space transformations multiple times, hence the "explosion". Or something like that along the line of space conversion, because the bones and the skin looks just fine.

Cheers, bzt

raysan5 commented 2 years ago

But if you don't mind me asking, I'm curious, any particular reason why to refuse CLI arguments? You don't have to use them if you don't want to, and I bet I'm not the only one who prefers them.

I try to keep examples as simple as possible and avoid the CLI arguments because when compiling examples for web they can't be used. But I like CLI a lot, actually all my tools have powerful CLI.

any idea on the animation issue? Any suggestion where should I look?

I'm afraid I don't know where it could be the issue, maybe on parent transform propagation between bones? Have you tried calculating a bind pose for one specific frame? You can try drawing the bones transformed with lines/boxes to see if they are correctly transformed...

bztsrc commented 2 years ago

avoid the CLI arguments because when compiling examples for web they can't be used

Ah, I see, make sense.

But I like CLI a lot, actually all my tools have powerful CLI.

:thumbsup:

Have you tried calculating a bind pose for one specific frame?

That's what I'm doing now dumping the transformation matrices and vertices along the way. (I'm doing the same thing in my viewer which works correctly and comparing the two outputs. But it's difficult and going slowly.)

You can try drawing the bones transformed with lines/boxes to see if they are correctly transformed

I'm over that, that was the very first thing I've checked and the bones look okay to me, that's why I'm stuck. (You can still see drawing the boxes for the bones in models_loading_m3d.c because I haven't removed that part.)

Thanks anyway, back to the boring matrix dumping debugging... :-( I hope I'll figure this one last part out soon. bzt

chriscamacho commented 2 years ago

I'd set aside the bank holiday weekend (at least in part!) to make a shader to replay animations (maybe with key frame morphing) but hit a fairly hard brick wall

https://gitlab.com/bztsrc/model3d/-/issues/3

Hopefully this is a quick fix for some change in blender...

bztsrc commented 2 years ago

Okay, now I'm totally and absolutely confused. In an effort to figure out where the problem is, I've modified the blender exporter like this:

  1. I've commented out all axis transformations in exporter (hence the model loaded with Z up, Y forward)
  2. I've commented out all bone space calculations in the exporter
  3. I've commented out all vertex calculations in the exporter

The exporter now saves the mesh and bone positions and quaternions as they are in blender, as-is.

The importer passes them to raylib's renderer as they are, no conversion nor transformation takes place there either (for the records, the only difference would be in how m3d_pose calculates the bone local space to model space transformation matrix, which matrix the LoadModelAnimationsM3D function does not use).

And guess what, this is what I got: cesiumman As you can see,

  1. the overall orientation is strange: if Z is up, then why is it upside down in raylib? Shouldn't it be rotated only 90 degrees (laying down on the floor)?
  2. the animation of head, spine and right leg are fine compared to each other, but not the arms and the left leg. The left leg in particular looks okay (has the expected axis alignment, just not in sync with the right leg's axis alignment)?

How is this even possible? Either all child bones should be misaligned, or neither of them, but how is it possible that just some bones have wrong axis alignment? Makes totally no sense to me.

I'm not sure if this is an issue in blender (do I have to use some "to model space" transformations explicitly?) or in the raylib's renderer (assuming the data is correct). Any help figuring this out would very much appreciated. Here's the model in question (warning, child bones are not parent local in this one!): cesium1.m3d.gz and the original GLTF I imported into blender: CesiumMan.glb.gz (the blender GLTF importer is buggy, it won't load the textures from the glb file, but that's a different story. Concentrate on the geometry for now.)

Cheers, bzt

chriscamacho commented 2 years ago

this is fantastic work, can't wait for a complete art path with bone animation, I have some interesting ideas for it !

bztsrc commented 2 years ago

Okay, here's an update. This has almost everything fixed, and bones are working properly, displayed consistently in models_loading_m3d.c as well as in m3dview. Now only the skin issue remains, which turns out was responsible for the right leg / left leg alignment problem (still exists, but since the model is properly oriented, now it appears on a different axis).

model3d.tgz

@raysan5 please update. Minor modification in src/rmodels.c (forgot to handle child bones properly), otherwise mostly changes under the examples directory. I've added a screenshot and two example models (Suzanne from blender and an animated seagull from Scorched3D. Latter being 11k in size, which includes the texture too :-D). Still not the final version, but I think an important step.

this is fantastic work, can't wait for a complete art path with bone animation, I have some interesting ideas for it !

@chriscamacho thank you very much! Hopefully you don't have to wait for much longer, although I have other projects running unfortunately. FYI, I've updated the Blender exporter too, CesiumMan's twisted animated armature is now exported correctly.

Cheers, bzt

raysan5 commented 2 years ago

@bztsrc Hi! Nice to see the implementation is taking shape! Congratulations on your progress!

I just implemented the latest changes you send!

bztsrc commented 2 years ago

And... wait for it... (drumroll)... TADAM!

cesiumman

After many unsuccessful hours of debugging, I realized there's nothing wrong with the skinning code, the data was wrong all along! The skin issue was entirely a blender API fault in the exporter.

So, here is a last update, this is the final version! All known bugs have been accounted for and fixed.

model3d.tgz

Changes:

That's all! Thank you very much for all the help you gave, it was a pleasure working with you! Once these changes are merged, you can safely close this issue!

Cheers, bzt

ps: next I'm planning to add Scalable Screen Font support to raylib for my own use (I personally dislike ttf big time, I prefer this single-header lib and its much compact font format). Knowing that raylib already has font support, would you be interested in an SSFN patch? No hard feelings if you say no.

raysan5 commented 2 years ago

@bztsrc AMAZING! Congratulations! This is a great addition to raylib! Animations support was highly demanded by the raylib community, IQM had many issues and glTF animations were not properly supported... Now we have a great file format with animations support! Thank you very much Zoltan! :D

Just reviewed the code and the example and merged it, now I'm officially announcing it to the community!

next I'm planning to add Scalable Screen Font support to raylib for my own use (I personally dislike ttf big time, I prefer this single-header lib and its much compact font format). Knowing that raylib already has font support, would you be interested in an SSFN patch? No hard feelings if you say no.

This is very interesting! raylib currently uses stb_truetype for TTF fonts rasterization and atlas is generated internally, how does it compare to SSFN??? I'm interested, feel free to open a new issue/discussion!

GuvaCode commented 2 years ago

And... wait for it... (drumroll)... TADAM!

yes, everything works as it should with your model. I tried to take another one and this is what comes out. blender plugin from your master branch

swat

Peter0x44 commented 2 years ago

First, thanks for the great work here! I'm sure it took a long time, this is something very nice raylib has been missing.

I noticed for some strange reasons the example seems to be trying to open the files ".png" and "" This doesn't look right to me, placing a breakpoint on LoadFileData shows this:

Thread 1 "models_loading_" hit Breakpoint 1, LoadFileData (fileName=0x55555653fd40 ".png", bytesRead=0x7fffffffdad0) at utils.c:185
185         unsigned char *data = NULL;

This call with that data seems to originate from _m3d_gettx, as this backtrace shows

#1  0x0000555555650f2e in m3d_loaderhook (fn=0x55555653fd40 ".png", len=0x7fffffffdad0) at rmodels.c:5127
#2  0x000055555561ad73 in _m3d_gettx (model=0x555556533170, readfilecb=0x555555650f0b <m3d_loaderhook>, freecb=0x555555650f30 <m3d_freehook>, fn=0x55555656a9d0 "") at external/m3d.h:2196

The same thing is also done for "". I can't see a possible reason for m3d.h to be doing this, maybe it needs some review?

Thread 1 "models_loading_" hit Breakpoint 1, LoadFileData (fileName=0x55555656a9d0 "", bytesRead=0x7fffffffdad0) at utils.c:185
185         unsigned char *data = NULL;
(gdb) bt
#0  LoadFileData (fileName=0x55555656a9d0 "", bytesRead=0x7fffffffdad0) at utils.c:185
#1  0x0000555555650f2e in m3d_loaderhook (fn=0x55555656a9d0 "", len=0x7fffffffdad0) at rmodels.c:5127
#2  0x000055555561adb0 in _m3d_gettx (model=0x555556533170, readfilecb=0x555555650f0b <m3d_loaderhook>, freecb=0x555555650f30 <m3d_freehook>, fn=0x55555656a9d0 "") at external/m3d.h:2200

here's a backtrace for the empty string case too, if it helps.

bztsrc commented 2 years ago

Thank you!

I tried to take another one and this is what comes out. blender plugin from your master branch

@GuvaCode: Can you please open an issue in my repo and attach the original model file? I'll look into it. Looks like the skeleton is fine, most of the mesh is fine, just one vertex per bone goes off? Interesting, I would definitely need the original model to debug this.

I noticed for some strange reasons the example seems to be trying to open the files ".png" and ""

@Peter0x44: again, please open an issue in my repo and attach the original model file. It looks like your model has no inlined textures, and that's why _m3d_gettx is trying to load it from a separate file. So far so good, the issue seems to be there's something wrong with getting the texture's name. I would like to take a look at the original model what texture file names is it using. There should be no textures without a name in an M3D file.

This is very interesting! raylib currently uses stb_truetype for TTF fonts rasterization and atlas is generated internally, how does it compare to SSFN??? I'm interested, feel free to open a new issue/discussion!

@raysan5: okay, I try to keep it brief, sorry if it's going to be long. TL;DR font atlas is not really viable for rendering ligatures, combined diacriticals and other tricky nuances of the waste UNICODE codeplane.

The problem with atlas is, that it works for English language which has only 26 letters, but not so much for UNICODE, because there are simply way too many codepoints (1114112 times font width times font height times 4 bytes) and sometimes you can't choose one glyph, you have to compose from multiple, and other times the same character can have multiple glyphs... Not to mention what happens if a certain glyph is missing and you have to look it up from another font file. Therefore SSFN does not use an atlas, instead it has an efficient internal cache (only for the glyphs which was already rendered at least once, and with only 1 byte per pixel), and composes that cache to an RGBA pixel buffer directly. But truth to be told, even without this glyph cache the SSFN renderer is blazing fast. (It took only 0.15 secs to draw the entire feature demo you see in the README (with more than a dozen vector/bitmaps/pixmaps fonts, including loading, decompressing, decoding, and generating dynamic styling for missing glyphs etc. rasterizing every time in lack of a cache) on my 10 years old and slow machine. I imagine on a brand new computer that would easily take no more than 0.075 secs.

So instead of generating an atlas texture, the API I'm planning to implement would draw an UTF-8 string to an existing texture, which then could be used to display the entire text (not just one letter) as many times as you want. (Currently a similar API exists ssfn_text, but that returns a new pixel buffer, because it's a drop-in-replacement for SDL_ttf's TTF_RenderUTF8_Blended(). I'd prefer drawing strings to existing raylib textures.)

And furthermore, while stb_truetype is a great lib, it can't be used to render pixmap fonts, so people have to implement their own pixel fonts all the time. Also TTF files are huge and ineffective in the first place. For example, an OpenType TTF, FreeSerif.otf is 2047k, exactly the same typeface encoded in SSFN is just 595k. Similar with TrueType TTF, for example UbuntuBold.ttf is 333k, the same typeface in SSFN is just 40k.

Cheers, bzt

GuvaCode commented 2 years ago

@GuvaCode: Can you please open an issue in my repo and attach the original model file? I'll look into it. Looks like the skeleton is fine, most of the mesh is fine, just one vertex per bone goes off? Interesting, I would definitely need the original model to debug this.

Good. I'll do it in a few hours.

Peter0x44 commented 2 years ago

@bztsrc the file I am using is simply the one included in the repo, at examples/models/resources/models/m3d/CesiumMan.m3d. I won't have time today, but I can file an issue in the repo tomorrow.