Quick answer: Instancing requires the same material. Use one material with [PerRendererData] shader properties set via MaterialPropertyBlock — not per-object material clones.
Hundreds of coins differ only in tint. Each got its own material instance, so GPU instancing never kicks in — one draw call per coin.
The Material-Clone Trap
// BAD: accessing .material clones it — breaks instancing
renderer.material.color = tint;
Reading renderer.material creates a unique material instance for that renderer. Now every coin has a different material; instancing can’t batch them.
Use MaterialPropertyBlock
MaterialPropertyBlock mpb = new();
renderer.GetPropertyBlock(mpb);
mpb.SetColor("_BaseColor", tint);
renderer.SetPropertyBlock(mpb);
The shared material stays shared; per-instance data rides along in the property block. Instancing batches them all.
Shader Setup
The shader must declare instanced properties:
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
And the material must have “Enable GPU Instancing” checked. URP/HDRP Lit shaders support this; custom shaders need the macros.
Verifying
Frame Debugger shows one “Draw Mesh (instanced)” call for all coins instead of hundreds. Each coin still shows its own tint.
“Instancing needs one material. Vary per-instance data with MaterialPropertyBlock, never material clones.”
Audit your code for .material access — use .sharedMaterial for reads, MaterialPropertyBlock for per-instance variation.