Wednesday 1 December 2010

GLSL Shader Manager design Part 3

(for some more refinements see this post)

In the previous post I discussed the design of the ShaderProgram class, in this final instalment I will discuss the overall ShaderManager class and it's basic usage.

The basic design consideration for the  manager is it will allow for the creation of Shaders and ShaderPrograms that will be independent of each other, these will be stored by name as a std::string and basic methods may be called to configure and load these.

It will also have the ability to access the ShaderPrograms via a lookup using the [] operator passing in the name of the shader.

The class diagram is as follows


The std::map containers for the shader programs and Shaders contain pointers to the classes so each of them can live as separate entities and Shader Programs may share multiple shaders.

Construction of the class will also create a nullProgram object as discussed in the previous post. The motivation for this is to allow the return of a class which will be a glUseProgram(0) but still be an object capable of being callable for methods without crashing.
ShaderManager::ShaderManager()
{
 m_debugState=true;
 m_nullProgram = new ShaderProgram("NULL");
}
To create a shader program we use the following method to construct a new ShaderProgram class

void ShaderManager::createShaderProgram(std::string _name)
{
 std::cerr<<"creating empty ShaderProgram "<<_name.c_str()<<"\n";
 m_shaderPrograms[_name]= new ShaderProgram(_name);
}
Next we can add shaders using the following methods
void ShaderManager::attachShader(
                                  std::string _name,
                                  SHADERTYPE _type
                                )
{
  m_shaders[_name]= new Shader(_name,_type);
}
At this stage the shader may not have any source attached to it so we have a method to allow the loading of the shader source.

void ShaderManager::loadShaderSource(std::string _shaderName, std::string _sourceFile)
{
  std::map <std::string, Shader * >::const_iterator shader=m_shaders.find(_shaderName);
  // make sure we have a valid shader and program
 if(shader!=m_shaders.end() )
  {
    shader->second->load(_sourceFile);
  }
  else {std::cerr<<"Warning shader not know in loadShaderSource "<<_shaderName.c_str();}
}

Once the source is loaded to compile the Shader we do the following
void ShaderManager::compileShader(std::string _name)
{
  // get an iterator to the shaders
  std::map <std::string, Shader * >::const_iterator shader=m_shaders.find(_name);
  // make sure we have a valid shader
 if(shader!=m_shaders.end())
  {
    // grab the pointer to the shader and call compile
    shader->second->compile();
  }
  else {std::cerr<<"Warning shader not know in compile "<<_name.c_str();}
}

Next we need to attach the shader to a program, as both classes are contained in maps we have to search for both and then call the relevant methods as show in the following code

void ShaderManager::attachShaderToProgram(std::string _program,std::string _shader)

{

  // get an iterator to the shader and program
  std::map <std::string, Shader * >::const_iterator shader=m_shaders.find(_shader);
  std::map <std::string, ShaderProgram * >::const_iterator program=m_shaderPrograms.find(_program);

  // make sure we have a valid shader and program
 if(shader!=m_shaders.end() && program !=m_shaderPrograms.end())
  {
    // now attach the shader to the program
    program->second->attatchShader(shader->second);
    // now increment the shader ref count so we know if how many references
    shader->second->incrementRefCount();

    if (m_debugState == true)
    {
      std::cerr<<_shader.c_str()<<" attached to program "<<_program.c_str()<<"\n";
    }
  }
  else {std::cerr<<"Warning cant attach "<<_shader.c_str() <<" to "<<_program.c_str()<<"\n";}
}

To overload the [] operator to work with a std::string we use the following
ShaderProgram * ShaderManager::operator[](const std::string &_programName)
{
  std::map <std::string, ShaderProgram * >::const_iterator program=m_shaderPrograms.find(_programName);
  // make sure we have a valid  program
 if(program!=m_shaderPrograms.end() )
  {
    return  program->second;
  }
  else
  {
    std::cerr<<"Warning Program not know in [] "<<_programName.c_str();
    std::cerr<<"returning a null program and hoping for the best\n";
    return m_nullProgram;
  }
}
For brevity the rest of the methods can be seen in the source download here (lecture 8)

Using the ShaderManager
The file GLWindow.cpp demonstrates the use of the ShaderManager, we must first initialise GLEW if we are using linux or windows (Mac OS X is fine)

The following example program is based on the code here and is basically OpenGL 3.x compatible, however I use GLSL Version 120 as the basis as mac OSX doesn't support higher at present (come on APPLE !)

We uses very basic shaders that allow the basic vertex and colour attributes to be named in the client program and the shader.
#version 120
attribute vec3 inPosition;
attribute vec3 inColour;
varying vec3 vertColour;

void main()
{
 gl_Position = vec4(inPosition, 1.0);
 vertColour = inColour;
}


#version 120

varying vec3 vertColour;

void main()
{
  gl_FragColor = vec4(vertColour,1.0);
}
Generating Vertex data
The vertex data is created using a vertex buffer object and a vertex array object these will then be bound to attributes in the shaders

void GLWindow::createTriangle()
{
 // First simple object
 float* vert = new float[9]; // vertex array
 float* col  = new float[9]; // color array

 vert[0] =-0.3; vert[1] = 0.5; vert[2] =-1.0;
 vert[3] =-0.8; vert[4] =-0.5; vert[5] =-1.0;
 vert[6] = 0.2; vert[7] =-0.5; vert[8]= -1.0;

 col[0] = 1.0; col[1] = 0.0; col[2] = 0.0;
 col[3] = 0.0; col[4] = 1.0; col[5] = 0.0;
 col[6] = 0.0; col[7] = 0.0; col[8] = 1.0;

 // Second simple object
 float* vert2 = new float[9]; // vertex array

 vert2[0] =-0.2; vert2[1] = 0.5; vert2[2] =-1.0;
 vert2[3] = 0.3; vert2[4] =-0.5; vert2[5] =-1.0;
 vert2[6] = 0.8; vert2[7] = 0.5; vert2[8]= -1.0;

 // Two VAOs allocation
  glGenVertexArrays(2, &m_vaoID[0]);

 // First VAO setup
  glBindVertexArray(m_vaoID[0]);

 glGenBuffers(2, m_vboID);

 glBindBuffer(GL_ARRAY_BUFFER, m_vboID[0]);
 glBufferData(GL_ARRAY_BUFFER, 9*sizeof(GLfloat), vert, GL_STATIC_DRAW);
  m_shaderManager["Simple"]->vertexAttribPointer("inPosition",3,GL_FLOAT,0,0);
  m_shaderManager["Simple"]->enableAttribArray("inPosition");
 glBindBuffer(GL_ARRAY_BUFFER, m_vboID[1]);

 glBufferData(GL_ARRAY_BUFFER, 9*sizeof(GLfloat), col, GL_STATIC_DRAW);
  m_shaderManager["Simple"]->vertexAttribPointer("inColour",3,GL_FLOAT,0,0);
  m_shaderManager["Simple"]->enableAttribArray("inColour");

 // Second VAO setup
  glBindVertexArray(m_vaoID[1]);

 glGenBuffers(1, &m_vboID[2]);

 glBindBuffer(GL_ARRAY_BUFFER, m_vboID[2]);
  ceckGLError(__FILE__,__LINE__);
 glBufferData(GL_ARRAY_BUFFER, 9*sizeof(GLfloat), vert2, GL_STATIC_DRAW);
 glVertexAttribPointer((GLuint)0, 3, GL_FLOAT, GL_FALSE, 0, 0);
 glEnableVertexAttribArray(0);
  glBindVertexArray(0);

 delete [] vert;
 delete [] vert2;
 delete [] col;
}
Next we load and build the Shader program using the ShaderManager class
void GLWindow::initializeGL()
{
  ngl::NGLInit *init = ngl::NGLInit::instance();
  init->initGlew();
  glClearColor(0.4f, 0.4f, 0.4f, 1.0f);

  m_shaderManager.createShaderProgram("Simple");

  m_shaderManager.attachShader("SimpleVertex",VERTEX);
  m_shaderManager.attachShader("SimpleFragment",FRAGMENT);
  m_shaderManager.loadShaderSource("SimpleVertex","shaders/Vertex.vs");
  m_shaderManager.loadShaderSource("SimpleFragment","shaders/Fragment.fs");

  m_shaderManager.compileShader("SimpleVertex");
  m_shaderManager.compileShader("SimpleFragment");
  m_shaderManager.attachShaderToProgram("Simple","SimpleVertex");
  m_shaderManager.attachShaderToProgram("Simple","SimpleFragment");


  m_shaderManager.bindAttribute("Simple",0,"inPosition");
  m_shaderManager.bindAttribute("Simple",1,"inColour");

  m_shaderManager.linkProgramObject("Simple");
  m_shaderManager["Simple"]->use();

  createTriangle();
}
You will notice before we link the shader program we assign the inPosition and inColour attributes to the attribute locations 0 and 1 respectively. This will be used in the drawing routine later.

void GLWindow::paintGL()
{

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  m_shaderManager["Simple"]->use();

  glBindVertexArray(m_vaoID[0]);  // select first VAO
  glDrawArrays(GL_TRIANGLES, 0, 3); // draw first object

  glBindVertexArray(m_vaoID[1]);  // select second VAO
  m_shaderManager["Simple"]->vertexAttrib3f("inColour",1.0,0.0,0.0);
  //glVertexAttrib3f((GLuint)1, 1.0, 0.0, 0.0); // set constant color attribute
  glDrawArrays(GL_TRIANGLES, 0, 3); // draw second object
}

Next Step
One of the initial goals of this design was for a standalone version of a shader manager that people could use in their own project, with very few external library dependancies (at present we only need OpenGL, GLEW and Qt for the project and some string IO).

The next step is to integrate this into my existing ngl:: library and replace the existing ShaderManager class. This is going to happen very soon and I will post an update on the integration and a fuller critique of the design once this integration has happened, and finally a teapot

No comments:

Post a Comment