Tutorial 7: RAW Height Maps

In previous tutorials we have been using the bitmap format for terrain height maps. Although this format is easy to use it is incredibly limiting in terms of the amount of terrain detail that you can render. From the height perspective you are limited to 256 discrete height steps which makes it impossible to have things like smooth rolling hills and steep jagged mountains represented in the same height map. To achieve that kind of detail we need to look at using a 16-bit format, and in this tutorial I will cover using the most straight forward one which is RAW.

Using a 16-bit RAW format we can now have 65536 discreet height steps. This provides us with the detail we are looking for to represent almost any terrain type. And secondly, the RAW format is extremely easy to read in since it just stores only the height values in a linear fashion. There is no header or any other type of metadata in the RAW file format. So you just open the file and read it straight into your array and it is ready for use. Note there are different formats within the RAW format, but we will just cover the default unsigned short 16 bit RAW version.

To create RAW files you generally need terrain building programs like World Machine to build them for you since you can't paint 16-bit RAW files manually. And likewise to view them you need to use another program or something you have written yourself that is 16-bit RAW file aware. I have converted one RAW file to 8-bit bitmap format and you can already see how the transitions are much smoother than most 8-bit height maps:

For this tutorial I used World Machine to create a one kilometer by one kilometer terrain. The file extension used is .r16. The color map is still a bitmap file but is sized 1025 by 1025 just like the RAW height map for one to one mapping. As you can see our terrain has far more detail now:


Setup.txt

The setup.txt file has been updated with the new RAW terrain file, the new color map, and the new dimensions and scaling value.

Terrain Filename: ../Engine/data/heightmap.r16
Color Map Filename: ../Engine/data/colormap.bmp
Terrain Height: 1025
Terrain Width: 1025
Terrain Scaling: 300.0
Diffuse Texture: ../Engine/data/textures/dirt01d.tga
Normal Texture: ../Engine/data/textures/dirt01n.tga

Terrainclass.h

The TerrainClass header has been updated to include a new private function for loading in RAW height maps. This new function will be used instead of the previous bitmap height map reader.

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


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


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureclass.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 TempVertexType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
    };

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

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

private:
    bool LoadSetupFile(char*, char*, float&, char*, char*, char*);

This is the new RAW file reader function.

    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&);

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

private:
    OpenGLClass* m_OpenGLPtr;
    int m_vertexCount, m_indexCount;
    unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
    int m_terrainHeight, m_terrainWidth;
    HeightMapType* m_heightMap;
    ModelType* m_terrainModel;
    TextureClass *m_Texture, *m_NormalMap;
};

#endif

Terrainclass.cpp

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


TerrainClass::TerrainClass()
{
    m_OpenGLPtr = 0;
    m_heightMap = 0;
    m_terrainModel = 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;


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

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

We now call the new LoadRawHeightMap function to load in RAW height maps. The previous LoadBitmapHeightMap is no longer utilized.

    // 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();

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

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

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

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

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

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

    return true;
}


void TerrainClass::Shutdown()
{
    // 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;
    }

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

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

    return;
}


bool TerrainClass::Render()
{
    // Set the diffuse texture for the terrain in the pixel shader texture unit 0.
    m_Texture->SetTexture(m_OpenGLPtr, 0);

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

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

    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;
}

The LoadRawHeightMap function loads 16-bit RAW height map files. It works in the exact same fashion as LoadBitmapHeightMap but handles the RAW format instead. Since this is a 16-bit format we use unsigned short instead of unsigned char to create the array that the data will be read into. Also when we parse through the array to copy the data into the height map structure we don't have to traverse it backwards because the RAW format is not stored upside down like bitmaps.

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;
    unsigned char* bitmapImage;
    unsigned char fileHeader[54];
    unsigned long count;
    int height, width, imageSize, i, j, k, index, error;


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

    // Read in the bitmap file header which is 54 bytes.
    count = fread(fileHeader, sizeof(unsigned char), 54, filePtr);
    if(count != 54)
    {
        return false;
    }

    // Get the width and height integers from the unsigned char header data.
    height = (int)fileHeader[23];
    height <<= 8;
    height += (int)fileHeader[22];

    width = (int)fileHeader[19];
    width <<= 8;
    width += (int)fileHeader[18];

    // 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;
    }

    // Calculate the size of the bitmap image data.  Since this is non-divide by 2 dimensions (eg. 257x257) need to add extra byte to each line.
    imageSize = m_terrainHeight * ((m_terrainWidth * 3) + 1);

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

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

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

    // Initialize the position in the 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++)
	{
	    // Bitmaps are upside down so load bottom to top into the array.
	    index = (m_terrainWidth * (m_terrainHeight - 1 - j)) + i;

	    m_heightMap[index].b = (float)bitmapImage[k] / 255.0f;      // Blue
	    m_heightMap[index].g = (float)bitmapImage[k + 1] / 255.0f;  // Green
	    m_heightMap[index].r = (float)bitmapImage[k + 2] / 255.0f;  // Red

	    k += 3;
	}

	// Compensate for extra byte at end of each line in non-divide by 2 bitmaps (eg. 257x257).
	k++;
    }

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

    return true;
}


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


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

    // Create the 3D terrain model array.
    m_terrainModel = new ModelType[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; iglGenVertexArrays(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 two vertex array attributes.
    m_OpenGLPtr->glEnableVertexAttribArray(0);  // Vertex position.
    m_OpenGLPtr->glEnableVertexAttribArray(1);  // Texture coordinates.
    m_OpenGLPtr->glEnableVertexAttribArray(2);  // Normals.
    m_OpenGLPtr->glEnableVertexAttribArray(3);  // Tangent
    m_OpenGLPtr->glEnableVertexAttribArray(4);  // Binormal
    m_OpenGLPtr->glEnableVertexAttribArray(5);  // Color.

    // 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 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)));

    // Specify the location and format of the tangent vector portion of the vertex buffer.
    m_OpenGLPtr->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.
    m_OpenGLPtr->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.
    m_OpenGLPtr->glVertexAttribPointer(5, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (14 * 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 TerrainClass::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 TerrainClass::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 as triangles using the index buffer.
    glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);

    return;
}

Summary

We now have the ability to render highly detailed terrain using the 16-bit RAW format for height maps.


To Do Exercises

1. Recompile the code and run the program. You should now see a far more detailed terrain.

2. Enable the wire frame mode. This will show the increase in detail and smooth transition between height points.

3. Create your own 16-bit RAW height map using any of the free terrain generation programs that support that format.


Source Code

Source Code and Data Files: gl4terlinux07_src.tar.gz

Back to Tutorial Index