Tutorial 11: Sky Domes

This OpenGL 4.0 terrain tutorial will cover how to implement sky domes so that we can generate a gradient colored sky that works well as a background to our terrain. The code in this tutorial builds off the previous terrain tutorial code.

To implement a sky dome you first need a sky dome model. The easiest one to use is just a regular sphere. For this tutorial I created a sphere with a radius of 2.0 and 20 subdivisions. I then triangulated it and exported it as a .obj formatted model. In the OpenGL 4.0 tutorial section I cover loading and rendering basic .obj models, so if you haven't seen those tutorials you may want to review them before proceeding. Also note that the radius of 2.0 is very important as the pixel shader is going to be dependent on that size value.

The next step to using a sky dome is that we need to render the sky dome around the camera position at all times. This way the sky dome is always surrounding the viewer regardless of where they move to. We do this by getting the camera position each frame and start the frame by translating the sky dome to be centered at the camera position and then render it there.

The third step is that we need to turn off back face culling when rendering the sky dome. Since we are inside the sky dome at all times the graphics card would cull the polygons since they are facing the wrong direction. So we turn off back face culling using a different raster state, render the sky dome, and then turn back face culling back on by re-enabling the original raster state.

The fourth step is that we also need to disable the Z buffer before rendering. If we don't turn it off we can't see anything outside the sky dome, only what is inside. Turning off the Z buffer before we render allows us to draw the sky dome entirely to the back buffer overwriting everything else regardless of distance. This is also the reason we need to render the sky dome before rendering the terrain or anything else. Once the sky dome is rendered we turn the Z buffer back on so that everything else is rendered according to depth again.

The fifth and final step is implemented in the shader. In the pixel shader we will pass in the current position of the pixel that we are rendering for the sky dome. We will treat the Y coordinate of this position as the height. And since the radius of the sphere was 2.0 this will be translated as +1.0f as the top of the sky dome and -1.0f as the bottom of the sky dome. And since we know the height is between +1.0 and -1.0 we can color the sky dome using the height as the interpolation value between two different colors to create a sky color gradient.

Now for the code section of this tutorial I have added two new classes called SkyDomeClass and SkyDomeShaderClass. I have also added the sky dome vertex and pixel shader GLSL programs. The SkyDomeClass basically encapsulates the sphere model of the sky dome as well as the two colors for the gradient. The SkyDomeShaderClass is used for rendering the sky dome model.

Framework

The frame work has been updated to include the SkyDomeClass and the SkyDomeShaderClass:

We will start the code section now by examining the new SkyDomeClass.


Skydomeclass.h

The SkyDomeClass is basically the ModelClass from the OpenGL 4.0 tutorials re-written for the purposes of rendering a sky dome. It contains the model object for the sky dome as well as the vertex and index buffer to render it. We also keep the color information for the apex and the center of the sky dome. Note that although this is a model with position, texture coordinates, and normals we still will only be rendering the position portion of the model since it requires no texturing or lighting.

////////////////////////////////////////////////////////////////////////////////
// Filename: skydomeclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SKYDOMECLASS_H_
#define _SKYDOMECLASS_H_


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


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


////////////////////////////////////////////////////////////////////////////////
// Class name: SkyDomeClass
////////////////////////////////////////////////////////////////////////////////
class SkyDomeClass
{
private:
    struct ModelType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
    };

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

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

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

    void GetApexColor(float*);
    void GetCenterColor(float*);

private:
    bool LoadSkyDomeModel(char*);
    void ReleaseSkyDomeModel();

    bool InitializeBuffers(OpenGLClass*);
    void ReleaseBuffers(OpenGLClass*);
    void RenderBuffers(OpenGLClass*);

private:
    ModelType* m_model;
    int m_vertexCount, m_indexCount;
    unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
    float m_apexColor[4], m_centerColor[4];
};

#endif

Skydomeclass.cpp

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

The class constructor initializes the model pointer to null.

SkyDomeClass::SkyDomeClass()
{
    m_model = 0;
}


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


SkyDomeClass::~SkyDomeClass()
{
}

The Initialize function first loads the sky dome model into the m_model structure. After that InitializeBuffers is called which loads the sky dome model into a vertex and index buffer that can be rendered by the video card. And finally we set the two colors of the sky dome. The apex color is the color at the top of the sky dome. The center color is the color at the horizon of the sky dome. In this tutorial everything below the horizon is set to be the same color as the horizon. So basically the gradient only goes from the top of the sky dome to the horizon.

bool SkyDomeClass::Initialize(OpenGLClass* OpenGL)
{
    char filename[256];
    bool result;


    // Load in the sky dome model.
    strcpy(filename, "../Engine/data/models/skydome.txt");

    result = LoadSkyDomeModel(filename);
    if(!result)
    {
        return false;
    }

    // Load the sky dome into a vertex and index buffer for rendering.
    result = InitializeBuffers(OpenGL);
    if(!result)
    {
        return false;
    }

    // Release the sky dome model.
    ReleaseSkyDomeModel();

    // Set the color at the top of the sky dome.
    m_apexColor[0] = 0.0f;
    m_apexColor[1] = 0.05f;
    m_apexColor[2] = 0.6f;
    m_apexColor[3] = 1.0f;

    // Set the color at the center of the sky dome.
    m_centerColor[0] = 0.0f;
    m_centerColor[1] = 0.5f;
    m_centerColor[2] = 0.8f;
    m_centerColor[3] = 1.0f;

    return true;
}

The Shutdown function releases the sky dome model and the vertex and index buffers.

void SkyDomeClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the vertex and index buffer that were used for rendering the sky dome.
    ReleaseBuffers(OpenGL);

    // Release the sky dome model.
    ReleaseSkyDomeModel();

    return;
}

The Render function calls the RenderBuffers function to put the sky dome geometry on the graphics pipeline for rendering.

void SkyDomeClass::Render(OpenGLClass* OpenGL)
{
    // Render the sky dome.
    RenderBuffers(OpenGL);

    return;
}

GetApexColor returns the color of the sky dome at the very top.

void SkyDomeClass::GetApexColor(float* color)
{
    color[0] = m_apexColor[0];
    color[1] = m_apexColor[1];
    color[2] = m_apexColor[2];
    color[3] = m_apexColor[3];
    return;
}

GetCenterColor returns the color of the sky dome at the horizon (or 0.0f to be exact).

void SkyDomeClass::GetCenterColor(float* color)
{
    color[0] = m_centerColor[0];
    color[1] = m_centerColor[1];
    color[2] = m_centerColor[2];
    color[3] = m_centerColor[3];
    return;
}

The LoadSkyDomeModel function loads in the sky dome model from our file format which was created by converting the .obj formatted sphere model.

bool SkyDomeClass::LoadSkyDomeModel(char* filename)
{
    ifstream fin;
    char input;
    int i;


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

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

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

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

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

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

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

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

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

    return true;
}

The ReleaseSkyDomeModel function releases the sky dome model structure.

void SkyDomeClass::ReleaseSkyDomeModel()
{
    if(m_model)
    {
        delete [] m_model;
        m_model = 0;
    }

    return;
}

The InitializeBuffers function loads the sky dome model structure into the vertex and index buffer.

bool SkyDomeClass::InitializeBuffers(OpenGLClass* OpenGL)
{
    VertexType* vertices;
    unsigned int* indices;
    int i;


    // 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 data.
    for(i=0; i<m_vertexCount; i++)
    {
        vertices[i].x = m_model[i].x;
        vertices[i].y = m_model[i].y;
        vertices[i].z = m_model[i].z;
        indices[i] = i;
    }

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

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

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

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

    // Enable the vertex array attribute.
    OpenGL->glEnableVertexAttribArray(0);  // Vertex position.

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

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

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

    // Release the arrays now that the vertex and index buffers have been created and loaded.
    delete [] vertices;
    vertices = 0;

    delete [] indices;
    indices = 0;

    return true;
}

The ReleaseBuffers function releases the vertex and index buffer that were used to render the sky dome.

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

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

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

    return;
}

RenderBuffers puts the sky dome geometry on the graphics pipe line for rendering.

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

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

    return;
}

Skydome.vs

The sky dome vertex shader program is very simple. We send through the regular position as usual, however we also send the position through to the pixel shader unmodified in a second variable called domePosition.

////////////////////////////////////////////////////////////////////////////////
// Filename: skydome.vs
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 domePosition;


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

    // Send the unmodified position through to the pixel shader.
    domePosition = vec4(inputPosition, 1.0f);
}

Skydome.ps

The pixel shader is where we do all the real work of rendering the sky dome. First the GradientBuffer will have had the apex and center color set so that we have the two colors to create the gradient from. In the pixel shader function we will take the height of the current pixel to determine where it is on the sky dome. We use that height as the interpolating value and then do an interpolation (mix) between the apex and the center color. The higher the height value the more the apex color will be present. The lower the height value the more the center color will be present.

Also remember that the radius of the sphere was 2.0 which gives us the -1.0f to +1.0f range in the pixel shader. If you use a different radius then you should change the values here or send them through in their own constant buffer.

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


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec4 domePosition;


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


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform vec4 apexColor;
uniform vec4 centerColor;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    float height;
    vec4 textureColor;


    // Determine the position on the sky dome where this pixel is located.
    height = domePosition.y;

    // The value ranges from -1.0f to +1.0f so change it to only positive values.
    if(height < 0.0)
    {
        height = 0.0f;
    }

    // Determine the gradient color by interpolating between the apex and center based on the height of the pixel in the sky dome.
    outputColor = mix(centerColor, apexColor, height);
}

Skydomeshaderclass.h

The SkyDomeShaderClass is used for rendering the sky dome. It is a standard shader that takes in the three regular matrices, but also takes the apex and center colors as input.

////////////////////////////////////////////////////////////////////////////////
// Filename: skydomeshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SKYDOMESHADERCLASS_H_
#define _SKYDOMESHADERCLASS_H_


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


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


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

    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

Skydomeshaderclass.cpp

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


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


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


SkyDomeShaderClass::~SkyDomeShaderClass()
{
}


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


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

Load in the sky dome vertex and pixel shader programs.

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

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

    return true;
}


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

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

    return;
}


bool SkyDomeShaderClass::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);

The layout only requires a single element which is the position.

    // Bind the shader input variables.
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");

    // 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 SkyDomeShaderClass::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* SkyDomeShaderClass::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 SkyDomeShaderClass::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 SkyDomeShaderClass::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 SkyDomeShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* apexColor, float* centerColor)
{
    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);

This is where we set the apex and center color variables in the pixel shader.

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

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

    return true;
}

Shadermanagerclass.h

The ShaderManagerClass has been updated to contain the SkyDomeShader.

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


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


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

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

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

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

#endif

Shadermanagerclass.cpp

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


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


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


ShaderManagerClass::~ShaderManagerClass()
{
}


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


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

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

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

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

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

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

    // Create and initialize the sky dome shader object.
    m_SkyDomeShader = new SkyDomeShaderClass;

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

    return true;
}


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

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

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

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

    return;
}


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


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


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


bool ShaderManagerClass::RenderSkyDomeShader(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* apexColor, float* centerColor)
{
    return m_SkyDomeShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, apexColor, centerColor);
}

Zoneclass.h

The SkyDomeClass has been added to the ZoneClass.

///////////////////////////////////////////////////////////////////////////////
// Filename: zoneclass.h
///////////////////////////////////////////////////////////////////////////////
#ifndef _ZONECLASS_H_
#define _ZONECLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
#include "inputclass.h"
#include "userinterfaceclass.h"
#include "cameraclass.h"
#include "positionclass.h"
#include "terrainclass.h"
#include "skydomeclass.h"


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

    bool Initialize(OpenGLClass*);
    void Shutdown(OpenGLClass*);
    bool Frame(OpenGLClass*, ShaderManagerClass*, FontClass*, UserInterfaceClass*, InputClass*, float);

private:
    bool Render(OpenGLClass*, ShaderManagerClass*, FontClass*, UserInterfaceClass*);
    void HandleMovementInput(InputClass*, float);
    bool RenderSkyDome(OpenGLClass*, ShaderManagerClass*, float*, float*);

private:
    CameraClass* m_Camera;
    PositionClass* m_Position;
    TerrainClass* m_Terrain;
    LightClass* m_Light;
    FrustumClass* m_Frustum;
    SkyDomeClass* m_SkyDome;
};

#endif

Zoneclass.cpp

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


ZoneClass::ZoneClass()
{
    m_Camera = 0;
    m_Position = 0;
    m_Terrain = 0;
    m_Light = 0;
    m_Frustum = 0;
    m_SkyDome = 0;
}


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


ZoneClass::~ZoneClass()
{
}


bool ZoneClass::Initialize(OpenGLClass* OpenGL)
{
    char configFilename[256];
    bool result;


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

    m_Camera->SetPosition(0.0f, 0.0f, -10.0f);
    m_Camera->Render();
    m_Camera->RenderBaseViewMatrix();

    // Create and initialize the position object.
    m_Position = new PositionClass;

    m_Position->SetPosition(500.0f, 50.0f, -10.0f);
    m_Position->SetRotation(0.0f, 0.0f, 0.0f);

    // Create and initialize the terrain object.
    m_Terrain = new TerrainClass;

    strcpy(configFilename, "../Engine/data/setup.txt");

    result = m_Terrain->Initialize(OpenGL, configFilename);
    if(!result)
    {
        cout << "Error: Could not initialize the terrain object." << endl;
        return false;
    }

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

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

    // Create the frustum object.
    m_Frustum = new FrustumClass;

    // Create and initialize the sky dome object.
    m_SkyDome = new SkyDomeClass;

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

    return true;
}


void ZoneClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the sky dome object.
    if(m_SkyDome)
    {
        m_SkyDome->Shutdown(OpenGL);
        delete m_SkyDome;
        m_SkyDome = 0;
    }

    // Release the frustum object.
    if(m_Frustum)
    {
        delete m_Frustum;
        m_Frustum = 0;
    }

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

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

    // Release the position object.
    if(m_Position)
    {
        delete m_Position;
        m_Position = 0;
    }

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

    return;
}


bool ZoneClass::Frame(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, FontClass* Font, UserInterfaceClass* UserInterface, InputClass* Input, float frameTime)
{
    float posX, posY, posZ, rotX, rotY, rotZ, height;
    bool result, foundHeight, heightLocked;


    // Set whether we lock to the terrain height or not.
    heightLocked = false;

    // Do the frame input processing for the movement.
    HandleMovementInput(Input, frameTime);

    // Get the view point position/rotation.
    m_Position->GetPosition(posX, posY, posZ);
    m_Position->GetRotation(rotX, rotY, rotZ);

    // Get the height of the triangle that is directly underneath the given camera position.
    foundHeight =  m_Terrain->GetHeightAtPosition(posX, posZ, height);
    if(foundHeight)
    {
        // If there was a triangle under the camera then position the camera just above it by two units.
        if(heightLocked)
        {
            posY = height + 2.0f;
        }
    }

    // Set the position of the camera and update the camera view matrix for rendering.
    m_Camera->SetPosition(posX, posY, posZ);
    m_Camera->SetRotation(rotX, rotY, rotZ);
    m_Camera->Render();

    // Update the UI with the position.
    UserInterface->UpdatePositonStrings(Font, posX, posY, posZ, rotX, rotY, rotZ);

    // Render the zone.
    result = Render(OpenGL, ShaderManager, Font, UserInterface);
    if(!result)
    {
        return false;
    }

    return true;
}


bool ZoneClass::Render(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, FontClass* Font, UserInterfaceClass* UserInterface)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16], baseViewMatrix[16], orthoMatrix[16];
    int nodesDrawn, nodesCulled;
    bool result;


    // Get the world, view, and ortho matrices from the opengl and camera objects.
    OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    OpenGL->GetProjectionMatrix(projectionMatrix);
    m_Camera->GetBaseViewMatrix(baseViewMatrix);
    OpenGL->GetOrthoMatrix(orthoMatrix);

    // Construct the frustum.
    m_Frustum->ConstructFrustum(OpenGL, viewMatrix, projectionMatrix);

    // Clear the scene.
    OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

We call the sky dome rendering function first before drawing anything else.

    // Render the sky dome.
    result = RenderSkyDome(OpenGL, ShaderManager, viewMatrix, projectionMatrix);
    if(!result)
    {
        return false;
    }

    // Render the terrain using the terrain shader.
    result = m_Terrain->Render(OpenGL, ShaderManager, m_Light, m_Frustum, worldMatrix, viewMatrix, projectionMatrix);
    if(!result)
    {
        return false;
    }

    // Update with nodes drawn/culled.
    m_Terrain->GetNodesDrawn(nodesDrawn, nodesCulled);

    result = UserInterface->UpdateNodeStrings(Font, nodesDrawn, nodesCulled);
    if(!result)
    {
        return false;
    }

    // Render the user interface.
    result = UserInterface->Render(OpenGL, ShaderManager, Font, worldMatrix, baseViewMatrix, orthoMatrix);
    if(!result)
    {
        return false;
    }

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

    return true;
}


void ZoneClass::HandleMovementInput(InputClass* Input, float frameTime)
{
    bool keyDown;


    // Set the frame time for calculating the updated position.
    m_Position->SetFrameTime(frameTime);

    // Check if the input keys have been pressed.  If so, then update the position accordingly.
    keyDown = Input->IsLeftPressed();
    m_Position->TurnLeft(keyDown);

    keyDown = Input->IsRightPressed();
    m_Position->TurnRight(keyDown);

    keyDown = Input->IsUpPressed();
    m_Position->MoveForward(keyDown);

    keyDown = Input->IsDownPressed();
    m_Position->MoveBackward(keyDown);

    keyDown = Input->IsAPressed();
    m_Position->MoveUpward(keyDown);

    keyDown = Input->IsZPressed();
    m_Position->MoveDownward(keyDown);

    keyDown = Input->IsPgUpPressed();
    m_Position->LookUpward(keyDown);

    keyDown = Input->IsPgDownPressed();
    m_Position->LookDownward(keyDown);

    keyDown = Input->IsQPressed();
    m_Position->StrafeLeft(keyDown);

    keyDown = Input->IsEPressed();
    m_Position->StrafeRight(keyDown);

    return;
}


bool ZoneClass::RenderSkyDome(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, float* viewMatrix, float* projectionMatrix)
{
    float translateMatrix[16];
    float apexColor[4], centerColor[4];
    float cameraPosition[3];
    bool result;


    // Get the sky dome colors.
    m_SkyDome->GetApexColor(apexColor);
    m_SkyDome->GetCenterColor(centerColor);

Here is where we obtain the position of the camera so that we can translate our sky dome to be centered around the camera always. Then we use the camera position to create a world matrix that is centered around the camera.

    // Get the position of the camera.
    m_Camera->GetPosition(cameraPosition);

    // Translate the sky dome to be centered around the camera position.
    OpenGL->MatrixTranslation(translateMatrix, cameraPosition[0], cameraPosition[1], cameraPosition[2]);

Before rendering the sky dome we turn off both back face culling and the Z buffer.

    // Turn off back face culling and turn off the Z buffer.
    OpenGL->TurnOffCulling();
    OpenGL->TurnZBufferOff();

Then we render the sky dome using the sky dome shader.

    // Render the sky dome using the sky dome shader.
    result = ShaderManager->RenderSkyDomeShader(translateMatrix, viewMatrix, projectionMatrix, apexColor, centerColor);
    if(!result)
    {
        return false;
    }

    m_SkyDome->Render(OpenGL);

When the rendering is complete we turn Z buffering and back face culling on again.

    // Turn back face culling back on and turn the Z buffer back on.
    OpenGL->TurnOnCulling();
    OpenGL->TurnZBufferOn();

    return true;
}

Summary

We now have a colored sky background for the terrain using a very simple shader and a low polygon sphere object.

Optimization Notes

The method I presented uses the painter's algorithm which paints everything from the back to the front. Traditionally most graphics software is written this way as it is simpler to understand and debug. However this does incur a performance cost which can be expensive. And if you need a couple extra fps to hit your 60 fps target then this may be something you want to try.

I was watching a talk from AMD that addressed this specific style of rendering (painter's algorithm) and they recommended against it. Their drivers and hardware are optimized for culling in the opposite direction. They gave the example that the sky and terrain often cover a small portion of the final image, and that buildings, trees, and other objects represent the majority of on screen pixels in the final image. So by first drawing the sky you write to the entire buffer and nothing is culled. You then write the terrain over top of that and cover another 30 percent of the scene with very little culling. Then you render everything else which covers most of what you just wrote to the frame buffer. So you can see where this is going in terms of performance.

AMD's recommendation is to render things like sky domes at the very end, and the graphics card will only plot the pixels needed in the final image. Changing to this method means you need a sky dome larger than your scene. Also things like back face culling and disabling the Z buffer cannot be used.


To Do Exercises

1. Recompile the code and run the program. Use the page up and page down keys to look at the sky dome color gradient.

2. Change the apex and center color in the skydomeclass.cpp file inside the Initialize function to see the effect it has on the color gradient.

3. Set the fill mode in the no back face culling raster state to wire frame to see how the sky dome moves with the camera.

4. Create multiple gradients in the sky dome. For example have a gradient from +1.0 to +0.5, and then one from +0.25 to +0.0, and one from 0.0 and below. Do this by creating multiple "if" statements using the height variable and then supply several gradient colors to the pixel shader by expanding the GradientBuffer constant buffer.

5. Modify the pixel shader so the gradient goes from east to west.

6. Create a list of gradient colors for each hour in the day. Then create a system that keeps track of the time and updates your shader with the current gradient for that time of the day. Speed up the time so you can see the transitions. Note you may need to do a 30 second transition/interpolation at each hour point to prevent the popping effect created by the sudden gradient transition.

7. Increase and decrease the polygon count of the sphere used for the sky dome. Observe the effect it has on the look of the gradient and the dome.


Source Code

Source Code and Data Files: gl4terlinux11.tar.gz

Back to Tutorial Index