The Code — Drawing the Model
Now we can start the code to draw the model! This is not difficult at all now that we have a careful arrangement of the data structures in memory.
void Modeclass="underline" :draw() {
GLboolean texEnabled = glIsEnabled( GL_TEXTURE_2D );
This first part saves the state of texture mapping within OpenGL so that the function does not disturb it. Note however that it does not preserve the material properties in the same way.
Now we loop through each of the meshes and draw them individually:
// Draw By Group
for ( int i = 0; i < m_numMeshes; i++ ) {
m_pMeshes[i] will be used to reference the current mesh. Now, each mesh has its own material properties, so we set up the OpenGL states according to that. If the materialIndex of the mesh is –1 however, there is no material for this mesh and it is drawn with the OpenGL defaults.
int materialIndex = m_pMeshes[i].m_materialIndex;
if ( materialIndex >= 0 ) {
glMaterialfv( GL_FRONT, GL_AMBIENT, m_pMaterials[materialIndex].m_ambient );
glMaterialfv( GL_FRONT, GL_DIFFUSE, m_pMaterials[materialIndex].m_diffuse );
glMaterialfv( GL_FRONT, GL_SPECULAR, m_pMaterials[materialIndex].m_specular );
glMaterialfv( GL_FRONT, GL_EMISSION, m_pMaterials[materialIndex].m_emissive );
glMaterialf( GL_FRONT, GL_SHININESS, m_pMaterials[materialIndex].m_shininess );
if ( m_pMaterials[materialIndex].m_texture > 0 ) {
glBindTexture( GL_TEXTURE_2D, m_pMaterials[materialIndex].m_texture );
glEnable( GL_TEXTURE_2D );
} else glDisable( GL_TEXTURE_2D );
} else {
glDisable( GL_TEXTURE_2D );
}
The material properties are set according to the values stored in the model. Note that the texture is only bound and enabled if it is greater than 0. If it is set to 0, you'll recall, there was no texture, so texturing is disabled. Texturing is also disabled if there was no material at all for the mesh.
glBegin( GL_TRIANGLES ); {
for ( int j = 0; j < m_pMeshes[i].m_numTriangles; j++ ) {
int triangleIndex = m_pMeshes[i].m_pTriangleIndices[j];
const Triangle* pTri = &m_pTriangles[triangleIndex];
for ( int k = 0; k < 3; k++ ) {
int index = pTri->m_vertexIndices[k];
glNormal3fv( pTri->m_vertexNormals[k] );
glTexCoord2f( pTri->m_s[k], pTri->m_t[k] );
glVertex3fv( m_pVertices[index].m_location );
}
}
}
glEnd();
}
The above section does the rendering of the triangles for the model. It loops through each of the triangles for the mesh, and then draws each of its three vertices, including the normal and texture coordinates. Remember that each triangle in a mesh and likewise each vertex in a triangle is indexed into the total model arrays (these are the two index variables used). pTri is a pointer to the current triangle in the mesh used to simplify the code following it.
if ( texEnabled ) glEnable( GL_TEXTURE_2D );
else glDisable( GL_TEXTURE_2D );
}
This final fragment of code sets the texture mapping state back to its original value.
The only other code of interest in the Model class is the constructor and destructor. These are self explanatory. The constructor initializes all members to 0 (or NULL for pointers), and the destructor deletes the dynamic memory for all of the model structures. You should note that if you call the loadModelData function twice for one Model object, you will get memory leaks. Be careful!
The final topic I will discuss here is the changes to the base code to render using the new Model class, and where I plan to go from here in a future tutorial introducing skeletal animation.
Model *pModel = NULL; // Holds The Model Data
At the top of the code in Lesson32.cpp the model is declared, but not initialised. It is created in WinMain:
pModel = new MilkshapeModel();
if ( pModel->loadModelData( "data/model.ms3d" ) == false ) {
MessageBox( NULL, "Couldn't load the model data/model.ms3d", "Error", MB_OK | MB_ICONERROR );
return 0; // If Model Didn't Load, Quit
}
The model is created here, and not in InitGL because InitGL gets called everytime we change the screen mode (losing the OpenGL context). But the model doesn't need to be reloaded, as its data remains intact. What doesn't remain intact are the textures that were bound to texture objects when we loaded the object. So the following line is added to InitGL:
pModel->reloadTextures();
This takes the place of calling LoadGLTextures as we used to. If there was more than one model in the scene, then this function must be called for all of them. If you get white objects all of a sudden, then your textures have been thrown away and not reloaded correctly.
Finally there is a new DrawGLScene function:
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
gluLookAt( 75, 75, 75, 0, 0, 0, 0, 1, 0 );
glRotatef(yrot,0.0f,1.0f,0.0f);
pModel->draw();
yrot+=1.0f;
return TRUE; // Keep Going
}
Simple? We clear the colour buffer, set the identity into the model/view matrix, and then set an eye projection with gluLookAt. If you haven't used gluLookAt before, essentially it places the camera at the position of the first 3 parameters, places the center of the scene at the position of the next 3 parameters, and the last 3 parameters describe the vector that is "up". In this case, we look from (75, 75, 75) to (0,0,0) — as the model is drawn about (0,0,0) unless you translate before drawing it — and the positive Y-axis is facing up. The function must be called first, and after loading the identity to behave in this fashion.
To make it a bit more interesting, the scene gradually rotates around the y-axis with glRotatef.
Finally, the model is drawn with its draw member function. It is drawn centered at the origin (assuming it was modelled around the origin in Milkshape 3D!), so If you want to position or rotate or scale it, simply call the appropriate GL functions before drawing it. Voila! To test it out – try making your own models in Milkshape (or use its import function), and load them instead by changing the line in WinMain. Or add them to the scene and draw several models!
What Next?
In a future tutorial for NeHe Productions, I will explain how to extend this class structure to incorporate skeletal animation. And if I get around to it, I will write more loader classes to make the program more versatile.
The step to skeletal animation is not as large as it may seem, although the math involved is much more tricky. If you don't understand much about matrices and vectors, now is the time to read up them! There are several resources on the web that can help you out.
See you then!
Some information about Brett Porter: Born in Australia, he studied at the University of Wollongong, recently graduating with a BCompSc and a BMath. He began programming in BASIC 12 years ago on a Commodore 64 "clone" called the VZ300, but soon moved up to Pascal, Intel assembly, C++ and Java. During the last few years 3D programming has become an interest and OpenGL has become his graphics API of choice. For more information visit his homepage at: http://rsn.gamedev.net.