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 😅.