Tutorial 22: Managing Multiple Shaders

One of the issues that quickly arises in a graphics application that uses multiple shaders is how to manage them. In the OpenGL 4.0 tutorial series I generally don't show how manage them as it makes the tutorials less focused and harder to comprehend. So, in this tutorial I will present one of the simpler methods for managing multiple shaders.

For this tutorial we will use a new class called ShaderManagerClass. What this class does is it encapsulates the loading, unloading, and usage of all the shaders that the application requires. In design patterns terminology this is usually referred to as a facade pattern as we are encapsulating and providing a singular simplified interface to a much larger body of code.

In this tutorial the ShaderManagerClass will contain the texture, light, and normal map shader objects. It will load and unload all three of them, and it will also provide interfaces to have them render objects. This allows us to create just a single ShaderManagerClass object and then pass a single pointer to the class object around the application so that rendering objects is just a single function call using the object pointer. This is similar to how the OpenGLClass has been used in previous tutorials.

The following image shows a sphere being rendered with a texture shader, a light shader, and a normal map shader all at the same time through the use of the ShaderManagerClass:


Framework

Now that we have encapsulated all the shaders inside ShaderManagerClass our frame work will look like the following:

We will start the code section by examining the ShaderManagerClass.


Shadermanagerclass.h

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

Here is where we include each shader class header file that we want to use in the application.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureshaderclass.h"
#include "lightshaderclass.h"
#include "normalmapshaderclass.h"


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

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

We create a single render function for each shader that the ShaderManagerClass handles.

    bool RenderTextureShader(float*, float*, float*);
    bool RenderLightShader(float*, float*, float*, float*, float*);
    bool RenderNormalMapShader(float*, float*, float*, float*, float*);

private:

The ShaderManagerClass contains a private class object for each shader type the application will be using.

    TextureShaderClass* m_TextureShader;
    LightShaderClass* m_LightShader;
    NormalMapShaderClass* m_NormalMapShader;
};

#endif

Shadermanagerclass.cpp

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

Initialize the private shader class objects to null in the class constructor.

ShaderManagerClass::ShaderManagerClass()
{
    m_TextureShader = 0;
    m_LightShader = 0;
    m_NormalMapShader = 0;
}


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


ShaderManagerClass::~ShaderManagerClass()
{
}


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

Create and initialize the texture shader object.

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

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

Create and initialize the light shader object.

    // Create and initialize the light shader object.
    m_LightShader = new LightShaderClass;

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

Create and initialize the normal map shader object.

    // Create and initialize the normal map shader object.
    m_NormalMapShader = new NormalMapShaderClass;

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

    return true;
}

The Shutdown function releases all the shaders that were in use by the application.

void ShaderManagerClass::Shutdown()
{
    // Release the normal map shader object.
    if(m_NormalMapShader)
    {
        m_NormalMapShader->Shutdown();
        delete m_NormalMapShader;
        m_NormalMapShader = 0;
    }

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

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

    return;
}

Here is where we create a specialized render function for each shader type that ShaderManagerClass uses. In the application we can now just pass around a pointer to the ShaderManagerClass object and then make a call to one of the following functions to render any model with the desired shader.

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


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


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

Applicationclass.h

In the ApplicationClass we include just the header for the ShaderManagerClass instead of all the individual shader headers. Likewise, we only need one ShaderManagerClass object.

////////////////////////////////////////////////////////////////////////////////
// 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 "cameraclass.h"
#include "modelclass.h"
#include "lightclass.h"
#include "shadermanagerclass.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 Render(float);

private:
    OpenGLClass* m_OpenGL;
    CameraClass* m_Camera;
    ModelClass* m_Model;
    LightClass* m_Light;
    ShaderManagerClass* m_ShaderManager;
};

#endif

Applicationclass.cpp

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


ApplicationClass::ApplicationClass()
{
    m_OpenGL = 0;
    m_Camera = 0;
    m_Model = 0;
    m_Light = 0;
    m_ShaderManager = 0;
}


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


ApplicationClass::~ApplicationClass()
{
}


bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
    char modelFilename[128], 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, -10.0f);
    m_Camera->Render();

We will just load a single model of a sphere with the two textures it needs. When we render, we will just render this model each time with the different shaders.

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

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

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

    result = m_Model->Initialize(m_OpenGL, modelFilename, textureFilename1, textureFilename2, true);
    if(!result)
    {
        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.0f, 0.0f, 1.0f);

We create and initialize the ShaderManagerClass object here.

    // Create and initialize the shader manager object.
    m_ShaderManager = new ShaderManagerClass;

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

    return true;
}


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

    // Release the light object.
    if(m_Light)
    {
        delete m_Light;
        m_Light = 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;
    }

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

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

    return true;
}


bool ApplicationClass::Render(float rotation)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16], rotateMatrix[16], translateMatrix[16];
    float diffuseLightColor[4], lightDirection[3];
    bool result;


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

    // Setup the rotation matrix.
    m_OpenGL->MatrixRotationY(rotateMatrix, rotation);

    // Get the light properties.
    m_Light->GetDirection(lightDirection);
    m_Light->GetDiffuseColor(diffuseLightColor);

Render the first sphere model using the texture shader from the ShaderManagerClass object.

    // Setup matrices.
    m_OpenGL->MatrixTranslation(translateMatrix, 0.0f, 1.0f, 0.0f);
    m_OpenGL->MatrixMultiply(worldMatrix, rotateMatrix, translateMatrix);

    // Render the model using the texture shader.
    result = m_ShaderManager->RenderTextureShader(worldMatrix, viewMatrix, projectionMatrix);  if(!result) { return false; }
    m_Model->Render(1);

Render the second sphere model using the light shader from the ShaderManagerClass object.

    // Setup matrices.
    m_OpenGL->MatrixTranslation(translateMatrix, -1.5f, -1.0f, 0.0f);
    m_OpenGL->MatrixMultiply(worldMatrix, rotateMatrix, translateMatrix);

    // Render the model using the light shader.
    result = m_ShaderManager->RenderLightShader(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor);  if(!result) { return false; }
    m_Model->Render(1);

Render the third sphere model using the normal map shader from the ShaderManagerClass object.

    // Setup matrices.
    m_OpenGL->MatrixTranslation(translateMatrix, 1.5f, -1.0f, 0.0f);
    m_OpenGL->MatrixMultiply(worldMatrix, rotateMatrix, translateMatrix);

    // Render the model using the normal map shader.
    result = m_ShaderManager->RenderNormalMapShader(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor);  if(!result) { return false; }
    m_Model->Render(2);

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

    return true;
}

Modelclass.cpp

Note we did make a small change to the ModelClass::Render function for this tutorial so we can specify which of the textures should be activated, as this model will be rendered by different shaders that require a different number of textures.

void ModelClass::Render(int textureCount)
{
    // Set the textures for the model.
    if(textureCount == 1)
    {
        m_Textures[0].SetTexture(m_OpenGLPtr);
    }
    else
    {
        m_Textures[0].SetTexture(m_OpenGLPtr);
        m_Textures[1].SetTexture(m_OpenGLPtr);
    }

    // Put the vertex and index buffers on the graphics pipeline to prepare them for drawing.
    RenderBuffers();

    return;
}

Summary

Now we have a single class which encapsulates all the shading functionality that our graphics application will need.


To Do Exercises

1. Compile and run the code. You should see three spheres each shaded with a different shader. Press escape to quit.

2. Add another shader to the ShaderManagerClass and render a fourth sphere using that shader.


Source Code

Source Code and Data Files: gl4linuxtut22_src.zip

Back to Tutorial Index