Lesson 9: Loading Wavefront OBJ 3D models
In this tutorial we will load Wavefront OBJ 3D models. These models are static models (e.g. the model itself does not have any movement definition). You can get a set of example models here : http://people.sc.fsu.edu/~jburkardt/data/obj/obj.html Remember to triangulate them in Blender or another 3d editing program.
Every OBJ model consists of a point cloud. For example, a simple pyramid consists of 5 points. The more complicated your model is, the more points you will have. For example, an airplane point cloud could look like this:
Wavefront OBJ
Every point is defined in the vector space using their (x,y,z) coordinate. The Wavefront OBJ format stores everything in plain text, and thus if you open an OBJ model in a text editor you will see a list like:
v -1.227048 -1.158217 1.417186 v -1.227048 -1.576351 1.426747 v -1.202536 -1.576423 1.426747 v -1.202536 -1.169531 1.417441 v 0.064052 -1.584108 1.403254 v 0.064052 -1.186619 1.394201
Where every line represents a vector in the vector space. Note that ‘v’ in front of the files which indicates a vector. Once you have loaded the vectors, you want to connect each of the faces. A face is a connection of multiple points, in the form of a triangle, quad etc. In our case we will only support triangles. If the concept of faces is still unclear to you, consider a cube has 6 faces (sides). Every face is defined on a single line and consists of several vectors, in our case exactly three. You will find a list like this in the file:
f 1 2 3 f 1 3 4 f 5 6 7 f 5 7 8 f 9 10 4 f 9 4 3 f 8 7 10 f 8 10 9
Note that the points indicate the index of the vector array you have stored earlier. E.g. every point of the face consists of an x,y and z value. Thus, every line consist of 9 numbers (3 for each coordinate).
Once we know all of the faces, we can display them and end up with something like this:
Wavefront OBJ Model
Finally, we need to load the normal vectors (e.g. orthagonal lines from the faces) to display the correct lighting. These can either be calculated from the faces we already have, or be loaded from the file. (vn is the indicator). We end up with this scene:
Wavefront OBJ Model OpenGL
In code this will look like this:
/* * * Demonstrates how to load and display an Wavefront OBJ file. * Using triangles and normals as static object. No texture mapping. * https://talkera.org/opengl/ * * OBJ files must be triangulated!!! * Non triangulated objects wont work! * You can use Blender to triangulate * */ #include#include #include #include #include #include #include #include #include #include #include #include #include #define KEY_ESCAPE 27 using namespace std; /************************************************************************ Window ************************************************************************/ typedef struct { int width; int height; char* title; float field_of_view_angle; float z_near; float z_far; } glutWindow; /*************************************************************************** OBJ Loading ***************************************************************************/ class Model_OBJ { public: Model_OBJ(); float* Model_OBJ::calculateNormal(float* coord1,float* coord2,float* coord3 ); int Model_OBJ::Load(char *filename); // Loads the model void Model_OBJ::Draw(); // Draws the model on the screen void Model_OBJ::Release(); // Release the model float* normals; // Stores the normals float* Faces_Triangles; // Stores the triangles float* vertexBuffer; // Stores the points which make the object long TotalConnectedPoints; // Stores the total number of connected verteces long TotalConnectedTriangles; // Stores the total number of connected triangles }; #define POINTS_PER_VERTEX 3 #define TOTAL_FLOATS_IN_TRIANGLE 9 using namespace std; Model_OBJ::Model_OBJ() { this->TotalConnectedTriangles = 0; this->TotalConnectedPoints = 0; } float* Model_OBJ::calculateNormal( float *coord1, float *coord2, float *coord3 ) { /* calculate Vector1 and Vector2 */ float va[3], vb[3], vr[3], val; va[0] = coord1[0] - coord2[0]; va[1] = coord1[1] - coord2[1]; va[2] = coord1[2] - coord2[2]; vb[0] = coord1[0] - coord3[0]; vb[1] = coord1[1] - coord3[1]; vb[2] = coord1[2] - coord3[2]; /* cross product */ vr[0] = va[1] * vb[2] - vb[1] * va[2]; vr[1] = vb[0] * va[2] - va[0] * vb[2]; vr[2] = va[0] * vb[1] - vb[0] * va[1]; /* normalization factor */ val = sqrt( vr[0]*vr[0] + vr[1]*vr[1] + vr[2]*vr[2] ); float norm[3]; norm[0] = vr[0]/val; norm[1] = vr[1]/val; norm[2] = vr[2]/val; return norm; } int Model_OBJ::Load(char* filename) { string line; ifstream objFile (filename); if (objFile.is_open()) // If obj file is open, continue { objFile.seekg (0, ios::end); // Go to end of the file, long fileSize = objFile.tellg(); // get file size objFile.seekg (0, ios::beg); // we'll use this to register memory for our 3d model vertexBuffer = (float*) malloc (fileSize); // Allocate memory for the verteces Faces_Triangles = (float*) malloc(fileSize*sizeof(float)); // Allocate memory for the triangles normals = (float*) malloc(fileSize*sizeof(float)); // Allocate memory for the normals int triangle_index = 0; // Set triangle index to zero int normal_index = 0; // Set normal index to zero while (! objFile.eof() ) // Start reading file data { getline (objFile,line); // Get line from file if (line.c_str()[0] == 'v') // The first character is a v: on this line is a vertex stored. { line[0] = ' '; // Set first character to 0. This will allow us to use sscanf sscanf(line.c_str(),"%f %f %f ", // Read floats from the line: v X Y Z &vertexBuffer[TotalConnectedPoints], &vertexBuffer[TotalConnectedPoints+1], &vertexBuffer[TotalConnectedPoints+2]); TotalConnectedPoints += POINTS_PER_VERTEX; // Add 3 to the total connected points } if (line.c_str()[0] == 'f') // The first character is an 'f': on this line is a point stored { line[0] = ' '; // Set first character to 0. This will allow us to use sscanf int vertexNumber[4] = { 0, 0, 0 }; sscanf(line.c_str(),"%i%i%i", // Read integers from the line: f 1 2 3 &vertexNumber[0], // First point of our triangle. This is an &vertexNumber[1], // pointer to our vertexBuffer list &vertexNumber[2] ); // each point represents an X,Y,Z. vertexNumber[0] -= 1; // OBJ file starts counting from 1 vertexNumber[1] -= 1; // OBJ file starts counting from 1 vertexNumber[2] -= 1; // OBJ file starts counting from 1 /******************************************************************** * Create triangles (f 1 2 3) from points: (v X Y Z) (v X Y Z) (v X Y Z). * The vertexBuffer contains all verteces * The triangles will be created using the verteces we read previously */ int tCounter = 0; for (int i = 0; i calculateNormal( coord1, coord2, coord3 ); tCounter = 0; for (int i = 0; i Faces_Triangles); free(this->normals); free(this->vertexBuffer); } void Model_OBJ::Draw() { glEnableClientState(GL_VERTEX_ARRAY); // Enable vertex arrays glEnableClientState(GL_NORMAL_ARRAY); // Enable normal arrays glVertexPointer(3,GL_FLOAT, 0,Faces_Triangles); // Vertex Pointer to triangle array glNormalPointer(GL_FLOAT, 0, normals); // Normal pointer to normal array glDrawArrays(GL_TRIANGLES, 0, TotalConnectedTriangles); // Draw the triangles glDisableClientState(GL_VERTEX_ARRAY); // Disable vertex arrays glDisableClientState(GL_NORMAL_ARRAY); // Disable normal arrays } /*************************************************************************** * Program code ***************************************************************************/ Model_OBJ obj; float g_rotation; glutWindow win; void display() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt( 0,1,20, 0,0,0, 0,1,0); glPushMatrix(); glRotatef(45,0,1,0); glRotatef(90,0,1,0); // g_rotation++; obj.Draw(); glPopMatrix(); glutSwapBuffers(); } void initialize () { glMatrixMode(GL_PROJECTION); glViewport(0, 0, win.width, win.height); GLfloat aspect = (GLfloat) win.width / win.height; glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(win.field_of_view_angle, aspect, win.z_near, win.z_far); glMatrixMode(GL_MODELVIEW); glShadeModel( GL_SMOOTH ); glClearColor( 0.0f, 0.1f, 0.0f, 0.5f ); glClearDepth( 1.0f ); glEnable( GL_DEPTH_TEST ); glDepthFunc( GL_LEQUAL ); glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST ); GLfloat amb_light[] = { 0.1, 0.1, 0.1, 1.0 }; GLfloat diffuse[] = { 0.6, 0.6, 0.6, 1 }; GLfloat specular[] = { 0.7, 0.7, 0.3, 1 }; glLightModelfv( GL_LIGHT_MODEL_AMBIENT, amb_light ); glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse ); glLightfv( GL_LIGHT0, GL_SPECULAR, specular ); glEnable( GL_LIGHT0 ); glEnable( GL_COLOR_MATERIAL ); glShadeModel( GL_SMOOTH ); glLightModeli( GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE ); glDepthFunc( GL_LEQUAL ); glEnable( GL_DEPTH_TEST ); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); } void keyboard ( unsigned char key, int x, int y ) { switch ( key ) { case KEY_ESCAPE: exit ( 0 ); break; default: break; } } int main(int argc, char **argv) { // set window values win.width = 640; win.height = 480; win.title = "OpenGL/GLUT OBJ Loader."; win.field_of_view_angle = 45; win.z_near = 1.0f; win.z_far = 500.0f; // initialize and run program glutInit(&argc, argv); // GLUT initialization glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH ); // Display Mode glutInitWindowSize(win.width,win.height); // set window size glutCreateWindow(win.title); // create Window glutDisplayFunc(display); // register Display Function glutIdleFunc( display ); // register Idle Function glutKeyboardFunc( keyboard ); // register Keyboard Handler initialize(); obj.Load("cessna.obj"); glutMainLoop(); // run GLUT mainloop return 0; }
You can compile using:
g++ obj.c -o obj -lGLU -lGL -lglut -fpermissive
If you do not see any model, make sure you have the correct file path, and that the camera is in the right position. If the model looks deformed, you may have forgotten to triangulate the object.