Implementing an OBJ Loader

Our plan of attack will be to load the file completely into memory and create a string per line. We will also create temporary float arrays for all the vertex positions, texture coordinates and normals we are going to load. Their size will be equal to the number of lines in the OBJ file times the number of components per attribute; that is, two for texture coordinates or three for normals. By this we overshoot the necessary amount of memory needed to store the data, but that's still better than allocating new arrays every time we have filled them up.

We also do the same for the indices that define each triangle. While the OBJ format is indeed an indexed format, we can't use those indices directly with our Vertices3 class. The reason for this is that a vertex attribute might be reused by multiple vertices, so there's a one-to-many relationship that is not allowed in OpenGL ES. Therefore we'll use a non-indexed Vertices3 instance and simply duplicate vertices. For our needs that's OK.

Let's see how we can implement all this. Listing 11-12 shows the code. Listing 11-12. ObjLoader.java, a Simple Class for Loading a Subset of the OBJ Format package com.badlogic.androidgames.framework.gl;

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List;

import com.badlogic.androidgames.framework.impl.GLGame;

public class ObjLoader {

public static Vertices3 load(GLGame game, String file) { InputStream in = null; try {

in = game.getFileIO().readAsset(file); List<String> lines = readLines(in);

float[] vertices = new float[lines.size() * 3]; float[] normals = new float[lines.size() * 3]; float[] uv = new float[lines.size() * 2];

int numVertices = 0; int numNormals = 0; int numUV = 0; int numFaces = 0;

The first thing we do is open an InputStream to the asset file specified by the file parameter. We then read in all lines of that file in a method called readLines() (defined in the code that follows). Based on the number of lines, we allocate float arrays that will store the x-, y- and z-coordinates of each vertex's position, the x-, y- and z-component of each vertex's normal, and the u- and v-components of each vertex's texture coordinates. Since we don't know how many vertices there are in the file, we just allocate more space than needed for the arrays. Each vertex attribute is stored in subsequent elements of the three arrays. The position of the first read vertex is in vertices[0], vertices[1], and vertices[2], and so on. We also keep track of the indices in the triangle definitions for each of the three attributes of a vertex. Additionally we have a couple of counters to keep track of how many things we have already loaded.

String line = lines.get(i);

Next we have a for loop that iterates through all the lines in the files.

vertices[vertexIndex] = Float.parseFloat(tokens[1]);

vertices[vertexIndex + 1] = Float.parseFloat(tokens[2]);

vertices[vertexIndex + 2] = Float.parseFloat(tokens[3]);

vertexIndex += 3;

numVertices++;

continue;

If the current line is a vertex position definition, we split the line by whitespaces, read the x-, y- and z-coordinate, and store it in the vertices array:

normals[normalIndex] = Float.parseFloat(tokens[1]);

normals[normalIndex + 1] = Float.parseFloat(tokens[2]);

normals[normalIndex + 2] = Float.parseFloat(tokens[3]);

normallndex += 3;

numNormals++;

continue;

uv[uvIndex] = Float.parseFloat(tokens[1]);

continue;

We do the same for normals and texture coordinates:

String[] parts = tokens[1].split("/"); facesVerts[faceIndex] = getIndex(parts[0], numVertices);

facesNormals[faceIndex] = getIndex(parts[2], numNormals); if (parts.length > 1)

facesUV[faceIndex] = getIndex(parts[l], numUV); faceIndex++;

facesVerts[faceIndex] = getIndex(parts[0], numVertices); if (parts.length > 2)

facesNormals[faceIndex] = getIndex(parts[2], numNormals); if (parts.length > l)

facesUV[faceIndex] = getIndex(parts[l], numUV); faceIndex++;

facesVerts[faceIndex] = getIndex(parts[0], numVertices); if (parts.length > 2)

facesNormals[faceIndex] = getIndex(parts[2], numNormals); if (parts.length > l)

facesUV[faceIndex] = getIndex(parts[l], numUV); faceIndex++; numFaces++; continue;

In this code, each vertex of a triangle (here called a face, as that is the term used in the OBJ format) is defined by a triplet of indices into the vertex position, texture coordinate, and normal arrays. The texture coordinate and normal indices can be omitted, so we keep track of this. The indices can also be negative, in which case we have to add them to the number of positions/texture coordinates/normals loaded so far. That's what the getIndex() method does for us.

* (3 + (numNormals > 0 ? 3 : 0) + (numUV > 0 ? 2 : 0))];

Once we have loaded all vertex positions, texture coordinates, normals, and triangles we can start assembling a float array holding the vertices in the format expected by a Vertices3 instance. The number of floats needed to store these vertices can be easily derived from the number of triangles we loaded and whether normal and texture coordinates are given.

for (int i = 0, vi = 0; i < numFaces * 3; i++) { int vertexIdx = facesVerts[i] * 3; verts[vi++] = vertices[vertexIdx]; verts[vi++] = vertices[vertexIdx + l]; verts[vi++] = vertices[vertexIdx + 2];

int uvIdx = facesUV[i] * 2; verts[vi++] = uv[uvIdx]; verts[vi++] = l - uv[uvIdx + l];

int normalldx = facesNormals[i] * 3; verts[vi++] = normals[normalIdx]; verts[vi++] = normals[normalIdx + 1]; verts[vi++] = normals[normalIdx + 2];

To fill the verts array we just loop over all the triangles, fetch the vertex attribute for each vertex of a triangle and put them into the verts array in the layout we usually use for a Vertices3 instance.

Vertices3 model = new Vertices3(game.getGLGraphics(), numFaces * 3,

0, false, numUV > 0, numNormals > 0); model.setVertices(verts, 0, verts.length); return model;

The last thing we do is instantiate the Vertices3 instance and set the vertices.

throw new RuntimeException("couldn't load '" + file + , ex);

The rest of the method just does some exception handling and closing of the InputStream.

static int getIndex(String index, int size) { int idx = Integer.porselnt(index); if (idx < 0)

return size + idx;

else return idx - 1;

The getIndex() method takes one of the indices given for an attribute of a vertex in a triangle definition, as well as the number of attributes loaded so far, and returns an index suitable to reference the attribute in one of our working arrays.

static List<String> readLines(InputStream in) throws IOException { List<String> lines = new ArrayList<String>();

BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line = null;

lines.add(line); return lines;

Finally there's the readLines() method, which just reads in each line of a file and returns all these lines as a List of strings.

To load a OBJ file from an asset we can use the ObjLoader as follows: Vertices3 model = ObjLoader.load(game, "mymodel.obj");

Pretty straightforward after all this index juggling, right? To render this Vertices3 instance we need to know how many vertices it has, though. Let's extend the Vertices3 class one more time, adding two methods to return the number of vertices as well as the number of indices currently defined in the instance. Listing 11-13 shows you the code.

Listing 11-13. An excerpt from Vertices3.java, Fetching the Number of Vertices and Indices public int getNumIndices() { return indices.limit();

public int getNumVertices() {

For the number of indices we just return the limit of the ShortBuffer storing the indices. For the number of vertices we do the same. However, since the limit is reported in the number of floats defined in the FloatBuffer, we have to divide it by the vertex size. Since we store that in number of bytes in vertexSize, we divide that member by 4.

0 -1

Post a comment