This tutorial will cover how to implement color maps for terrain in OpenGL 4.0 using GLSL and C++.
The code in this tutorial is based on the previous terrain tutorial.
Terrain color mapping is the use of a colored bitmap image to determine the color of vertices in a terrain mesh.
The bitmap image is usually the same size as the height map to ensure one to one translation of bitmap pixel colors to height map vertices.
For example we will use the following 257x257 bitmap image as the color map for the terrain:

Although in this tutorial we are doing a 1 to 1 mapping, you can increase the mapping to something like 4 to 1 with a color map that is 1025x1025 and is applied to a 257x257 terrain.
However, the texturing should ultimately determine the majority of the color information per polygon and the color map is for a general tint to the area.
So going into great detail with a color map is not really needed.
Most terrain engines will only use them to convey the basic color information for distant rendered terrain.
To generate color maps you have many options.
They can be artist drawn which is the most common method.
You can also have programs like World Machine generate a color map based on the dimensions and properties of the terrain.
Real world image data is also commonly used.
And of course you can write your own procedural color generator that reads in the terrain and all of its textures and shaders, and then makes a color map based on that information.
Now when we apply our color map to the terrain we are first going to read a color for the color map for each vertex in the height map.
We basically use the exact same loading function that we used for the height map but we instead read the color map and load into a RGB member of the height map structure.
This will give each vertex its own RGB color component.
And when we render the terrain using just the RGB color and directional lighting we can produce the following look to the terrain:

And then we apply the texturing which gives the final rendered result:

There are times where you may want to remove the color from the texture so that the color map will control more of the final look to the terrain.
A texture without color that is used in this fashion is usually referred to as a detail map as it only contributes look but not color to the final output.
You can use Photoshop or other paint programs to desaturate your textures if you are going for more of a color map controlled look.
Setup.txt
The setup.txt file has been modified to include an entry for the color map filename.
Terrain Filename: ../Engine/data/heightmap.bmp
Color Map Filename: ../Engine/data/colormap.bmp
Terrain Height: 257
Terrain Width: 257
Terrain Scaling: 12.0
Diffuse Texture: ../Engine/data/textures/dirt01d.tga
Terrain.vs
The terrain shader has been modified to accept a RGB color value per vertex which it gets from the color map portion of the terrain model.
This color is the passed through to the pixel shader.
////////////////////////////////////////////////////////////////////////////////
// Filename: terrain.vs
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
in vec3 inputNormal;
in vec3 inputColor;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;
out vec3 normal;
out vec4 color;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
// Calculate the position of the vertex against the world, view, and projection matrices.
gl_Position = vec4(inputPosition, 1.0f) * worldMatrix;
gl_Position = gl_Position * viewMatrix;
gl_Position = gl_Position * projectionMatrix;
// Store the texture coordinates for the pixel shader.
texCoord = inputTexCoord;
// Calculate the normal vector against the world matrix only.
normal = inputNormal * mat3(worldMatrix);
// Normalize the normal vector.
normal = normalize(normal);
// Store the input color for the pixel shader to use.
color = vec4(inputColor, 1.0f);
}
Terrain.ps
The pixel shader is updated to handle the new input RGBA color component.
We will combine the input color map value with the texture color.
Note we also multiply by 2.0 since multiplications make the color darker and it needs a brightening value to compensate for the multiplication method of combining colors.
////////////////////////////////////////////////////////////////////////////////
// Filename: terrain.ps
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec3 normal;
in vec4 color;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D shaderTexture;
uniform vec3 lightDirection;
uniform vec4 diffuseLightColor;
////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
vec4 textureColor;
vec3 lightDir;
float lightIntensity;
// Sample the pixel color from the texture using the sampler at this texture coordinate location.
textureColor = texture(shaderTexture, texCoord);
// Combine the color map value into the texture color.
textureColor = clamp((textureColor * color * 2.0f), 0.0f, 1.0f);
// Invert the light direction for calculations.
lightDir = -lightDirection;
// Calculate the amount of light on this pixel.
lightIntensity = clamp(dot(normal, lightDir), 0.0f, 1.0f);
// Determine the final amount of diffuse color based on the diffuse color combined with the light intensity.
outputColor = clamp((diffuseLightColor * lightIntensity), 0.0f, 1.0f);
// Multiply the texture pixel and the final diffuse color to get the final pixel color result.
outputColor = outputColor * textureColor;
}
Terrainshaderclass.h
The TerrainShaderClass has been modified to include the new attribute location of the RGB color coming from the color map.
////////////////////////////////////////////////////////////////////////////////
// Filename: terrainshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _TERRAINSHADERCLASS_H_
#define _TERRAINSHADERCLASS_H_
//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class name: TerrainShaderClass
////////////////////////////////////////////////////////////////////////////////
class TerrainShaderClass
{
public:
TerrainShaderClass();
TerrainShaderClass(const TerrainShaderClass&);
~TerrainShaderClass();
bool Initialize(OpenGLClass*);
void Shutdown();
bool SetShaderParameters(float*, float*, float*, float*, float*);
private:
bool InitializeShader(char*, char*);
void ShutdownShader();
char* LoadShaderSourceFile(char*);
void OutputShaderErrorMessage(unsigned int, char*);
void OutputLinkerErrorMessage(unsigned int);
private:
OpenGLClass* m_OpenGLPtr;
unsigned int m_vertexShader;
unsigned int m_fragmentShader;
unsigned int m_shaderProgram;
};
#endif
Terrainshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
// 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);
We need to add the new attribute location of the RGB color coming from the color map.
// 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, "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)
{
cout << "World matrix not set." << endl;
}
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)
{
cout << "View matrix not set." << endl;
}
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)
{
cout << "Projection matrix not set." << endl;
}
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 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;
}
Terrainclass.h
The TerrainClass has been modified to include RGB color components and color map loading functionality.
////////////////////////////////////////////////////////////////////////////////
// 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:
The VertexType, HeightMapType, and ModelType structures have been updated to include a RGB color component.
struct VertexType
{
float x, y, z;
float tu, tv;
float nx, ny, nz;
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 r, g, b;
};
struct VectorType
{
float x, y, z;
};
public:
TerrainClass();
TerrainClass(const TerrainClass&);
~TerrainClass();
bool Initialize(OpenGLClass*, char*);
void Shutdown();
bool Render();
private:
bool LoadSetupFile(char*, char*, float&, char*, char*);
bool LoadBitmapHeightMap(char*);
void SetTerrainCoordinates(float);
void CalculateNormals();
There is a new function for loading the color map into the height map array.
bool LoadColorMap(char*);
void BuildTerrainModel();
void ReleaseHeightMap();
void ReleaseTerrainModel();
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;
};
#endif
Terrainclass.cpp
///////////////////////////////////////////////////////////////////////////////
// Filename: terrainclass.cpp
///////////////////////////////////////////////////////////////////////////////
#include "terrainclass.h"
TerrainClass::TerrainClass()
{
m_OpenGLPtr = 0;
m_heightMap = 0;
m_terrainModel = 0;
m_Texture = 0;
}
TerrainClass::TerrainClass(const TerrainClass& other)
{
}
TerrainClass::~TerrainClass()
{
}
bool TerrainClass::Initialize(OpenGLClass* OpenGL, char* setupFilename)
{
char terrainFilename[256], textureFilename[256], colorMapFilename[256];
float heightScale;
bool result;
// Store a pointer to the OpenGL object.
m_OpenGLPtr = OpenGL;
The LoadSetupFile function now returns the filename of the color map.
// Get the terrain filename, dimensions, and so forth from the setup file.
result = LoadSetupFile(setupFilename, terrainFilename, heightScale, textureFilename, colorMapFilename);
if(!result)
{
return false;
}
// Initialize the terrain height map with the data from the bitmap file.
result = LoadBitmapHeightMap(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();
The new LoadColorMap function is called to load in the color map bitmap file into the height map array.
// 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();
// 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;
}
return true;
}
void TerrainClass::Shutdown()
{
// 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);
// 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)
{
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;
Here is where we read in the color map filename from the setup.txt file.
// 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;
// Close the setup file.
fin.close();
return true;
}
bool TerrainClass::LoadBitmapHeightMap(char* terrainFilename)
{
FILE* filePtr;
unsigned char* bitmapImage;
unsigned char fileHeader[54];
unsigned long count;
int height, width, imageSize, i, j, k, index, error;
unsigned char pixelHeight;
// Start by creating the array structure to hold the height map data.
m_heightMap = new HeightMapType[m_terrainWidth * m_terrainHeight];
// Open the bitmap map file in binary.
filePtr = fopen(terrainFilename, "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 height 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 we use non-divide by 2 dimensions (eg. 257x257) we need to add an 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 height map array.
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 height map array.
index = (m_terrainWidth * (m_terrainHeight - 1 - j)) + i;
// Get the grey scale pixel value from the bitmap image data at this location.
pixelHeight = bitmapImage[k];
// Store the pixel value as the height at this point in the height map array.
m_heightMap[index].y = (float)pixelHeight;
// Increment the bitmap image data index.
k+=3;
}
// Compensate for the extra byte at end of each line in non-divide by 2 bitmaps (eg. 257x257).
k++;
}
// Release the bitmap image data now that the height map array has been loaded.
delete [] bitmapImage;
bitmapImage = 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;
}
The new LoadColorMap function is almost identical to the LoadHeightMap function.
It opens a bitmap file and loads in the RGB color component into the height map structure array.
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;
}
BuildTerrainModel has been updated to load the RGB color component from the height map into the terrain model.
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;
}
The InitializeBuffers function has also been updated to load the RGB color component from the terrain model into the terrain vertex buffer.
bool TerrainClass::InitializeBuffers()
{
VertexType* vertices;
unsigned int* indices;
int i;
// Calculate the number of vertices in the terrain.
m_vertexCount = (m_terrainHeight - 1) * (m_terrainWidth - 1) * 6;
// Set the index count to the same as the vertex count.
m_indexCount = m_vertexCount;
// Create the vertex array.
vertices = new VertexType[m_vertexCount];
// Create the index array.
indices = new unsigned int[m_indexCount];
// Load the vertex array and index array with 3D terrain model data.
for(i=0; i<m_vertexCount; i++)
{
vertices[i].x = m_terrainModel[i].x;
vertices[i].y = m_terrainModel[i].y;
vertices[i].z = m_terrainModel[i].z;
vertices[i].tu = m_terrainModel[i].tu;
vertices[i].tv = m_terrainModel[i].tv;
vertices[i].nx = m_terrainModel[i].nx;
vertices[i].ny = m_terrainModel[i].ny;
vertices[i].nz = m_terrainModel[i].nz;
vertices[i].r = m_terrainModel[i].r;
vertices[i].g = m_terrainModel[i].g;
vertices[i].b = m_terrainModel[i].b;
indices[i] = i;
}
// Allocate an OpenGL vertex array object.
m_OpenGLPtr->glGenVertexArrays(1, &m_vertexArrayId);
// Bind the vertex array object to store all the buffers and vertex attributes we create here.
m_OpenGLPtr->glBindVertexArray(m_vertexArrayId);
// Generate an ID for the vertex buffer.
m_OpenGLPtr->glGenBuffers(1, &m_vertexBufferId);
// Bind the vertex buffer and load the vertex (position and color) data into the vertex buffer.
m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
m_OpenGLPtr->glBufferData(GL_ARRAY_BUFFER, m_vertexCount * sizeof(VertexType), vertices, GL_STATIC_DRAW);
// Enable the 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); // 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 color vector portion of the vertex buffer.
m_OpenGLPtr->glVertexAttribPointer(3, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (8 * 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
With the addition of color mapping we now have a fine grain control over the look and color of the terrain.

To Do Exercises
1. Recompile the code and use the input keys to move around. You should see terrain that has lighting, texturing, and color mapping applied.
2. Modify the shader to just return the color map color in the pixel shader.
3. Create your own color map and run the program using it.
4. Modify the amount of color that is combined with the texture in the pixel shader to control the desired amount of color the color map adds to the end result.
5. Desaturate the dirt texture and see the effect is has on allowing the color map to control more of the color contribution.
Source Code
Source Code and Data Files: gl4terlinux05_src.tar.gz