In my engine I made the actual draw call a virtual method of Material object instead of Mesh object.
I.e.
Mesh::display() calls scheduleRender, the latter appends small RenderData structure to render list. RenderData contains links to index and vertex buffers, backlinks to mesh and material, sorting keys and some other data
After sorting etc. a Material::render is called
The reason is, that only material instance (which manages shaders) knows, what actual data it requires (vertexes, normals, texcoods, colors, all kinds of matrices, special textures, lights, reflections, sky, aerial thickness etc. etc.)
That is a good insight.
With this in mind, I have redesigned what I pasted earlier. So what if CDrawable would consist of virtual functions for binding all sorts of arrays. It would allow passing shader varying and uniform locations.
This gets a bit lengthy, but hopefully it is not too messy.
The beef is in the function drawDrawables, which can draw culled and sorted object instances while shaders, geometry and culling are well decoupled in the code.
// cullable.hh
// Just a very basic class to be used by a quadtree or some other vis.
class CCullable
{
public:
vector3f getOrigin() const;
vector3f getBoxSize() const;
vector3f getAlignedBoxSize() const;
};
// quadtree.hh
#include "cullable.hh"
class CQuadNode
{
private:
// The outer vector is for groupping cullables by their kind.
// I want to cull meshes, particle systems and so on with a single tree.
vector<vector<CCullable*>> pCullables;
CQuadNode *children[4];
};
class CQuadTree
{
private:
CQuadNode *root;
public:
void insertCullables(const vector<CCullable*> &cullables, int group);
// Return indices to visible cullables for each group.
vector<vector<int>> frustumCull(vector3f cameraPos, vector3f cameraDir, float fov, float range);
};
// transformable.hh
// This acts as a base class for object instances.
class CTransformable
{
protected:
vector3f origin;
vector3f rotation;
vector3f scale;
bool deleted;
bool selected;
matrix44f_c trans, invTrans;
void buildMatrices();
public:
void translate(const vector3f &v);
void rotate(const vector3f &v);
void scale(const vector3f &v);
void loadMatrix();
matrix44f_c getMatrix();
matrix44f_c getInvMatrix();
};
// These are useful for editing a scene.
vector<CTransformable*> getSelectedTransformables(const vector<CTransformable*> &t);
void translateTransformables(const vector<CTransformable*> &t, vector3f v);
void rotateTransformables(const vector<CTransformable*> &t, vector3f v);
void scaleTransformables(const vector<CTransformable*> &t, vector3f v);
// shader.hh
// This acts as a base class for specific shaders.
// The methods here allow the abstract function drawDrawables to tell
// drawables, what data the shader requires.
class CShaderBase : CShader
{
public:
virtual bool requireTexCoords() const;
virtual bool requireNormals() const;
virtual bool requireTangents() const;
virtual bool requireWeights() const;
virtual bool requireBoneindices() const;
virtual bool requireTexture() const;
virtual GLint getTangentLoc() const;
virtual GLint getWeightLoc() const;
virtual GLint getBoneIndexLoc() const;
virtual void bindTexture(CTexture *pTex);
virtual void loadObjectSpaceUniforms(matrix44f_c invTrans);
};
// drawable.hh
#include "transformable.hh"
#include "shader.hh"
// This is a base class for drawable object instances, that may
// share some common data. A drawable is allowed to consist of groups.
// The idea is that each group is associated with one texture/material.
class CDrawable : public CTransformable
{
public:
virtual int getNumGroups();
virtual void bindGroupVertexBuffer(int index);
virtual void bindGroupNormalBuffer(int index);
virtual void bindGroupTexCoordBuffer(int index);
virtual void bindGroupTangentBuffer(int index, GLint loc);
virtual void bindGroupWeightBuffer(int index, GLint loc);
virtual void bindGroupBoneBuffer(int index, GLint loc);
virtual void bindGroupIndexBuffer(int index);
virtual void drawGroup(int index);
virtual CTexture *getGroupTexture(int index);
// Allow the drawable to load uniforms for a shader.
// These can be e.g. bone matrices for skinned meshes.
virtual void loadGroupUniforms(int index, GLint *locArray, int numLoc);
// Returns an integer that can be used to sort group bindings.
// For all drawables with the same data id, bindGroup is called only
// once per group index and only drawGroup is invoked for each such group.
virtual int getDataID();
};
// Sort indices by data id. The indices refer to the drawables vector.
void sortDrawables(const vector<CDrawable*> &drawables, vector<int> &indices);
// The indices point to the drawables vector, but indices.size() may be less than
// drawables.size() as some may be culled. Indices should also be sorted by data ID.
void drawDrawables(const vector<CDrawable*> &drawables, const vector<int> &indices, CShaderBase *sh)
{
int prevDataID = -1;
for(int i=0; i<indices.size(); i++){
CDrawable *p = drawables[indices];
int dataID = p->getDataID();
sh->loadObjectSpaceUniforms(p->getInvMatrix());
// This function is a simplified version of what would be used.
// As the indices may be sorted by dataID, one should loop through
// groups first and then object instances and not vise versa as in here.
for(int j=0; j<p->getNumGroups(); j++){
// If object source data changes, bind the new data.
if(dataID!=prevDataID){
p->bindGroupVertexBuffer(j);
if(sh->requireTexCoords())
p->bindTexCoordBuffer(j);
if(sh->requireNormals())
p->bindNormalBuffer(j);
if(sh->requireTangents())
p->bindTangentBuffer(j, sh->getTangentLoc());
if(sh->requireWeights())
p->bindWeightBuffer(j, sh->getWeightLoc());
if(sh->requireBoneIndices())
p->bindBoneIndexBuffer(j, sh->getBoneIndexLoc());
if(sh->requireTexture())
sh->bindTexture(p->getGroupTexture(j));
}
p->loadGroupUniforms(j, sh->getUniformLocArray(), sh->getNumUniformLoc());
glPushMatrix();
p->loadMatrix();
p->drawGroup(j);
glPopMatrix();
}
prevDataID = dataID;
}
}
// mesh.hh
// I don't want to make this module and class to be involved with
// any of the above modules. Coupling is done in the engine module.
class CMeshInstance
{
protected:
CMesh *pData[maxLevelsOfDetail]; // These are pointers to the actual shared data.
int currentLevelOfDetail;
public:
int getNumGroups();
void bindGroupVertexBuffer(int index);
void bindGroupNormalBuffer(int index);
void bindGroupTexCoordBuffer(int index);
void bindGroupTangentBuffer(int index, GLint loc);
void bindGroupWeightBuffer(int index, GLint loc);
void bindGroupBoneBuffer(int index, GLint loc);
void setGroupSkinMatricesUniform(int index, GLint loc, int maxBones);
void bindGroupIndexBuffer(int index);
void drawGroup(int index);
};
// engine.hh
class CShaderStaticMeshDirLight : public CShaderBase
{
// Implement the virtual routines here.
// e.g.
vector3f cameraPosWorldSpace;
vector3f lightDirWorldSpace;
void loadObjectSpaceUniforms(matrix44f_c invTrans);
};
#include "quadtree.hh"
#include "drawable.hh"
#include "mesh.hh"
// This couples the mesh instance to everything else.
class CMeshObject : public CCullable, public CDrawable, public CMeshInstance
{
public:
// Not sure if this hack works. Mesh sorting would be based on data address.
int getDataID(){
return (int)pData[currentLevelOfDetail];
}
void loadUniforms(int index, GLint *locArray, int numLoc){
setGroupSkinMatricesUniform(index, locArray[0], 50);
}
};
class CEngine
{
private:
CQuadTree *qt;
vector<CMeshObject*> meshObjects;
CShaderStaticMeshDirLight *shaderStaticMeshDirLight;
public:
bool init();
void draw();
};
bool CEngine::init()
{
// ...
qt->insertCullables(meshObjects, 0);
// ...
}
void CEngine::draw()
{
// Frustum cull all objects with quadtree.
vector<vector<int>> objectIndices;
objectIndices = qt->frustumCull(cameraPos, cameraDir, fov, range);
// Sort visible mesh objects according to data ID:s.
vector<int> &meshIndices = objectIndices[0];
sortDrawables(meshObjects, meshIndices);
// Draw meshes visible & sorted meshes and use directional light shader.
shaderStaticMeshDirLight->bind();
shaderStaticMeshDirLight->setCameraPos(cameraPos);
shaderStaticMeshDirLight->setLightDir(lightDir);
drawDrawables(meshObjects, meshIndices, shaderStaticMeshDirLight);
shaderStaticMeshDirLight->unbind();
}
Edit: a few typos from the code.