Day 39 - 🔬 Deep dive - Mesh Instancing

General / 24 June 2026

If you're submitting one draw call per rock, per tree, or per blade of grass - you're leaving a lot of performance on the table.


The Draw Call Problem

Every time the CPU tells the GPU to render something, that instruction is a draw call. The GPU itself is extremely fast at processing geometry. The bottleneck is usually the CPU-to-GPU communication: setting shader state, binding buffers, uploading constants, issuing the call. Do that ten thousand times per frame and you've burned your frame budget on overhead before the GPU has drawn a single interesting pixel.

This shows up hard with repeated geometry. A forest of a thousand identical trees, scatter rocks across a terrain, bolts on a piece of machinery. Individually cheap, collectively expensive if each one is its own draw call.


What Instancing Actually Does

GPU instancing lets you submit one draw call that renders N copies of the same mesh. Instead of re-issuing the draw command for each copy, you pass the GPU a buffer of per-instance data: transforms, colors, material parameters, and the GPU handles the repetition internally, in parallel.

The mesh data (vertex buffer, index buffer) is uploaded once. The instance buffer holds everything that differs between copies. The vertex shader receives both, reading the shared geometry and the per-instance transform to position each copy correctly.

Standard approach:
  for each tree:
    set transform
    issue draw call          <- N draw calls, N state changes

Instanced approach:
  upload instance buffer     <- N transforms, colors, etc.
  issue one draw call        <- GPU iterates internally

The GPU is built for this. Running the same shader across thousands of instances in parallel is exactly what it was designed to do.


Per-Instance Data

The instance buffer is more flexible than it first appears. Each instance can carry:

  • World transform - position, rotation, scale (typically a 4x4 matrix or a packed 3x4)
  • Color tint - subtle variation that breaks visual repetition without unique materials
  • Custom floats - wind phase offset, wetness, damage state, anything you want to vary per copy
  • LOD index - some renderers pack the LOD selection directly into the instance data

This is important because the common objection to instancing "but all my rocks look the same" is solved here. You don't need unique meshes or unique materials to get visual variation. You need per-instance parameters that drive variation inside a shared shader.


The Hard Constraints

Instancing has a few rules that are worth internalizing early:

Same mesh. All instances in a single draw call must share the same vertex and index buffer. If you have three rock variants, that's three instanced draw calls, not one; still a big win over thousands of individual calls, but not a single call.

Same material and shader. All instances must use the same shader pipeline state. Different blend modes, different textures bound to different slots: those break instancing and require a separate batch.

Depth sorting. Transparent or alpha-blended geometry needs to be sorted back-to-front before rendering, which complicates instancing. You can still instance transparent geometry, but you'll either need to sort the instance buffer CPU-side each frame, or accept artifacts. Opaque geometry has no such problem.

Culling. A naive instanced draw call submits all N instances regardless of visibility. For large counts, you want GPU-driven culling: a compute pass that reads the instance buffer, tests each instance against the frustum and occlusion data, and writes only visible instances to an indirect draw buffer. The GPU then executes the draw against that filtered list.


How This Sits in the Pipeline

Instancing changes what the CPU submits but doesn't change the GPU pipeline itself: the same vertex shader, rasterizer, pixel shader, and output merger stages all run as normal, just across many instances in parallel.

Where it interacts with earlier topics: a depth prepass works naturally with instanced geometry. You run the opaque instanced meshes through the Z-prepass first, same draw call, different render state; then the main pass benefits from Early-Z rejection across all instances. The combination is very effective for dense foliage or scatter geometry where overdraw would otherwise be significant.


In Practice

The decisions that matter in production:

  • Group by mesh and material first. Before reaching for instancing, make sure your asset pipeline isn't creating unnecessary material or mesh variants. Every unique combination is a separate batch.
  • Use color tint and parameter variation aggressively. A well-parameterized shader with per-instance floats for tint, scale noise, and phase offsets can make a thousand identical meshes read as varied without breaking the batch.
  • Lean on engine support. Unreal's Instanced Static Mesh (ISM) and Hierarchical Instanced Static Mesh (HISM) components handle the CPU-side batching, LOD selection, and culling for you. HISM adds a spatial tree for efficient frustum culling on very large counts. Unity's GPU instancing flag on materials does the same job on that side of the fence.
  • Watch for the batch-breaker. Dynamic lights, decals, or per-object material overrides are common culprits that silently break instancing and push assets back to individual draw calls. Profile and verify rather than assume.


Practical Takeaway

Instancing is one of the highest-leverage optimizations available for scatter geometry, foliage, and props. The constraint is sameness (same mesh, same material) but per-instance data gives you more variation than it looks like on the surface. The real work is upstream: keeping your asset pipeline clean enough that instancing can actually happen.

This is my interpretation based on what I've read in books and online; things do change as technology evolves.

© 2026 Stefan Groenewoud - All views are my own, not those of my employer.