Multimesh loading

Another question from Reddit: When importing OBJ files with multiple meshes, the bounding boxes of each mesh is the full OBJ mesh content..

The blog post lead to the fix of a bug in the OBJ loader, which is released with raylib 5.5. The OBJ loader code used in the following WASM examples is eventually no longer reproducing the issue described in this blog post.

Raylib multimesh imports are currently not so well supported in my experience.

Let's start with loading a GLTF file with multiple meshes and render it with bounding boxes for each mesh like in the Reddit question:

  • 💾
  1 #include <raylib.h>
  2 #include "preferred_size.h"
  3 #include <math.h>
  4 #include <raymath.h>
  5 
  6 void DrawTextBox(const char *text, int x, int y, int width, int height, float alignX, float alignY)
  7 {
  8   DrawRectangle(x, y, width, height, WHITE);
  9   DrawRectangleLinesEx((Rectangle){x, y, width, height}, 2.0f, BLACK);
 10   Font font = GetFontDefault();
 11   float fontSize = font.baseSize * 2;
 12   float spacing = 2;
 13   Vector2 textSize = MeasureTextEx(font, text, fontSize, spacing);
 14   float textX = x + (width - textSize.x) * alignX;
 15   float textY = y + (height - textSize.y) * alignY;
 16   Vector2 pos = {textX, textY};
 17   DrawTextEx(font, text, pos, fontSize, spacing, BLACK);
 18 }
 19 
 20 int main(void)
 21 {
 22   int screenWidth = 600, screenHeight = 350;
 23   GetPreferredSize(&screenWidth, &screenHeight);
 24   InitWindow(screenWidth, screenHeight, "Loading models with multiple meshes");
 25 
 26   Camera camera = {0};
 27   camera.position = (Vector3){8.0f, 7.0f, 5.0f};
 28   camera.target = (Vector3){0.0f, 1.0f, 0.0f};
 29   camera.up = (Vector3){0.0f, 1.0f, 0.0f};
 30   camera.fovy = 45.0f;
 31   camera.projection = CAMERA_PERSPECTIVE;
 32 
 33   Model model = LoadModel("data/quadset.glb");
 34 
 35   Vector3 position = {0.0f, 0.0f, 0.0f};
 36 
 37   SetTargetFPS(30);
 38 
 39   while (!WindowShouldClose())
 40   {
 41   if (IsPaused())
 42     {
 43       // canvas is not visible in browser - do nothing
 44       continue;
 45     }
 46     
 47     if (IsMouseButtonDown(MOUSE_LEFT_BUTTON))
 48       UpdateCamera(&camera, CAMERA_FIRST_PERSON);
 49 
 50     BeginDrawing();
 51     ClearBackground(GRAY);
 52     DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), SKYBLUE, LIGHTGRAY);
 53 
 54     BeginMode3D(camera);
 55     DrawModel(model, position, 1.0f, WHITE);
 56     for (int i = 0; i < model.meshCount; i++)
 57     {
 58       BoundingBox box = GetMeshBoundingBox(model.meshes[i]);
 59       DrawBoundingBox(box, RED);
 60     }
 61     DrawGrid(10, 1.0f);
 62     EndMode3D();
 63 
 64     DrawTextBox("GLTF loading multiple meshes", GetScreenWidth() / 2 - 180, 10, 360, 40, 0.5f, 0.5f);
 65     EndDrawing();
 66   }
 67 
 68   UnloadModel(model);
 69   CloseWindow();
 70 
 71   return 0;
 72 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

You can click with the mouse on the canvas and use WASD to move around.

The mesh loading works and the bounding boxes are correct. Let's check out OBJ loading next:

  • 💾
  1 #include <raylib.h>
  2 #include "preferred_size.h"
  3 #include <math.h>
  4 #include <raymath.h>
  5 
  6 void DrawTextBox(const char *text, int x, int y, int width, int height, float alignX, float alignY)
  7 {
  8   DrawRectangle(x, y, width, height, WHITE);
  9   DrawRectangleLinesEx((Rectangle){x, y, width, height}, 2.0f, BLACK);
 10   Font font = GetFontDefault();
 11   float fontSize = font.baseSize * 2;
 12   float spacing = 2;
 13   Vector2 textSize = MeasureTextEx(font, text, fontSize, spacing);
 14   float textX = x + (width - textSize.x) * alignX;
 15   float textY = y + (height - textSize.y) * alignY;
 16   Vector2 pos = {textX, textY};
 17   DrawTextEx(font, text, pos, fontSize, spacing, BLACK);
 18 }
 19 
 20 int main(void)
 21 {
 22   int screenWidth = 600, screenHeight = 350;
 23   GetPreferredSize(&screenWidth, &screenHeight);
 24   InitWindow(screenWidth, screenHeight, "Loading models with multiple meshes");
 25 
 26   Camera camera = {0};
 27   camera.position = (Vector3){8.0f, 7.0f, 5.0f};
 28   camera.target = (Vector3){0.0f, 1.0f, 0.0f};
 29   camera.up = (Vector3){0.0f, 1.0f, 0.0f};
 30   camera.fovy = 45.0f;
 31   camera.projection = CAMERA_PERSPECTIVE;
 32 
 33   Model model = LoadModel("data/quadset.obj");
 34   Texture2D texture = LoadTexture("data/palette.png");
 35   for (int i = 0; i < model.materialCount; i++)
 36   {
 37     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = texture;
 38   }
 39 
 40   Vector3 position = {0.0f, 0.0f, 0.0f};
 41 
 42   SetTargetFPS(30);
 43 
 44   while (!WindowShouldClose())
 45   {
 46   if (IsPaused())
 47     {
 48       // canvas is not visible in browser - do nothing
 49       continue;
 50     }
 51     
 52     if (IsMouseButtonDown(MOUSE_LEFT_BUTTON))
 53       UpdateCamera(&camera, CAMERA_FIRST_PERSON);
 54 
 55     BeginDrawing();
 56     ClearBackground(GRAY);
 57     DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), SKYBLUE, LIGHTGRAY);
 58 
 59     BeginMode3D(camera);
 60     DrawModel(model, position, 1.0f, WHITE);
 61     for (int i = 0; i < model.meshCount; i++)
 62     {
 63       BoundingBox box = GetMeshBoundingBox(model.meshes[i]);
 64       DrawBoundingBox(box, RED);
 65     }
 66     DrawGrid(10, 1.0f);
 67     EndMode3D();
 68 
 69     DrawTextBox("OBJ loading multiple meshes", GetScreenWidth() / 2 - 180, 10, 360, 40, 0.5f, 0.5f);
 70     EndDrawing();
 71   }
 72 
 73   UnloadModel(model);
 74   CloseWindow();
 75 
 76   return 0;
 77 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

The following picture shows how the bounding boxes looked like before the fix; the WASM above may be recompiled with the fixed code.

The original bug as described in the Reddit post

The objects look OK, but the bounding boxes are messy and look incorrect. Why is there are huge bounding box around the whole model?

Let's load both file alongside, add a toggle to swap the model, and add a mode to draw only selected bounding boxes and meshes. The button row on left can be hovered to draw only particular bounding boxes and meshes:

  • 💾
  1 #include <raylib.h>
  2 #include "preferred_size.h"
  3 #include <math.h>
  4 #include <raymath.h>
  5 #include <rlgl.h>
  6 
  7 int DrawTextBox(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color bg)
  8 {
  9   int mouseX = GetMouseX();
 10   int mouseY = GetMouseY();
 11   int isHovered = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
 12   
 13   DrawRectangle(x, y, width, height, isHovered ? SKYBLUE : bg);
 14   DrawRectangleLinesEx((Rectangle){x, y, width, height}, 2.0f, BLACK);
 15   Font font = GetFontDefault();
 16   float fontSize = font.baseSize * 2;
 17   float spacing = 2;
 18   Vector2 textSize = MeasureTextEx(font, text, fontSize, spacing);
 19   float textX = x + (width - textSize.x) * alignX;
 20   float textY = y + (height - textSize.y) * alignY;
 21   Vector2 pos = {textX, textY};
 22   DrawTextEx(font, text, pos, fontSize, spacing, BLACK);
 23   if (isHovered)
 24   {
 25     return IsMouseButtonPressed(MOUSE_LEFT_BUTTON) ? 1 : -1;
 26   }
 27   return 0;
 28 }
 29 
 30 int main(void)
 31 {
 32   int screenWidth = 600, screenHeight = 350;
 33   GetPreferredSize(&screenWidth, &screenHeight);
 34   InitWindow(screenWidth, screenHeight, "Loading models with multiple meshes");
 35 
 36   Camera camera = {0};
 37   camera.position = (Vector3){8.0f, 7.0f, 5.0f};
 38   camera.target = (Vector3){0.0f, 1.0f, 0.0f};
 39   camera.up = (Vector3){0.0f, 1.0f, 0.0f};
 40   camera.fovy = 45.0f;
 41   camera.projection = CAMERA_PERSPECTIVE;
 42 
 43   Model modelGLTF = LoadModel("data/quadset.glb");
 44   Model modelOBJ = LoadModel("data/quadset.obj");
 45   Texture2D texture = LoadTexture("data/palette.png");
 46   for (int i = 0; i < modelOBJ.materialCount; i++)
 47   {
 48     modelOBJ.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = texture;
 49   }
 50 
 51   Vector3 position = {0.0f, 0.0f, 0.0f};
 52 
 53   SetTargetFPS(30);
 54 
 55   int modelIndex = 0;
 56   Model model = modelGLTF;
 57   while (!WindowShouldClose())
 58   {
 59     if (IsPaused())
 60     {
 61       // canvas is not visible in browser - do nothing
 62       continue;
 63     }
 64 
 65     if (IsMouseButtonDown(MOUSE_LEFT_BUTTON))
 66       UpdateCamera(&camera, CAMERA_FIRST_PERSON);
 67 
 68     BeginDrawing();
 69     ClearBackground(GRAY);
 70     DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), SKYBLUE, LIGHTGRAY);
 71 
 72     if (DrawTextBox("GLTF", 10, 10, 100, 30, 0.5f, 0.5f, modelIndex == 0 ? YELLOW : WHITE) == 1)
 73     {
 74       model = modelGLTF;
 75       modelIndex = 0;
 76     }
 77 
 78     if (DrawTextBox("OBJ", 10, 40, 100, 30, 0.5f, 0.5f, modelIndex == 1 ? YELLOW : WHITE) == 1)
 79     {
 80       model = modelOBJ;
 81       modelIndex = 1;
 82     }
 83 
 84     int drawInfo = -1;
 85     for (int i = 0; i < model.meshCount; i++)
 86     {
 87       int y = 80 + i * 30;
 88       if (DrawTextBox(TextFormat("Mesh %d", i), 10, y, 100, 30, 0.5f, 0.5f, WHITE))
 89       {
 90         drawInfo = i;
 91         DrawTextBox(TextFormat("VertexCount: %d", model.meshes[i].vertexCount), 
 92           GetScreenWidth() - 210, GetScreenHeight() - 40, 200, 30, 0.5f, 0.5f, WHITE);
 93       }
 94     }
 95 
 96     BeginMode3D(camera);
 97     if (drawInfo < 0)
 98       DrawModel(model, position, 1.0f, WHITE);
 99 
100     for (int i = drawInfo >= 0 ? drawInfo : 0; i < model.meshCount; i++)
101     {
102       BoundingBox box = GetMeshBoundingBox(model.meshes[i]);
103       DrawBoundingBox(box, RED);
104       
105       if (drawInfo >= 0)
106       {
107         DrawMesh(model.meshes[i], model.materials[model.meshMaterial[i]], model.transform);
108         break;
109       }
110     }
111 
112     EndMode3D();
113 
114     DrawTextBox("Debugging OBJ / GLTF", GetScreenWidth() - 310, 10, 300, 40, 0.5f, 0.5f, WHITE);
115     EndDrawing();
116   }
117 
118   UnloadModel(modelGLTF);
119   UnloadModel(modelOBJ);
120   CloseWindow();
121 
122   return 0;
123 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

Exploring the data via the improvised UI reveals the issue:

Highlighting mesh 1, we can see that the mesh is fragmented

When loading the OBJ and GLTF files into Blender, the GLTF file shows also correct transforms - something the raylib mesh format doesn't support. The meshes of the OBJ import are also properly split, not like the raylib OBJ import.

The OBJ import in Blender lacks transform information; I believe the OBJ file does not support this:

Blender imported models showing that OBJ does not support transform exports

At this point it makes sense to check the raylib source code to see how both loaders work.

The source code comments are already telling the rest of the story of what we need to know:

  1 // Load OBJ mesh data
  2 //
  3 // Keep the following information in mind when reading this
  4 //  - A mesh is created for every material present in the obj file
  5 //  - the model.meshCount is therefore the materialCount returned from tinyobj
  6 //  - the mesh is automatically triangulated by tinyobj
  7 
  8 (...)
  9 
 10 // GLTF (excerpt):
 11 //  - Transforms, including parent-child relations, are applied on the mesh data, but the
 12 //    hierarchy is not kept (as it can't be represented).
 13 //  - Mesh instances in the glTF file (i.e. same mesh linked from multiple nodes)
 14 //    are turned into separate raylib Meshes.

That the transforms aren't supported is clear from the Model struct itself, but I have no idea why the OBJ loader is creating these messy meshes. The meshes I've exported use only a single material, so I would have expected that the OBJ loader would create a single mesh - but instead it still created the same amount of meshes.

Another downside of the GLTF loader is, that it doesn't support mesh instances - so even if the GLTF file has only 4 meshes and uses them hundreds of times, the loader will create hundreds of individual meshes - which makes sense when not supporting transforms, but it's pretty bad for performance and memory.

Conclusions

The reason why I investigated this is not only the question on reddit, but also because I have struggled with model loading in raylib as well; the current only viable solution is to export objects individually. This is often not a good solution because there can be hundreds of meshes we'd like to use - and exporting and importing them individually as files is quite a pain. On top of that, the raylib models don't have names or anything, so it's also not possible to identify meshes, which is another reason to use a single file.

The exporting and importing can be automated, so it can be dealt with, but the solution isn't optimal.

I believe this is something that's not easily to fixed; model loading is a complex topic, especially when it comes to animations. I am still considering to make an extension of the raylib model struct to support names and transforms and making this work for the GLTF loader. This would have a sensible scope and would probably solve a lot of problems already. A point to consider on top is that GLTF is not a game friendly format. It would be better to have a custom format that is optimized for fast mesh loading.

But I don't think I can afford looking into this in the near future.

Post scriptum

After pointing out the problem on the raylib discord, I decided to look into the issue. I am documenting the process and fix here for completeness.

This here is the complete function for converting the OBJ loader data structure into a raylib model, highlighting the 3 lines that are causing the issue:

  1 // Load OBJ mesh data
  2 //
  3 // Keep the following information in mind when reading this
  4 //  - A mesh is created for every material present in the obj file
  5 //  - the model.meshCount is therefore the materialCount returned from tinyobj
  6 //  - the mesh is automatically triangulated by tinyobj
  7 static Model LoadOBJ(const char *fileName)
  8 {
  9     tinyobj_attrib_t objAttributes = { 0 };
 10     tinyobj_shape_t* objShapes = NULL;
 11     unsigned int objShapeCount = 0;
 12 
 13     tinyobj_material_t* objMaterials = NULL;
 14     unsigned int objMaterialCount = 0;
 15 
 16     Model model = { 0 };
 17     model.transform = MatrixIdentity();
 18 
 19     char* fileText = LoadFileText(fileName);
 20 
 21     if (fileText == NULL)
 22     {
 23         TRACELOG(LOG_ERROR, "MODEL Unable to read obj file %s", fileName);
 24         return model;
 25     }
 26 
 27     char currentDir[1024] = { 0 };
 28     strcpy(currentDir, GetWorkingDirectory()); // Save current working directory
 29     const char* workingDir = GetDirectoryPath(fileName); // Switch to OBJ directory for material path correctness
 30     if (CHDIR(workingDir) != 0)
 31     {
 32         TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", workingDir);
 33     }
 34 
 35     unsigned int dataSize = (unsigned int)strlen(fileText);
 36 
 37     unsigned int flags = TINYOBJ_FLAG_TRIANGULATE;
 38     int ret = tinyobj_parse_obj(&objAttributes, &objShapes, &objShapeCount, &objMaterials, &objMaterialCount, fileText, dataSize, flags);
 39 
 40     if (ret != TINYOBJ_SUCCESS)
 41     {
 42         TRACELOG(LOG_ERROR, "MODEL Unable to read obj data %s", fileName);
 43         return model;
 44     }
 45 
 46     UnloadFileText(fileText);
 47 
 48     unsigned int faceVertIndex = 0;
 49     unsigned int nextShape = 1;
 50     int lastMaterial = -1;
 51     unsigned int meshIndex = 0;
 52 
 53     // count meshes
 54     unsigned int nextShapeEnd = objAttributes.num_face_num_verts;
 55 
 56     // see how many verts till the next shape
 57 
 58     if (objShapeCount > 1) nextShapeEnd = objShapes[nextShape].face_offset;
 59 
 60     // walk all the faces
 61     for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++)
 62     {
63 if (faceId >= nextShapeEnd)
64 { 65 // try to find the last vert in the next shape 66 nextShape++; 67 if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset; 68 else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces 69 meshIndex++; 70 } 71 else if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial) 72 { 73 meshIndex++;// if this is a new material, we need to allocate a new mesh 74 } 75 76 lastMaterial = objAttributes.material_ids[faceId]; 77 faceVertIndex += objAttributes.face_num_verts[faceId]; 78 } 79 80 // allocate the base meshes and materials 81 model.meshCount = meshIndex + 1; 82 model.meshes = (Mesh*)MemAlloc(sizeof(Mesh) * model.meshCount); 83 84 if (objMaterialCount > 0) 85 { 86 model.materialCount = objMaterialCount; 87 model.materials = (Material*)MemAlloc(sizeof(Material) * objMaterialCount); 88 } 89 else // we must allocate at least one material 90 { 91 model.materialCount = 1; 92 model.materials = (Material*)MemAlloc(sizeof(Material) * 1); 93 } 94 95 model.meshMaterial = (int*)MemAlloc(sizeof(int) * model.meshCount); 96 97 // see how many verts are in each mesh 98 unsigned int* localMeshVertexCounts = (unsigned int*)MemAlloc(sizeof(unsigned int) * model.meshCount); 99 100 faceVertIndex = 0; 101 nextShapeEnd = objAttributes.num_face_num_verts; 102 lastMaterial = -1; 103 meshIndex = 0; 104 unsigned int localMeshVertexCount = 0; 105 106 nextShape = 1; 107 if (objShapeCount > 1) 108 nextShapeEnd = objShapes[nextShape].face_offset; 109 110 // walk all the faces 111 for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++) 112 { 113 bool newMesh = false; // do we need a new mesh?
114 if (faceId >= nextShapeEnd)
115 { 116 // try to find the last vert in the next shape 117 nextShape++; 118 if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset; 119 else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces 120 121 newMesh = true; 122 } 123 else if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial) 124 { 125 newMesh = true; 126 } 127 128 lastMaterial = objAttributes.material_ids[faceId]; 129 130 if (newMesh) 131 { 132 localMeshVertexCounts[meshIndex] = localMeshVertexCount; 133 134 localMeshVertexCount = 0; 135 meshIndex++; 136 } 137 138 faceVertIndex += objAttributes.face_num_verts[faceId]; 139 localMeshVertexCount += objAttributes.face_num_verts[faceId]; 140 } 141 localMeshVertexCounts[meshIndex] = localMeshVertexCount; 142 143 for (int i = 0; i < model.meshCount; i++) 144 { 145 // allocate the buffers for each mesh 146 unsigned int vertexCount = localMeshVertexCounts[i]; 147 148 model.meshes[i].vertexCount = vertexCount; 149 model.meshes[i].triangleCount = vertexCount / 3; 150 151 model.meshes[i].vertices = (float*)MemAlloc(sizeof(float) * vertexCount * 3); 152 model.meshes[i].normals = (float*)MemAlloc(sizeof(float) * vertexCount * 3); 153 model.meshes[i].texcoords = (float*)MemAlloc(sizeof(float) * vertexCount * 2); 154 model.meshes[i].colors = (unsigned char*)MemAlloc(sizeof(unsigned char) * vertexCount * 4); 155 } 156 157 MemFree(localMeshVertexCounts); 158 localMeshVertexCounts = NULL; 159 160 // fill meshes 161 faceVertIndex = 0; 162 163 nextShapeEnd = objAttributes.num_face_num_verts; 164 165 // see how many verts till the next shape 166 nextShape = 1; 167 if (objShapeCount > 1) nextShapeEnd = objShapes[nextShape].face_offset; 168 lastMaterial = -1; 169 meshIndex = 0; 170 localMeshVertexCount = 0; 171 172 // walk all the faces 173 for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++) 174 { 175 bool newMesh = false; // do we need a new mesh?
176 if (faceId >= nextShapeEnd)
177 { 178 // try to find the last vert in the next shape 179 nextShape++; 180 if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset; 181 else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces 182 newMesh = true; 183 } 184 // if this is a new material, we need to allocate a new mesh 185 if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial) newMesh = true; 186 lastMaterial = objAttributes.material_ids[faceId]; 187 188 if (newMesh) 189 { 190 localMeshVertexCount = 0; 191 meshIndex++; 192 } 193 194 int matId = 0; 195 if (lastMaterial >= 0 && lastMaterial < (int)objMaterialCount) 196 matId = lastMaterial; 197 198 model.meshMaterial[meshIndex] = matId; 199 200 for (int f = 0; f < objAttributes.face_num_verts[faceId]; f++) 201 { 202 int vertIndex = objAttributes.faces[faceVertIndex].v_idx; 203 int normalIndex = objAttributes.faces[faceVertIndex].vn_idx; 204 int texcordIndex = objAttributes.faces[faceVertIndex].vt_idx; 205 206 for (int i = 0; i < 3; i++) 207 model.meshes[meshIndex].vertices[localMeshVertexCount * 3 + i] = objAttributes.vertices[vertIndex * 3 + i]; 208 209 for (int i = 0; i < 3; i++) 210 model.meshes[meshIndex].normals[localMeshVertexCount * 3 + i] = objAttributes.normals[normalIndex * 3 + i]; 211 212 for (int i = 0; i < 2; i++) 213 model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + i] = objAttributes.texcoords[texcordIndex * 2 + i]; 214 215 model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + 1] = 1.0f - model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + 1]; 216 217 for (int i = 0; i < 4; i++) 218 model.meshes[meshIndex].colors[localMeshVertexCount * 4 + i] = 255; 219 220 faceVertIndex++; 221 localMeshVertexCount++; 222 } 223 } 224 225 if (objMaterialCount > 0) ProcessMaterialsOBJ(model.materials, objMaterials, objMaterialCount); 226 else model.materials[0] = LoadMaterialDefault(); // Set default material for the mesh 227 228 tinyobj_attrib_free(&objAttributes); 229 tinyobj_shapes_free(objShapes, objShapeCount); 230 tinyobj_materials_free(objMaterials, objMaterialCount); 231 232 for (int i = 0; i < model.meshCount; i++) 233 UploadMesh(model.meshes + i, true); 234 235 // Restore current working directory 236 if (CHDIR(currentDir) != 0) 237 { 238 TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", currentDir); 239 } 240 241 return model; 242 }

It took me a few hours to figure out what is going on there:

I am happy that I could finally contribute something back to raylib - and I hope that the fix is actually correct 😅.

🍪