Let's write a class to load a Wavefront OBJ file and render it with OpenGL. I have created a model of a futuristic tank (see Figure 11-5), which was built with AC3D (www.inivis.com/) and exported it as mytank.obj and mytank.mtl. My artistic skills are limited; feel free to replace my model with your own 3D object. You can use any 3D modeler software that has the capability to export OBJ files.
The class we will be building is called Model3D and will be responsible for reading the model and storing its geometry (vertices, texture coordinates, models, etc.). It will also be able to send the geometry to OpenGL to render the model. See Table 11-4 for the details of what Model3D needs to store.
Table 11-4. Information Stored in the Model3D Class Name Purpose self.vertices A list of vertices (3D points), stored as tuples of three values (for x, y, and z)
self.tex_coords A list of texture coordinates, stored as tuples of two values (for s and t)
self.normals A list of normals, stored as tuples of three values (for x, y, and z)
self.materials A dictionary of Material objects, so we can look up the texture file name given the name of the material self.face_groups A list of FaceGroup objects that will store the faces for each material self.display_list_id A display list id that we will use to speed up OpenGL rendering
In addition to the Model3D class, we need to define a class to store materials and face groups, which are polygons that share the same material. Listing 11-3 is the beginning of the Model3D class.
Listing 11-3. Class Definitions in model3d.py
# A few imports we will need later from OpenGL.GL import * from OpenGL.GLU import *
import pygame import os.path class Material(object):
self.name = "" self.texture_fname = None self.texture_id = None class FaceGroup(object):
self.tri_indices =  self.material_name = ""
# Display list id for quick rendering self.display_list_id = None
Now that we have the basic class definitions, we can add a method to Model3D that will open an OBJ file and read the contents. Inside the read_obj method (see Listing 11-4), we go through each line of the file and parse it into a command string and a data list. A number of if statements decide what to do with the information store in data.
Listing 11-4. Method to Parse OBJ Files def read_obj(self, fname):
current_face_group = None file_in = file(fname)
for line in file_in:
# Parse command and data from each line words = line.split() command = words data = words[1:]
if command == 'mtllib': # Material library
# Find the file name of the texture model_path = os.path.split(fname) mtllib_path = os.path.join( model_path, data ) self.read_mtllib(mtllib_path)
elif command == 'v': # Vertex x, y, z = data vertex = (float(x), float(y), float(z)) self.vertices.append(vertex)
elif command == 'vt': # Texture coordinate s, t = data tex_coord = (float(s), float(t)) self.tex_coords.append(tex_coord)
elif command == 'vn': # Normal x, y, z = data normal = (float(x), float(y), float(z)) self.normals.append(normal)
elif command == 'usemtl' : # Use material current_face_group = FaceGroup() current_face_group.material_name = data self.face_groups.append( current_face_group )
assert len(data) == 3, "Sorry, only triangles are supported"
# Parse indices from triples for word in data:
vi, ti, ni = word.split('/') # Subtract 1 because Obj indexes start at one, indices = (int(vi) - 1, int(ti) - 1, int(ni) -current_face_group.tri_indices.append(indices)
# Read all the textures used in the model for material in self.materials.itervalues():
model_path = os.path.split(fname) texture_path = os.path.join(model_path, material.texture_fname) texture_surface = pygame.image.load(texture_path) texture_data = pygame.image.tostring(texture_surface, 'RGB', True)
# Create and bind a texture id material.texture = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, material.texture)
GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri( GL_TEXTURE_2D,
# Upload texture and build map-maps width, height = texture_surface.get_rect().size gluBuild2DMipmaps( GL_TEXTURE_2D, 3, width, height,
One of the first commands in an OBJ file is usually mtllib, which tells us the name of the material library file. When this command is encountered, we pass the file name of the material library to the read_mtllib method (which we will write later).
If the command consists of geometry (vertex, texture coordinate, or normal), it is converted to a tuple of float values and stored in the appropriate list. For instance, the line v 10 20 30 would be converted to the tuple (10, 20, 30) and appended to self .vertices.
rather than zero 1)
Before each group of faces is a usemtl command, which tells us which material subsequent faces will use. When read_obj encounters this command, it creates a new FaceGroup object to store the material name and the face information that will follow.
Faces are defined with the f command, and consist of a word for each vertex in the face (3 for triangles, 4 for quads, etc.). Each word contains indices into the vertex, texture coordinate, and normal lists, separated by a forward slash character (/). For instance, the following line defines a triangle where the first point uses vertex 3, texture coordinate 8, and normal 10. These triplets of indices are stored in the current face group, and will be used to reconstruct the model shape when we come to render it.
Following the code to parse each line in the OBJ file we enter a loop that reads the textures in the material dictionary and uploads them to OpenGL. We will be using mip mapping and high-quality texture scaling.
Was this article helpful?