Tutorial 15: Procedural Terrain Texturing

In this OpenGL 4.0 terrain tutorial we will cover the use of procedural parameters for determining the placement and application of different textures/materials to the terrain during runtime.

Procedural parameters are usually calculated in our shaders based on input textures or the terrain data itself. One of the most popular parameters is to use the height of the pixel being rendered and apply a texture using different height bands. In this way you can specify the ground texture to be below a certain height, then a rock texture for anything above that height, and finally a snow texture for anything in the final height range.

Another common procedural parameter to use in the slope of the current pixel. The slope can be easily calculated (one minus the Y normal) and has the properties of reflecting real world growth and deposition patterns. For example, in this tutorial we use the slope to determine where the rock gets exposed, and everything less than that slope is covered with snow. This represents a real world pattern where anything with too much slope the snow will never accumulate on. Likewise this can be extended to growth patterns, erosion patterns, and so forth. Here is an example of just using snow and rock:

It should be noted that you can use slope in many different fashions. For example you can expose or blend multiple textures using different ranges of slope. In this particular tutorial we use anything above 0.2f to be exposed rock, and anything below to be a linear interpolation of snow and rock based on the slope value. The linear interpolation allows a smooth transition between the rock and the snow instead of sharp and noticeable cut-off. This smooth transition can be seen in the image below where I just render the slope value in red:

Now most modern terrain rendering engines and terrain generators use a combination of a number of different procedural parameters combined with painted texture placement masks to generate realistic terrain. You can output the resulting data in what is usually called a splat map. However you can get the same data using these same procedural parameters in your pixel shader to create your own real time splat map that maybe even animates over time.

Here is another example where I generate my own real time splat map using the following. 1) If height is below 20.0 then color it blue to represent below water. 2) If height is above 20.0 then color it green to represent grass. 3) If the slope is greater than 0.3 color it gray to represent cliff rock. 4) If the slope is less than 0.05 and the height is greater than 20.0, then color it brown for dirt.

That is a very basic example of how they generally work. Sometimes really detailed terrain splat maps have way more parameters. And in that case it becomes more feasable to just render out the splat maps and use them as texture inputs instead of doing a lot of calculations for every pixel instead of a quick texture look up. Most splat maps will also be combined with noise textures to create more variation close up.

For this tutorial we still use the code from the previous tutorial. We will only be looking at the four files we have changed to implement the snow and rock effect. We will also be using a new stone texture and two new normal maps for the stone and snow. The code section will start with us looking at the changesd to the terrain pixel shader code:


Terrain.ps

We haven't change the terrain vertex shader, but the terrain pixel shader has changed quite a bit. First we have a second normal map which is used for the snow. Also, I didn't add a diffuse texture for the snow since I just use the color white in the shader. In the shader code we now begin by calculating the slope of this specific pixel, and this will be our key procedural parameter for determining the final output color. Next we setup the two materials in the same fashion; we first sample the texture and the normal map, then calculate the lighting for that material using the normal map, and then finally we combine the light with the texture to complete the material. We do this for both the rock and the snow material.

Finally we combine the materials based on the slope value. If the slope is above 0.2f then we only render the rock material. If it is below 0.2f then we do a linear interpolation based on the slope value between the snow and the rock material to create a smooth transition from the rock to the snow.

////////////////////////////////////////////////////////////////////////////////
// Filename: terrain.ps
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec3 normal;
in vec3 tangent;
in vec3 binormal;
in vec4 color;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D shaderTexture;
uniform sampler2D normalTexture;
uniform sampler2D normalTexture2;
uniform vec3 lightDirection;
uniform vec4 diffuseLightColor;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    vec4 textureColor;
    vec4 bumpMap;
    vec3 bumpNormal;
    vec3 lightDir;
    float lightIntensity;
    float slope;
    vec4 lightColor;
    vec4 material1;
    vec4 material2;
    vec4 finalColor;
    float blendAmount;


    // Invert the light direction for calculations.
    lightDir = -lightDirection;

Create our first rock material.

    // Setup the first material.
    textureColor = texture(shaderTexture, texCoord);
    bumpMap = texture(normalTexture, texCoord);
    bumpMap = (bumpMap * 2.0f) - 1.0f;
    bumpNormal = (bumpMap.x * tangent) + (bumpMap.y * binormal) + (bumpMap.z * normal);
    bumpNormal = normalize(bumpNormal);
    lightIntensity = clamp(dot(bumpNormal, lightDir), 0.0f, 1.0f);
    lightColor =  clamp((diffuseLightColor * lightIntensity), 0.0f, 1.0f);
    material1 = textureColor * lightColor;

Create our second snow material.

    // Setup the second material.
    textureColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);  // Snow color.
    bumpMap = texture(normalTexture2, texCoord);
    bumpMap = (bumpMap * 2.0f) - 1.0f;
    bumpNormal = (bumpMap.x * tangent) + (bumpMap.y * binormal) + (bumpMap.z * normal);
    bumpNormal = normalize(bumpNormal);
    lightIntensity = clamp(dot(bumpNormal, lightDir), 0.0f, 1.0f);
    lightColor =  clamp((diffuseLightColor * lightIntensity), 0.0f, 1.0f);
    material2 = textureColor * lightColor;

Calculate the slope of this pixel based on it's Y normal.

    // Calculate the slope of this point.
    slope = 1.0f - normal.y;

Use the slope to determine the resulting color from the two materials.

    // Determine which material to use based on slope.
    if(slope < 0.2)
    {
        blendAmount = slope / 0.2f;
        finalColor = mix(material2, material1, blendAmount);
    }
    if(slope >= 0.2)
    {
        finalColor = material1;
    }

    outputColor = finalColor;
}

Terrainclass.h

We will add an additonal normal map to the TerrainClass header file.

////////////////////////////////////////////////////////////////////////////////
// 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"
#include "terrainnodeclass.h"
#include "quadtreeclass.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*, FrustumClass*, float*, float*, float*);

    void GetNodesDrawn(int&, int&);
    bool GetHeightAtPosition(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&);

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

    void CalculateMeshDimensions();

private:
    int m_vertexCount;
    int m_terrainHeight, m_terrainWidth;
    HeightMapType* m_heightMap;
    ModelType* m_terrainModel;
    TerrainNodeClass* m_Nodes;
    int m_nodeCount;
    TextureClass *m_Texture, *m_NormalMap, *m_NormalMap2;
    QuadTreeClass* m_QuadTree;
    int* m_renderList;
    float m_minHeight, m_maxHeight;
};

#endif

Terrainclass.cpp

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


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


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


TerrainClass::~TerrainClass()
{
}


bool TerrainClass::Initialize(OpenGLClass* OpenGL, char* setupFilename)
{
    char terrainFilename[256], textureFilename[256], colorMapFilename[256], normalFilename[256], normalFilename2[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();

    // Calculate the dimensions of the mesh for the quad tree's usage.
    CalculateMeshDimensions();

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

We will overload the two textures we want for rock, instead of using the dirt and it's normal from the setup.txt file. We also add our third texture for the snow normal map.

    // Override the setup.txt textures with rock and snow textures.
    strcpy(textureFilename, "../Engine/data/textures/rock01d.tga");
    strcpy(normalFilename, "../Engine/data/textures/rock01n.tga");
    strcpy(normalFilename2, "../Engine/data/textures/snow01n.tga");

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

We load our new second normal map here. It will be used for the snow material's normal map.

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

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

    return true;
}

We release the second normal map in the Shutdown function.

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

    // 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 terrain nodes.
    ReleaseTerrainNodes(OpenGL);

    return;
}


bool TerrainClass::Render(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, LightClass* Light, FrustumClass* Frustum, float* worldMatrix, float* viewMatrix, float* projectionMatrix)
{
    float diffuseLightColor[4], lightDirection[3];
    int i, j, renderCount;
    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 = false;

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

    // Get the list of nodes to render.
    m_QuadTree->Render(Frustum, m_minHeight, m_maxHeight);
    m_QuadTree->GetRenderList(m_renderList, renderCount);

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

Set the new second normal map in the terrain pixel shader before rendering the terrain geometry.

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

    // Put the node vertex and index buffers on the graphics pipeline to prepare them for drawing.
    for(i=0; i<renderCount; i++)
    {
        // Only render the list of nodes that the quad tree determined are visible.
        j = m_renderList[i];
        m_Nodes[j].Render(OpenGL);
    }

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

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

        for(i=0; i<renderCount; i++)
        {
            // Only render the node bounding boxes that the quad tree determined are visible.
            j = m_renderList[i];
            m_Nodes[j].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;
}


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

    // Create and initialize the quad tree object.
    m_QuadTree = new QuadTreeClass;
    m_QuadTree->Initialize((m_terrainWidth-1), (nodeWidth - 1), 512.0f, 512.0f);

    // Create the render list for the quad tree.
    m_renderList = new int[m_nodeCount];

    return true;
}


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


    // Release the render list.
    if(m_renderList)
    {
        delete [] m_renderList;
        m_renderList = 0;
    }

    // Release the quad tree object.
    if(m_QuadTree)
    {
        m_QuadTree->Shutdown();
        delete m_QuadTree;
        m_QuadTree = 0;
    }

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


void TerrainClass::CalculateMeshDimensions()
{
    int i;


    // Determine the minimum and maximum height of the terrain for the quad tree object.
    m_minHeight = m_terrainModel[0].y;
    m_maxHeight = m_terrainModel[0].y;

    for(i=1; i<m_vertexCount; i++)
    {
        if(m_terrainModel[i].y < m_minHeight)
        {
            m_minHeight = m_terrainModel[i].y;
        }

        if(m_terrainModel[i].y > m_maxHeight)
        {
            m_maxHeight = m_terrainModel[i].y;
        }
    }

    return;
}


void TerrainClass::GetNodesDrawn(int& nodesDrawn, int& nodesCulled)
{
    m_QuadTree->GetNodesDrawn(nodesDrawn, nodesCulled);
    return;
}


bool TerrainClass::GetHeightAtPosition(float positionX, float positionZ, float& height)
{
    int nodeID;
    bool result;


    result = m_QuadTree->GetHeightAtPosition(positionX, positionZ, nodeID);
    if(!result)
    {
        return false;
    }

    m_Nodes[nodeID].GetHeightAtPosition(positionX, positionZ, height);

    return true;
}

Terrainshaderclass.cpp

We have added the second normal map to the TerrainShaderClass.

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


TerrainShaderClass::TerrainShaderClass()
{
    m_OpenGLPtr = 0;
}


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


TerrainShaderClass::~TerrainShaderClass()
{
}


bool TerrainShaderClass::Initialize(OpenGLClass* OpenGL)
{
    char vsFilename[128];
    char psFilename[128];
    bool result;


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

    // Set the location and names of the shader files.
    strcpy(vsFilename, "../Engine/terrain.vs");
    strcpy(psFilename, "../Engine/terrain.ps");

    // Initialize the vertex and pixel shaders.
    result = InitializeShader(vsFilename, psFilename);
    if(!result)
    {
        return false;
    }

    return true;
}


void TerrainShaderClass::Shutdown()
{
    // Shutdown the shader.
    ShutdownShader();

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

    return;
}


bool TerrainShaderClass::InitializeShader(char* vsFilename, char* fsFilename)
{
    const char* vertexShaderBuffer;
    const char* fragmentShaderBuffer;
    int status;


    // Load the vertex shader source file into a text buffer.
    vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
    if(!vertexShaderBuffer)
    {
        return false;
    }

    // Load the fragment shader source file into a text buffer.
    fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
    if(!fragmentShaderBuffer)
    {
        return false;
    }

    // Create a vertex and fragment shader object.
    m_vertexShader = m_OpenGLPtr->glCreateShader(GL_VERTEX_SHADER);
    m_fragmentShader = m_OpenGLPtr->glCreateShader(GL_FRAGMENT_SHADER);

    // Copy the shader source code strings into the vertex and fragment shader objects.
    m_OpenGLPtr->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
    m_OpenGLPtr->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);

    // Release the vertex and fragment shader buffers.
    delete [] vertexShaderBuffer;
    vertexShaderBuffer = 0;

    delete [] fragmentShaderBuffer;
    fragmentShaderBuffer = 0;

    // Compile the shaders.
    m_OpenGLPtr->glCompileShader(m_vertexShader);
    m_OpenGLPtr->glCompileShader(m_fragmentShader);

    // Check to see if the vertex shader compiled successfully.
    m_OpenGLPtr->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
    if(status != 1)
    {
        // If it did not compile then write the syntax error message out to a text file for review.
        OutputShaderErrorMessage(m_vertexShader, vsFilename);
        return false;
    }

    // Check to see if the fragment shader compiled successfully.
    m_OpenGLPtr->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
    if(status != 1)
    {
        // If it did not compile then write the syntax error message out to a text file for review.
        OutputShaderErrorMessage(m_fragmentShader, fsFilename);
        return false;
    }

    // Create a shader program object.
    m_shaderProgram = m_OpenGLPtr->glCreateProgram();

    // Attach the vertex and fragment shader to the program object.
    m_OpenGLPtr->glAttachShader(m_shaderProgram, m_vertexShader);
    m_OpenGLPtr->glAttachShader(m_shaderProgram, m_fragmentShader);

    // Bind the shader input variables.
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 1, "inputTexCoord");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 2, "inputNormal");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 3, "inputTangent");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 4, "inputBinormal");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 5, "inputColor");

    // Link the shader program.
    m_OpenGLPtr->glLinkProgram(m_shaderProgram);

    // Check the status of the link.
    m_OpenGLPtr->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
    if(status != 1)
    {
        // If it did not link then write the syntax error message out to a text file for review.
        OutputLinkerErrorMessage(m_shaderProgram);
        return false;
    }

    return true;
}


void TerrainShaderClass::ShutdownShader()
{
    // Detach the vertex and fragment shaders from the program.
    m_OpenGLPtr->glDetachShader(m_shaderProgram, m_vertexShader);
    m_OpenGLPtr->glDetachShader(m_shaderProgram, m_fragmentShader);

    // Delete the vertex and fragment shaders.
    m_OpenGLPtr->glDeleteShader(m_vertexShader);
    m_OpenGLPtr->glDeleteShader(m_fragmentShader);

    // Delete the shader program.
    m_OpenGLPtr->glDeleteProgram(m_shaderProgram);

    return;
}


char* TerrainShaderClass::LoadShaderSourceFile(char* filename)
{
    FILE* filePtr;
    char* buffer;
    long fileSize, count;
    int error;


    // Open the shader file for reading in text modee.
    filePtr = fopen(filename, "r");
    if(filePtr == NULL)
    {
        return 0;
    }

    // Go to the end of the file and get the size of the file.
    fseek(filePtr, 0, SEEK_END);
    fileSize = ftell(filePtr);

    // Initialize the buffer to read the shader source file into, adding 1 for an extra null terminator.
    buffer = new char[fileSize + 1];

    // Return the file pointer back to the beginning of the file.
    fseek(filePtr, 0, SEEK_SET);

    // Read the shader text file into the buffer.
    count = fread(buffer, 1, fileSize, filePtr);
    if(count != fileSize)
    {
        return 0;
    }

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

    // Null terminate the buffer.
    buffer[fileSize] = '\0';

    return buffer;
}


void TerrainShaderClass::OutputShaderErrorMessage(unsigned int shaderId, char* shaderFilename)
{
    long count;
    int logSize, error;
    char* infoLog;
    FILE* filePtr;


    // Get the size of the string containing the information log for the failed shader compilation message.
    m_OpenGLPtr->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);

    // Increment the size by one to handle also the null terminator.
    logSize++;

    // Create a char buffer to hold the info log.
    infoLog = new char[logSize];

    // Now retrieve the info log.
    m_OpenGLPtr->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);

    // Open a text file to write the error message to.
    filePtr = fopen("shader-error.txt", "w");
    if(filePtr == NULL)
    {
        cout << "Error opening shader error message output file." << endl;
        return;
    }

    // Write out the error message.
    count = fwrite(infoLog, sizeof(char), logSize, filePtr);
    if(count != logSize)
    {
        cout << "Error writing shader error message output file." << endl;
        return;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        cout << "Error closing shader error message output file." << endl;
        return;
    }

    // Notify the user to check the text file for compile errors.
    cout << "Error compiling shader.  Check shader-error.txt for error message.  Shader filename: " << shaderFilename << endl;

    return;
}


void TerrainShaderClass::OutputLinkerErrorMessage(unsigned int programId)
{
    long count;
    FILE* filePtr;
    int logSize, error;
    char* infoLog;


    // Get the size of the string containing the information log for the failed shader compilation message.
    m_OpenGLPtr->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);

    // Increment the size by one to handle also the null terminator.
    logSize++;

    // Create a char buffer to hold the info log.
    infoLog = new char[logSize];

    // Now retrieve the info log.
    m_OpenGLPtr->glGetProgramInfoLog(programId, logSize, NULL, infoLog);

    // Open a file to write the error message to.
    filePtr = fopen("linker-error.txt", "w");
    if(filePtr == NULL)
    {
        cout << "Error opening linker error message output file." << endl;
        return;
    }

    // Write out the error message.
    count = fwrite(infoLog, sizeof(char), logSize, filePtr);
    if(count != logSize)
    {
        cout << "Error writing linker error message output file." << endl;
        return;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        cout << "Error closing linker error message output file." << endl;
        return;
    }

    // Pop a message up on the screen to notify the user to check the text file for linker errors.
    cout << "Error linking shader program.  Check linker-error.txt for message." << endl;

    return;
}


bool TerrainShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* lightDirection, float* diffuseLightColor)
{
    float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16];
    int location;


    // Transpose the matrices to prepare them for the shader.
    m_OpenGLPtr->MatrixTranspose(tpWorldMatrix, worldMatrix);
    m_OpenGLPtr->MatrixTranspose(tpViewMatrix, viewMatrix);
    m_OpenGLPtr->MatrixTranspose(tpProjectionMatrix, projectionMatrix);

    // Install the shader program as part of the current rendering state.
    m_OpenGLPtr->glUseProgram(m_shaderProgram);

    // Set the world matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "worldMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr ->glUniformMatrix4fv(location, 1, false, tpWorldMatrix);

    // Set the view matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "viewMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpViewMatrix);

    // Set the projection matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpProjectionMatrix);

    // Set the texture in the pixel shader to use the data from the first texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "shaderTexture");
    if(location == -1)
    {
        cout << "Shader texture not set." << endl;
    }
    m_OpenGLPtr->glUniform1i(location, 0);

    // Set the normal map texture in the pixel shader to use the data from the second texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "normalTexture");
    if(location == -1)
    {
        cout << "Normal texture not set." << endl;
    }
    m_OpenGLPtr->glUniform1i(location, 1);

Set the second normal map here.

    // Set the normal map texture in the pixel shader to use the data from the second texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "normalTexture2");
    if(location == -1)
    {
        cout << "Normal texture two not set." << endl;
    }
    m_OpenGLPtr->glUniform1i(location, 2);

    // Set the light direction in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightDirection");
    if(location == -1)
    {
        cout << "Light direction not set." << endl;
    }
    m_OpenGLPtr->glUniform3fv(location, 1, lightDirection);

    // Set the diffuse light color in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "diffuseLightColor");
    if(location == -1)
    {
        cout << "Diffuse light color not set." << endl;
    }
    m_OpenGLPtr->glUniform4fv(location, 1, diffuseLightColor);

    return true;
}

Summary

We now have terrain that can be automatically texture mapped per pixel based on the slope.


To Do Exercises

1. Recompile the code and run the program. Examine how the slope affects the rock exposure.

2. Modify the slope values in the pixel shader to see the effect.

3. Add a third material and a second interpolation in the pixel shader for it.

4. Try different materials and different slope values other than just rock and snow.

5. Use a height parameter. Similar to the sky dome tutorial, send it through the vertex shader as an unmodified position coordinate.


Source Code

Source Code and Data Files: gl4terlinux15.tar.gz

Back to Tutorial Index