Parsing an OBJ wavefront file draft - Fish-In-A-Suit/Conquest GitHub Wiki
This tutorial is taken from here.
OBJ (or .OBJ) is a geometry definition open file format developed by Wavefront Technologies which has been widely adopted. An OBJ file defines the vertices, texture coordinates and polygons that compose a 3D model. Itβs a relative easy format to parse since is text based and each line defines an element (a vertex, a texture coordinate, etc.).
Each line in .obj file starts with a token that identifies the type of element it holds:
token | explanation | example |
---|---|---|
# | comment | / |
v | vertex coordinates (x, y, z) | v 0.155 0.211 0.32 1.0 |
vn | vertex normal coordinates (x, y, z) | vn 0.71 0.21 0.82 |
vt | texture coordinates (x, y) | vt 0.500 1 |
f | polygonal face element (f v1 v2 v3 or f v1/t1/n1 v2/t2/n2 V3/t3/n3) |
f 6 3 1 or f 6/4/1 3/5/3 7/6/5
|
The token f
defines a face. With information contained in these lines, the indices array, which is used to tell OpenGL in which order to connect vertices, can be constructed. Note that this .obj parser only handles faces as triangles (when exporting .obj models, one must select the checkbox "Triangulate faces").
(1) Read all of the lines of a file
(2) Initialize the following storage containers into which parsed values from lines are stored
- List vertices = new ArrayList<>();
- List textures = new ArrayList<>();
- List normals = new ArrayList<>();
- List faces = new ArrayList<>();
(3) Parse the lines based on the starting token
- case "v": create a new Vector3f out of line data and add it to vertices
- case "vt": create a new Vector2f out of line data and add it to textures
- case "vn": create a new Vector3f out of line data and add it to normals
- case "f": create a new Face out of three PFE tokens (v/vt/vn eg.
8/4/2
) and add it to faces
(4) reorder vertices, textures, normals and faces
Face represents a definition of a face. It is composed by a list of indices groups (since we are dealing with triangles, we will have three indices groups):
Class IndexGroup holds the information for a specific group of indices of a face:
protected static class IdxGroup {
β
public static final int NO_VALUE = -1;
β
public int idxPos;
β
public int idxTextCoord;
β
public int idxVecNormal;
β
public IdxGroup() {
idxPos = NO_VALUE;
idxTextCoord = NO_VALUE;
idxVecNormal = NO_VALUE;
}
}
And this is the Face class:
protected static class Face {
β
/**
* List of idxGroup groups for a face triangle (3 vertices per face).
*/
private IdxGroup[] idxGroups = new IdxGroup[3];
β
public Face(String v1, String v2, String v3) {
idxGroups = new IdxGroup[3];
// Parse the lines
idxGroups[0] = parseLine(v1);
idxGroups[1] = parseLine(v2);
idxGroups[2] = parseLine(v3);
}
β
private IdxGroup parseLine(String line) {
IdxGroup idxGroup = new IdxGroup();
β
String[] lineTokens = line.split("/");
int length = lineTokens.length;
idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1;
if (length > 1) {
// It can be empty if the obj does not define text coords
String textCoord = lineTokens[1];
idxGroup.idxTextCoord = textCoord.length() > 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE;
if (length > 2) {
idxGroup.idxVecNormal = Integer.parseInt(lineTokens[2]) - 1;
}
}
β
return idxGroup;
}
β
public IdxGroup[] getFaceVertexIndices() {
return idxGroups;
}
}
The GameEntity class expects four arrays, one for position coordinates, other for texture coordinates, other for vector normals and naother one for the indices. The first three arrays shall have the same number of elements since the indices array is unique (note that the same number of elements does not imply the same length. Position elements (vertex coordinates) are 3D and are composed by three floats. Texture elements, texture coordinates, are 2D and thus are composed by two floats). OpenGL does not allow us to define different indices arrays per type of element (if so, we would not need to repeat vertices while applying textures).
When you open an OBJ line you will first probably see that the list that holds the vertices positions has a higher number of elements than the lists that hold the texture coordinates and the number of vertices. Thatβs something that we need to solve. Letβs use a simple example which defines a quad with a texture with a pixel height:
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
β
vt 0 1
vt 1 1
β
vn 0 0 1
β
f 1/2/1 2/1/1 3/2/1
f 1/2/1 3/2/1 4/1/1
When we have finished parsing the file we have the following lists (the number of each element is its position in the file upon order of appearance):
Now we will use the face definitions to construct the final arrays including the indices. A thing to take into consideration is that the order in which textures coordinates and vector normals are defined does not correspond to the orders in which vertices are defined. If the size of the lists would be the same and they were ordered, face definition lines would only just need to include a number per vertex.
So we need to order the data and setup accordingly to our needs. The first thing that we must do is create three arrays and one list, one for the vertices, other for the texture coordinates, other for the normals and the list for the indices. The vertices array will have a copy of the list of vertices.
Now we start processing the faces. The first index group of the first face is 1/2/1
. We use the first index in the index group, the one that defines the geometric vertex position, to construct the index list. Letβs name it as posIndex.
Our face is specifiying that the we should add the index of the element that occupies the first position into our indices list. So we put the value of posIndex minus one into the indicesList (we must substract 1 since arrays start at 0 but OBJ file format assumes that they start at 1).
Then we use the rest of the indices of the index group to set up the texturesArray and normalsArray. The second index in the index group is 2. What we must do is put the second texture coordinate in the same position as the one that occupies the vertex designated posIndex (V1).
Then we pick the third index, which is 1, so what we must do is put the first vector normal coordinate in the same position as the one that occupies the vertex designated posIndex (V1).
After we have processed the first face the arrays and lists will be like this:
After we have processed the second face the arrays and lists will be like this:
The second face defines vertices which already have been assigned, but they contain the same values, so thereβs no problem in reprocessing this. I hope the process has been clarified enough, it can be some tricky until you get it. The methods that reorder the data are set below. Keep in mind that what we have are float arrays so we must transform those arrays of vertices, textures and normals into arrays of floats. So the length of these arrays will be the length of the vertices list multiplied by the number three in the case of vertices and normals or multiplied by two in the case of texture coordinates.