Tutorial 7: 3D Model Rendering

We have already been rendering 3D models in the previous tutorials; however, they were composed of a single triangle and were fairly uninteresting. Now that the basics have been covered, we'll move forward to render a more complex object. In this case the object will be a cube. Before we get into how to render more complex models, we will first talk about model formats.

There are many tools available that allow users to create 3D models. Blender and Maya are two of the more popular 3D modeling programs on Linux. There are also many other tools with less features but still can do the basics for what we need.

Regardless of which tool you choose to use they will all export their models into numerous different formats. My suggestion is that you create your own model format and write a parser to convert their export format into your own format. The reason for this is that the 3D modeling package that you use may change over time and their model format will also change. Also, you may be using more than one 3D modeling package so you will have multiple different formats to deal with. So, if you have your own format and convert their formats to your own then your code will never need to change. You will only need to change your parser program(s) for changing those formats to your own. As well most 3D modeling packages export a ton of junk that is only useful to that modeling program and you don't need any of it in your model format.

The most important part to making your own format is that it covers everything you need it to do and that it is simple for you to use. You can also consider making a couple different formats for different objects as some may have animation data, some may be static, and so forth.

The model format I'm going to present is very basic. It will contain a line for each vertex in the model. Each line will match the vertex format used in the code which will be position vector (x, y, z), texture coordinates (tu, tv), and the normal vector (nx, ny, nz). The format will also have the vertex count at the top so you can read the first line and build the memory structures needed before reading in the data. The format will also require that every three lines make a triangle, and that the vertices in the model format are presented in clockwise order. Here is the model file for the cube we are going to render:


Cube.txt

Vertex Count: 36

Data:

-1.0  1.0 -1.0 0.0 0.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
 1.0 -1.0 -1.0 1.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 0.0 0.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0  1.0 1.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 0.0 0.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
-1.0 -1.0  1.0 1.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 0.0 0.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0 -1.0 1.0 1.0 -1.0  0.0  0.0
-1.0  1.0  1.0 0.0 0.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
 1.0  1.0 -1.0 1.0 1.0  0.0  1.0  0.0
-1.0 -1.0 -1.0 0.0 0.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
 1.0 -1.0  1.0 1.0 1.0  0.0 -1.0  0.0

So, as you can see there are 36 lines of x, y, z, tu, tv, nx, ny, nz data. Every three lines composes its own triangle giving us 12 triangles that will form a cube. The format is very straight forward and can be read directly into our vertex buffers and rendered without any modifications.

Now one thing to watch out for is that some 3D modeling programs export the data in different orders such as left-hand or right-hand coordinate systems. We have setup OpenGL 4.0 to use a left-handed coordinate system and so the model data needs to match that. Keep an eye out for those differences and ensure your parsing program can handle converting data into the correct format/order.


Modelclass.h

For this tutorial all we needed to do was make some minor changes to the ModelClass for it to render 3D models from our text model files.

////////////////////////////////////////////////////////////////////////////////
// Filename: modelclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_

Note the fstream library has always been included to handle easy reading from text files.

//////////////
// INCLUDES //
//////////////
#include <fstream>
using namespace std;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class Name: ModelClass
////////////////////////////////////////////////////////////////////////////////
class ModelClass
{
private:
    struct VertexType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
    };

The first change is the addition of a new structure to represent the model format. It is called ModelType. It contains position, texture, and normal vectors the same as our file format does.

    struct ModelType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
    };

public:
    ModelClass();
    ModelClass(const ModelClass&);
    ~ModelClass();

The Initialize function will now take as input the character string file name of the model to be loaded.

    bool Initialize(OpenGLClass*, char*, char*, bool);
    void Shutdown();
    void Render();

    void SetTexture(unsigned int);

private:
    bool InitializeBuffers();
    void ShutdownBuffers();
    void RenderBuffers();

    bool LoadTexture(char*, bool);
    void ReleaseTexture();

We also have two new functions to handle loading and unloading the model data from the text file.

    bool LoadModel(char*);
    void ReleaseModel();

private:
    OpenGLClass* m_OpenGLPtr;
    int m_vertexCount, m_indexCount;
    unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
    TextureClass* m_Texture;

The final change is a new private variable called m_model which is going to be an array of the new private structure ModelType. This variable will be used to read in and hold the model data before it is placed in the vertex buffer.

    ModelType* m_model;
};

#endif

Modelclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: modelclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "modelclass.h"


ModelClass::ModelClass()
{
    m_OpenGLPtr = 0;
    m_Texture = 0;

The new model structure is set to null in the class constructor.

    m_model = 0;
}


ModelClass::ModelClass(const ModelClass& other)
{
}


ModelClass::~ModelClass()
{
}

The Initialize function now takes as input the file name of the model that should be loaded.

bool ModelClass::Initialize(OpenGLClass* OpenGL, char* modelFilename, char* textureFilename, bool wrap)
{
    bool result;


    // Store a pointer to the OpenGL object.
    m_OpenGLPtr = OpenGL;

In the Initialize function we now call the new LoadModel function first. It will load the model data from the file name we provide into the new m_model array. Once this model array is filled, we can then build the vertex and index buffers from it. Since InitializeBuffers now depends on this model data you have to make sure to call the functions in the correct order.

    // Load in the model data.
    result = LoadModel(modelFilename);
    if(!result)
    {
        return false;
    }

    // Initialize the vertex and index buffer that hold the geometry for the model.
    result = InitializeBuffers();
    if(!result)
    {
        return false;
    }

    // Load the texture for this model.
    result = LoadTexture(textureFilename, wrap);
    if(!result)
    {
        return false;
    }

    return true;
}


void ModelClass::Shutdown()
{
    // Release the texture used for this model.
    ReleaseTexture();

    // Release the vertex and index buffers.
    ShutdownBuffers();

In the Shutdown function we add a call to the ReleaseModel function to delete the m_model array data once we are done.

    // Release the model data.
    ReleaseModel();

    // Release the pointer to the OpenGL object.
    m_OpenGLPtr = 0;

    return;
}


void ModelClass::Render()
{
    // Put the vertex and index buffers on the graphics pipeline to prepare them for drawing.
    RenderBuffers();

    return;
}


bool ModelClass::InitializeBuffers()
{
    VertexType* vertices;
    unsigned int* indices;
    int i;

Take note that we will no longer manually set the vertex and index count here. Once we get to the ModelClass::LoadModel function you will see that we read the vertex and index counts in at that point instead.

    // Create the vertex array.
    vertices = new VertexType[m_vertexCount];

    // Create the index array.
    indices = new unsigned int[m_indexCount];

Loading the vertex and index arrays has changed a bit. Instead of setting the values manually we loop through all the elements in the new m_model array and copy that data from there into the vertex array. The index array is easy to build as each vertex we load has the same index number as the position in the array it was loaded into.

    // Load the vertex array and index array with data.
    for(i=0; i<m_vertexCount; i++)
    {
        vertices[i].x  = m_model[i].x;
        vertices[i].y  = m_model[i].y;
        vertices[i].z  = m_model[i].z;
        vertices[i].tu = m_model[i].tu;
        vertices[i].tv = m_model[i].tv;
        vertices[i].nx = m_model[i].nx;
        vertices[i].ny = m_model[i].ny;
        vertices[i].nz = m_model[i].nz;

        indices[i] = i;
    }

    // Allocate an OpenGL vertex array object.
    m_OpenGLPtr->glGenVertexArrays(1, &m_vertexArrayId);

    // Bind the vertex array object to store all the buffers and vertex attributes we create here.
    m_OpenGLPtr->glBindVertexArray(m_vertexArrayId);

    // Generate an ID for the vertex buffer.
    m_OpenGLPtr->glGenBuffers(1, &m_vertexBufferId);

    // Bind the vertex buffer and load the vertex (position and color) data into the vertex buffer.
    m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
    m_OpenGLPtr->glBufferData(GL_ARRAY_BUFFER, m_vertexCount * sizeof(VertexType), vertices, GL_STATIC_DRAW);

    // Enable the three vertex array attributes.
    m_OpenGLPtr->glEnableVertexAttribArray(0);  // Vertex position.
    m_OpenGLPtr->glEnableVertexAttribArray(1);  // Texture coordinates.
    m_OpenGLPtr->glEnableVertexAttribArray(2);  // Normals

    // Specify the location and format of the position portion of the vertex buffer.
    m_OpenGLPtr->glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(VertexType), 0);

    // Specify the location and format of the texture coordinates portion of the vertex buffer.
    m_OpenGLPtr->glVertexAttribPointer(1, 2, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (3 * sizeof(float)));

    // Specify the location and format of the normal vector portion of the vertex buffer.
    m_OpenGLPtr->glVertexAttribPointer(2, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (5 * sizeof(float)));

    // Generate an ID for the index buffer.
    m_OpenGLPtr->glGenBuffers(1, &m_indexBufferId);

    // Bind the index buffer and load the index data into it.
    m_OpenGLPtr->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
    m_OpenGLPtr->glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount* sizeof(unsigned int), indices, GL_STATIC_DRAW);

    // Now that the buffers have been loaded we can release the array data.
    delete [] vertices;
    vertices = 0;

    delete [] indices;
    indices = 0;

    return true;
}


void ModelClass::ShutdownBuffers()
{
    // Release the vertex array object.
    m_OpenGLPtr->glBindVertexArray(0);
    m_OpenGLPtr->glDeleteVertexArrays(1, &m_vertexArrayId);

    // Release the vertex buffer.
    m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, 0);
    m_OpenGLPtr->glDeleteBuffers(1, &m_vertexBufferId);

    // Release the index buffer.
    m_OpenGLPtr->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    m_OpenGLPtr->glDeleteBuffers(1, &m_indexBufferId);

    return;
}


void ModelClass::RenderBuffers()
{
    // Bind the vertex array object that stored all the information about the vertex and index buffers.
    m_OpenGLPtr->glBindVertexArray(m_vertexArrayId);

    // Render the vertex buffer using the index buffer.
    glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);

    return;
}


bool ModelClass::LoadTexture(char* textureFilename, bool wrap)
{
    bool result;


    // Create and initialize the texture object.
    m_Texture = new TextureClass;

    result = m_Texture->Initialize(m_OpenGLPtr, textureFilename, 0, wrap);
    if(!result)
    {
        return false;
    }

    return true;
}


void ModelClass::ReleaseTexture()
{
    // Release the texture object.
    if(m_Texture)
    {
        m_Texture->Shutdown();
        delete m_Texture;
        m_Texture = 0;
    }

    return;
}


void ModelClass::SetTexture(unsigned int textureUnit)
{
    // Set the texture for the model.
    m_Texture->SetTexture(m_OpenGLPtr, textureUnit);

    return;
}

This is the new LoadModel function which handles loading the model data from the text file into the m_model array variable. It opens the text file and reads in the vertex count first. After reading the vertex count it creates the ModelType array and then reads each line into the array. Both the vertex count and index count are now set in this function.

bool ModelClass::LoadModel(char* filename)
{
    ifstream fin;
    char input;
    int i;


    // Open the model file.
    fin.open(filename);

    // If it could not open the file then exit.
    if(fin.fail())
    {
        return false;
    }

    // Read up to the value of vertex count.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }

    // Read in the vertex count.
    fin >> m_vertexCount;

    // Set the number of indices to be the same as the vertex count.
    m_indexCount = m_vertexCount;

    // Create the model using the vertex count that was read in.
    m_model = new ModelType[m_vertexCount];

    // Read up to the beginning of the data.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }
    fin.get(input);
    fin.get(input);

    // Read in the vertex data.
    for(i=0; i<m_vertexCount; i++)
    {
        fin >> m_model[i].x >> m_model[i].y >> m_model[i].z;
        fin >> m_model[i].tu >> m_model[i].tv;
        fin >> m_model[i].nx >> m_model[i].ny >> m_model[i].nz;

        // Invert the V coordinate to match the OpenGL texture coordinate system.
        m_model[i].tv = 1.0f - m_model[i].tv;
    }

    // Close the model file.
    fin.close();

    return true;
}

The ReleaseModel function handles deleting the model data array.

void ModelClass::ReleaseModel()
{
    if(m_model)
    {
        delete [] m_model;
        m_model = 0;
    }

    return;
}

Applicationclass.h

The header for the ApplicationClass has not changed since the previous tutorial.

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_NEAR = 0.3f;
const float SCREEN_DEPTH = 1000.0f;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "openglclass.h"
#include "modelclass.h"
#include "cameraclass.h"
#include "lightshaderclass.h"
#include "lightclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class Name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
    ApplicationClass();
    ApplicationClass(const ApplicationClass&);
    ~ApplicationClass();

    bool Initialize(Display*, Window, int, int);
    void Shutdown();
    bool Frame(InputClass*);

private:
    bool Render(float);

private:
    OpenGLClass* m_OpenGL;
    ModelClass* m_Model;
    CameraClass* m_Camera;
    LightShaderClass* m_LightShader;
    LightClass* m_Light;
};

#endif

Applicationclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "applicationclass.h"


ApplicationClass::ApplicationClass()
{
    m_OpenGL = 0;
    m_Model = 0;
    m_Camera = 0;
    m_LightShader = 0;
    m_Light = 0;
}


ApplicationClass::ApplicationClass(const ApplicationClass& other)
{
}


ApplicationClass::~ApplicationClass()
{
}


bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
    char modelFilename[128];
    char textureFilename[128];
    bool result;


    // Create and initialize the OpenGL object.
    m_OpenGL = new OpenGLClass;

    result = m_OpenGL->Initialize(display, win, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, VSYNC_ENABLED);
    if(!result)
    {
        return false;
    }

    // Create and initialize the camera object.
    m_Camera = new CameraClass;

    m_Camera->SetPosition(0.0f, 0.0f, -5.0f);
    m_Camera->Render();

We set the path to the cube model file here.

    // Set the file name of the model.
    strcpy(modelFilename, "../Engine/data/cube.txt");

    // Set the file name of the texture.
    strcpy(textureFilename, "../Engine/data/stone01.tga");

    // Create and initialize the model object.
    m_Model = new ModelClass;

The model initialization now takes in the filename of the model file it is loading. In this tutorial we will use the cube.txt file so this model loads in a 3D cube object for rendering.

    result = m_Model->Initialize(m_OpenGL, modelFilename, textureFilename, false);
    if(!result)
    {
        return false;
    }

    // Create and initialize the light shader object.
    m_LightShader = new LightShaderClass;

    result = m_LightShader->Initialize(m_OpenGL);
    if(!result)
    {
        return false;
    }

    // Create and initialize the light object.
    m_Light = new LightClass;

    m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
    m_Light->SetDirection(0.0f, 0.0f, 1.0f);

    return true;
}


void ApplicationClass::Shutdown()
{
    // Release the light object.
    if(m_Light)
    {
        delete m_Light;
        m_Light = 0;
    }

    // Release the light shader object.
    if(m_LightShader)
    {
        m_LightShader->Shutdown();
        delete m_LightShader;
        m_LightShader = 0;
    }

    // Release the model object.
    if(m_Model)
    {
        m_Model->Shutdown();
        delete m_Model;
        m_Model = 0;
    }

    // Release the camera object.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    // Release the OpenGL object.
    if(m_OpenGL)
    {
        m_OpenGL->Shutdown();
        delete m_OpenGL;
        m_OpenGL = 0;
    }

    return;
}


bool ApplicationClass::Frame(InputClass* Input)
{
    static float rotation = 360.0f;
    bool result;


    // Check if the escape key has been pressed, if so quit.
    if(Input->IsEscapePressed() == true)
    {
        return false;
    }

    // Update the rotation variable each frame.
    rotation -= 0.0174532925f * 1.0f;
    if(rotation <= 0.0f)
    {
        rotation += 360.0f;
    }

    // Render the graphics scene.
    result = Render(rotation);
    if(!result)
    {
        return false;
    }

    return true;
}


bool ApplicationClass::Render(float rotation)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16];
    float diffuseLightColor[4], lightDirection[3];
    bool result;


    // Clear the buffers to begin the scene.
    m_OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the world, view, and projection matrices from the opengl and camera objects.
    m_OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_OpenGL->GetProjectionMatrix(projectionMatrix);

    // Rotate the world matrix by the rotation value so that the triangle will spin.
    m_OpenGL->MatrixRotationY(worldMatrix, rotation);

    // Get the light properties.
    m_Light->GetDirection(lightDirection);
    m_Light->GetDiffuseColor(diffuseLightColor);

    // Set the light shader as the current shader program and set the matrices that it will use for rendering.
    result = m_LightShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor);
    if(!result)
    {
        return false;
    }

    // Set the texture for the model in the pixel shader.
    m_Model->SetTexture(0);

    // Render the model.
    m_Model->Render();

    // Present the rendered scene to the screen.
    m_OpenGL->EndScene();

    return true;
}

Summary

With the changes to the ModelClass we can now load in 3D models and render them. The format used here is just for basic static objects with lighting, however it is a good start to understanding how model formats work.


To Do Exercises

1. Recompile the code and run the program. You should get a rotating cube with the stone texture on it. Press escape to quit once done.

2. Find a decent 3D modeling package (hopefully something free) and create your own simple models and export them. Start looking at the format.

3. Write a simple parser program that takes the model exports and converts it to the format used here. Replace cube.txt with your model and run the program.


Source Code

Source Code and Data Files: gl4linuxtut07_src.tar.gz

Back to Tutorial Index