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
GLuint tex; // GL texture handle
bool generateMipmaps; // texture properties
bool Load(char *filename);
void Bind(int texunit);
// shader class
GLuint program; // GL shader program handle
int uniformDiffuseLoc; // uniform location handles
int uniformAmbientLoc; // ...
bool Load(char *filename);
// material definition (could be authored and stored in text files)
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; // holds uniform diffuse color value
GLfloat ambient; // holds uniform ambient color value
GLfloat alphaTest; // alpha test cutoff
// contains a group of polygons with same
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);
// contains geometry for a model with different materials
int matNum; // number of materials on model
CMeshLODMat mat; // LOD material class array
bool ReadChunk(FILE *f); // reads part of model file
// contains array of models with different Levels Of Detail (LOD)
int lodNum; // number of LOD levels
CMeshLOD lod; // array of LODs
bool Load(char *filename); // loads a model from file
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.