Parsing Blender Obj Files with C
30 Oct 2022 By: Kyle Cooper
The most basic method of parsing a obj file exported from blender. This guide uses C/C++ without any additional libraries. We try to stick with raw types. For example most guides will have you put the vertices in a a Vector3<Float>. In this guide we are doing both. We are parsing into a custom vf3 and then flattening them into an array and use a stride of 3.
For this parser, we only care about the basic structure, normals and texture coordinates of the object.
Our vf3 (or vector of 3 floats looks like the following).
typedef struct vf3
{
union
{
struct
{
f32 x;
f32 y;
f32 z;
};
f32 e[3];
};
} vf3;
The goal is to render this in our ‘renderer’ using OpenGL. We skip over the OpenGL instructions in this doc. Essentially you need an index buffer and attribArray.
Exporting The Blender Obj File
First, open Blender. By default it should just render a cube: Then go to File > Export. Select WaveFront(.obj). A settings menu will appear. Expand Geometry and select ‘Triangulate Faces’, this is important as our cube will not render correctly. Blender assumes we render Quads when exporting but we want to render with Triangles.
You will get something like the following. This is not the actual file, I have truncated most of the data
# Blender v2.93.0 OBJ File: ''
# www.blender.org
mtllib triangulate-faces.mtl
o Cube
v 1.000000 1.000000 -1.000000
...
vt 0.875000 0.500000
...
vn 0.0000 1.0000 0.0000
...
usemtl Material
s off
f 5/1/1 3/2/1 1/3/1
...
The .obj file describes the structure of the exported object, another file with a .mtl
extension describes the materials, we can ignore this for now.
The following is a cheat sheet of what the above means;
o
describes the object name we want to createv
the vertices of the object. Notices how there are fewer than the total vertices because we use index buffering as most of the vertices are repeatedvt
these are ouruv
s or texture coordinates for the objects
can be ignored, has to do with shadingusemtl
can be ignoredf
these are our indices, if you did not export from blender with ‘Triangulate Faces’ you will have a shorter list here, thus causing OpenGL to not render correctly. The layout is as follows:- The index pattern is
f v/vt/vn
where v, vt and vn are the index to render for the specific type. So for V we render vertices in that order. vt is our texture coordinates and vn is our normal Ideally we want to parse out these into an array of indices so[0,1,3,5,6]
- The f stands for ‘Face’ or a triangle face this is essentially a list of how the object goes together. We use this kind of list, rather than all the vertices because we generally have lots of repetition in our vertices so this helps save space. Its easy to see when you look at how many vertices you need to specify if you ‘hand write’ out all the vertices for a cube.
- The index pattern is
The Crude Parser
- We need to load the file. In my case I have a custom built file reader, but really it uses the OSes ReadFile in the fileapi.h file
- Once we have the raw contents You can start parsing.
You may need to make this ‘guide’ fit to your use case
Read the contents of the file into a struct called LoadObj. You can change this to suite your needs because we are really just checking to make sure we read the file.
typedef struct LoadedObj
{
char *fileName;
char *contents;
ui32 size;
} LoadedObj;
static LoadedObj
LoadObjFile( DebugReadEntireFileType *ReadFileContents,
char *fileName)
{
LoadedObj result = {};
// This is where you want to read the file with w.e. function you have
debug_file_data_from_read readContents =
ReadFileContents(threadContext, fileName);
Assert(readContents.size > 0); //We could not read the file, you can fail more gracefully
if (readContents.size > 0)
{
result.contents = (char *)readContents.contents;
result.size = readContents.size;
}
return result;
}
Start parsing Here is the mesh structure we use to store our mesh
typedef struct MeshStructureAttributes
{
LoadedObj *asset; // reference to the obj, if ytou need
ui32* indices; //our vertex indicies
f32* verticies; // the vertexes in an array
vf3* textures;
vf3* norms;
ui32* faces;
// total counts for each of the above
ui32 vertCount;
ui32 textureCount;
ui32 normalCount;
ui32 faceCount;
//keep stride info if you need, really this is usually 3 anyway
ui32 faceStride = 3;
// The # of indicies in the faces, that represent a vert
ui32 indiciesCountForVerts;
} MeshStructureAttributes;
Below is my ‘controller’ function for parsing out the file. It calls out to other custom functions. I am not using any libraries here.
static staticMeshStructureAttributes*
ParseObjFileContents(memory_arena *memArena, LoadedObj meshObjFile)
{
MeshStructureAttributes *result = PushStruct(memArena, MeshStructureAttributes);
char *contents = meshObjFile.contents;
Assert(contents > 0); // TODO we will want to fail more gracefully
if (contents != 0)
{
char v = 'v';
char space = ' ';
char t = 't';
char n = 'n';
char f = 'f';
ui32 firstVertIndex = 0;
ui32 firstTextureIndex = 0;
ui32 firstNormalIndex = 0;
ui32 firstFaceIndex = 0;
ui32 faceRows = 0;
ui32 position = 0;
// Within this while loop our goal is to find the lengths of each type. As
// well as the first index of where the types reside
while (*contents++)
{
// You may want to build in some protection in case you are worried
// about out of bounds issues
char charAtCurrent = *contents;
// we sometimes want to look at the next char, for instance v and space to
// indicate a vertex
char charAtNext = *(contents + 1);
if (charAtNext == '\0')
{
// Ideally we should never get here otherwise we need better checking
Assert(
charAtCurrent > 0 &&
charAtCurrent < 127 &&
charAtCurrent == '\n'
);
break;
}
// NOTE the position + 3 is determined by the spaces and
// chars in the obj file + 1
if (charAtCurrent == v && charAtNext == space)
{
if (firstVertIndex == 0)
{
firstVertIndex = position + 3;
}
result->vertCount++;
}
if (charAtCurrent == v && charAtNext == t)
{
if (firstTextureIndex == 0)
{
firstTextureIndex = position + 4;
}
result->textureCount++;
}
if (charAtCurrent == v && charAtNext == n)
{
if (firstNormalIndex == 0)
{
firstNormalIndex = position + 4;
}
result->normalCount++;
}
if (charAtCurrent == f && charAtNext == space)
{
if (firstFaceIndex == 0)
{
firstFaceIndex = position + 3;
}
faceRows++;
}
position++;
}
// face count is rows * columns where columns is 3 * 4 because
// we group our textures in 3s with 4 in each row. Essentially you
// look at the rows of f 1/2/3
// becarful here though because this may not be true for all obj files,
// change as you need!
result->faceCount = faceRows*(3*4);
// Here is where you allocate (malloc if you need). See my notes on memory
// below
result->verts = PushVerts(memArena, result->vertCount, vf3);
result->norms = PushNormals(memArena, result->normalCount, vf3);
result->textures = PushTextures(memArena, result->textureCount, vf3);
result->faces = PushFaces(memArena, result->faceCount, ui32);
// Now we parse our verts as need
{
// Here is where we create out vertex for a vf3* of the vertices
// we could go directly to a list right here if you need
contents = meshObjFile.contents;
contents += firstVertIndex;
char* verts = contents;
ParseVertsFromLoadedObjToVF3AndAppend(verts, result->verts, result->vertCount, 3);
}
{
contents = meshObjFile.contents;
contents += firstTextureIndex;
char* texture = contents;
ParseVertsFromLoadedObjToVF3AndAppend(texture, result->textures, result->textureCount, 2);
}
{
contents = meshObjFile.contents;
contents += firstNormalIndex;
char* norms = contents;
ParseVertsFromLoadedObjToVF3AndAppend(norms, result->norms, result->normalCount, 3);
}
{
// Here is where we go through our faces or indicies
contents = meshObjFile.contents;
contents += firstFaceIndex;
char* faces = contents;
ui32 facePosition = 0;
// Count should be row * columns of faces within
// the groupings of 3 ex. f 3/4/5
for (ui32 current = 0;
current < result->faceCount;
++current)
{
i32 num = 0;
b32 updated = false;
// since we want to only get the uints from the face indicies (not f
// or ' ' or / etc) we need to check if the number is a num
while (IsNum(*faces))
{
// We can convert the num we found to a number
num = num * 10 + (i32)*faces - '0';
faces++;
updated = true;
}
if (updated == true)
{
result->faces[facePosition++] = num;
}
while (*faces && !IsNum(*faces))
{
faces++;
}
}
}
}
// The next function will flatten our indicies and verts as we need
result->indices = FlattenIndiciesFromObjMesh(memArena, result);
// With this function we flatten the vf3 array we created earlier. This might
// not be the most efficent way of creating this array if you ONLY want this
// type of array
result->verticies = FlattenVerts(memArena, result);
return result;
}
Our ParseVertsFromLoadedObjToVF3 looks like the following: This will convert the given array into an array of vf3s. If you want the flatten array and not an array of vf3s you can skip this step and instead just create the flat array.
inline void
ParseVertsFromLoadedObjToVF3AndAppend(
char* contentAtStartOfIndex,
vf3 *arrayOfVertsToAppendTo,
ui32 totalVertCount,
ui32 floatsPerVert
)
{
Assert(floatsPerVert <= 3);
char* verts = contentAtStartOfIndex;
for (ui32 currentVert = 0;
currentVert < totalVertCount;
++currentVert)
{
f32 floatsPer[3] = {};
for (ui32 currentVf3 = 0;
currentVf3 < floatsPerVert;
++currentVf3)
{
// StringToFloat is a similar function to atof
f32 val = StringToFloat(verts);
floatsPer[currentVf3] = val;
while (*verts != ' ')
{
verts++;
}
verts++;
}
vf3 completeVert = {floatsPer[0], floatsPer[1], floatsPer[2]};
arrayOfVertsToAppendTo[currentVert] = completeVert;
}
}
In this approach I flatten the vf3 array back into a flat array. You could probably skip this step OR head head directly to this step if you just want the flatten array
internal f32*
FlattenVerts(memory_arena *memArena, MeshStructureAttributes *meshData)
{
vf3* verts = meshData->verts;
ui32 vertCount = meshData->vertCount * 3;
f32* result = PushArray(memArena, vertCount, f32);
ui32 currentVert = 0;
ui32 v = 0;
while (currentVert < vertCount)
{
vf3 *vert = verts + currentVert;
result[v] = vert->x;
result[++v] = vert->y;
result[++v] = vert->z;
++v;
++currentVert;
}
return result;
}
You should not have an various arrays of data to use as you need
Memory Allocation
You can use whatever you fell comfortable with. I use a ‘memory arena’ to push onto my pre-allocated chunk of memory. Where I have Push[XXX]
you could replace with something like malloc, and manage the memory how you please.
Areas for Improvement and Future Goals
- Ideally, we would just forgo any method of putting this into an array of vf3 and just use a array of floats for the vertex
- With this we would also pack the vertex array, normals and texture cords into a single array. Then just use AttribArray in Opengl and give an offset for each vertex
- Not all blender obj files are the same and we may want to account for this, but we are not looking for this kind of solution here
- There are potentially some performance improvements that we could do. However, the best would be to binarize the data and create our on assets pack. After all, these assets while be a ‘static’ part of our game