Multiple materials with one glDrawElements()


I have been working on loading various 3D models to test something I am making. For me models can have multiple objects and each object can have one material assigned to them. They are loaded to a VBO/IBO combo and are rendered with glDrawElements().
My system has been working quite good so far but while I was loading .3ds models I noticed that one object can have multiple materials assigned to it. For each material of an object a number of faces and indices to these faces are given inside the .3ds file.

I realized that there is no way to pass these values into a shader, unless you turn the material values into attributes and actually send them in along with the VBO/IBO combo. But I would guess that this is a colossal waste of space. 

So my question is, what do people do in these cases normally? Do they subdivide somehow the object into as many objects as the number of materials? Is this the only feasible way? Would it be bad practice to reject these models? How common are they? So far I have only found one but I have to admit I did not do much searching.


It depends on many things. What “material” means is at the top of the list.

“Material” can mean many different things in different contexts. It could just mean texture. It could mean all of the surface parameters for a particular shader. It could mean the shader itself.

If the object is rendered with a single shader, and each of the sub-materials is just different textures, you could use an array texture.

If the different materials have non-texture parameters, then you’ll probably need to split them into different draw calls. Alternatively, you could have a material-ID attribute that is used to select material parameters from a uniform array.

If the different materials are different shaders (different shader code), then you either need to split the object or convert the multiple shaders into one. You pick which codepath to use based on the material-ID attribute.

I’d go with splitting (except in the texture case if you have arrays available), unless there were some profiling-determined performance need to do it the other way.

1 Like

In most games, each of these materials is split up into it’s own drawcall, so that’s one glDrawElements per material.

There are various ways to optimize this.

One aspect is to only bind shader programs when needed, for example when two materials have the same shader/material with only textures differing.

Another big part involves how you structure your VBOs. For example you can create one VBO+IBO per material. This may be beneficial for example when a certain material/shader requires vertex colors, but another material on the same model doesn’t require it and/or have different vertex attributes (tangents, multiple UV channels). Conversely, sometimes it is more efficient (performance wise) to create one VBO for the whole model, and have multiple IBOs per material, so you don’t have to switch between VBOs.

In other situations you may want to interleave attributes, which complicates the above.

Then there’s ‘atlasses’, a technique in which you combine textures/materials onto a single one large sheet and let a complex shader do the work to sort out which polygons should have what texture/material properties (only try this if you know what you are doing).

So it really depends on what you want to do, what kind of geometry data you have (what vertex attributes, and how much) , how different your shader/materials are, and what you demand of performance. So the only accurate answer is: “it depends”.

Personally, I prefer simplicity, and create one VBO+IBO for each group of polygons that have the same material, as this can be nicely abstracted with classes, and is easy to write exporter tools for, ease of file parsing etc.

Pseudo code (read from bottom up):

// texture class
class CTexture
 GLuint tex;            // GL texture handle
 bool generateMipmaps;  // texture properties
 // etc
 bool Load(char *filename);
 void Bind(int texunit);

// shader class
class CShader
 GLuint program; // GL shader program handle
 int uniformDiffuseLoc; // uniform location handles
 int uniformAmbientLoc; // ...
 // etc
 bool Load(char *filename);
 void Bind();

// material definition (could be authored and stored in text files)
class CMaterial
 CTexture *diffuseTex;   // points to a diffuse texture class
 CTexture *normalTex;    // points to a normal map texture class
 CShader *shader;        // points to a shader class instance
 GLfloat diffuse[4];     // holds uniform diffuse color value
 GLfloat ambient[3];     // holds uniform ambient color value
 GLfloat alphaTest;      // alpha test cutoff
 // etc
 void Bind();

// contains a group of polygons with same
class CMeshLODMat
 CMaterial *material; // pointer to material class
 int numVerts;     // number of vertices in VBO
 int numElements;  // i.e. number of triangles*3
 bool hasNormals;  // ...
 vec3 vert[];      // vertex array read from file
 vec3 norm[];      // vertex normal array read from file
 vec2 texc[];      // texture coordinate array
 ushort index[];   // index array
 GLuint vbo;       // GL handle to VBO constructed from the above
 GLuint ibo;       // GL handle to IBO constructed from the above
 bool ReadChunk(FILE *f);
 void Draw();

// contains geometry for a model with different materials
class CMeshLOD
 int matNum;              // number of materials on model
 CMeshLODMat mat[];       // LOD material class array
 bool ReadChunk(FILE *f); // reads part of model file
 void Draw();

// contains array of models with different Levels Of Detail (LOD)
class CMesh
 int lodNum;      // number of LOD levels
 CMeshLOD lod[];  // array of LODs
 bool Load(char *filename); // loads a model from file
 void Draw();

I’m also obliged to warn you that .3ds is a terrible format, due to complexity and simply broken data storage. Attributes such as vertex colors, texture coordinates and vertex normals are subjected to data corruption in certain (but very common) conditions. Look at WaveFront/OBJ for a more elegant interchange format, but avoid the 3dsmax OBJ exporter like the pest; it is dysfunctional at best and writes unnecessarily large and messy files.

It is not that difficult to write MEL/MAX/PYTHON scripts to export data from your favorite modeling package, and will probably give you far more simpler, efficient and flexibility than any other asset interchange format in the long run (forget XML/COLLADA) if it is only a small to medium size project such as a game.

Good luck, I hope that gives you some ideas.

Thanks you both for your comments. They certainly have been very helpful.

@remdul: Thanks for your nice pseudocode example. This is kind of what I imagined I would need to do. I am using kind of different VBO/IBO combo but I guess the principle is the same.

As for the .3ds file I have been warned many a times against using it for many different reasons. To be honest the reason I am using it is my principle of having no external dependencies and trying to learn by doing that lead me to using a .3ds parser I had written a way back. The final format that is used in my system is my own but it comes out of converting .3ds files at the moment. As you say I will have to look at OBJ files for something more modern, as soon as possible.

Basically what I meant by a material is just surface properties such as ambient/diffuse/specular/shininess values.
As you said

If the different materials have non-texture parameters, then you’ll probably need to split them into different draw calls. Alternatively, you could have a material-ID attribute that is used to select material parameters from a uniform array.

That’s also a nice idea. Use a uniform array of all the materials. I wonder is there any performance difference between that or subdividing the object into smaller objects and issuing different draw calls?

There likely is, thought I can’t say if it will improve performance or worsen it. nVidia drivers for a long time did shader recompiles (!) whenever uniform values were changed (glUniform*). I don’t know if this still is the case, but many developers decided to work around it by passing uniforms as vertex attributes instead, and although that often required massive and unnecissary data duplication, it was still faster than making a handfull of glUniform calls.

So while reducing the number of drawcalls generally improves performance, in reality there are many subtleties. If you have lots of complex geometry, fewer drawcalls+glUniform may be faster, but if you are rendering many instances of simple models, you might as well pass the info to the shader via vertex attributes (or immediate mode calls). So again, the answer is “it depends”.

Again, my personal preferance is to keep things simple, and use the structure I outlined in my code sample. It is more ‘future proof’, as future driver updates may (or may not) completely shift the bottlenecks. So in the end you may just decide to keep things simple and make life easy. Again, it depends on the nature of your project as well, long term, short term, performance requirements, hardware compatibility etc.

It is always hard to decide, so the best advice I can give you is to first ‘get things working’ and save your worries about optimization for later. As they say, “premature optimization is the root of all evil”.

Thanks for the advice redmul. I agree with the concept of getting things working first and doing the optimization later but as I have kind of long-term requirements from the system I am working on I don’t want to take it down roads that may prove perilous later and will require me to change lots of the code. So I wanna keep some balance of planning ahead just a bit.

I see you are saying that method A is faster than method B and vice/versa but that creates the question in my mind: “How can you test this?” Since all these run in the graphics card how can one run benchmarks to see the speed of one method over the other? The only debugging method I know for shaders is glsl Devil and that only helps with debugging the shader itself. What’s the standard method to test speed/performance/bottlenecks in shaders?

P.S.: Sorry, but it seems one question answered leads to another question popping up. >.>

This topic was automatically closed 183 days after the last reply. New replies are no longer allowed.