I've implemented a simple material/shader system for the first time, which seems to work fairly well. I've tried to focus on good separation of concerns and the relationship between classes. I'd really appreciate it if anyone can have a quick look through and help me think about what's good and what's bad.
It's not an enormous design but I don't want to drop a mountain of code, so I'll step through it and explain the important parts.
The first thing I've done is to make a distinction between shaders and materials. A shader is the actual program, and owns a set of uniforms. A material holds a reference to a shader program, and contains a collection of values for the shader uniforms. So several materials can reference the same shader, but with different parameters.
On the shader side I've split the functionality into ShaderProgram and ShaderUniform classes, where each type of uniform is represented by a derived class. The uniform classes are just a thin interface to call the correct glUniformxxx() function.
ShaderUniform.h
[source lang="cpp"]class ShaderUniform{public: virtual ~ShaderUniform() {} enum Type { TypeFloat, TypeVec3 /* etc. */ }; // Allows safe casting to the correct uniform sub-type. Type getType() const;};typedef std::shared_ptr<ShaderUniform> ShaderUniformPtr;// An example type:class FloatUniform : public ShaderUniform{public: FloatUniform(GLint uniformLocation); // Calls glUniform1f() void setValue(float value);};[/source]
The ShaderProgram is essentially an interface to the uniforms exposed by the shader. The uniforms are enumerated in the constructor, when the program is linked.
ShaderProgram.h
[source lang="cpp"]class ShaderProgram{public: // Link the shader and enumerate the available uniforms. ShaderProgram(VertexShader *vs, FragmentShader *fs); // Get a reference to a named uniform. ShaderUniformPtr getUniform(const std::string& name); void bind(); // Calls glUseProgram();private: std::map< std::string, ShaderUniformPtr > _uniforms;}[/source]
The higher level material system comprises two classes - Material and MaterialParameter. Each parameter relates to a single shader uniform. Parameter values are stored locally, and the associated uniform is updated when bind() is called.
MaterialParameter.h
[source lang="cpp"]class MaterialParameter{public: virtual ~MaterialParameter() {} enum Type { FloatType, Vec3Type /* etc. */ }; // Allows safe casting to the correct parameter sub-type. Type getType() const; void bind();};class FloatParameter : public MaterialParameter{public: FloatParameter(ShaderUniformPtr uniform); void setValue(float value); void bind(); // Calls ShaderUniform::setValue().private: ShaderUniformPtr _uniform; float _value;};[/source]
Finally, the material class holds a shader program and collection of parameters.
Material.h
[source lang="cpp"]class Material{public: Material(ShaderProgram *program); void setParameter(const std::string& name, float value); void setParameter(const std::string& name, const Vec3& value); // etc. // Calls ShaderProgram::bind() and MaterialParameter::bind() for each parameter. void bind();private: ShaderProgram* _program; std::map< std::string, MaterialParameter* > _parameters;};[/source]
The setParameter() methods first check if the parameter is cached already. If not, the associated ShaderUniform is requested from the program and used to construct a new parameter, which is then cached. This also involves some type checking:
Material.cpp
[source lang="cpp"]void Material::setParameter(const std::string& name, float value){ MaterialParameter *parameter = 0; // Is the parameter already cached? std::map< std::string, MaterialParameter* >::iterator itr = _parameters.find(name); if(itr != _parameters.end()) { parameter = itr->second; } else { // Not cached - create a new one. parameter = new FloatParameter(_program->getUniform(name)); _parameters.insert(std::make_pair(name, parameter)); } // Check the type. if(parameter->getType() != Parameter::FloatType) { // throw type-mismatch. } // Set the new value. static_cast<FloatParameter*>(parameter)->setValue(value);}[/source]
Overall I think it's OK, but there are a couple of parts I'm not crazy about.
- Constant need for type checks when a uniform is updated.
- Static casts: I'm not sure if this is good usage?
- The ShaderUniform and MaterialParameter classes essentially represent the same thing.
Any comments or suggestions whatsoever are greatly appreciated.
Cheers!






