Tutorial 8: Terrain Nodes

One of the issues with large data sets is that you eventually need to create a partitioning scheme to deal with and process the data efficiently. When we start rendering several kilometers of highly detailed terrain we find ourselves in that situation. Just one square kilometer is already over two million polygons, and ideally we would like to render way more than one kilometer.

The first step to prepare for partitioning is to subdivide the terrain it into evenly sized nodes (or cells).

Picking the right amount of data to store in each node is also key to good performance. Generally you don't want terrain nodes to be too large as the purpose of partitioning is so that we can do math on a smaller subset of polygons. You also don't want the nodes too small that the search to find the right node is also causing a performance hit. The key is to make the node size adjustable so that you can quickly change the size and see the change in performance. This will help you quickly determine the correct node size for your purposes.

Now do note that once you have figured out your node size you need to lock that in permanently. A lot of other art resources and systems are going to be dependent on the node size, and future changes can be very expensive in rebuilding those resources if you change your node size in the future.

In this tutorial we will be splitting our terrain up into 65x65 vertex nodes. This is a common sized often used in the industry. The following screenshot shows just three of the 65x65 terrain nodes rendered with an yellow bounding box around each:

Now if we render those same three nodes in wireframe you can see that each node has 64 quads inside of it. And those 64 quads are composed of a 65x65 vertex set:

And finally if we draw the entire terrain rendering it cell by cell with the yellow bounding box we can see our fully partitioned terrain:

Note that the primary reason for splitting the terrain up into nodes is so that we can achieve rendering efficiency by determining and only drawing what should be visible. However, this tutorial will just be focused around getting the basic node rendering scheme in place.


Framework

The framework for this tutorial has a new class called TerrainNodeClass which will handle storing the data for the node as well as rendering it. The ColorShaderClass has also been added to render the yellow bounding boxes.

We will start the code section by looking at the modified TerrainClass.


Terrainclass.h

We have made some major changes to the TerrainClass. First thing is that we have included the header for the new TerrainNodeClass. We also have a new private array of terrain nodes. And we also have initialization and shutdown functions for the terrain nodes. The largest change is that we no longer build the vertex and index buffer for the terrain in this class, all of that functionality has been moved into the TerrainNodeClass. The TerrainClass still reads the height map and builds the terrain model, but we now send the terrain model to the nodes so that each node can construct its own buffers for rendering the portion of terrain assigned to it. Once the nodes are loaded the TerrainClass will release the terrain model data as it is no longer needed in the TerrainClass.

////////////////////////////////////////////////////////////////////////////////
// Filename: terrainclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TERRAINCLASS_H_
#define _TERRAINCLASS_H_


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


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

We have included the header for the new TerrainNodeClass.

#include "terrainnodeclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: TerrainClass
////////////////////////////////////////////////////////////////////////////////
class TerrainClass
{
private:
    struct VertexType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
        float tx, ty, tz;
        float bx, by, bz;
        float r, g, b;
    };

    struct HeightMapType
    {
        float x, y, z;
        float nx, ny, nz;
        float r, g, b;
    };

    struct ModelType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
        float tx, ty, tz;
        float bx, by, bz;
        float r, g, b;
    };

    struct VectorType
    {
        float x, y, z;
    };

    struct TargaHeader
    {
        unsigned char data1[12];
        unsigned short width;
        unsigned short height;
        unsigned char bpp;
        unsigned char data2;
    };

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

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

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

private:
    bool LoadSetupFile(char*, char*, float&, char*, char*, char*);
    bool LoadRawHeightMap(char*);
    void SetTerrainCoordinates(float);
    void CalculateNormals();
    bool LoadColorMap(char*);
    void BuildTerrainModel();
    void ReleaseHeightMap();
    void ReleaseTerrainModel();

    void CalculateTerrainVectors();
    void CalculateTangentBinormal(TempVertexType, TempVertexType, TempVertexType, VectorType&, VectorType&);

These are the two new functions that will create and load the terrain nodes, as well as releasing the nodes once we are done using them.

    bool LoadTerrainNodes(OpenGLClass*);
    void ReleaseTerrainNodes(OpenGLClass*);

private:
    int m_vertexCount;
    int m_terrainHeight, m_terrainWidth;
    HeightMapType* m_heightMap;
    ModelType* m_terrainModel;

This is the new terrain node array and a count variable to keep track of how many nodes are in the array.

    TerrainNodeClass* m_Nodes;
    int m_nodeCount;
    TextureClass *m_Texture, *m_NormalMap;
};

#endif

Terrainclass.cpp

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

Initialize the new terrain node array pointer to null in the class constructor.

TerrainClass::TerrainClass()
{
    m_heightMap = 0;
    m_terrainModel = 0;
    m_Nodes = 0;
    m_Texture = 0;
    m_NormalMap = 0;
}


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


TerrainClass::~TerrainClass()
{
}


bool TerrainClass::Initialize(OpenGLClass* OpenGL, char* setupFilename)
{
    char terrainFilename[256], textureFilename[256], colorMapFilename[256], normalFilename[256];
    float heightScale;
    bool result;


    // Get the terrain filename, dimensions, and so forth from the setup file.
    result = LoadSetupFile(setupFilename, terrainFilename, heightScale, textureFilename, colorMapFilename, normalFilename);
    if(!result)
    {
        return false;
    }

    // Initialize the terrain height map with the data from the raw file.
    result = LoadRawHeightMap(terrainFilename);
    if(!result)
    {
        return false;
    }

    // Setup the X and Z coordinates for the height map as well as scale the terrain height by the height scale value.
    SetTerrainCoordinates(heightScale);

    // Calculate the normals for the terrain data.
    CalculateNormals();

    // Load in the color map for the terrain.
    result = LoadColorMap(colorMapFilename);
    if(!result)
    {
        return false;
    }

    // Now build the 3D model of the terrain.
    BuildTerrainModel();

    // We can now release the height map since it is no longer needed in memory once the 3D terrain model has been built.
    ReleaseHeightMap();

    // Calculate the tangent and binormal for the terrain model.
    CalculateTerrainVectors();

Instead of creating vertex and index buffers for the whole terrain we now instead load the terrain model into the terrain node array. This function will also create the array of terrain nodes before loading them with the terrain model data.

    // Copy the terrain model data into the individual terrain nodes.
    result = LoadTerrainNodes(OpenGL);
    if(!result)
    {
        return false;
    }

    // Release the terrain model now that the nodes have been loaded.
    ReleaseTerrainModel();

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

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

    // Create and initialize the normal map texture object.
    m_NormalMap = new TextureClass;

    result = m_NormalMap->Initialize(OpenGL, normalFilename, false);
    if(!result)
    {
        return false;
    }

    return true;
}


void TerrainClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the normal map texture object.
    if(m_NormalMap)
    {
        m_NormalMap->Shutdown();
        delete m_NormalMap;
        m_NormalMap = 0;
    }

    // Release the diffuse texture object.
    if(m_Texture)
    {
        m_Texture->Shutdown();
        delete m_Texture;
        m_Texture = 0;
    }

The Shutdown function now calls the new ShutdownTerrainNodes function to release the terrain node data when we are done using it.

    // Release the terrain nodes.
    ReleaseTerrainNodes(OpenGL);

    return;
}


bool TerrainClass::Render(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, LightClass* Light, float* worldMatrix, float* viewMatrix, float* projectionMatrix)
{
    float diffuseLightColor[4], lightDirection[3];
    int i;
    bool result, wireFrame, renderNodeLines;


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

    // Set wireframe mode off.
    wireFrame = false;

    // Set node bounding box line rendering on.
    renderNodeLines = true;

    // Enable wireframe mode to see triangles composing the terrain clearly.
    if(wireFrame)
    {
        OpenGL->EnableWireframe();
    }

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

    // Set the diffuse texture for the terrain in the pixel shader texture unit 0.
    m_Texture->SetTexture(OpenGL, 0);

    // Set the normal map texture for the terrain in the pixel shader texture unit 1.
    m_NormalMap->SetTexture(OpenGL, 1);

Instead of rendering the entire terrain in a single vertex and index buffer, we now instead render the array of terrain node objects.

    // Put the node vertex and index buffers on the graphics pipeline to prepare them for drawing.
    for(i=0; i<m_nodeCount; i++)
    {
        m_Nodes[i].Render(OpenGL);
    }

    // Disable wireframe mode after rendering terrain.
    if(wireFrame)
    {
        OpenGL->DisableWireframe();
    }

We also render the yellow bounding boxes around each node using the color shader. This is very useful for debugging.

    // Render the node bounding box lines.
    if(renderNodeLines)
    {
        result = ShaderManager->RenderColorShader(worldMatrix, viewMatrix, projectionMatrix);
        if(!result)
        {
            return false;
        }

        for(i=0; i<m_nodeCount; i++)
        {
            m_Nodes[i].RenderLines(OpenGL);
        }
    }

    return true;
}


bool TerrainClass::LoadSetupFile(char* filename, char* terrainFilename, float& heightScale, char* textureFilename, char* colorMapFilename, char* normalFilename)
{
    ifstream fin;
    char input;


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

    // Read up to the terrain file name.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }

    // Read in the terrain file name.
    fin >> terrainFilename;

    // Read up to the color map file name.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }

    // Read in the color map file name.
    fin >> colorMapFilename;

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

    // Read in the terrain height.
    fin >> m_terrainHeight;

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

    // Read in the terrain width.
    fin >> m_terrainWidth;

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

    // Read in the terrain height scaling.
    fin >> heightScale;

    // Read up to the texture file name.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }

    // Read in the texture file name.
    fin >> textureFilename;

    // Read up to the normal map texture file name.
    fin.get(input);
    while(input != ':')
    {
        fin.get(input);
    }

    // Read in the normal map file name.
    fin >> normalFilename;

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

    return true;
}


bool TerrainClass::LoadRawHeightMap(char* terrainFilename)
{
    FILE* filePtr;
    unsigned short* rawImage;
    unsigned int imageSize, count;
    int error, i, j, index;


    // Create the float array to hold the height map data.
    m_heightMap = new HeightMapType[m_terrainWidth * m_terrainHeight];

    // Open the 16 bit raw height map file for reading in binary.
    filePtr = fopen(terrainFilename, "rb");
    if(filePtr == NULL)
    {
        return false;
    }

    // Calculate the size of the raw image data.
    imageSize = m_terrainHeight * m_terrainWidth;

    // Allocate memory for the raw image data.
    rawImage = new unsigned short[imageSize];

    // Read in the raw image data.
    count = fread(rawImage, sizeof(unsigned short), imageSize, filePtr);
    if(count != imageSize)
    {
        return false;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        return false;
    }

    // Copy the image data into the height map array.
    for(j=0; j<m_terrainHeight; j++)
    {
        for(i=0; i<m_terrainWidth; i++)
        {
            index = (m_terrainWidth * j) + i;

            // Store the height at this point in the height map array.
            m_heightMap[index].y = (float)rawImage[index];
        }
    }

    // Release the bitmap image data.
    delete [] rawImage;
    rawImage = 0;

    return true;
}


void TerrainClass::SetTerrainCoordinates(float heightScale)
{
    int i, j, index;


    // Loop through all the elements in the height map array and adjust their coordinates correctly.
    for(j=0; j<m_terrainHeight; j++)
    {
        for(i=0; i<m_terrainWidth; i++)
        {
            index = (m_terrainWidth * j) + i;

            // Set the X and Z coordinates.
            m_heightMap[index].x = (float)i;
            m_heightMap[index].z = -(float)j;

            // Move the terrain depth into the positive range.  For example from (0, -256) to (256, 0).
            m_heightMap[index].z += (float)(m_terrainHeight - 1);

            // Scale the height.
            m_heightMap[index].y /= heightScale;
        }
    }

    return;
}


void TerrainClass::CalculateNormals()
{
    int i, j, index1, index2, index3, index;
    float vertex1[3], vertex2[3], vertex3[3], vector1[3], vector2[3], sum[3], length;
    VectorType* normals;


    // Create a temporary array to hold the face normal vectors.
    normals = new VectorType[(m_terrainHeight-1) * (m_terrainWidth-1)];

    // Go through all the faces in the mesh and calculate their normals.
    for(j=0; j<(m_terrainHeight-1); j++)
    {
        for(i=0; i<(m_terrainWidth-1); i++)
        {
            index1 = ((j+1) * m_terrainWidth) + i;      // Bottom left vertex.
            index2 = ((j+1) * m_terrainWidth) + (i+1);  // Bottom right vertex.
            index3 = (j * m_terrainWidth) + i;          // Upper left vertex.

            // Get three vertices from the face.
            vertex1[0] = m_heightMap[index1].x;
            vertex1[1] = m_heightMap[index1].y;
            vertex1[2] = m_heightMap[index1].z;

            vertex2[0] = m_heightMap[index2].x;
            vertex2[1] = m_heightMap[index2].y;
            vertex2[2] = m_heightMap[index2].z;

            vertex3[0] = m_heightMap[index3].x;
            vertex3[1] = m_heightMap[index3].y;
            vertex3[2] = m_heightMap[index3].z;

            // Calculate the two vectors for this face.
            vector1[0] = vertex1[0] - vertex3[0];
            vector1[1] = vertex1[1] - vertex3[1];
            vector1[2] = vertex1[2] - vertex3[2];
            vector2[0] = vertex3[0] - vertex2[0];
            vector2[1] = vertex3[1] - vertex2[1];
            vector2[2] = vertex3[2] - vertex2[2];

            index = (j * (m_terrainWidth - 1)) + i;

            // Calculate the cross product of those two vectors to get the un-normalized value for this face normal.
            normals[index].x = (vector1[1] * vector2[2]) - (vector1[2] * vector2[1]);
            normals[index].y = (vector1[2] * vector2[0]) - (vector1[0] * vector2[2]);
            normals[index].z = (vector1[0] * vector2[1]) - (vector1[1] * vector2[0]);

            // Calculate the length.
            length = (float)sqrt((normals[index].x * normals[index].x) + (normals[index].y * normals[index].y) +
                     (normals[index].z * normals[index].z));

            // Normalize the final value for this face using the length.
            normals[index].x = (normals[index].x / length);
            normals[index].y = (normals[index].y / length);
            normals[index].z = (normals[index].z / length);
        }
    }

    // Now go through all the vertices and take a sum of the face normals that touch this vertex.
    for(j=0; j<m_terrainHeight; j++)
    {
        for(i=0; i<m_terrainWidth; i++)
        {
            // Initialize the sum.
            sum[0] = 0.0f;
            sum[1] = 0.0f;
            sum[2] = 0.0f;

            // Bottom left face.
            if(((i-1) >= 0) && ((j-1) >= 0))
            {
                index = ((j-1) * (m_terrainWidth-1)) + (i-1);

                sum[0] += normals[index].x;
                sum[1] += normals[index].y;
                sum[2] += normals[index].z;
            }

            // Bottom right face.
            if((i<(m_terrainWidth-1)) && ((j-1) >= 0))
            {
                index = ((j - 1) * (m_terrainWidth - 1)) + i;

                sum[0] += normals[index].x;
                sum[1] += normals[index].y;
                sum[2] += normals[index].z;
            }

            // Upper left face.
            if(((i-1) >= 0) && (j<(m_terrainHeight-1)))
            {
                index = (j * (m_terrainWidth-1)) + (i-1);

                sum[0] += normals[index].x;
                sum[1] += normals[index].y;
                sum[2] += normals[index].z;
            }

            // Upper right face.
            if((i < (m_terrainWidth-1)) && (j < (m_terrainHeight-1)))
            {
                index = (j * (m_terrainWidth-1)) + i;

                sum[0] += normals[index].x;
                sum[1] += normals[index].y;
                sum[2] += normals[index].z;
            }

            // Calculate the length of this normal.
            length = (float)sqrt((sum[0] * sum[0]) + (sum[1] * sum[1]) + (sum[2] * sum[2]));

            // Get an index to the vertex location in the height map array.
            index = (j * m_terrainWidth) + i;

            // Normalize the final shared normal for this vertex and store it in the height map array.
            m_heightMap[index].nx = (sum[0] / length);
            m_heightMap[index].ny = (sum[1] / length);
            m_heightMap[index].nz = (sum[2] / length);
        }
    }

    // Release the temporary normals.
    delete [] normals;
    normals = 0;

    return;
}


bool TerrainClass::LoadColorMap(char* colorMapFilename)
{
    FILE* filePtr;
    TargaHeader targaFileHeader;
    unsigned char* targaImage;
    unsigned long count, imageSize;
    int height, width, bpp, error, i, j, k, index;


    // Open the color map file in binary.
    filePtr = fopen(colorMapFilename, "rb");
    if(filePtr == NULL)
    {
        return false;
    }

    // Read in the file header.
    count = fread(&targaFileHeader, sizeof(TargaHeader), 1, filePtr);
    if(count != 1)
    {
        return false;
    }

    // Get the important information from the header.
    width = (int)targaFileHeader.width;
    height = (int)targaFileHeader.height;
    bpp = (int)targaFileHeader.bpp;

    // Make sure the color map dimensions are the same as the terrain dimensions for easy 1 to 1 mapping.
    if((height != m_terrainHeight) || (width != m_terrainWidth))
    {
        return false;
    }

    // Make sure we are dealing with 24 bit color maps.
    if(bpp != 24)
    {
        return false;
    }

    // Calculate the size of the 24 bit targa image data.
    imageSize = width * height * 3;

    // Allocate memory for the targa image data.
    targaImage = new unsigned char[imageSize];

    // Read in the targa image data.
    count = fread(targaImage, 1, imageSize, filePtr);
    if(count != imageSize)
    {
        return false;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        return false;
    }

    // Initialize the position in the targa image data buffer.
    k=0;

    // Read the image data into the color map portion of the height map structure.
    for(j=0; j<m_terrainHeight; j++)
    {
        for(i=0; i<m_terrainWidth; i++)
        {
            // Targa are saved upside down by default in most image editors, so load bottom to top into the array.
            index = (m_terrainWidth * (m_terrainHeight - 1 - j)) + i;

            m_heightMap[index].r = (float)targaImage[k+2] / 255.0f;
            m_heightMap[index].g = (float)targaImage[k+1] / 255.0f;
            m_heightMap[index].b = (float)targaImage[k+0] / 255.0f;
            k+=3;
        }
    }

    // Release the targa image data.
    delete [] targaImage;
    targaImage = 0;

    return true;
}


void TerrainClass::BuildTerrainModel()
{
    int i, j, index, index1, index2, index3, index4;


    // Calculate the number of vertices in the 3D terrain model.
    m_vertexCount = (m_terrainHeight - 1) * (m_terrainWidth - 1) * 6;

    // Create the 3D terrain model array.
    m_terrainModel = new ModelType[m_vertexCount];

    // Initialize the index into the height map array.
    index = 0;

    // Load the 3D terrain model with the height map terrain data.
    // We will be creating 2 triangles for each of the four points in a quad.
    for(j=0; j<(m_terrainHeight-1); j++)
    {
        for(i=0; i<(m_terrainWidth-1); i++)
        {
            // Get the indexes to the four points of the quad.
            index1 = (m_terrainWidth * j) + i;          // Upper left.
            index2 = (m_terrainWidth * j) + (i+1);      // Upper right.
            index3 = (m_terrainWidth * (j+1)) + i;      // Bottom left.
            index4 = (m_terrainWidth * (j+1)) + (i+1);  // Bottom right.

            // Now create two triangles for that quad.
            // Triangle 1 - Upper left.
            m_terrainModel[index].x = m_heightMap[index1].x;
            m_terrainModel[index].y = m_heightMap[index1].y;
            m_terrainModel[index].z = m_heightMap[index1].z;
            m_terrainModel[index].tu = 0.0f;
            m_terrainModel[index].tv = 1.0f;
            m_terrainModel[index].nx = m_heightMap[index1].nx;
            m_terrainModel[index].ny = m_heightMap[index1].ny;
            m_terrainModel[index].nz = m_heightMap[index1].nz;
            m_terrainModel[index].r = m_heightMap[index1].r;
            m_terrainModel[index].g = m_heightMap[index1].g;
            m_terrainModel[index].b = m_heightMap[index1].b;
            index++;

            // Triangle 1 - Upper right.
            m_terrainModel[index].x = m_heightMap[index2].x;
            m_terrainModel[index].y = m_heightMap[index2].y;
            m_terrainModel[index].z = m_heightMap[index2].z;
            m_terrainModel[index].tu = 1.0f;
            m_terrainModel[index].tv = 1.0f;
            m_terrainModel[index].nx = m_heightMap[index2].nx;
            m_terrainModel[index].ny = m_heightMap[index2].ny;
            m_terrainModel[index].nz = m_heightMap[index2].nz;
            m_terrainModel[index].r = m_heightMap[index2].r;
            m_terrainModel[index].g = m_heightMap[index2].g;
            m_terrainModel[index].b = m_heightMap[index2].b;
            index++;

            // Triangle 1 - Bottom left.
            m_terrainModel[index].x = m_heightMap[index3].x;
            m_terrainModel[index].y = m_heightMap[index3].y;
            m_terrainModel[index].z = m_heightMap[index3].z;
            m_terrainModel[index].tu = 0.0f;
            m_terrainModel[index].tv = 0.0f;
            m_terrainModel[index].nx = m_heightMap[index3].nx;
            m_terrainModel[index].ny = m_heightMap[index3].ny;
            m_terrainModel[index].nz = m_heightMap[index3].nz;
            m_terrainModel[index].r = m_heightMap[index3].r;
            m_terrainModel[index].g = m_heightMap[index3].g;
            m_terrainModel[index].b = m_heightMap[index3].b;
            index++;

            // Triangle 2 - Bottom left.
            m_terrainModel[index].x = m_heightMap[index3].x;
            m_terrainModel[index].y = m_heightMap[index3].y;
            m_terrainModel[index].z = m_heightMap[index3].z;
            m_terrainModel[index].tu = 0.0f;
            m_terrainModel[index].tv = 0.0f;
            m_terrainModel[index].nx = m_heightMap[index3].nx;
            m_terrainModel[index].ny = m_heightMap[index3].ny;
            m_terrainModel[index].nz = m_heightMap[index3].nz;
            m_terrainModel[index].r = m_heightMap[index3].r;
            m_terrainModel[index].g = m_heightMap[index3].g;
            m_terrainModel[index].b = m_heightMap[index3].b;
            index++;

            // Triangle 2 - Upper right.
            m_terrainModel[index].x = m_heightMap[index2].x;
            m_terrainModel[index].y = m_heightMap[index2].y;
            m_terrainModel[index].z = m_heightMap[index2].z;
            m_terrainModel[index].tu = 1.0f;
            m_terrainModel[index].tv = 1.0f;
            m_terrainModel[index].nx = m_heightMap[index2].nx;
            m_terrainModel[index].ny = m_heightMap[index2].ny;
            m_terrainModel[index].nz = m_heightMap[index2].nz;
            m_terrainModel[index].r = m_heightMap[index2].r;
            m_terrainModel[index].g = m_heightMap[index2].g;
            m_terrainModel[index].b = m_heightMap[index2].b;
            index++;

            // Triangle 2 - Bottom right.
            m_terrainModel[index].x = m_heightMap[index4].x;
            m_terrainModel[index].y = m_heightMap[index4].y;
            m_terrainModel[index].z = m_heightMap[index4].z;
            m_terrainModel[index].tu = 1.0f;
            m_terrainModel[index].tv = 0.0f;
            m_terrainModel[index].nx = m_heightMap[index4].nx;
            m_terrainModel[index].ny = m_heightMap[index4].ny;
            m_terrainModel[index].nz = m_heightMap[index4].nz;
            m_terrainModel[index].r = m_heightMap[index4].r;
            m_terrainModel[index].g = m_heightMap[index4].g;
            m_terrainModel[index].b = m_heightMap[index4].b;
            index++;
        }
    }

    return;
}


void TerrainClass::ReleaseHeightMap()
{
    // Release the height map array.
    if(m_heightMap)
    {
        delete [] m_heightMap;
        m_heightMap = 0;
    }

    return;
}


void TerrainClass::ReleaseTerrainModel()
{
    // Release the terrain model data.
    if(m_terrainModel)
    {
        delete [] m_terrainModel;
        m_terrainModel = 0;
    }

    return;
}


void TerrainClass::CalculateTerrainVectors()
{
    int vertexCount, faceCount, i, index;
    TempVertexType vertex1, vertex2, vertex3;
    VectorType tangent, binormal;


    // Calculate the number of vertices in the terrain.
    vertexCount = (m_terrainHeight - 1) * (m_terrainWidth - 1) * 6;

    // Calculate the number of faces in the terrain model.
    faceCount = vertexCount / 3;

    // Initialize the index to the model data.
    index = 0;

    // Go through all the faces and calculate the the tangent, binormal, and normal vectors.
    for(i=0; i<faceCount; i++)
    {
        // Get the three vertices for this face from the terrain model.
        vertex1.x = m_terrainModel[index].x;
        vertex1.y = m_terrainModel[index].y;
        vertex1.z = m_terrainModel[index].z;
        vertex1.tu = m_terrainModel[index].tu;
        vertex1.tv = m_terrainModel[index].tv;
        vertex1.nx = m_terrainModel[index].nx;
        vertex1.ny = m_terrainModel[index].ny;
        vertex1.nz = m_terrainModel[index].nz;
        index++;

        vertex2.x = m_terrainModel[index].x;
        vertex2.y = m_terrainModel[index].y;
        vertex2.z = m_terrainModel[index].z;
        vertex2.tu = m_terrainModel[index].tu;
        vertex2.tv = m_terrainModel[index].tv;
        vertex2.nx = m_terrainModel[index].nx;
        vertex2.ny = m_terrainModel[index].ny;
        vertex2.nz = m_terrainModel[index].nz;
        index++;

        vertex3.x = m_terrainModel[index].x;
        vertex3.y = m_terrainModel[index].y;
        vertex3.z = m_terrainModel[index].z;
        vertex3.tu = m_terrainModel[index].tu;
        vertex3.tv = m_terrainModel[index].tv;
        vertex3.nx = m_terrainModel[index].nx;
        vertex3.ny = m_terrainModel[index].ny;
        vertex3.nz = m_terrainModel[index].nz;
        index++;

        // Calculate the tangent and binormal of that face.
        CalculateTangentBinormal(vertex1, vertex2, vertex3, tangent, binormal);

        // Store the tangent and binormal for this face back in the model structure.
        m_terrainModel[index-1].tx = tangent.x;
        m_terrainModel[index-1].ty = tangent.y;
        m_terrainModel[index-1].tz = tangent.z;
        m_terrainModel[index-1].bx = binormal.x;
        m_terrainModel[index-1].by = binormal.y;
        m_terrainModel[index-1].bz = binormal.z;

        m_terrainModel[index-2].tx = tangent.x;
        m_terrainModel[index-2].ty = tangent.y;
        m_terrainModel[index-2].tz = tangent.z;
        m_terrainModel[index-2].bx = binormal.x;
        m_terrainModel[index-2].by = binormal.y;
        m_terrainModel[index-2].bz = binormal.z;

        m_terrainModel[index-3].tx = tangent.x;
        m_terrainModel[index-3].ty = tangent.y;
        m_terrainModel[index-3].tz = tangent.z;
        m_terrainModel[index-3].bx = binormal.x;
        m_terrainModel[index-3].by = binormal.y;
        m_terrainModel[index-3].bz = binormal.z;
    }

    return;
}


void TerrainClass::CalculateTangentBinormal(TempVertexType vertex1, TempVertexType vertex2, TempVertexType vertex3, VectorType& tangent, VectorType& binormal)
{
    float vector1[3], vector2[3];
    float tuVector[2], tvVector[2];
    float den;
    float length;


    // Calculate the two vectors for this face.
    vector1[0] = vertex2.x - vertex1.x;
    vector1[1] = vertex2.y - vertex1.y;
    vector1[2] = vertex2.z - vertex1.z;

    vector2[0] = vertex3.x - vertex1.x;
    vector2[1] = vertex3.y - vertex1.y;
    vector2[2] = vertex3.z - vertex1.z;

    // Calculate the tu and tv texture space vectors.
    tuVector[0] = vertex2.tu - vertex1.tu;
    tvVector[0] = vertex2.tv - vertex1.tv;

    tuVector[1] = vertex3.tu - vertex1.tu;
    tvVector[1] = vertex3.tv - vertex1.tv;

    // Calculate the denominator of the tangent/binormal equation.
    den = 1.0f / (tuVector[0] * tvVector[1] - tuVector[1] * tvVector[0]);

    // Calculate the cross products and multiply by the coefficient to get the tangent and binormal.
    tangent.x = (tvVector[1] * vector1[0] - tvVector[0] * vector2[0]) * den;
    tangent.y = (tvVector[1] * vector1[1] - tvVector[0] * vector2[1]) * den;
    tangent.z = (tvVector[1] * vector1[2] - tvVector[0] * vector2[2]) * den;

    binormal.x = (tuVector[0] * vector2[0] - tuVector[1] * vector1[0]) * den;
    binormal.y = (tuVector[0] * vector2[1] - tuVector[1] * vector1[1]) * den;
    binormal.z = (tuVector[0] * vector2[2] - tuVector[1] * vector1[2]) * den;

    // Calculate the length of the tangent.
    length = (float)sqrt((tangent.x * tangent.x) + (tangent.y * tangent.y) + (tangent.z * tangent.z));

    // Normalize the tangent and then store it.
    tangent.x = tangent.x / length;
    tangent.y = tangent.y / length;
    tangent.z = tangent.z / length;

    // Calculate the length of the binormal.
    length = (float)sqrt((binormal.x * binormal.x) + (binormal.y * binormal.y) + (binormal.z * binormal.z));

    // Normalize the binormal and then store it.
    binormal.x = binormal.x / length;
    binormal.y = binormal.y / length;
    binormal.z = binormal.z / length;

    return;
}

LoadTerrainNodes is the function where we create the array of terrain node objects. We specify the size of the node (65x65 for this tutorial) and then create the array. Once the array is created we loop through it and initialize each node with its part of the terrain model. We send a pointer to the terrain model and indices (i and j) for the current position of the node so that it knows where to read in the data from the terrain model to build the current node.

bool TerrainClass::LoadTerrainNodes(OpenGLClass* OpenGL)
{
    int nodeWidth, arrayHeight, arrayWidth, i, j, index;


    // Set the width of the node.  64 +1 (0-64) since dealing with 1025 sized terrain and need even division.
    nodeWidth = 65;

    // Set size of the node array that we are breaking the terrain up into.  Must alight with the terrain size and the node width.
    arrayHeight = 16;
    arrayWidth = 16;

    // Set the number of nodes in the 2D array.
    m_nodeCount = arrayHeight * arrayWidth;

    // Create the terrain node array.
    m_Nodes = new TerrainNodeClass[m_nodeCount];

    // Loop through and initialize all the terrain nodes.
    for(j=0; j<arrayHeight; j++)
    {
        for(i=0; i<arrayWidth; i++)
        {
            index = (arrayWidth * j) + i;

            m_Nodes[index].Initialize(OpenGL, m_terrainModel, m_terrainWidth, nodeWidth, nodeWidth, i, j);
        }
    }

    return true;
}

The ShutdownTerrainNodes is used to shutdown each of the nodes and then release the array of terrain node objects.

void TerrainClass::ReleaseTerrainNodes(OpenGLClass* OpenGL)
{
    int i;


    // Release the terrain node array.
    if(m_Nodes)
    {
        for(i=0; i<m_nodeCount; i++)
        {
            m_Nodes[i].Shutdown(OpenGL);
        }

        delete [] m_Nodes;
        m_Nodes = 0;
    }

    return;
}

Terrainnodeclass.h

TerrainNodeClass is a new class that encapsulates the functionality of rendering and other calculations for individual terrain nodes. Each terrain node is created from a subset of the terrain model and represents a unique 65x65 vertex section of that terrain. The VertexType structure in this class definition must be the same as the one in the TerrainClass definition. We also use additional buffers to build the yellow line list bounding box around this node for debugging purposes. Pretty much everything related to rendering was just moved out of TerrainClass and placed in this new class.

////////////////////////////////////////////////////////////////////////////////
// Filename: terrainnodeclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TERRAINNODECLASS_H_
#define _TERRAINNODECLASS_H_


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


////////////////////////////////////////////////////////////////////////////////
// Class name: TerrainNodeClass
////////////////////////////////////////////////////////////////////////////////
class TerrainNodeClass
{
private:
    struct VertexType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
        float tx, ty, tz;
        float bx, by, bz;
        float r, g, b;
    };

    struct LineVertexType
    {
        float x, y, z;
        float r, g, b;
    };

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

    void Initialize(OpenGLClass*, void*, int, int, int, int, int);
    void Shutdown(OpenGLClass*);
    void Render(OpenGLClass*);

    void RenderLines(OpenGLClass*);

private:
    void InitializeBuffers(OpenGLClass*, VertexType*, int, int, int, int, int);
    void ShutdownBuffers(OpenGLClass*);
    void RenderBuffers(OpenGLClass*);

    void BuildLines(OpenGLClass*);
    void ReleaseLines(OpenGLClass*);

private:
    int m_indexCount;
    unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
    int m_indexCount2;
    unsigned int m_vertexArrayId2, m_vertexBufferId2, m_indexBufferId2;
    float m_maxX, m_maxY, m_maxZ, m_minX, m_minY, m_minZ;
};

#endif

Terrainnodeclass.cpp

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


TerrainNodeClass::TerrainNodeClass()
{
}


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


TerrainNodeClass::~TerrainNodeClass()
{
}

The Initialize function calls the functions that create the buffers for the terrain node, and builds the bounding box line buffers.

void TerrainNodeClass::Initialize(OpenGLClass* OpenGL, void* terrainModelPtr, int terrainWidth, int nodeWidth, int nodeHeight, int nodeIndexX, int nodeIndexY)
{
    VertexType* terrainModel;


    // Coerce the pointer to the terrain model data into the vertex type of the model.
    terrainModel = (VertexType*)terrainModelPtr;

    // Copy just the node subset from the terrain model data.
    InitializeBuffers(OpenGL, terrainModel, terrainWidth, nodeWidth, nodeHeight, nodeIndexX, nodeIndexY);

    // Release the pointer to the terrain model.
    terrainModel = 0;

    // Build the debug lines for the node's bounding box.
    BuildLines(OpenGL);

    return;
}

Shutdown will release the buffers used to render the node terrain data and the bounding box lines.

void TerrainNodeClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the debug bounding box lines rendering buffers.
    ReleaseLines(OpenGL);

    // Release the buffers used to render the terrain node.
    ShutdownBuffers(OpenGL);

    return;
}

The Render function will put the vertex and index buffer for this terrain node on the GPU for rendering.

void TerrainNodeClass::Render(OpenGLClass* OpenGL)
{
    // Render the terrain node's buffers.
    RenderBuffers(OpenGL);

    return;
}

InitializeBuffers creates the buffers used for rendering the terrain node. The terrain model that was built in the TerrainClass is passed into this function, and then an index into the terrain model is created based on the physical location of this cell using nodeIndexX and nodeIndexY. The dimensions of this node are also calculated in this function so that we can build the line bounding box in a seperate function after.

void TerrainNodeClass::InitializeBuffers(OpenGLClass* OpenGL, VertexType* terrainModel, int terrainWidth, int nodeHeight, int nodeWidth, int nodeIndexX, int nodeIndexY)
{
    VertexType* vertices;
    unsigned int* indices;
    int vertexCount, modelIndex, index, i, j;


    // Calculate the number of vertices in this terrain node.
    vertexCount = (nodeHeight - 1) * (nodeWidth - 1) * 6;

    // Set the index count to the same as the vertex count.
    m_indexCount = vertexCount;

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

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

    // Setup the indexes into the terrain model data we are using to build this smaller node subset.
    modelIndex = ((nodeIndexX * (nodeWidth - 1)) + (nodeIndexY * (nodeHeight - 1) * (terrainWidth - 1))) * 6;

    // Load the vertex array and index array with data.
    index = 0;
    for(j=0; j<(nodeHeight - 1); j++)
    {
        for(i=0; i<((nodeWidth - 1) * 6); i++)
        {
            vertices[index].x  = terrainModel[modelIndex].x;
            vertices[index].y  = terrainModel[modelIndex].y;
            vertices[index].z  = terrainModel[modelIndex].z;
            vertices[index].tu = terrainModel[modelIndex].tu;
            vertices[index].tv = terrainModel[modelIndex].tv;
            vertices[index].nx = terrainModel[modelIndex].nx;
            vertices[index].ny = terrainModel[modelIndex].ny;
            vertices[index].nz = terrainModel[modelIndex].nz;
            vertices[index].r  = terrainModel[modelIndex].r;
            vertices[index].g  = terrainModel[modelIndex].g;
            vertices[index].b  = terrainModel[modelIndex].b;
            vertices[index].tx = terrainModel[modelIndex].tx;
            vertices[index].ty = terrainModel[modelIndex].ty;
            vertices[index].tz = terrainModel[modelIndex].tz;
            vertices[index].bx = terrainModel[modelIndex].bx;
            vertices[index].by = terrainModel[modelIndex].by;
            vertices[index].bz = terrainModel[modelIndex].bz;
            indices[index] = index;
            modelIndex++;
            index++;
        }
        modelIndex += (terrainWidth * 6) - (nodeWidth * 6);
    }

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

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

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

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

    // Enable the vertex array attributes.
    OpenGL->glEnableVertexAttribArray(0);  // Vertex position.
    OpenGL->glEnableVertexAttribArray(1);  // Texture coordinates.
    OpenGL->glEnableVertexAttribArray(2);  // Normals.
    OpenGL->glEnableVertexAttribArray(3);  // Tangent
    OpenGL->glEnableVertexAttribArray(4);  // Binormal
    OpenGL->glEnableVertexAttribArray(5);  // Color.

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

    // Specify the location and format of the texture portion of the vertex buffer.
    OpenGL->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.
    OpenGL->glVertexAttribPointer(2, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (5 * sizeof(float)));

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

    // Specify the location and format of the binormal vector portion of the vertex buffer.
    OpenGL->glVertexAttribPointer(4, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (11 * sizeof(float)));

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

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

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

    // Calculate the node dimensions so we can build bounding box lines.
    m_maxX = vertices[0].x;
    m_minX = vertices[0].x;
    m_maxY = vertices[0].y;
    m_minY = vertices[0].y;
    m_maxZ = vertices[0].z;
    m_minZ = vertices[0].z;

    for(i=1; i<vertexCount; i++)
    {
        if(vertices[i].x > m_maxX)  { m_maxX = vertices[i].x; }
        if(vertices[i].x < m_minX)  { m_minX = vertices[i].x; }

        if(vertices[i].y > m_maxY)  { m_maxY = vertices[i].y; }
        if(vertices[i].y < m_minY)  { m_minY = vertices[i].y; }

        if(vertices[i].z > m_maxZ)  { m_maxZ = vertices[i].z; }
        if(vertices[i].z < m_minZ)  { m_minZ = vertices[i].z; }
    }

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

    delete [] indices;
    indices = 0;

    return;
}

The ShutdownBuffers releases the buffers used for rendering the terrain node.

void TerrainNodeClass::ShutdownBuffers(OpenGLClass* OpenGL)
{
    // Release the vertex array object.
    OpenGL->glBindVertexArray(0);
    OpenGL->glDeleteVertexArrays(1, &m_vertexArrayId);

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

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

    return;
}

RenderBuffers puts the terrain node vertex and index buffers on the GPU for rendering.

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

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

    return;
}

The BuildLines function creates the bounding box that surrounds the terrain node. It is made up of a series of lines creating a box around the exact dimensions of the terrain node. This is used for debugging purposes mostly.

void TerrainNodeClass::BuildLines(OpenGLClass* OpenGL)
{
    LineVertexType* vertices;
    unsigned int* indices;
    float red, green, blue;
    int vertexCount2, index;


    // Set the color of the bound box lines to yellow.
    red = 1.0f;
    green = 1.0f;
    blue = 0.0f;

    // Set the number of vertices for the 12 lines in the box.
    vertexCount2 = 24;

    // Set the index count to the same as the vertex count.
    m_indexCount2 = vertexCount2;

    // Create the vertex array.
    vertices = new LineVertexType[vertexCount2];

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

    // Initialize the index into the vertices.
    index = 0;

    // Base 4 lines.
    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    // Top 4 lines.
    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    // Vertical 4 lines.
    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_minZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_minX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_minY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

    vertices[index].x = m_maxX;
    vertices[index].y = m_maxY;
    vertices[index].z = m_maxZ;
    vertices[index].r = red;
    vertices[index].g = green;
    vertices[index].b = blue;
    indices[index] = index;
    index++;

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

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

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

    // Bind the vertex buffer and load the vertex data into the vertex buffer.
    OpenGL->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId2);
    OpenGL->glBufferData(GL_ARRAY_BUFFER, vertexCount2 * sizeof(LineVertexType), vertices, GL_STATIC_DRAW);

    // Enable the two vertex array attributes.
    OpenGL->glEnableVertexAttribArray(0);  // Vertex position.
    OpenGL->glEnableVertexAttribArray(1);  // Vertex color.

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

    // Specify the location and format of the color portion of the vertex buffer.
    OpenGL->glVertexAttribPointer(1, 3, GL_FLOAT, false, sizeof(LineVertexType), (unsigned char*)NULL + (3 * sizeof(float)));

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

    // Bind the index buffer and load the index data into it.
    OpenGL->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId2);
    OpenGL->glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount2 * 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;
}

The ReleaseLines function releases the vertex and index buffer that were used to render the bounding box around the terrain node.

void TerrainNodeClass::ReleaseLines(OpenGLClass* OpenGL)
{
    // Release the vertex array object.
    OpenGL->glBindVertexArray(0);
    OpenGL->glDeleteVertexArrays(1, &m_vertexArrayId2);

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

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

    return;
}

The RenderLines function puts the vertex and index buffers for the bounding box on the GPU for rendering. It is rendered as a list of twelve lines.

void TerrainNodeClass::RenderLines(OpenGLClass* OpenGL)
{
    // Bind the vertex array object that stored all the information about the vertex and index buffers.
    OpenGL->glBindVertexArray(m_vertexArrayId2);

    // Render the vertex buffer as lines using the index buffer.
    glDrawElements(GL_LINES, m_indexCount2, GL_UNSIGNED_INT, 0);

    return;
}

Shadermanagerclass.h

We have updated the ShaderManagerClass to have the ColorShaderClass. It will be used for rendering the yellow colored line bounding box around each terrain node.

////////////////////////////////////////////////////////////////////////////////
// Filename: shadermanagerclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SHADERMANAGERCLASS_H_
#define _SHADERMANAGERCLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "colorshaderclass.h"
#include "fontshaderclass.h"
#include "terrainshaderclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: ShaderManagerClass
////////////////////////////////////////////////////////////////////////////////
class ShaderManagerClass
{
public:
    ShaderManagerClass();
    ShaderManagerClass(const ShaderManagerClass&);
    ~ShaderManagerClass();

    bool Initialize(OpenGLClass*);
    void Shutdown();

    bool RenderColorShader(float*, float*, float*);
    bool RenderFontShader(float*, float*, float*, float*);
    bool RenderTerrainShader(float*, float*, float*, float*, float*);

private:
    ColorShaderClass* m_ColorShader;
    FontShaderClass* m_FontShader;
    TerrainShaderClass* m_TerrainShader;
};

#endif

Shadermanagerclass.cpp

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


ShaderManagerClass::ShaderManagerClass()
{
    m_ColorShader = 0;
    m_FontShader = 0;
    m_TerrainShader = 0;
}


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


ShaderManagerClass::~ShaderManagerClass()
{
}


bool ShaderManagerClass::Initialize(OpenGLClass* OpenGL)
{
    bool result;


    // Create and initialize the color shader object.
    m_ColorShader = new ColorShaderClass;

    result = m_ColorShader->Initialize(OpenGL);
    if(!result)
    {
        return false;
    }

    // Create and initialize the font shader object.
    m_FontShader = new FontShaderClass;

    result = m_FontShader->Initialize(OpenGL);
    if(!result)
    {
        return false;
    }

    // Create and initialize the terrain shader object.
    m_TerrainShader = new TerrainShaderClass;

    result = m_TerrainShader->Initialize(OpenGL);
    if(!result)
    {
        return false;
    }

    return true;
}


void ShaderManagerClass::Shutdown()
{
    // Release the terrain shader object.
    if(m_TerrainShader)
    {
        m_TerrainShader->Shutdown();
        delete m_TerrainShader;
        m_TerrainShader = 0;
    }

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

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

    return;
}


bool ShaderManagerClass::RenderColorShader(float* worldMatrix, float* viewMatrix, float* projectionMatrix)
{
    return m_ColorShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix);
}


bool ShaderManagerClass::RenderFontShader(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* pixelColor)
{
    return m_FontShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, pixelColor);
}


bool ShaderManagerClass::RenderTerrainShader(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* lightDirection, float* diffuseLightColor)
{
    return m_TerrainShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor);
}

Summary

We have now successfully divided our terrain into individual nodes so that we can move onto more efficient rendering in future tutorials.


To Do Exercises

1. Recompile the code and run the program. You should now see a terrain divided up into nodes surrounded by yellow bounding boxes.

2. Render just a couple individual cells instead of the entire terrain.

3. Toggle on wireframe so you can see the exact number of quads in each terrain cell.


Source Code

Source Code and Data Files: gl4terlinux08.tar.gz

Back to Tutorial Index