DEV Community

Graham Long
Graham Long

Posted on

Simple .obj (Object) file parser in Kotlin

.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
Alt Text
I then selected File->Export->Wavefront (.obj).
I named the file square.obj and ensured that the export settings were as follows:
Blender Export Settings
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.
Sample Object File
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.

Code for parsing Vertices

“vn“ - Normals

These lines are optional and when present are parsed in much the same way as the vertices.

Code for parsing Normals

“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 Pairs rather than a Triples.

Code for parsing UVs

“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 Triples each representing a point of the triangle.

Generating the vertices, uvs and normals

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/normal Triples 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 Triples are already in the faceMap then the index value is added to the indices final list.

checking duplicate

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.

Adding a new index

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.

Discussion (0)