Skip to content

Commit 91a9888

Browse files
authored
[rModels] Correctly split obj meshes by material (#4285)
* Correctly split meshes from tinyobj by material so they can be represented by raylib correctly * PR Feedback
1 parent f5ef357 commit 91a9888

File tree

1 file changed

+200
-92
lines changed

1 file changed

+200
-92
lines changed

src/rmodels.c

Lines changed: 200 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,8 @@ static void ProcessMaterialsOBJ(Material *materials, tinyobj_material_t *mats, i
20162016
// NOTE: Uses default shader, which only supports MATERIAL_MAP_DIFFUSE
20172017
materials[m] = LoadMaterialDefault();
20182018

2019+
if (mats == NULL) continue;
2020+
20192021
// Get default texture, in case no texture is defined
20202022
// NOTE: rlgl default texture is a 1x1 pixel UNCOMPRESSED_R8G8B8A8
20212023
materials[m].maps[MATERIAL_MAP_DIFFUSE].texture = (Texture2D){ rlGetTextureIdDefault(), 1, 1, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 };
@@ -4073,132 +4075,238 @@ static void BuildPoseFromParentJoints(BoneInfo *bones, int boneCount, Transform
40734075
// - the mesh is automatically triangulated by tinyobj
40744076
static Model LoadOBJ(const char *fileName)
40754077
{
4078+
tinyobj_attrib_t objAttributes = { 0 };
4079+
tinyobj_shape_t* objShapes = NULL;
4080+
unsigned int objShapeCount = 0;
4081+
4082+
tinyobj_material_t* objMaterials = NULL;
4083+
unsigned int objMaterialCount = 0;
4084+
40764085
Model model = { 0 };
4086+
model.transform = MatrixIdentity();
40774087

4078-
tinyobj_attrib_t attrib = { 0 };
4079-
tinyobj_shape_t *meshes = NULL;
4080-
unsigned int meshCount = 0;
4088+
char* fileText = LoadFileText(fileName);
40814089

4082-
tinyobj_material_t *materials = NULL;
4083-
unsigned int materialCount = 0;
4090+
if (fileText == NULL)
4091+
{
4092+
TRACELOG(LOG_ERROR, "MODEL Unable to read obj file %s", fileName);
4093+
return model;
4094+
}
40844095

4085-
char *fileText = LoadFileText(fileName);
4096+
char currentDir[1024] = { 0 };
4097+
strcpy(currentDir, GetWorkingDirectory()); // Save current working directory
4098+
const char* workingDir = GetDirectoryPath(fileName); // Switch to OBJ directory for material path correctness
4099+
if (CHDIR(workingDir) != 0)
4100+
{
4101+
TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", workingDir);
4102+
}
40864103

4087-
if (fileText != NULL)
4104+
unsigned int dataSize = (unsigned int)strlen(fileText);
4105+
4106+
unsigned int flags = TINYOBJ_FLAG_TRIANGULATE;
4107+
int ret = tinyobj_parse_obj(&objAttributes, &objShapes, &objShapeCount, &objMaterials, &objMaterialCount, fileText, dataSize, flags);
4108+
4109+
if (ret != TINYOBJ_SUCCESS)
40884110
{
4089-
unsigned int dataSize = (unsigned int)strlen(fileText);
4111+
TRACELOG(LOG_ERROR, "MODEL Unable to read obj data %s", fileName);
4112+
return model;
4113+
}
4114+
4115+
UnloadFileText(fileText);
4116+
4117+
unsigned int faceVertIndex = 0;
4118+
unsigned int nextShape = 1;
4119+
int lastMaterial = -1;
4120+
unsigned int meshIndex = 0;
4121+
4122+
// count meshes
4123+
unsigned int nextShapeEnd = objAttributes.num_face_num_verts;
4124+
4125+
// see how many verts till the next shape
40904126

4091-
char currentDir[1024] = { 0 };
4092-
strcpy(currentDir, GetWorkingDirectory()); // Save current working directory
4093-
const char *workingDir = GetDirectoryPath(fileName); // Switch to OBJ directory for material path correctness
4094-
if (CHDIR(workingDir) != 0)
4127+
if (objShapeCount > 1) nextShapeEnd = objShapes[nextShape].face_offset;
4128+
4129+
// walk all the faces
4130+
for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++)
4131+
{
4132+
if (faceVertIndex >= nextShapeEnd)
40954133
{
4096-
TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", workingDir);
4134+
// try to find the last vert in the next shape
4135+
nextShape++;
4136+
if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset;
4137+
else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces
4138+
meshIndex++;
4139+
}
4140+
else if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial)
4141+
{
4142+
meshIndex++;// if this is a new material, we need to allocate a new mesh
40974143
}
40984144

4099-
unsigned int flags = TINYOBJ_FLAG_TRIANGULATE;
4100-
int ret = tinyobj_parse_obj(&attrib, &meshes, &meshCount, &materials, &materialCount, fileText, dataSize, flags);
4145+
lastMaterial = objAttributes.material_ids[faceId];
4146+
faceVertIndex += objAttributes.face_num_verts[faceId];
4147+
}
41014148

4102-
if (ret != TINYOBJ_SUCCESS) TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to load OBJ data", fileName);
4103-
else TRACELOG(LOG_INFO, "MODEL: [%s] OBJ data loaded successfully: %i meshes/%i materials", fileName, meshCount, materialCount);
4149+
// allocate the base meshes and materials
4150+
model.meshCount = meshIndex + 1;
4151+
model.meshes = (Mesh*)MemAlloc(sizeof(Mesh) * model.meshCount);
41044152

4105-
// WARNING: We are not splitting meshes by materials (previous implementation)
4106-
// Depending on the provided OBJ that was not the best option and it just crashed
4107-
// so, implementation was simplified to prioritize parsed meshes
4108-
model.meshCount = meshCount;
4153+
if (objMaterialCount > 0)
4154+
{
4155+
model.materialCount = objMaterialCount;
4156+
model.materials = (Material*)MemAlloc(sizeof(Material) * objMaterialCount);
4157+
}
4158+
else // we must allocate at least one material
4159+
{
4160+
model.materialCount = 1;
4161+
model.materials = (Material*)MemAlloc(sizeof(Material) * 1);
4162+
}
4163+
4164+
model.meshMaterial = (int*)MemAlloc(sizeof(int) * model.meshCount);
4165+
4166+
// see how many verts are in each mesh
4167+
unsigned int* localMeshVertexCounts = (unsigned int*)MemAlloc(sizeof(unsigned int) * model.meshCount);
4168+
4169+
faceVertIndex = 0;
4170+
nextShapeEnd = objAttributes.num_face_num_verts;
4171+
lastMaterial = -1;
4172+
meshIndex = 0;
4173+
unsigned int localMeshVertexCount = 0;
41094174

4110-
// Set number of materials available
4111-
// NOTE: There could be more materials available than meshes but it will be resolved at
4112-
// model.meshMaterial, just assigning the right material to corresponding mesh
4113-
model.materialCount = materialCount;
4114-
if (model.materialCount == 0)
4175+
nextShape = 1;
4176+
if (objShapeCount > 1)
4177+
nextShapeEnd = objShapes[nextShape].face_offset;
4178+
4179+
// walk all the faces
4180+
for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++)
4181+
{
4182+
bool newMesh = false; // do we need a new mesh?
4183+
if (faceVertIndex >= nextShapeEnd)
41154184
{
4116-
model.materialCount = 1;
4117-
TRACELOG(LOG_INFO, "MODEL: No materials provided, setting one default material for all meshes");
4185+
// try to find the last vert in the next shape
4186+
nextShape++;
4187+
if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset;
4188+
else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces
4189+
4190+
newMesh = true;
41184191
}
4119-
else if (model.materialCount > 1 && model.meshCount > 1)
4192+
else if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial)
41204193
{
4121-
// TEMP warning about multiple materials, to be removed when proper splitting code is implemented
4122-
// any obj with multiple materials will need to have it's materials assigned by the user in code to work at this time
4123-
TRACELOG(LOG_INFO, "MODEL: OBJ has multiple materials, manual material assignment will be required.");
4194+
newMesh = true;
41244195
}
41254196

4126-
// Init model meshes and materials
4127-
model.meshes = (Mesh *)RL_CALLOC(model.meshCount, sizeof(Mesh));
4128-
model.meshMaterial = (int *)RL_CALLOC(model.meshCount, sizeof(int)); // Material index assigned to each mesh
4129-
model.materials = (Material *)RL_CALLOC(model.materialCount, sizeof(Material));
4197+
lastMaterial = objAttributes.material_ids[faceId];
41304198

4131-
// Process each provided mesh
4132-
for (int i = 0; i < model.meshCount; i++)
4199+
if (newMesh)
41334200
{
4134-
// WARNING: We need to calculate the mesh triangles manually using meshes[i].face_offset
4135-
// because in case of triangulated quads, meshes[i].length actually report quads,
4136-
// despite the triangulation that is efectively considered on attrib.num_faces
4137-
unsigned int tris = 0;
4138-
if (i == model.meshCount - 1) tris = attrib.num_faces - meshes[i].face_offset;
4139-
else tris = meshes[i + 1].face_offset;
4140-
4141-
model.meshes[i].vertexCount = tris*3;
4142-
model.meshes[i].triangleCount = tris; // Face count (triangulated)
4143-
model.meshes[i].vertices = (float *)RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float));
4144-
model.meshes[i].texcoords = (float *)RL_CALLOC(model.meshes[i].vertexCount*2, sizeof(float));
4145-
model.meshes[i].normals = (float *)RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float));
4146-
model.meshMaterial[i] = 0; // By default, assign material 0 to each mesh
4147-
4148-
// Process all mesh faces
4149-
for (unsigned int face = 0, f = meshes[i].face_offset, v = 0, vt = 0, vn = 0; face < tris; face++, f++, v += 3, vt += 3, vn += 3)
4150-
{
4151-
// Get indices for the face
4152-
tinyobj_vertex_index_t idx0 = attrib.faces[f*3 + 0];
4153-
tinyobj_vertex_index_t idx1 = attrib.faces[f*3 + 1];
4154-
tinyobj_vertex_index_t idx2 = attrib.faces[f*3 + 2];
4201+
localMeshVertexCounts[meshIndex] = localMeshVertexCount;
41554202

4156-
// Fill vertices buffer (float) using vertex index of the face
4157-
for (int n = 0; n < 3; n++) { model.meshes[i].vertices[v*3 + n] = attrib.vertices[idx0.v_idx*3 + n]; }
4158-
for (int n = 0; n < 3; n++) { model.meshes[i].vertices[(v + 1)*3 + n] = attrib.vertices[idx1.v_idx*3 + n]; }
4159-
for (int n = 0; n < 3; n++) { model.meshes[i].vertices[(v + 2)*3 + n] = attrib.vertices[idx2.v_idx*3 + n]; }
4203+
localMeshVertexCount = 0;
4204+
meshIndex++;
4205+
}
41604206

4161-
if (attrib.num_texcoords > 0)
4162-
{
4163-
// Fill texcoords buffer (float) using vertex index of the face
4164-
// NOTE: Y-coordinate must be flipped upside-down
4165-
model.meshes[i].texcoords[vt*2 + 0] = attrib.texcoords[idx0.vt_idx*2 + 0];
4166-
model.meshes[i].texcoords[vt*2 + 1] = 1.0f - attrib.texcoords[idx0.vt_idx*2 + 1];
4207+
faceVertIndex += objAttributes.face_num_verts[faceId];
4208+
localMeshVertexCount += objAttributes.face_num_verts[faceId];
4209+
}
4210+
localMeshVertexCounts[meshIndex] = localMeshVertexCount;
41674211

4168-
model.meshes[i].texcoords[(vt + 1)*2 + 0] = attrib.texcoords[idx1.vt_idx*2 + 0];
4169-
model.meshes[i].texcoords[(vt + 1)*2 + 1] = 1.0f - attrib.texcoords[idx1.vt_idx*2 + 1];
4212+
for (int i = 0; i < model.meshCount; i++)
4213+
{
4214+
// allocate the buffers for each mesh
4215+
unsigned int vertexCount = localMeshVertexCounts[i];
41704216

4171-
model.meshes[i].texcoords[(vt + 2)*2 + 0] = attrib.texcoords[idx2.vt_idx*2 + 0];
4172-
model.meshes[i].texcoords[(vt + 2)*2 + 1] = 1.0f - attrib.texcoords[idx2.vt_idx*2 + 1];
4173-
}
4217+
model.meshes[i].vertexCount = vertexCount;
4218+
model.meshes[i].triangleCount = vertexCount / 3;
41744219

4175-
if (attrib.num_normals > 0)
4176-
{
4177-
// Fill normals buffer (float) using vertex index of the face
4178-
for (int n = 0; n < 3; n++) { model.meshes[i].normals[vn*3 + n] = attrib.normals[idx0.vn_idx*3 + n]; }
4179-
for (int n = 0; n < 3; n++) { model.meshes[i].normals[(vn + 1)*3 + n] = attrib.normals[idx1.vn_idx*3 + n]; }
4180-
for (int n = 0; n < 3; n++) { model.meshes[i].normals[(vn + 2)*3 + n] = attrib.normals[idx2.vn_idx*3 + n]; }
4181-
}
4182-
}
4220+
model.meshes[i].vertices = (float*)MemAlloc(sizeof(float) * vertexCount * 3);
4221+
model.meshes[i].normals = (float*)MemAlloc(sizeof(float) * vertexCount * 3);
4222+
model.meshes[i].texcoords = (float*)MemAlloc(sizeof(float) * vertexCount * 2);
4223+
model.meshes[i].colors = (unsigned char*)MemAlloc(sizeof(unsigned char) * vertexCount * 4);
4224+
}
4225+
4226+
MemFree(localMeshVertexCounts);
4227+
localMeshVertexCounts = NULL;
4228+
4229+
// fill meshes
4230+
faceVertIndex = 0;
4231+
4232+
nextShapeEnd = objAttributes.num_face_num_verts;
4233+
4234+
// see how many verts till the next shape
4235+
nextShape = 1;
4236+
if (objShapeCount > 1) nextShapeEnd = objShapes[nextShape].face_offset;
4237+
lastMaterial = -1;
4238+
meshIndex = 0;
4239+
localMeshVertexCount = 0;
4240+
4241+
// walk all the faces
4242+
for (unsigned int faceId = 0; faceId < objAttributes.num_faces; faceId++)
4243+
{
4244+
bool newMesh = false; // do we need a new mesh?
4245+
if (faceVertIndex >= nextShapeEnd)
4246+
{
4247+
// try to find the last vert in the next shape
4248+
nextShape++;
4249+
if (nextShape < objShapeCount) nextShapeEnd = objShapes[nextShape].face_offset;
4250+
else nextShapeEnd = objAttributes.num_face_num_verts; // this is actually the total number of face verts in the file, not faces
4251+
newMesh = true;
41834252
}
4253+
// if this is a new material, we need to allocate a new mesh
4254+
if (lastMaterial != -1 && objAttributes.material_ids[faceId] != lastMaterial) newMesh = true;
4255+
lastMaterial = objAttributes.material_ids[faceId];;
41844256

4185-
// Init model materials
4186-
if (materialCount > 0) ProcessMaterialsOBJ(model.materials, materials, materialCount);
4187-
else model.materials[0] = LoadMaterialDefault(); // Set default material for the mesh
4257+
if (newMesh)
4258+
{
4259+
localMeshVertexCount = 0;
4260+
meshIndex++;
4261+
}
41884262

4189-
tinyobj_attrib_free(&attrib);
4190-
tinyobj_shapes_free(meshes, model.meshCount);
4191-
tinyobj_materials_free(materials, materialCount);
4263+
int matId = 0;
4264+
if (lastMaterial >= 0 && lastMaterial < (int)objMaterialCount)
4265+
matId = lastMaterial;
41924266

4193-
UnloadFileText(fileText);
4267+
model.meshMaterial[meshIndex] = matId;
41944268

4195-
// Restore current working directory
4196-
if (CHDIR(currentDir) != 0)
4269+
for (int f = 0; f < objAttributes.face_num_verts[faceId]; f++)
41974270
{
4198-
TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", currentDir);
4271+
int vertIndex = objAttributes.faces[faceVertIndex].v_idx;
4272+
int normalIndex = objAttributes.faces[faceVertIndex].vn_idx;
4273+
int texcordIndex = objAttributes.faces[faceVertIndex].vt_idx;
4274+
4275+
for (int i = 0; i < 3; i++)
4276+
model.meshes[meshIndex].vertices[localMeshVertexCount * 3 + i] = objAttributes.vertices[vertIndex * 3 + i];
4277+
4278+
for (int i = 0; i < 3; i++)
4279+
model.meshes[meshIndex].normals[localMeshVertexCount * 3 + i] = objAttributes.normals[normalIndex * 3 + i];
4280+
4281+
for (int i = 0; i < 2; i++)
4282+
model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + i] = objAttributes.texcoords[texcordIndex * 2 + i];
4283+
4284+
model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + 1] = 1.0f - model.meshes[meshIndex].texcoords[localMeshVertexCount * 2 + 1];
4285+
4286+
for (int i = 0; i < 4; i++)
4287+
model.meshes[meshIndex].colors[localMeshVertexCount * 4 + i] = 255;
4288+
4289+
faceVertIndex++;
4290+
localMeshVertexCount++;
41994291
}
42004292
}
42014293

4294+
if (objMaterialCount > 0) ProcessMaterialsOBJ(model.materials, objMaterials, objMaterialCount);
4295+
else model.materials[0] = LoadMaterialDefault(); // Set default material for the mesh
4296+
4297+
tinyobj_attrib_free(&objAttributes);
4298+
tinyobj_shapes_free(objShapes, objShapeCount);
4299+
tinyobj_materials_free(objMaterials, objMaterialCount);
4300+
4301+
for (int i = 0; i < model.meshCount; i++)
4302+
UploadMesh(model.meshes + i, true);
4303+
4304+
// Restore current working directory
4305+
if (CHDIR(currentDir) != 0)
4306+
{
4307+
TRACELOG(LOG_WARNING, "MODEL: [%s] Failed to change working directory", currentDir);
4308+
}
4309+
42024310
return model;
42034311
}
42044312
#endif

0 commit comments

Comments
 (0)