Tutorial 32: Glass and Ice

This tutorial will cover how to implement a glass and ice shader in OpenGL 4.0 using GLSL and C++. The code in this tutorial is based on the previous tutorials.

Glass and ice are both implemented in a shader the same way. A normal map is used to "bend" how light travels through the glass or ice. Each pixel in the normal map is used to offset sampling of any pixel behind the glass or ice surface. This creates the bending of light effect which closely simulates how light moves through glass and ice surfaces and then illuminates the objects behind them. We call this bending effect perturbation.

The difference between glass and ice is minimal in terms of how we code the shader. Both the glass and ice will use different color textures for representing the color of the surface so in the shader it is simply a different texture input for color. The normal map for glass and ice will also have different characteristics common to each surface type, but once again for the shader it is just a different texture input to be used as a look up table for normal vectors. The final difference is the amount of perturbation used. For perturbation amount we use a scaling variable we call refractionScale. This variable allows us to manually reduce the perturbation of light for glass surfaces and to also increase it to simulate the more aggressive perturbation of light that occurs in ice.

Now if you read and understood the water shader tutorial you will realize the technique in this tutorial is just a subset of the water rendering technique and works in the same fashion without the reflection. However, note that you can add a very slight reflection to create an even more realistic glass or ice surface, but I will leave that as an exercise for the reader and focus just on the basic effect for this tutorial.

We will now go over the basic algorithm this shader uses and then see some step-by-step image examples for both glass and ice.


Shader Algorithm

Step 1: Render the scene that is behind the glass to a texture, this is called the refraction.

Step 2: Project the refraction texture onto the glass surface.

Step 3: Perturb the texture coordinates of the refraction texture using a normal map to simulate light traveling through glass.

Step 4: Combine the perturbed refraction texture with a glass color texture for the final result.

We will now examine how to implement each step for both glass and ice.


Glass

So, first we need to render our entire scene that is viewable behind the glass to a texture. And then we project that render to texture onto the glass surface so it appears that the glass is just a see-through view of the scene although it is really a 2D texture rendered onto two triangles. We use render to texture and texture projection to do this which was covered in previous tutorials.

To simplify the example instead of having a complicated scene with numerous objects we will just say that our scene is a single square with a texture on it. So, rendering the scene to texture and then projecting it onto the glass model produces the following refraction result:

If the scene were more complex your window would actually become invisible and everything would look the same. The reason being is that if the texture is perfectly projected it would just cover the same 3D scene section with a 2D texture of the same scene making the resulting glass model a perfectly clear see-through glass with no way to differentiate it from the 3D scene itself. To even determine what is your glass model and what is the scene you will need to dim or brighten the glass texture to see that it actually is still there for debugging purposes.

Now that the scene is projected onto a texture you need a normal map so you can eventually perturb the refraction texture to make it look like it is behind glass. We will use the following normal map which will give a stripped faceted look to the glass:

Now that we have a normal map, we can use each individual pixel in the normal map as a look up for how to modify what pixel in the refraction texture is sampled. This allows us to sample the refraction texture slightly above, beside, and below to simulate light not traveling straight through but instead being bent slightly such as it is in glass. The scale of light being bent is controlled by the refractionScale variable which we set fairly low for glass; in this example it was set to 0.01. Note that this is entirely dependent on the normal map used as the normals can vary little or greatly in the normal map which prevents us from really having a scale value that will always work.

So now if we sample the refraction texture using the normal map texture as a lookup with the scale being 0.01, we get the following image:

The basic effect is mostly complete now. However, most glass has a tint or color associated with it and sometimes other markings. For the glass in this example, we will use the following color texture:

We take the color texture and the perturbed refraction and combine them to get the final glass effect:


Ice

Ice works exactly the same as glass with just different inputs into the shader. To start with we have the same scene of the textured square projected onto the ice surface model:

However, with ice we want a different look to the final surface so we will use a different color texture:

Also, the normal map will need to be different to simulate all the tiny bumps all over the surface with ice. Fortunately, the color texture has just the right amount of noise in it to be used to make an ice normal map. Simply take the color texture above and use the Nivida normal map filter in Photoshop with a Scale of 5 and it creates the following normal map:

Now if we use that normal map and a stronger refractionScale such as 0.1 for ice (instead of how we used 0.01 for glass) we get the following heavily perturbed refraction image:

Finally, if we combine the perturbed refraction texture with the ice color texture the resulting image is very realistic:

One final comment before we get into the code is that when you see these shaders working on surfaces that have motion behind them (such as a spinning cube behind the glass or ice) they look incredibly real.


Framework

The frame work for this tutorial is similar to the previous tutorials. The only new class added is the GlassShaderClass which handles the glass and ice shading. The RenderTextureClass is used in this tutorial for rendering the 3D scene to a texture. Also, the TextureShaderClass is used to render the spinning cube model for the regular scene that will be behind the glass or ice object.

We will start the code section by examining the GLSL code for the glass shader.


Glass.vs

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


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
in vec3 inputNormal;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;

This shader has a refractionPosition output variable for the refraction vertex coordinates that will be passed into the pixel shader.

out vec4 refractionPosition;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    mat4 viewProjectWorld;


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

Create the matrix used for transforming the input vertex coordinates to the projected coordinates.

    // Create the view projection world matrix for refraction.
    viewProjectWorld = viewMatrix * projectionMatrix;
    viewProjectWorld = worldMatrix * viewProjectWorld;

Transform the input vertex coordinates to the projected values and pass it into the pixel shader.

    // Calculate the input position against the viewProjectWorld matrix.
    refractionPosition = vec4(inputPosition, 1.0f) * viewProjectWorld;
}

Glass.ps

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


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec4 refractionPosition;


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


///////////////////////
// UNIFORM VARIABLES //
///////////////////////

The glass shader uses three different textures. The colorTexture is the basic surface color used for the glass. The normalTexture is the normal map look up table containing all the normal vectors. And finally, the refractionTexture contains the 3D scene that is behind the glass rendered to a 2D texture.

uniform sampler2D colorTexture;
uniform sampler2D normalTexture;
uniform sampler2D refractionTexture;

The refractionScale variable is used for scaling the amount of perturbation to the refraction texture. This is generally low for glass and higher for ice.

uniform float refractionScale;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    vec2 refractTexCoord;
    vec4 normalMap;
    vec3 normal;
    vec4 refractionColor;
    vec4 textureColor;

First convert the input projected homogenous coordinates (-1, +1) to (0, 1) texture coordinates.

    // Calculate the projected refraction texture coordinates.
    refractTexCoord.x = refractionPosition.x / refractionPosition.w / 2.0f + 0.5f;
    refractTexCoord.y = refractionPosition.y / refractionPosition.w / 2.0f + 0.5f;

Next sample the normal map and move it from (0, 1) texture coordinates to (-1, 1) coordinates.

    // Sample the normal from the normal map texture.
    normalMap = texture(normalTexture, texCoord);

    // Expand the range of the normal from (0,1) to (-1,+1).
    normal = (normalMap.xyz * 2.0f) - 1.0f;

Now perturb the refraction texture sampling location by the normals that were calculated. Also multiply the normal by the refraction scale to increase or decrease the perturbation.

    // Re-position the texture coordinate sampling position by the normal map value to simulate light distortion through glass.
    refractTexCoord = refractTexCoord + (normal.xy * refractionScale);

Next sample the refraction texture using the perturbed coordinates and sample the color texture using the normal input texture coordinates.

    // Sample the texture pixel from the refraction texture using the perturbed texture coordinates.
    refractionColor = texture(refractionTexture, refractTexCoord);

    // Sample the texture pixel from the glass color texture.
    textureColor = texture(colorTexture, texCoord);

Finally combine the refraction and color texture for the final result.

    // Evenly combine the glass color and refraction value for the final color.
    outputColor = mix(refractionColor, textureColor, 0.5f);
}

Glassshaderclass.h

The GlassShaderClass is based on the TextureShaderClass with slight changes for glass shading.

////////////////////////////////////////////////////////////////////////////////
// Filename: glassshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GLASSSHADERCLASS_H_
#define _GLASSSHADERCLASS_H_


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


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


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

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

    bool SetShaderParameters(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

Glassshaderclass.cpp

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


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


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


GlassShaderClass::~GlassShaderClass()
{
}


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


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

We load the glass.vs and glass.ps GLSL shader files here.

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

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

    return true;
}


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

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

    return;
}


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

    // 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 GlassShaderClass::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* GlassShaderClass::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 GlassShaderClass::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 GlassShaderClass::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 GlassShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float refractionScale)
{
    float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16];
    int location;

Transpose and set the matrices.

    // 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 color texture of the glass or ice in the first texture unit.

    // Set the texture in the pixel shader to use the data from the first texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "colorTexture");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 0);

Set the normal map used to perturb the sampling of the scene in the second texture unit.

    // Set the texture in the pixel shader to use the data from the second texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "normalTexture");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 1);

Set the refraction texture of the regular scene in the third pixel shader texture unit.

    // Set the texture in the pixel shader to use the data from the third texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "refractionTexture");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 2);

Set the refraction scale in the pixel shader.

    // Set the reflect refract scale in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "refractionScale");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1f(location, refractionScale);

    return true;
}

Applicationclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_NEAR = 0.3f;
const float SCREEN_DEPTH = 1000.0f;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "openglclass.h"
#include "modelclass.h"
#include "cameraclass.h"
#include "textureshaderclass.h"
#include "rendertextureclass.h"

The new GlassShaderClass header file is included now.

#include "glassshaderclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class Name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
    ApplicationClass();
    ApplicationClass(const ApplicationClass&);
    ~ApplicationClass();

    bool Initialize(Display*, Window, int, int);
    void Shutdown();
    bool Frame(InputClass*);

private:
    bool RenderSceneToTexture(float);
    bool Render(float);

private:
    OpenGLClass* m_OpenGL;
    CameraClass* m_Camera;

We create a model for the spinning cube and the glass window.

    ModelClass* m_Model;
    ModelClass* m_WindowModel;

We need a render to texture object to render the spinning cube part of the scene.

    RenderTextureClass* m_RenderTexture;

The texture shader is used to render the normal scene. The glass shader is used to render the glass window model.

    TextureShaderClass* m_TextureShader;
    GlassShaderClass* m_GlassShader;
};

#endif

Applicationclass.cpp

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


ApplicationClass::ApplicationClass()
{
    m_OpenGL = 0;
    m_Camera = 0;
    m_Model = 0;
    m_WindowModel = 0;
    m_RenderTexture = 0;
    m_TextureShader = 0;
    m_GlassShader = 0;
}


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


ApplicationClass::~ApplicationClass()
{
}


bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
    char modelFilename[128];
    char textureFilename1[128], textureFilename2[128];
    bool result;


    // Create and initialize the OpenGL object.
    m_OpenGL = new OpenGLClass;

    result = m_OpenGL->Initialize(display, win, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, VSYNC_ENABLED);
    if(!result)
    {
        return false;
    }

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

    m_Camera->SetPosition(0.0f, 0.0f, -5.0f);
    m_Camera->Render();

Create a model for the cube that will be spinning behind the glass window. It has a normal map associated with it but is not used so you can ignore it. I did this just to make the initialize generic.

    // Set the file name of the model.
    strcpy(modelFilename, "../Engine/data/cube.txt");

    // Set the file name of the textures.
    strcpy(textureFilename1, "../Engine/data/stone01.tga");
    strcpy(textureFilename2, "../Engine/data/normal03.tga");

    // Create and initialize the model object.
    m_Model = new ModelClass;

    result = m_Model->Initialize(m_OpenGL, modelFilename, textureFilename1, textureFilename2, false);
    if(!result)
    {
        return false;
    }

Create a model for the glass window. It uses the square .obj model since the window will just be two triangles that make up a square. It also uses a texture called glass01.tga for the glass color and a normal map called normal.tga for the perturbation of the glass refraction.

    // Set the file name of the model.
    strcpy(modelFilename, "../Engine/data/square.txt");

    // Set the file name of the textures.
    strcpy(textureFilename1, "../Engine/data/glass01.tga");
    strcpy(textureFilename2, "../Engine/data/normal03.tga");

    // Create and initialize the model object.
    m_WindowModel = new ModelClass;

    result = m_WindowModel->Initialize(m_OpenGL, modelFilename, textureFilename1, textureFilename2, false);
    if(!result)
    {
        return false;
    }

The render to texture object will be used to render the refraction of the scene to a texture and then passed into the glass shader as input.

    // Create and initialize the reflection render to texture object.  Set to texture unit 2 for the glass shader.
    m_RenderTexture = new RenderTextureClass;

    result = m_RenderTexture->Initialize(m_OpenGL, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, 0, 2);
    if(!result)
    {
        return false;
    }

The texture shader is used to render the spinning cube.

    // Create and initialize the texture shader object.
    m_TextureShader = new TextureShaderClass;

    result = m_TextureShader->Initialize(m_OpenGL);
    if(!result)
    {
        return false;
    }

This is where the new glass shader is created and initialized.

    // Create and initialize the glass shader object.
    m_GlassShader = new GlassShaderClass;

    result = m_GlassShader->Initialize(m_OpenGL);
    if(!result)
    {
        return false;
    }

    return true;
}


void ApplicationClass::Shutdown()
{
    // Release the glass shader object.
    if(m_GlassShader)
    {
        m_GlassShader->Shutdown();
        delete m_GlassShader;
        m_GlassShader = 0;
    }

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

    // Release the render to texture object.
    if(m_RenderTexture)
    {
        m_RenderTexture->Shutdown();
        delete m_RenderTexture;
        m_RenderTexture = 0;
    }

    // Release the window model object.
    if(m_WindowModel)
    {
        m_WindowModel->Shutdown();
        delete m_WindowModel;
        m_WindowModel = 0;
    }

    // Release the model object.
    if(m_Model)
    {
        m_Model->Shutdown();
        delete m_Model;
        m_Model = 0;
    }

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

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

    return;
}


bool ApplicationClass::Frame(InputClass* Input)
{
    static float rotation = 360.0f;
    bool result;


    // Check if the escape key has been pressed, if so quit.
    if(Input->IsEscapePressed() == true)
    {
        return false;
    }

We update the rotation of the cube each frame and send the same value into both the RenderSceneToTexture and Render function to keep the rotation in sync.

    // Update the rotation variable each frame.
    rotation -= 0.0174532925f * 1.0f;
    if(rotation <= 0.0f)
    {
        rotation += 360.0f;
    }

First, we render the 3D scene to a texture so the glass shader will have a refraction texture as input.

    // Render the cube spinning scene to texture first.
    result = RenderSceneToTexture(rotation);
    if(!result)
    {
        return false;
    }

Then we render the scene again normally and render the glass over top of it with the perturbed and colored refraction texture rendered on the glass model.

    // Render the graphics scene.
    result = Render(rotation);
    if(!result)
    {
        return false;
    }

    return true;
}

The RenderSceneToTexture function just renders the 3D spinning cube scene to a render texture.

bool ApplicationClass::RenderSceneToTexture(float rotation)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16];
    bool result;


    // Set the render target to be the render to texture and clear it.
    m_RenderTexture->SetRenderTarget();
    m_RenderTexture->ClearRenderTarget(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the world, view, and projection matrices from the opengl and camera objects.
    m_OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_OpenGL->GetProjectionMatrix(projectionMatrix);

    // Rotate the world matrix by the rotation value so that the cube will spin.
    m_OpenGL->MatrixRotationY(worldMatrix, rotation);

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

    // Render the model.
    m_Model->Render();

    // Reset the render target back to the original back buffer and not the render to texture anymore.  And reset the viewport back to the original.
    m_OpenGL->SetBackBufferRenderTarget();
    m_OpenGL->ResetViewport();

    return true;
}


bool ApplicationClass::Render(float rotation)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16];
    float refractionScale;
    bool result;

First set the refraction scale to modify how much perturbation occurs in the glass.

    // Set the refraction scale for the glass shader.
    refractionScale = 0.01f;

    // Clear the buffers to begin the scene.
    m_OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the world, view, and projection matrices from the opengl and camera objects.
    m_OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_OpenGL->GetProjectionMatrix(projectionMatrix);

Then render the 3D spinning cube scene as normal.

    // Rotate the world matrix by the rotation value so that the triangle will spin.
    m_OpenGL->MatrixRotationY(worldMatrix, rotation);

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

    // Render the model.
    m_Model->Render();

Now render the window model using the glass shader with the color texture, normal map, refraction render to texture, and refraction scale as input.

    // Translate to back where the window model will be rendered.
    m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 0.0f, -1.5f);

    // Set the glass shader as the current shader program and set the parameters that it will use for rendering.
    result = m_GlassShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, refractionScale);
    if(!result)
    {
        return false;
    }

    // Render the window model.
    m_WindowModel->Render();

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

    return true;
}

Summary

We can now render both glass and ice effects through the use of refraction and a normal map for perturbation.


To Do Exercises

1. Recompile and run the program. You should get a spinning cube behind green perturbed glass. Press escape to quit.

2. Implement the ice effect by using the provided ice color and normal texture and changing the refraction scale to 0.1f.

3. Change the value of the refractionScale to see how it affects the perturbation.

4. Modify the combination of the color texture and the perturbed refraction texture in the pixel shader to get different output results.

5. Make your own glass color texture and normal map to get your own glass effect to work (also modify the refractionScale so it looks right for your normal map).


Source Code

Source Code and Data Files: gl4linuxtut32_src.zip

Back to Tutorial Index