Tuesday 27 March 2012

Creating and Loading .OBJ Models

In order to have the ability to play around with OpenGL in a virtual environment other than a handful of randomly scattered polygons, I thought I better look into a bit of simple 3D model making.

A quick scour of the internet revealed that 3DS Max seems to be somewhat of an industry standard, however that requires a substantial investment so I looked for alternatives. Blender (http://www.blender.org/) seemed to be an open source favourite, matching much of what 3DS Max does so I decided to give it a go and download it.

The tutorials are pretty good on the website and get you going pretty quickly. Through the use of predefined meshes and the "snap" tool, I was able to knock up a curious looking little room fairly quickly, also defining vertex normals along the way. A particularly useful feature was being able to "triangulate" all the polygons in the model, thereby turning them all into a combination of triangles.


Once the model had been made, I chose to export the model as a .OBJ file. The .OBJ file is a file format defined by Wavefront, the group behind another 3D modelling tool, Maya. It defines information about the model in an ASCII text file. This has the downside of resulting in large file sizes but is apparently popular to use due to its ASCII format which is much simpler to read and load than a binary file such as the .3DS format. For this reason, I thought it'd be a good place to start with learning how to load and handle 3D models from files before moving on to more complex formats later.

Exporting a model to .OBJ format in Blender affords the option of including a lot of data describing the model in the output file. Since I'm just starting out, I've decided to only include vertex information and to exclude all the other information such as material and normal data which I will gradually introduce into my programs in future posts.


I quickly discovered the model information output to the .OBJ file isn't particularly object/program friendly. From various sources I was told that a lot of information is optional and quite often completely absent from the file. The information is typically given in sentences with a short code indicating the kind of information to follow. for Instance, a "v" at the beginning of a sentence indicates vertex data and is succeeded by three float values (x, y, z) separated by a space. Face information is presented in a similar fashion: prefixed by "f" and presenting a list of the indices of the vertices that make up the face with 1 being the first vertex defined and n being the last vertex defined in the file representing a model with a total number of n vertices.


With this file, one can read in the model information and reconstruct it in the OpenGL program. In order to do this I created a few classes that would store the model information so that the model can be passed around and accessed within the program with a single object. The method of reading in the file was pretty straight forward.

In terms of 3D modelling, the file was composed of the following elements:

  • Vertices: prefixed by 'v' and containing 3 float values: the x, y and z coordinates of the vertex in the model relative to the model origin in Blender.
  • Faces: A face refers to one polygon that makes up the model. Prefixed by 'f' and containing 3 integer values: the indices of the vertices that make up the face. The vertex index represents the nth vertex defined in the file, starting at 1, not 0.
  • Object: An object is a smaller construction within the model. A model is made up of objects and an object is made up of faces. Objects are not represented within the file (an object name line can be added but is metadata) and a large model can be declared as just a single object. However, objects provide a logical structure to the model. They are represented by a vertex/faces block. E.g. the snippet of the .OBJ file above contains information for 2 objects.

After reading in a line from the file, the first character is checked and the corresponding method to handle that data type is called (vertex, face). Since faces can be defined anywhere in the file (in this case, after the vertex definitions for the vertices that make up the face), an object is processed after the final face declaration before a vertex definition. In order to do this, before a vertex is read and processed from the file, a check must be made to determine whether a face was just read and therefore we reached the end of an object. If so, the previously read information is stored in a modelObject class which is stored in the model object. By carrying on this process, the file is read in and a model object is obtained which is made up of modelObject objects which is comprised of faces which in turn store the vertices.

Displaying the model is simply a case of accessing the model object during frame drawing and looping through the objects and faces and drawing each face as a triangle.

Next I plan to include vertex normals to the model and describe how to use them to introduce lighting to a scene.

As an aside, I also learnt some interesting C++ tips from errors that I made. My C++ isn't good and my endeavour to learn OpenGL is also an endeavour to improve my C++ coding. The first thing I learnt is that declaring a new object within a code block without using the new operator, creates the object on the function stack and not the heap.


This means that as soon as the end of the function is reached, the entire stack for the function is deleted, including the new object i.e. it no longer exists. This means that if you were planning to use it in the future, the information will be lost and garbage will be used. In order to persist an object for further use in other parts of the program (e.g. via pointers), the object should be declared on the heap by using the new operator. Subsequently, the delete operator should be used when the object is no longer required otherwise a memory leak will be introduced.

This also lead on to the discovery of the Rule of Three, a C++ coding principle. This generally states that if within a class, you declare one of the following:
  • Destructor
  • Copy Constructor
  • Assignment Operator
Then the other two should be implemented. This is particularly necessary for the case where a destructor is declared. This is because by declaring a destructor, you probably have non primitive data contained within the class that needs to be handled correctly (e.g. use of the delete operator). C++ generates a default copy constructor and assignment operator that only performs shallow copying. For most applications, this is insufficient. In order to fix memory related issues, a copy constructor and assignment operator function should be implemented which correctly control the copying of dynamic memory.

Within the OBJ loader program, I made this mistake and it caused object data that I had moved into the model object to be garbage once the temporary data I had used had been collected by the system. By implementing the copy constructor and assignment operator function within the necessary objects, logical copying of the data was performed and the integrity of the model data was maintained.



No comments:

Post a Comment