Sunday 28 November 2010

GLSL Shader Manager design Part 2

In the previous post I discussed the initial design of the Shader Manager class and outlined the design of the Shader Class. This post will concentrate on the design of the ShaderProgram class which contains the actual Shader sources and be linked and ready to use with GLSL.

The Orange book states

“Each shader object is compiled independently. To create a program,  applications need a mechanism for specifying a list of shader objects to be linked. You can specify the list of shaders objects to be linked by creating a program object and attaching to it all the shader objects needed to create the program”. (Rost 2009)

To this end we need to create an empty program object then attach the existing shaders to the program. The attachment can be made before the shader is compiled, however the Program object can't be linked unless the shaders are compiled. As shaders may be attached to more than one Program we basically need to hold a list of pointers to the shader objects rather than the shaders themselves.

The ShaderProgram will also be the main access point to the shaders once loaded on the GPU so it will have all the methods for accessing the attributes in the program. As OpenGL is a C based library there is no method overloading so we need to implement a method for each of the different accessors, in this cas the class have over 50 methods, however for brevity the class diagram only show the basic ones as seen in the following class diagram.


As part of the design it was also decided to allow attributes in the shader to be bound using a std::string so they could be referenced by name and not the numeric id.

The constructor for the class is as follows

ShaderProgram::ShaderProgram(std::string _name)
{
  // we create a special NULL program so the shader manager can return
  // a NULL object.
 if (_name !="NULL")
  {
    m_programID = glCreateProgram();
  }
  else
  {
    m_programID=0;
  }
  std::cerr <<"Created program id is "<<m_programid><<"\n";
  m_debugState=true;
  m_programName=_name;
  m_linked=false;
  m_active=false;
}
In the constructor we check for a special name called "NULL" this integrates with the ShaderManager class so we can create an empty default shader object with an ID of 0. GLSL uses the 0 shader to represent the "fixed functionality" pipeline. By default the ShaderManager class will create a NULL shader so that if the named one passed by the user is not found a fixed shader will be returned and calls to this object will not crash the system.

To attach a shader we use the following code

void ShaderProgram::attatchShader(Shader *_shader)
{
  m_shaders.push_back(_shader);
  glAttachShader(m_programID,_shader->getShaderHandle());
}


Any number of shaders may be attached to the ShaderProgram, once we are ready we can link the shader

void ShaderProgram::link()
{
  glLinkProgram(m_programID);
  if(m_debugState==true)
  {
    std::cerr <<"linking Shader "<< m_programName.c_str()<<"\n";
  }
  GLint infologLength = 0;

  glGetProgramiv(m_programID,GL_INFO_LOG_LENGTH,&infologLength);
  std::cerr<<"Link Log Length "<<infologLength<<"\n";

  if(infologLength > 0)
  {
    char *infoLog = new char[infologLength];
    GLint charsWritten  = 0;

    glGetProgramInfoLog(m_programID, infologLength, &charsWritten, infoLog);

    std::cerr<<infoLog<<std::endl;
    delete [] infoLog;
    glGetProgramiv(m_programID, GL_LINK_STATUS,&infologLength);
    if( infologLength == GL_FALSE)
    {
      std::cerr<<"Program link failed exiting \n";
      exit(EXIT_FAILURE);
    }
  }
  m_linked=true;
}
At present if the link fails the program will exit, I'm not sure if this is actually the best approach but will do for now as this a developmental system.

If we wish to bind the attributes in the shader we can do it before the link of the programs or after however each approach needs a different coding approach. At present binding is only available before linking, however this will be modified in the next version to check the link state and use the appropriate method. Attributes must be specified by the user using a numeric value and the name to be bound. Once this has been done the user may then specify attributes by name only.

void ShaderProgram::bindAttrib(GLuint _index, std::string _attribName)
{
  if(m_linked == true)
  {
    std::cerr<<"Warning binding attribute after link\n";
  }
  m_attribs[_attribName]=_index;
  glBindAttribLocation(m_programID,_index,_attribName.c_str());
  std::cerr<<"bindAttribLoc "<<m_programID<<" index "<<_index<<" name "<<_attribName<<"\n";
  ceckGLError(__FILE__,__LINE__);
}
Once an attribute is bound we can access it via name using the following functions

bool ShaderProgram::vertexAttribPointer(
                                        const char* _name,
                                        GLint _size,
                                        GLenum _type,
                                        GLsizei _stride,
                                        const GLvoid *_data,
                                        bool _normalise
                                       ) const
{

  std::map <std::string, GLuint >::const_iterator attrib=m_attribs.find(_name);
  // make sure we have a valid  program
 if(attrib!=m_attribs.end() )
  {
    glVertexAttribPointer(attrib->second,_size,_type,_normalise,_stride,_data);
    return  true;
  }
  else
  {
   return false;
  }
}

void ShaderProgram::vertexAttrib1f(
                                  const char * _name,
                                  GLfloat   _v0
                                  ) const
{
  std::map <std::string, GLuint >::const_iterator attrib=m_attribs.find(_name);
  // make sure we have a valid  program
 if(attrib!=m_attribs.end() )
  {
    glVertexAttrib1f(attrib->second, _v0);

  }

}

Accessing Uniforms
To access the uniform data within the shader we need to query the linked program object to get the numeric location of the variable, once this is found we can modify the variable using the OpenGL functions. The following method returns the numeric ID for a uniform and is used in the other functions

GLuint ShaderProgram::getUniformLocation(
                                          const char* _name
                                        ) const
{
  GLint loc = glGetUniformLocation( m_programID ,_name);
  if (loc == -1)
  {
    std::cerr<<"Uniform \""<<_name<<"\" not found in Program \""<<m_programName<<"\"\n";
  }
  return loc;
}

Now for every access variable type in OpenGL we can write a method to change the uniform, for example to change a uniform float we use the following code

void ShaderProgram::setUniform1f(
                                  const char* _varname,
                                  float _v0
                                ) const
{
  glUniform1f(getUniformLocation(_varname),_v0);
}

The rest of the class is rather repetitive code to allow the different attributes and uniforms to be accessed, the next post will look at the overall management class for the Shader system with examples of how it's used.

A Note on std::map
Whilst writing this I found the way I'd been using the std::map could lead to errors. If you consider the following code

#include <iostream>
#include <map>

int main()
{

  std::map<std::string,int> mymap;
  mymap["a"]=1;
  mymap["b"]=2;
  std::cout<<mymap.size()<<"\n";
}
Will print out a size of 2 as expected and we can access the map values using the ["a"] type syntax. However if we do the following
#include <iostream>
#include <map>

int main()
{

  std::map<std::string,int> mymap;
  mymap["a"]=1;
  mymap["b"]=2;
  std::cout<<mymap.size()<<"\n";
  std::cout<<mymap["c"]<<"\n";
  std::cout<<mymap.size()<<"\n";
}

The program still works but as the key "c" is not know it will output a value of 0 for the line mymap["c"] however it will also insert a new map entry and the 2nd call to mymap.size will return the size 3.
To overcome this behaviour we must use the find method as shown in some of the examples above.
References
Rost, R, Licea-Kane B (2009). OpenGL Shading Language. 3rd. ed. New York: Addison Wesley.

No comments:

Post a Comment