Tutorial 32: Glass and Ice

This tutorial will cover how to implement a glass and ice shader in DirectX 10 using HLSL 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.

We will now go over the basic algorithm this shader uses and then see some step by step image examples of 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 a 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. Make sure you at least run the executable for this tutorial to see what I'm talking about.


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 object.

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


Glass.fx

////////////////////////////////////////////////////////////////////////////////
// Filename: glass.fx
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
matrix worldMatrix;
matrix viewMatrix;
matrix projectionMatrix;

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.

Texture2D colorTexture;
Texture2D normalTexture;
Texture2D 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.

float refractionScale;


///////////////////
// SAMPLE STATES //
///////////////////
SamplerState SampleType
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};


//////////////
// TYPEDEFS //
//////////////
struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0;
};

The PixelInputType structure has a new refractionPosition variable for the refraction vertex coordinates that will be passed into the pixel shader.

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float4 refractionPosition : TEXCOORD1;
};


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
PixelInputType GlassVertexShader(VertexInputType input)
{
    PixelInputType output;
    matrix viewProjectWorld;

    
    // Change the position vector to be 4 units for proper matrix calculations.
    input.position.w = 1.0f;

    // Calculate the position of the vertex against the world, view, and projection matrices.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);
    
    // Store the texture coordinates for the pixel shader.
    output.tex = input.tex;

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

    // Create the view projection world matrix for refraction.
    viewProjectWorld = mul(viewMatrix, projectionMatrix);
    viewProjectWorld = mul(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.
    output.refractionPosition = mul(input.position, viewProjectWorld);

    return output;
}


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 GlassPixelShader(PixelInputType input) : SV_Target
{
    float2 refractTexCoord;
    float4 normalMap;
    float3 normal;
    float4 refractionColor;
    float4 textureColor;
    float4 color;

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

    // Calculate the projected refraction texture coordinates.
    refractTexCoord.x = input.refractionPosition.x / input.refractionPosition.w / 2.0f + 0.5f;
    refractTexCoord.y = -input.refractionPosition.y / input.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 = normalTexture.Sample(SampleType, input.tex);

    // 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 = refractionTexture.Sample(SampleType, refractTexCoord);

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

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

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

    return color;
}


////////////////////////////////////////////////////////////////////////////////
// Technique
////////////////////////////////////////////////////////////////////////////////
technique10 GlassTechnique
{
    pass pass0
    {
        SetVertexShader(CompileShader(vs_4_0, GlassVertexShader()));
        SetPixelShader(CompileShader(ps_4_0, GlassPixelShader()));
        SetGeometryShader(NULL);
    }
}

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 <d3d10.h>
#include <d3dx10.h>
#include <fstream>
using namespace std;


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

	bool Initialize(ID3D10Device*, HWND);
	void Shutdown();
	void Render(ID3D10Device*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D10ShaderResourceView*, 
		    ID3D10ShaderResourceView*, ID3D10ShaderResourceView*, float);

private:
	bool InitializeShader(ID3D10Device*, HWND, WCHAR*);
	void ShutdownShader();
	void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*);

	void SetShaderParameters(D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D10ShaderResourceView*, 
				 ID3D10ShaderResourceView*, ID3D10ShaderResourceView*, float);
	void RenderShader(ID3D10Device*, int);

private:
	ID3D10Effect* m_effect;
	ID3D10EffectTechnique* m_technique;
	ID3D10InputLayout* m_layout;

	ID3D10EffectMatrixVariable* m_worldMatrixPtr;
	ID3D10EffectMatrixVariable* m_viewMatrixPtr;
	ID3D10EffectMatrixVariable* m_projectionMatrixPtr;

The glass shader requires three texture inputs. These three pointers are for the color, normal map, and refraction texture.

	ID3D10EffectShaderResourceVariable* m_colorTexturePtr;
	ID3D10EffectShaderResourceVariable* m_normalTexturePtr;
	ID3D10EffectShaderResourceVariable* m_refractionTexturePtr;

The glass shader also needs a refraction scale value which the m_refractionScalePtr pointer provides an interface to.

	ID3D10EffectScalarVariable* m_refractionScalePtr;
};

#endif

Glassshaderclass.cpp

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


GlassShaderClass::GlassShaderClass()
{
	m_effect = 0;
	m_technique = 0;
	m_layout = 0;

	m_worldMatrixPtr = 0;
	m_viewMatrixPtr = 0;
	m_projectionMatrixPtr = 0;

Initialize the three texture pointers and the refraction scale pointer to null in the class constructor.

	m_colorTexturePtr = 0;
	m_normalTexturePtr = 0;
	m_refractionTexturePtr = 0;

	m_refractionScalePtr = 0;
}


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


GlassShaderClass::~GlassShaderClass()
{
}


bool GlassShaderClass::Initialize(ID3D10Device* device, HWND hwnd)
{
	bool result;

Initialize the glass shader with the new glass.fx HLSL file.

	// Initialize the shader that will be used to draw the triangles.
	result = InitializeShader(device, hwnd, L"../Engine/glass.fx");
	if(!result)
	{
		return false;
	}

	return true;
}


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

	return;
}

The Render function now takes as input the color texture, normal map texture, refraction texture, and refraction scale value. These values are set in the shader first using the SetShaderParameters function before the rendering occurs in the RenderShader function which is called afterward.

void GlassShaderClass::Render(ID3D10Device* device, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix,
			      D3DXMATRIX projectionMatrix, ID3D10ShaderResourceView* colorTexture, 
			      ID3D10ShaderResourceView* normalTexture, ID3D10ShaderResourceView* refractionTexture,
			      float refractionScale)
{
	// Set the shader parameters that will be used for rendering.
	SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, colorTexture, normalTexture, refractionTexture, 
			    refractionScale);

	// Now render the prepared buffers with the shader.
	RenderShader(device, indexCount);

	return;
}


bool GlassShaderClass::InitializeShader(ID3D10Device* device, HWND hwnd, WCHAR* filename)
{
	HRESULT result;
	ID3D10Blob* errorMessage;
	D3D10_INPUT_ELEMENT_DESC polygonLayout[2];
	unsigned int numElements;
	D3D10_PASS_DESC passDesc;


	// Initialize the error message.
	errorMessage = 0;

	// Load the shader in from the file.
	result = D3DX10CreateEffectFromFile(filename, NULL, NULL, "fx_4_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, 
					    device, NULL, NULL, &m_effect, &errorMessage, NULL);
	if(FAILED(result))
	{
		// If the shader failed to compile it should have writen something to the error message.
		if(errorMessage)
		{
			OutputShaderErrorMessage(errorMessage, hwnd, filename);
		}
		// If there was  nothing in the error message then it simply could not find the shader file itself.
		else
		{
			MessageBox(hwnd, filename, L"Missing Shader File", MB_OK);
		}

		return false;
	}

The name of the technique is set to GlassTechnique to match the shader technique name in the HLSL file.

	// Get a pointer to the technique inside the shader.
	m_technique = m_effect->GetTechniqueByName("GlassTechnique");
	if(!m_technique)
	{
		return false;
	}

	// Now setup the layout of the data that goes into the shader.
	polygonLayout[0].SemanticName = "POSITION";
	polygonLayout[0].SemanticIndex = 0;
	polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[0].InputSlot = 0;
	polygonLayout[0].AlignedByteOffset = 0;
	polygonLayout[0].InputSlotClass = D3D10_INPUT_PER_VERTEX_DATA;
	polygonLayout[0].InstanceDataStepRate = 0;

	polygonLayout[1].SemanticName = "TEXCOORD";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT;
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D10_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D10_INPUT_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;

	// Get a count of the elements in the layout.
	numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

	// Get the description of the first pass described in the shader technique.
	m_technique->GetPassByIndex(0)->GetDesc(&passDesc);

	// Create the input layout.
	result = device->CreateInputLayout(polygonLayout, numElements, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, 
					   &m_layout);
	if(FAILED(result))
	{
		return false;
	}

	// Get pointers to the three matrices inside the shader so they can be updated from this class.
	m_worldMatrixPtr = m_effect->GetVariableByName("worldMatrix")->AsMatrix();
	m_viewMatrixPtr = m_effect->GetVariableByName("viewMatrix")->AsMatrix();
	m_projectionMatrixPtr = m_effect->GetVariableByName("projectionMatrix")->AsMatrix();

The three texture pointers and the refraction scale pointer are setup here.

	// Get pointers to the texture resources inside the shader.
	m_colorTexturePtr = m_effect->GetVariableByName("colorTexture")->AsShaderResource();
	m_normalTexturePtr = m_effect->GetVariableByName("normalTexture")->AsShaderResource();
	m_refractionTexturePtr = m_effect->GetVariableByName("refractionTexture")->AsShaderResource();

	// Get a pointer to the refraction scale variable inside the shader.
	m_refractionScalePtr = m_effect->GetVariableByName("refractionScale")->AsScalar();

	return true;
}


void GlassShaderClass::ShutdownShader()
{

The three texture pointers and the refraction scale pointer are released here in the ShutdownShader function.

	// Release the refraction scale pointer.
	m_refractionScalePtr = 0;

	// Release the pointers to the textures in the shader.
	m_colorTexturePtr = 0;
	m_normalTexturePtr = 0;
	m_refractionTexturePtr = 0;

	// Release the pointers to the matrices inside the shader.
	m_worldMatrixPtr = 0;
	m_viewMatrixPtr = 0;
	m_projectionMatrixPtr = 0;

	// Release the pointer to the shader layout.
	if(m_layout)
	{
		m_layout->Release();
		m_layout = 0;
	}

	// Release the pointer to the shader technique.
	m_technique = 0;

	// Release the pointer to the shader.
	if(m_effect)
	{
		m_effect->Release();
		m_effect = 0;
	}

	return;
}


void GlassShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename)
{
	char* compileErrors;
	unsigned long bufferSize, i;
	ofstream fout;


	// Get a pointer to the error message text buffer.
	compileErrors = (char*)(errorMessage->GetBufferPointer());

	// Get the length of the message.
	bufferSize = errorMessage->GetBufferSize();

	// Open a file to write the error message to.
	fout.open("shader-error.txt");

	// Write out the error message.
	for(i=0; i<bufferSize; i++)
	{
		fout << compileErrors[i];
	}

	// Close the file.
	fout.close();

	// Release the error message.
	errorMessage->Release();
	errorMessage = 0;

	// Pop a message up on the screen to notify the user to check the text file for compile errors.
	MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_OK);

	return;
}


void GlassShaderClass::SetShaderParameters(D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix, 
					   ID3D10ShaderResourceView* colorTexture, ID3D10ShaderResourceView* normalTexture, 
					   ID3D10ShaderResourceView* refractionTexture, float refractionScale)
{
	// Set the world matrix variable inside the shader.
	m_worldMatrixPtr->SetMatrix((float*)&worldMatrix);

	// Set the view matrix variable inside the shader.
	m_viewMatrixPtr->SetMatrix((float*)&viewMatrix);

	// Set the projection matrix variable inside the shader.
	m_projectionMatrixPtr->SetMatrix((float*)&projectionMatrix);

The color, normal, and refraction textures are set in the shader here.

	// Bind the textures.
	m_colorTexturePtr->SetResource(colorTexture);
	m_normalTexturePtr->SetResource(normalTexture);
	m_refractionTexturePtr->SetResource(refractionTexture);

The refractionScale value in the shader is set here.

	// Set the refraction scale variable inside the shader.
	m_refractionScalePtr->SetFloat(refractionScale);

	return;
}


void GlassShaderClass::RenderShader(ID3D10Device* device, int indexCount)
{
	D3D10_TECHNIQUE_DESC techniqueDesc;
	unsigned int i;
	

	// Set the input layout.
	device->IASetInputLayout(m_layout);

	// Get the description structure of the technique from inside the shader so it can be used for rendering.
	m_technique->GetDesc(&techniqueDesc);

	// Go through each pass in the technique (should be just one currently) and render the triangles.
	for(i=0; i<techniqueDesc.Passes; ++i)
	{
		m_technique->GetPassByIndex(i)->Apply(0);
		device->DrawIndexed(indexCount, 0, 0);
	}

	return;
}

Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


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


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

The new GlassShaderClass header file is included now.

#include "glassshaderclass.h"


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

	bool Initialize(int, int, HWND);
	void Shutdown();
	void Frame();

private:
	void RenderToTexture(float);
	void Render(float);

private:
	D3DClass* m_D3D;
	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

Graphicsclass.cpp

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


GraphicsClass::GraphicsClass()
{
	m_D3D = 0;
	m_Camera = 0;
	m_Model = 0;
	m_WindowModel = 0;
	m_RenderTexture = 0;
	m_TextureShader = 0;

The new GlassShaderClass object is initialized to null in the class constructor.

	m_GlassShader = 0;
}


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


GraphicsClass::~GraphicsClass()
{
}


bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	bool result;

		
	// Create the Direct3D object.
	m_D3D = new D3DClass;
	if(!m_D3D)
	{
		return false;
	}

	// Initialize the Direct3D object.
	result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK);
		return false;
	}

	// Create the camera object.
	m_Camera = new CameraClass;
	if(!m_Camera)
	{
		return false;
	}

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 the last parameter of the Initialize. I did this just to make the function generic.

	// Create the model object.
	m_Model = new ModelClass;
	if(!m_Model)
	{
		return false;
	}

	// Initialize the model object.
	result = m_Model->Initialize(m_D3D->GetDevice(), "../Engine/data/cube.txt", L"../Engine/data/seafloor.dds", L"../Engine/data/bump03.dds");
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK);
		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.dds for the glass color and a normal map called bump03.dds for the perturbation of the glass refraction.

	// Create the window model object.
	m_WindowModel = new ModelClass;
	if(!m_WindowModel)
	{
		return false;
	}

	// Initialize the window model object.
	result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/glass01.dds", L"../Engine/data/bump03.dds");
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the window model object.", L"Error", MB_OK);
		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 the render to texture object.
	m_RenderTexture = new RenderTextureClass;
	if(!m_RenderTexture)
	{
		return false;
	}

	// Initialize the render to texture object.
	result = m_RenderTexture->Initialize(m_D3D->GetDevice(), screenWidth, screenHeight);
	if(!result)
	{
		return false;
	}

The texture shader is used to render the spinning cube.

	// Create the texture shader object.
	m_TextureShader = new TextureShaderClass;
	if(!m_TextureShader)
	{
		return false;
	}

	// Initialize the texture shader object.
	result = m_TextureShader->Initialize(m_D3D->GetDevice(), hwnd);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the texture shader object.", L"Error", MB_OK);
		return false;
	}

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

	// Create the glass shader object.
	m_GlassShader = new GlassShaderClass;
	if(!m_GlassShader)
	{
		return false;
	}

	// Initialize the glass shader object.
	result = m_GlassShader->Initialize(m_D3D->GetDevice(), hwnd);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the glass shader object.", L"Error", MB_OK);
		return false;
	}

	return true;
}


void GraphicsClass::Shutdown()
{

This new glass shader is released here in the Shutdown function.

	// 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 Direct3D object.
	if(m_D3D)
	{
		m_D3D->Shutdown();
		delete m_D3D;
		m_D3D = 0;
	}

	return;
}


void GraphicsClass::Frame()
{
	static float rotation = 0.0f;

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

	// Update the rotation variable each frame.
	rotation += (float)D3DX_PI * 0.005f;
	if(rotation > 360.0f)
	{
		rotation -= 360.0f;
	}

The position of the camera is set here also.

	// Set the position of the camera.
	m_Camera->SetPosition(0.0f, 0.0f, -10.0f);

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

	// Render the scene to texture first.
	RenderToTexture(rotation);

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.

	// Now render the final scene.
	Render(rotation);

	return;
}

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

void GraphicsClass::RenderToTexture(float rotation)
{
	D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix;


	// Set the render target to be the render to texture.
	m_RenderTexture->SetRenderTarget(m_D3D->GetDevice(), m_D3D->GetDepthStencilView());

	// Clear the render to texture.
	m_RenderTexture->ClearRenderTarget(m_D3D->GetDevice(), m_D3D->GetDepthStencilView(), 0.0f, 0.0f, 0.0f, 1.0f);

	// Generate the view matrix based on the camera's position.
	m_Camera->Render();

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

	// Multiply the world matrix by the rotation.
	D3DXMatrixRotationY(&worldMatrix, rotation);

	// Put the cube model vertex and index buffers on the graphics pipeline to prepare them for drawing.
	m_Model->Render(m_D3D->GetDevice());

	// Render the cube model using the texture shader.
	m_TextureShader->Render(m_D3D->GetDevice(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture());

	// Reset the render target back to the original back buffer and not the render to texture anymore.
	m_D3D->SetBackBufferRenderTarget();

	return;
}


void GraphicsClass::Render(float rotation)
{
	D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix;
	float refractionScale;

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_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

	// Generate the view matrix based on the camera's position.
	m_Camera->Render();

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

Then render the 3D spinning cube scene as normal.

	// Multiply the world matrix by the rotation.
	D3DXMatrixRotationY(&worldMatrix, rotation);

	// Put the cube model vertex and index buffers on the graphics pipeline to prepare them for drawing.
	m_Model->Render(m_D3D->GetDevice());

	// Render the cube model using the texture shader.
	m_TextureShader->Render(m_D3D->GetDevice(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture());

	// Reset the world matrix.
	m_D3D->GetWorldMatrix(worldMatrix);

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.
	D3DXMatrixTranslation(&worldMatrix, 0.0f, 0.0f, -1.5f);

	// Put the window model vertex and index buffers on the graphics pipeline to prepare them for drawing.
	m_WindowModel->Render(m_D3D->GetDevice());

	// Render the window model using the glass shader.
	m_GlassShader->Render(m_D3D->GetDevice(), m_WindowModel->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, 
			      m_WindowModel->GetTexture(), m_WindowModel->GetNormalMap(), m_RenderTexture->GetShaderResourceView(),
			      refractionScale);

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

	return;
}

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. To see the ice effect change the following function in GraphicsClass::Initialize from:

	result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/glass01.dds", L"../Engine/data/bump03.dds");

To:

	result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/ice01.dds", L"../Engine/data/icebump01.dds");

And change the refractionScale to 0.1f and move the camera closer:

	m_Camera->SetPosition(0.0f, 0.0f, -5.0f);
	refractionScale = 0.1f;

Now recompile and run the program with those three changes to see the ice effect.

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 and get your own personal glass shader effect to work (also modify the refractionScale so it looks right for your normal map).


Source Code

Visual Studio 2008 Project: dx10tut32.zip

Source Only: dx10src32.zip

Executable Only: dx10exe32.zip

Back to Tutorial Index