I read somewhere (I can’t find the article) that all draw calls are not equal. If I render 100 meshes with the same material, the first draw call will need to compile the shader and will be less performant than the 99 others that use the same shader. First of all, is this right?
Well, the shaders are pre-compiled…but, you will see them compile in editor automatically as they are stored in the data cache. If you delete the cache, all shaders will need to be re-built. This data cache can be shared so that each team member doesn’t have to compile shaders independently. However, that’s a little out of scope for this discussion.
The shader is essentially just a list of instructions, like a script. Once compiled, it can be re-used. Material instances will apply variables (parameters) to the master material, such as color, opacity, or other parameters you throw at it. What you are really leveraging by using material instances, is only needing to load the texture sample in memory once and the ability to make easy variations (unless you change the texture sample as a parameter).
When a material instance is modified, then only the variables are changed, no need to re-compile, which is why you can see real-time changes very quickly, as the material instance is modified.
However, if 2 different objects, say one sphere and one cube, but they use the same material, it will still require a draw call for each object. Each unique model in the scene requires a draw call no matter what materials are used. Instanced static meshes, only require 1 draw call. For example, 100 hex bolts on am aircraft, would only be one draw call if that are instanced static meshes. However, if 50 of those bolts are steel, and 50 are a painted metal, then you would have a draw call for the steel bolts, then a draw call for the painted metal bolts.
— Multiple Materials —
Another caveat is using multiple materials on 1 mesh.
So for example, let’s say you have a highly detailed car model that is 3 million triangles but all one mesh. Each time you add a new material to it, such as paint, glass, leather, plastic, etc, then that 3 million triangle mesh needs to get entirely re-drawn with another draw call. Even if you only see a tiny portion of the car on camera, the entire object must be called.
So 5 x draw calls on a 3 million triangle mesh is gonna take a hit performance wise if all you want to do is zoom up and look at the tires. In this case, it is best to split the object up and allow culling to do it’s job.
Just be mindful, that millions of objects all go into an index buffer…then it gets sorted on what to draw and what not to draw on a culling list…so just because you’re not drawing it, doesn’t mean it’s not impacting performance. Generally speaking, you don’t want millions of objects, as it will hit performance when creating the culling lists and index buffer.
— Shader Complexity —
Sometimes draw calls are not as important as the shader’s complexity in terms of performance. Be careful when leveraging certain material functions that create animation as these can trigger each tick. Also, be mindful of how transparency is used as this can create overdraw, which requires additional draw calls to sort. Stacked transparency is another performance killer as it multiples the performance hit with each new stack.
You can view a shader’s instructions, texture samples, and shader count in the material by clicking window > Stats:

Some studios have a built-in warning if artists go over a pre-determined instruction count that says “get a programmer immediately”…because they are creating too complex of materials.
You can view overdraw and shader complexity in the optimization viewmodes:

Secondly, are material instances considered as different shaders? Let’s say I have a master material and 100 instances of it, all with different parameters, and I render my 100 meshes with a different material instance for each. Does each draw call need to compile its own shader, or it will happen only once for the master version? (I’m aware that static switches lead to different shaders, my question is more about dynamic parameters like scalar value or textures).
Each variation will be compiled which exponentially increases the compile time for shaders. Also, each variation is an additional draw call even if it is used on an instanced static mesh you duplicated.
So for example, you have a master metal material with a color parameter you want to change.
You create 2 material instances…one red, one green.
Then you create 100 ball bearings that are all instanced static meshes.
99 are of those ball bearings are red, but only 1 is green.
You will have 1 draw call for the red ones (since they are instanced)
And one draw call for the green one since the material is different.
So I’m wondering if I should “refactor” my materials into 1 flexible master with a lot of instances.
Probably not. Unless you need some kind of global control that affects every single material…
One example for this would be a master material for glass that includes a switch for ray traced settings or non-ray traced settings.
It’s hard to give exact advice on how to create material relationships as each project is different, however, the best advice I could give is to create master materials based on categories of behavior. Such as metals, woods, glass, and such…
In my experience, most materials from marketplace assets are over-built to allow end users to easily modify the material to their needs. However, this adds to the instruction count and creates unnecessary bloat which also increases the data footprint.