Multimesh loading
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 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:
- The OBJ meshes are a mess; triangles are scattered in each mesh. The bounding boxes are correct, it's simply that the mesh data is completely jumbled like a jigsaw puzzle; drawing all works, but the individual meshes make no sense
- The GLTF meshes have correct bounding boxes and mesh rendering
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:
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:
- The comparison (faceId >= nextShapeEnd) used to compare (faceVertIndex >= nextShapeEnd)
- The OBJ format uses separate data arrays for points, normals and uvs.
- A list of "faces" refer to an index in each of these arrays.
- A mesh vertex is a combination of these 3 arrays.
- Since faces can have different amount of vertices, the loops in the function determine the count of vertices per mesh by iterating the face array.
- During this iteration, it checks if the faceId is greater than the nextShapeEnd to determine if a new mesh should be started.
- The initial comparision used "faceVertIndex" instead of "faceId" which is the vertex index counter, while the nextShapeEnd integer is a "faceId" value.
- Due to this mixup, the mesh was split at the wrong index, terminating the mesh content too early and carrying the rest of the mesh data to the next mesh.
- Since for the last mesh the rest of the data was carried over, the last mesh would always fill the gaps, resulting in the huge bounding box and leading to a correct looking outcome when rendereed fully
I am happy that I could finally contribute something back to raylib - and I hope that the fix is actually correct 😅.