.obj file basic
An .obj file is one of many ways to represent the data for a 3D model.
Full details of the file contents can be found elsewhere but for our purposes we are looking for lines which start with:
- “v “ - these lines contain the data for the vertices.
- “vn“ - these lines contain the data for the normals (we haven’t used these yet but they are required for applying lighting and shading)
- “vt” - these lines contain the uv coordinates used to map textures to the model
- “f “ - these lines define the faces, each line links the vertices, uv coordinates and normals for the 3 point of one triangle.
Parsing the file
I Opened Blender and generated a simple square to demonstrate the parser
I then selected File->Export->Wavefront (.obj).
I named the file square.obj and ensured that the export settings were as follows:
Opening the generated file in notepad++ shows the contents of the file, you can see the 4 different line prefixes which I mentioned above v, vn, vt and f.
To parse this file I used a BufferedReader
to read each line with the forEachLine
function, this allows us to access each line in turn using the it
reference.
“v “ - Vertices
Parsing the vertices is done by splitting the line each time a space is encountered, Kotlin makes this easy for us by providing the split
function.
This function returns a List
of 4 strings, the first being the "v " prefix and the other 3 being the x, y and z components of the vertex.
I generate a Triple
from the x, y, z Float values and add them to an ArrayList
of vertices.
“vn“ - Normals
These lines are optional and when present are parsed in much the same way as the vertices.
“vt” - UV Coordinates
These lines are also optional and again are parsed in much the same way as the others except the UV coordinates are stored in an ArrayList
of Pair
s rather than a Triple
s.
“f “ - Faces
These lines are different from the rest, they are the glue that sticks the whole file together.
First I split the string using " " as a delimiter, each of the 3 data strings in the returned List
represent a single point in the a triangle.
Then I split the string using "/" this generates a List
of up to 3 integer values indexing the vertex, the optional uv and the optional normal.
It should be noted at this point that the indices start at 1 not 0.
The resulting 3 sets of 3 indices are then used to generate 3 Triple
s each representing a point of the triangle.
The final step is to generate our final list of vertices, uvs, normals and indices from this information, to do this I use a variable nextIndex
and a HashMap, Short>
called faceMap
, HashMaps have a look up time complexity of O(1) which is crucial to parsing larger object files in a reasonable time.
-
nextIndex
tracks the next index to be placed in the final list of indices. -
faceMap
holds all the vertex/uv/normalTriple
s that have already been added to the final lists of vertices, UVs and normals as the keys and the associated index as the value.
If any of the vertex/uv/normal Triple
s are already in the faceMap
then the index value is added to the indices final list.
If not, the faceMap
is updated with the vertex/uv/normal Triple
and nextIndex
, the relevant vertex, uv and normal values are added to the final lists and finally the nextIndex
is added to the indices final list and incremented.
Running through the process by hand
Using our square.obj file as an example lets go through the process.
Once the vertices, normals and uvs have been processed we will have the following 3 lists:
- Vertices: (0.000000, 1.000000, 1.000000), (-0.000000, -1.000000, 1.000000), (0.000000, 1.000000, -1.000000), (-0.000000, -1.000000, -1.000000)
- UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000), (0.0000, 1.0000)
- Normals: (1.0000, -0.0000, 0.0000)
Going through the faces:
The First vertex/uv/normal triple would be (2, 1, 1), this is not in the faceMap
as the faceMap
is empty so we add ((2, 1, 1), 0) to the faceMap
, then add the 2nd vertex to the final vertices list, the 1st UV to the final UVs list and the 1st Normal to the final normals list and the value of nextIndex
to the final indices list before incrementing it, giving the variables at this stage as:
- Final Vertices: (-0.000000, -1.000000, 1.000000)
- Final UVs: (1.0000, 1.0000)
- Final Normals: (1.0000, -0.0000, 0.0000)
- Final Indices: 0
-
faceMap
: ((2, 1, 1), 0) -
nextIndex
: 1
The Second vertex/uv/normal triple would be (3, 2, 1), this is not in the faceMap
so we add ((3, 2, 1), 1) to the faceMap
, then add the 3rd vertex to the final vertices list, the 2nd UV to the final UVs list and the 1st Normal to the final normals list and the value of nextIndex
to the final indices list before incrementing it, giving the variables at this stage as:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1
-
faceMap
: ((2, 1, 1), 0), ((3, 2, 1), 1) -
nextIndex
: 2
The Third vertex/uv/normal triple would be (1, 3, 1), this is not in the faceMap
so we add ((1, 3, 1), 2) to the faceMap
, then add the 1st vertex to the final vertices list, the 3rd UV to the final UVs list and the 1st Normal to the final normals list and the value of nextIndex
to the final indices list before incrementing it, giving the variables at this stage as:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000), (-0.000000, 1.000000, 1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1, 2
-
faceMap
: ((2, 1, 1), 0), ((3, 2, 1), 1), ((1, 3, 1), 2) -
nextIndex
: 3
The Fourth vertex/uv/normal triple would be (2, 1, 1), this is in the faceMap
so we add just add the value associated with (2, 1, 1) to the final indices which is 0:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000), (-0.000000, 1.000000, 1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1, 2, 0
-
faceMap
: ((2, 1, 1), 0), ((3, 2, 1), 1), ((1, 3, 1), 2) -
nextIndex
: 3
The Fifth vertex/uv/normal triple would be (4, 4, 1), this is not in the faceMap
so we add ((4, 4, 1), 3) to the faceMap
, then add the 4th vertex to the final vertices list, the 4th UV to the final UVs list and the 1st Normal to the final normals list and the value of nextIndex
to the final indices list before incrementing it, giving the variables at this stage as:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000), (-0.000000, 1.000000, 1.000000), (-0.000000, -1.000000, -1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000), (0.0000, 1.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1, 2, 0, 3
-
faceMap
: ((2, 1, 1), 0), ((3, 2, 1), 1), ((1, 3, 1), 2), ((4, 4, 1), 3) -
nextIndex
: 4
The Sixth vertex/uv/normal triple would be (3, 2, 1), this is in the faceMap
so we add just add the value associated with (3, 2, 1) to the final indices which is 1:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000), (-0.000000, 1.000000, 1.000000), (-0.000000, -1.000000, -1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000), (0.0000, 1.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1, 2, 0, 3, 1
-
faceMap
: ((2, 1, 1), 0), ((3, 2, 1), 1), ((1, 3, 1), 2), ((4, 4, 1), 3) -
nextIndex
: 4
This is all of the faces described in the square.obj file therefore our final vertices, UVs, normals and indices are:
- Final Vertices: (-0.000000, -1.000000, 1.000000), (-0.000000, 1.000000, -1.000000), (-0.000000, 1.000000, 1.000000), (-0.000000, -1.000000, -1.000000)
- Final UVs: (1.0000, 1.0000), (0.0000, 0.0000), (1.0000, 0.0000), (0.0000, 1.0000)
- Final Normals: (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000), (1.0000, -0.0000, 0.0000)
- Final Indices: 0, 1, 2, 0, 3, 1
Conclusion
I hope this blogpost has explained the basics of an object file and how one is parsed, this has been a fun class to work on and the source code can be found here.
With this class and a GenericGameObject
class I can now generate 3D models in Blender and easily import them into my game as assets, until next time.
Top comments (0)