In which you explore the OBJ specification.
After your renderer reads in the OBJ file, it must parse the text. The OBJ file may have several different forms. Some forms are easier to parse then others. You are likely to encounter one of the following three forms described below in the OBJ files that you download or export.
The simplest form is an OBJ file that contains only positions and triangles. Here's the OBJ file that is produced when exporting the default cube in Blender without normals and with faces triangulated:
# Blender v3.0.0 OBJ File: ''
# www.blender.org
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
s off
f 5 3 1
f 3 8 4
f 7 6 8
f 2 8 6
f 1 4 2
f 5 2 6
f 5 7 3
f 3 7 8
f 7 5 6
f 2 4 8
f 1 3 4
f 5 1 2
The v
lines describe the vertex positions. The f
lines describe the indices of the triangular faces. Note that the indices are 1-based. You will need to correct for this since JavaScript arrays use 0-based indices.
The parser that turns the text above into arrays looks like this in pseudocode:
positions = []
indices = []
for each line
tokenize line
if first token is v
append position to positions
else if first token is f
append triangle to indices
The lines that don't start with v
or f
can be ignored at this point in your learning. The o
lines specify what object the next vertices belong to, which implies that multiple models can be defined in an OBJ file. This particular file only has one object—which is named "Cube". The s
line specifies if vertices should be shared between adjacent faces to smooth out the shading.
If you leave Write Normals checked in Blender's export options, then you'll see a few changes in the OBJ file. The normals appear on lines marked vn
and the face indices include some slashes and a normal index:
# Blender v3.0.0 OBJ File: ''
# www.blender.org
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
s off
f 5//1 3//1 1//1
f 3//2 8//2 4//2
f 7//3 6//3 8//3
f 2//4 8//4 6//4
f 1//5 4//5 2//5
f 5//6 2//6 6//6
f 5//1 7//1 3//1
f 3//2 7//2 8//2
f 7//3 5//3 6//3
f 2//4 4//4 8//4
f 1//5 3//5 4//5
f 5//6 1//6 2//6
Observe that there are 8 vertex positions but only 6 normals. They are not linked in a one-to-one association. The face indices selectively mix and match the positions and normals. For example, the first triangle includes position 5 and normal 1, while the last triangle also includes position 5 but with normal 6.
The GPU expects a one-to-one association for vertex attributes. You'll need to write code that creates a vertex for each unique combination of a position and normal. As with the simpler format, you still first read the data into arrays, but the parsing code is adapted to consider vn
lines and the arrays are only temporary:
tmpPositions = []
tmpNormals = []
tmpIndices = []
for each line
tokenize line
if first token is v
append position to tmpPositions
else if first token is vn
append normal to tmpNormals
else if first token is f
append slashTokens to tmpIndices
After this step, you need to build a new set of arrays where vertex i
corresponds to normal i
. You do this by walking through the slash tokens of the faces to discover how positions and normals are combined. Every time you encounter a combination that you haven't seen before, you have a new vertex.
To track which slash tokens you've seen before and which vertex index they correspond to, you need a dictionary or map. The vertex expansion algorithm looks like this in pseudocode:
positions = []
normals = []
indices = []
slashTokenToIndex = {}
for each triangle in tmpIndices
for each slashToken in triangle
# Create a new vertex if this combo hasn't been seen before.
if slashTokenToIndex doesn't have key slashToken
insert slashToken -> positions.length in slashTokenToIndex
separate fields of slashToken
look up tmpPosition and tmpNormal from tmp arrays
append tmpPosition to positions
append tmpNormal to normals
look up slashToken in slashTokenToIndex
append index to indices
After this code executes, the positions and normals arrays will correspond.
If you leave Triangulate Faces unchecked in Blender's export options, then you'll see quadrilaterals and other polygons on the f
lines of the OBJ file:
# Blender v3.0.0 OBJ File: ''
# www.blender.org
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
s off
f 1 5 7 3
f 4 3 7 8
f 8 7 5 6
f 6 2 4 8
f 2 1 3 4
f 6 5 1 2
The GPU only accepts triangles. You will need to decimate faces with more than three vertices into a collection of triangles. If you know that each face has only convex angles, then you can choose one vertex to be the base of a triangle fan and take this vertex and the remaining vertices two at a time to form the collection. For example, your parser might respond to an f
line with code like this:
else if first token is f
append tokens 1, 2, and 3 to indices
append tokens 1, 3, and 4 to indices
append tokens 1, 4, and 5 to indices
...
But you want to write this so it handles polygons with an arbitrary number of vertices. This loop works for triangles, quadrilaterals, and polygons of any order:
else if first token is f
for i in 2..tokenCount - 2
append tokens 1, i, and i + 1 to indices
If the faces are not convex, you'll need a more sophisticated triangulation algorithm. In practice, this is usually not a concern. Most 3D artists use only convex polygons.