One of the most useful skills in creative coding is reading all kinds of data and files. And reading Wavefront .obj files in Processing is definitely one of the more fun applications. There are several libraries that will read .obj files but writing one for ourselves is a good exercise. Especially since we will have more control over what we want to do.
File format
Wavefront .obj files are plain text files describing polygon meshes. The link above describes the entire format comprehensively. But that’s not what we need right now. Let’s start with an example, a simple pyramid (download).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# pyramid # Vertices, x y z v 0.0 1.0 0.0 v -1.0 0.0 -1.0 v 1.0 0.0 -1.0 v 1.0 0.0 1.0 v -1.0 0.0 1.0 # Faces # Four sides f 1 2 3 f 1 3 4 f 1 4 5 f 1 5 2 # Square underside, two triangles f 5 4 3 f 5 3 2 |
The example file contains everything needed to draw the pyramid: the positions of the 5 corners, the vertices, and how the corners are connected, the faces. There are three different kinds of lines in this example file. The first type is a comment line, starting with #. The other two types describe the essential elements of a mesh: vertices and faces.
Lines starting with v give the position of a point on the mesh. The order in which the vertices are given defines an index. The first vertex index is “1”. Faces, the lines starting with f, are defined by the indices of the vertices they connect in some consistent order. Several faces can share the same vertex. Obj files support faces with 3 vertices, triangles, and faces with 4 vertices, quads. Most files will be triangle-based.
The pyramid file contains five vertices, one top vertex 1, and four base vertices, 2 to 5. The four sides of the pyramid connect the top “1” with each of the lines at the base, “2 3”, “3 4”, “4 5” and “5 1”. The bottom of the pyramid is a square “5 4 3 2”, in the file given as two triangles “5 4 3” and “5 3 2”. Don’t worry about the order, we’re reading files, not writing them (yet).
Reading .obj files in Processing
Since these files are text files, Processing has little problems reading them, all at once (reference) or line by line (reference). This is my preferred way (download):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
BufferedReader reader; String line; void setup() { readFile("pyramid.obj"); } void readFile(String file) { reader = createReader(file); do { try { line = reader.readLine(); } catch (IOException e) { e.printStackTrace(); line = null; } if (line == null) { return; } else { println(line); } } while(true); } |
This reads the file but does nothing with it except println(line)
. Handling each line requires us to do three things:
- Determine the type of the line
- If not a comment line, read the vertex or face data
- Add the vertex or face to our collection
Determining the type is a matter of looking at the first part of the line and see if it’s a vertex, a face or another type. Looking at the first character only wouldn’t be sufficient for arbitrary .obj files. The full spec includes the types “vn” and “vt”, which would be indistinguishable from “v”, but we’re not going to use those. Instead, we’re using Processing’s splitTokens()
functionality, cutting each line in parts separated by whitespace (reference).
1 2 3 4 5 6 7 8 9 10 |
void determineType(String line){ String[] parts=splitTokens(line); if(parts[0].equals("v")){ println("This is a vertex: " + line); }else if(parts[0].equals("f")){ println("This is a face: " + line); }else{ println("Let's skip this line: "+line); } } |
How to store the vertices and the faces? Each vertex has three coordinates and a face has three or four indices. We create two small classes to hold the data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Vertex{ float x,y,z; Vertex(float x, float y, float z){ this.x=x; this.y=y; this.z=z; } } class Face{ int[] indices; Face(int[] indices){ this.indices=indices; } } |
Also, we don’t know beforehand how many there will be. We’ll be using two ArrayList to store the vertices and faces while the file is read. Reading the vertex information is straightforward. Each line “v x y z” has been split into 4 parts. We ignore the first part, “v”, and use float()
to convert the text string to a floating point number (reference).
1 2 3 4 |
void addVertex(String[] parts){ Vertex vertex=new Vertex(float(parts[1]),float(parts[2]),float(parts[3])); vertices.add(vertex); } |
The face data requires an additional step. In our example file, all faces are defined as “f index1 index2 index3”. In general, a face line in an .obj file can hold three indices, “vertexIndex1/textureIndex1/normalIndex1”. We only need the first one. splitTokens()
to the rescue, this time using “/” to split each part of the string into further chunks.
Additionally, we need to correct the vertex index we read from the file. The first index in a Wavefront .obj file is 1. This intuitive, daily life way of counting is called one-based indexing. That vertex is the first that will be added to the ArrayList. Processing uses zero-based indexing, the first element is at index 0. This is very sensible from a computer science point-of-view. To retrieve that first element, we write vertices.get(0)
. As a consequence, if the file refers to index 1254, that vertex is stored at ArrayList index 1253. This invites such a common type of error that it has it’s own Wikipedia entry. To compensate for the different indexing schemes, we subtract 1 from every index we read from the file.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void addFace(String[] parts){ int numberOfVertices=parts.length-1; String part; String[] subParts; int[] indices=new int[numberOfVertices]; for(int i=0;i<numberOfVertices;i++){ part=parts[i+1]; subParts=splitTokens(part,"/"); indices[i]=int(subParts[0])-1; } Face face= new Face(indices); faces.add(face); } |
Putting it all together
Last task: get Processing to draw the faces of the mesh we loaded. For convenience, we add drawing functions to Vertex en Face. Putting everything together (download):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
BufferedReader reader; String line; class Vertex{ float x,y,z; Vertex(float x, float y, float z){ this.x=x; this.y=y; this.z=z; } void drawVertex(){ vertex(x,y,z); } } class Face{ int[] indices; int numberOfVertices; Face(int[] indices){ this.indices=indices; numberOfVertices=indices.length; } void drawFace(){ beginShape(); for(int i=0;i<numberOfVertices;i++){ vertices.get(indices[i]).drawVertex(); } endShape(); } } ArrayList<Vertex> vertices; ArrayList<Face> faces; void setup() { size(800,800,P3D); smooth(8); readFile("pyramid.obj"); } void readFile(String file) { vertices=new ArrayList<Vertex>(); faces=new ArrayList<Face>(); reader = createReader(file); do { try { line = reader.readLine(); } catch (IOException e) { e.printStackTrace(); line = null; } if (line == null) { return; } else { processLine(line); } } while (true); } void processLine(String line){ String[] parts=splitTokens(line); if(parts[0].equals("v")){ addVertex(parts); }else if(parts[0].equals("f")){ addFace(parts); } else{ //doNothing } } void addVertex(String[] parts){ Vertex vertex=new Vertex(float(parts[1]),float(parts[2]),float(parts[3])); vertices.add(vertex); } void addFace(String[] parts){ int numberOfVertices=parts.length-1; String part; String[] subParts; int[] indices=new int[numberOfVertices]; for(int i=0;i<numberOfVertices;i++){ part=parts[i+1]; subParts=splitTokens(part,"/"); indices[i]=int(subParts[0])-1; } Face face= new Face(indices); faces.add(face); } void draw(){ background(245); translate(width/2, height/2); lights(); rotateY(map(mouseX,0,width,-PI,PI)); rotateX(map(mouseY,0,height,PI,-PI)); scale(200); strokeWeight(0.01); for(Face face:faces){ face.drawFace(); } } |