When you are building a game world that needs to feel alive - dense forests, sprawling cities, battlefields with hundreds of units - you hit a fundamental rendering bottleneck long before your polygon count becomes the problem. The bottleneck is draw calls.
Every time Unity tells the GPU to render an object, it issues a draw call. Each draw call has CPU overhead: setting up the material, binding textures, configuring the render state. On desktop hardware, you can get away with a few thousand draw calls per frame. On mobile, you often need to stay under 200 to maintain a stable 60 FPS.
This is the challenge we faced when engineering the environment systems for Domi Online - an MMORPG with dense, interactive forests where every single tree can be chopped down. We needed to render 10,000+ trees across the visible world with minimal draw calls on mid-range hardware, while still allowing players to interact with individual objects.
In this article, we will walk through the GPU instancing techniques we use to solve this problem, covering the SRP Batcher, manual GPU instancing, LOD strategies, and the interactive-object swap pattern that makes dense worlds both beautiful and functional.
Understanding the Draw Call Problem
Before diving into solutions, it is worth understanding why draw calls are so expensive, particularly on mobile.
What Happens During a Draw Call
Each draw call involves the CPU performing several operations:
- State changes: Setting the shader, material properties, and textures for the object
- Buffer binding: Pointing the GPU to the correct vertex and index buffers
- Uniform uploads: Sending per-object data (transform matrices, colour tints) to the GPU
- The actual draw command: Telling the GPU to process the vertices
On desktop GPUs, steps 1-3 are fast because the driver is highly optimised and the CPU-GPU bus has high bandwidth. On mobile GPUs (Adreno, Mali, Apple GPU), these operations are comparatively expensive. The GPU itself can handle millions of triangles, but the CPU overhead of setting up each draw call becomes the limiting factor.
The Naive Approach Fails Fast
Consider a forest scene with 5,000 trees. If each tree is a separate GameObject with its own MeshRenderer and material, Unity issues 5,000 draw calls per frame just for the trees. Add ground, rocks, grass, buildings, and NPCs, and you are looking at 10,000+ draw calls. On a mid-range mobile device, that is a slideshow.
Key Takeaway: On mobile hardware, the number of draw calls - not polygon count - is typically the primary rendering bottleneck. Reducing draw calls from thousands to dozens is the single most impactful optimisation you can make for dense game worlds.
Solution 1: The SRP Batcher
Unity's Scriptable Render Pipeline (SRP) Batcher is the first line of defence against excessive draw calls when using URP (Universal Render Pipeline) or HDRP.
How It Works
The SRP Batcher does not reduce draw calls in the traditional sense. Instead, it reduces the cost per draw call by keeping material data persistent on the GPU. Instead of re-uploading material properties every frame, the SRP Batcher caches them in a dedicated GPU buffer (Constant Buffer). Only per-object data (the transform matrix) needs to be updated each frame.
This means:
- Objects using the same shader (even with different material properties) can be batched efficiently
- The CPU overhead per draw call drops dramatically
- You no longer need to force all objects onto a single material atlas to get batching benefits
When to Use It
The SRP Batcher works best when:
- You are using URP or HDRP (it does not work with the Built-In Render Pipeline)
- Your objects share the same shader but may have different material instances
- You have many unique objects that cannot be statically batched (moving objects, procedurally placed objects)
Limitations
The SRP Batcher is not a silver bullet:
- It still issues individual draw calls - it just makes them cheaper
- It does not help with objects using fundamentally different shaders
- On very low-end mobile hardware, the per-draw-call cost can still add up even with SRP Batcher enabled
For Domi Online, the SRP Batcher gave us a solid baseline, but we needed to go further for the truly dense forest areas.
Solution 2: GPU Instancing
GPU Instancing is the heavy hitter for rendering thousands of identical (or near-identical) objects. Instead of issuing one draw call per object, you issue a single draw call that tells the GPU: "render this mesh 5,000 times, here are the 5,000 transform matrices."
How GPU Instancing Works in Unity
Unity's GPU Instancing works at the material level. When you enable "GPU Instancing" on a material, Unity groups all renderers using that material and mesh combination into instanced batches. Each batch is a single draw call that renders up to several thousand instances.
The key requirements:
- Same mesh: All instances must use the same Mesh asset
- Same material: All instances must share the same Material with GPU Instancing enabled
- Instancing-compatible shader: The shader must include instancing variants (most URP shaders support this)
Per-Instance Properties
One common misconception is that all instanced objects must look identical. In practice, you can vary per-instance properties:
- Transform (position, rotation, scale) - handled automatically
- Colour tints - using
MaterialPropertyBlock - Custom float or vector values - for wind sway amounts, health-based colour changes, growth stages
This allowed us to create forests in Domi Online where trees appeared varied despite sharing the same mesh and material.
Performance Gains
In our testing on mid-range Android devices (Snapdragon 7-series), GPU instancing produced dramatic results:
- 5,000 individual trees: ~5,000 draw calls, 12 FPS
- 5,000 instanced trees: ~3 draw calls, 58 FPS
That is a reduction from thousands of draw calls to single digits. The GPU handles the per-instance transforms internally using hardware-accelerated instancing, which is orders of magnitude faster than the CPU issuing individual draw calls.
Solution 3: The Interactive Object Swap Pattern
Here is where the engineering gets interesting. GPU instancing works brilliantly for static scenery, but Domi Online's trees are not static scenery. They are interactive objects that players can chop down.
An instanced tree is not a GameObject - it is just a transform in a buffer. You cannot attach scripts, colliders, or animation controllers to it. So how do you make instanced objects interactive?
The Swap Pattern
We developed what we call the "swap pattern," and it works like this:
-
Default state: All trees are rendered via GPU instancing. They are cheap, static, and beautiful. No GameObjects exist for them.
-
Player approaches: When a player moves within interaction range of a tree, the system identifies the nearest instanced tree using spatial hashing.
-
Swap in: The instanced tree is removed from the instance buffer and replaced with a full GameObject "Interactive Tree" prefab at the same position. This prefab has a collider, interaction script, and chopping animation.
-
Interaction complete: After the player chops the tree, the Interactive Tree plays a falling animation and transitions to a "Stump" prefab.
-
Swap out: Once the player moves away, the stump can optionally be converted back to an instanced mesh (a stump mesh) for efficient rendering.
Why This Works
At any given moment, the vast majority of trees in the world are static and non-interactive. Only the 2-3 trees nearest to the player need full GameObject functionality. By maintaining thousands of trees as instanced meshes and only "activating" the ones the player can actually interact with, we keep the draw call count low while preserving full interactivity.
The swap is imperceptible to the player - it happens before they are close enough to notice any visual difference.
Key Takeaway: GPU instancing and interactivity are not mutually exclusive. The swap pattern lets you render thousands of objects efficiently while maintaining full interaction capability for the ones players can actually reach. Only promote objects to full GameObjects when the player needs them.
Solution 4: LOD Strategies for Mobile
Level of Detail (LOD) is a well-established technique, but applying it effectively on mobile requires specific considerations.
Distance-Based LOD Groups
Unity's built-in LOD Group component lets you define multiple mesh representations at different detail levels:
- LOD 0: Full-detail mesh (1,000+ triangles) for objects near the camera
- LOD 1: Medium-detail mesh (200-500 triangles) for mid-range objects
- LOD 2: Low-detail mesh (50-100 triangles) or billboard for distant objects
- Culled: Objects beyond a certain distance are not rendered at all
LOD and Instancing Together
A critical optimisation: each LOD level can be independently instanced. This means:
- All LOD 0 trees (near the camera) are instanced together in one batch
- All LOD 1 trees (mid-range) are instanced in a separate batch
- All LOD 2 trees (distant) are instanced in a third batch
You end up with 3 draw calls for an entire forest, regardless of whether it contains 1,000 or 10,000 trees.
Mobile-Specific LOD Considerations
On mobile, we apply additional LOD strategies:
-
Aggressive culling distances: Mobile screens are smaller, so distant objects contribute less visual value. We cull objects at shorter distances than we would on PC.
-
Billboard LODs: The furthest LOD level uses a camera-facing quad with a baked texture rather than a 3D mesh. This is extremely cheap to render and works well for trees and foliage.
-
Dynamic LOD bias: On lower-end devices (detected at startup), we shift all LOD transitions closer to the camera, reducing the number of high-detail meshes rendered at any time.
-
LOD cross-fading: Rather than hard-popping between LOD levels (which is visually jarring), we use a brief dithering transition. The GPU cost of dithering is negligible compared to the visual improvement.
Putting It All Together: The Full Pipeline
Here is how all these techniques combine in practice for a scene like Domi Online's open-world forests:
Rendering Pipeline (Per Frame)
- Frustum culling eliminates all objects outside the camera's view
- Occlusion culling eliminates objects hidden behind terrain or large structures
- LOD evaluation assigns each remaining object to the appropriate detail level
- GPU instancing batches all objects at each LOD level into minimal draw calls
- Interactive swap promotes nearby objects to full GameObjects when players approach
- SRP Batcher handles any remaining non-instanced objects (UI, unique props, NPCs) efficiently
Results
On Domi Online, this pipeline achieves:
- 10,000+ trees rendered in the visible world
- Under 100 total draw calls for the entire environment
- Stable 30+ FPS on mid-range mobile hardware
- Full interactivity preserved for every tree in the world
The performance gain is not incremental - it is the difference between "unplayable" and "smooth" on mobile hardware.
Common Pitfalls
Over the course of developing these systems, we encountered several pitfalls worth highlighting:
1. Forgetting MaterialPropertyBlock Limits
While MaterialPropertyBlock lets you vary per-instance properties, overusing it can break instancing. If you set unique properties on too many objects, Unity may fail to batch them, negating the performance benefit. Keep per-instance data minimal: transform, colour tint, and one or two custom floats at most.
2. Shadow Casting with Instanced Objects
Instanced objects can cast shadows, but shadow map rendering effectively doubles your draw calls (once for the camera, once for each shadow cascade). On mobile, consider disabling shadow casting for instanced foliage and using baked shadow textures or ambient occlusion instead.
3. Physics Colliders on Instanced Objects
Instanced meshes do not have colliders. If you need collision detection (for example, preventing players from walking through trees), you can use a separate, invisible collision layer with simple box or capsule colliders placed at tree positions. These are far cheaper than full mesh colliders and do not require GameObjects with renderers.
4. Dynamic Batching Conflicts
Unity's dynamic batching and GPU instancing can conflict. If both are enabled, Unity may choose dynamic batching for small meshes, which is often less efficient than instancing. Disable dynamic batching when using GPU instancing to ensure the instancing path is always used.
When GPU Instancing Is Not the Right Solution
GPU instancing is powerful, but it is not always the best approach:
- Unique objects (a single boss character, a unique building) do not benefit from instancing since there is nothing to batch
- Skinned meshes (animated characters with bone rigs) cannot be GPU instanced in Unity - use other optimisation strategies for NPCs
- Objects with many material variants (different textures, different shaders) break instancing batches. If your forest has 50 tree species each with unique bark textures, consider texture atlasing to consolidate materials
For these cases, the SRP Batcher, static batching (for immovable objects), and manual mesh combining are better alternatives.
Applying This to Your Project
If you are building a game with dense environments - whether that is a city builder, a farming sim, an RTS with hundreds of units, or an open-world RPG - the same principles apply:
- Profile first. Use Unity's Frame Debugger to see exactly how many draw calls your scene generates and why.
- Enable GPU instancing on materials for any object that appears multiple times.
- Implement LOD groups with at least 3 levels, including a billboard LOD for the furthest distance.
- Use the swap pattern for objects that need to be both numerous and interactive.
- Test on target hardware. Editor performance is not indicative of mobile performance. Profile on the lowest-spec device you plan to support.
These are not theoretical techniques - they are the exact systems we built for a live MMORPG that runs on mid-range mobile hardware. They work.



