Rendering at scale: Efficient strategies for massive object counts

This is the fourth post in a series written by Mega Cat Studios. In this post, Matthew Wojtechko looks at performance lessons learned from using both URP and HDRP, and explores advanced solutions like custom Batch Renderer Group implementations for grass rendering, where every millisecond counts.
Read the other blog posts in this series:
Rendering thousands of objects without dropping frames is a constant challenge. Dense cityscapes and sprawling landscapes have become the hallmark of modern gaming. But while these vistas are breathtaking for players, they can be a headache for engineers tasked with maintaining a smooth 60 frames per second (fps).
And if you’re targeting platforms with less power, like mobile or untethered devices, it’s not just those vistas that are a problem. Any environment can easily make the hardware churn if it includes any level of detail.
We’ve been pushing the capabilities of Unity’s rendering systems for years now at Mega Cat Studios, and it took every trick in our bag to get the endearing art style of Backyard Baseball up and running for PCs and consoles, without sacrificing artistic intent.
In this post, we’ll explore techniques like static batching and vertex animation textures, and share some of the problems we encountered in our biggest 3D title to date, all with the goal of helping you keep your frame rate smooth as object counts explode.

Investigate what’s causing rendering issues and then optimize
Rendering is a collaboration between your CPU and GPU.
“The CPU is the coach, and the GPU is the athlete. The CPU calls the play and the GPU carries them out,” says Liam Dudas, optimization engineer on Backyard Baseball.
Sometimes the work the GPU has to do is more time-consuming than other tasks. Complex meshes, shaders, and lighting can cause slowdowns, and the best way to improve performance is to reduce their complexity by asking the artist to optimize these assets.
Other times, it’s not what the CPU asks the GPU to do, but how it asks it.
Is the CPU executing its instructions optimally? Is it taking a long time to figure out what to draw, making the CPU twirl its thumbs instead of getting the rest of the game to run? Is the CPU providing information redundantly? A good coach can inspire an unexpected comeback from his team, but when it comes to optimization, we can’t just get these systems to “try harder.” It’s all about understanding the information they’re dealing with and how they communicate it.
This comes down to optimizing the draw calls the CPU passes to the GPU. The more work we can pack into one call, the better, especially when there are tens of thousands of objects to render.
There are plenty of tricks to achieve this – but a word of caution before you do.
Don’t just optimize willy-nilly, for nothing more than the thrill of clever code. Always benchmark first. Find where your project is suffering the most. The first question you should always ask: Is your bottleneck in the CPU or the GPU?
Some of the solutions in this post increase CPU speed, others GPU speed. You don’t want to get carried away optimizing one processor when the other one is actually the source of your frame spikes. Once you decide whether your game is CPU- or GPU-bound, investigate which procedure is taking the longest, then optimize that. Unity’s profiling best practices guide is a great place to start, which includes a handy flowchart that provides guidance on the often mysterious process of profiling.
“I have to do a lot of profiling,” says Liam. “I can tell when the CPU is doing a lot, and the GPU isn't, and vice versa. Really, our worst bottleneck has been both of them at different times.”
We included a slew of our favorite resources at the end of this post to help you on your own profiling journey.
And with that disclaimer out of the way, let’s explore optimizations.
Simplifying and culling
When rendering, we need to determine which meshes to render and where.
The more meshes we have and the more draw calls we need, the more the CPU has to keep figuring out what to do while the GPU waits for its next job.
We could speed up the process by rendering fewer meshes. For example, you could swap complex meshes with imposters, billboards, or low-poly versions; or you could break up a sprawling world into smaller levels with less to render at once. These are tried-and-true solutions you should use when you can, but they limit the level of detail in your environments.
One optimization that avoids this, and is simple to implement, is occlusion culling, where the camera checks what’s within its field of view and avoids rendering what the player can’t see. This saves time at the cost of increased memory usage, but it’s often worth it in environments with many stationary meshes, like detailed rooms in an office building. This optimization is an industry standard, but it’s often not enough on its own.
Static batching
Another way we can limit the number of meshes we render without sacrificing variety is by combining them into a single mesh.
You can do this yourself manually. Or, so long as the meshes share the same material, Unity can do this for you in a process called batching. Set a game object to static, and Unity will automatically batch all of the meshes with the same material together to cut down the draw calls. Use this option on anything that stays static, like trees and walls.
This drastically reduces RAM usage and CPU overhead, allowing the GPU to render more content at once. But keep in mind, more GPU memory will be used to store the combined meshes.
Static batching is often our #1 key optimization process, particularly for Backyard Baseball. Using URP’s batching system is simple, so the most challenging part is figuring out how to use as few materials as possible across as many models as possible. That balancing act is why we have some of the most creative cats on our optimization and art teams.
GPU instancing
This is a good time to start distinguishing between a thing… versus an instance of that thing.
Let’s say you have a mesh. A mesh is an asset. And its material is an asset. They are single files that exist in your file structure. For every object with a 3D appearance in your game, a mesh describes its shape, and then a material describes how its surface looks.
But because Unity simplifies its details, some developers don’t realize that when their game is running, they actually have multiple copies of these things instantiated into their game world. When your player explores a forest, they’re moving through many copies of that one tree stored as an asset in their game’s files.
This is why we used GPU instancing for the foliage in Backyard Baseball. Instead of rendering each copy of the tree with unique calls to the GPU, the CPU instead asks the GPU to render all identical models at once. If you’re not careful, this could result in every tree in the forest looking identical. But, with some effective parameters in the material’s shader, for example, a value to customize the color of the leaves, you can pass to the GPU one model and material, along with the unique colors for each tree, rather than asking for each tree individually.

When you toggle on the Enable Instance setting on a material, you tell Unity to combine into the same draw call different material instances that use the same material. This is a great way to balance both variation and performance in your meshes. And, unlike static batching, GPU instancing can be used on game objects that move in your scene, so you’re not restricted to only optimizing things like walls and trees, but also props like debris from a wreckage or office supplies.
Vertex animation textures
Static batching and GPU instancing are two different ways of optimizing meshes and materials. Sometimes, though, it’s the animations that are causing the bottleneck. Take the simple forest example from earlier. Now, imagine its branches and leaves are swaying in the breeze, all with unique and natural animation. Running 10,000 unique animation processes in the usual way just won’t cut it. The poor CPU will be the one gasping for air if you aren’t careful!
One trick is using vertex animation textures (VAT). A VAT hijacks a GPU’s processing power to try its hand at animation.
For David Chávez Armenteros, senior developer at Mega Cat Studios, using VAT to simulate a crowd was one of his favorite graphics implementations to optimize.
“Using standard skinned mesh renderers with the Animator, you can only render a few dozen characters before performance starts to drop,” he says. “But by combining VAT with GPU instancing, you can push thousands of animated entities on screen at once.”
With VAT, each mesh vertex reads its animated position from a texture each frame. To do this, we encode positional and rotational data into RGB values. Then a special shader reads all the pixels in a texture to determine how to move all the vertices.
This is what we used for the swaying grass in Backyard Baseball. It’s an efficient way to create smooth animations, but remember, it's solely visual–the collider does not change. This makes VAT ideal for scenarios where movement is essential but precise collision detection is not.

And every optimization comes with a tradeoff. VAT frees up the CPU, at the cost of some extra burden on RAM. This all has to be balanced against other rendering solutions in the game.
“The most intense thing about the system was balancing memory usage and GPU-compute-based culling computations to contend with the limited resources of untethered devices,” says Jordan Latta, technical artist for Backyard Baseball.
URP vs HDRP
The first choice you make when optimizing graphics is which render pipeline you choose when you start a project. The Universal Render Pipeline (URP) has good baseline performance and is an absolute must for mobile and untethered devices. The High Definition Render Pipeline (HDRP) targets high-fidelity visuals and offers more specialized instancing behavior.
Generally, we recommend using URP over HDRP in Unity. Perhaps you have a use case that requires HDRP, but we are motivated by this simple principle: Keep your rendering as efficient as possible.
HDRP gives you some high-end capabilities, but if you know you’re not targeting realistic visual fidelity, we recommend you pass.
More optimization tricks
Sometimes, the problem with having thousands of objects on screen isn’t the rendering at all. It’s your own code. In those cases, here are some tricks you can employ:
- Consolidate thousands of Update loops into one. Instead of giving every GameObject its own MonoBehaviours, consolidate them into one so they share the same Update function. A centralized manager helps avoid thousands of individual update loops.
- Instead of running everything on the same thread, use multithreading. Explore if the job system and the Burst compiler could be good options for your game.
- Cash in on caches. Instead of grabbing the main camera or a GameObject’s Transform every frame, cache them once and then reuse that reference. Tarodev demonstrates the performance improvement and explains these other optimizations as well.
Perhaps the single greatest rendering boost you can get for your next project is using the latest LTS version of Unity. Unity 6.3 LTS boasts a host of improvements, including optimizations to improve performance.
For Backyard Baseball, our simplest, best optimization win was reigning in our texture use.
There are a few sneaky ways in which you can accidentally use double the textures you need:
- Shaders: If a shader uses a texture on two different occasions, and it does so without sharing the same reference, it may load the texture in multiple places in memory.
- Scripts: Similarly, different areas of your codebase can end up loading the same texture via different references.
- Version Control: When two developers add the same file to their local branch, sometimes both files are kept when the branches get merged, taking up storage space that’s just redundant.
We try to be aware of these pitfalls so we can prevent them before they even happen, but being too stringent slows down the fast-paced iteration cycle. That’s where our optimization cats come in to audit shaders, assets, and scripts when the project enters the later phases of its development cycle.
At Mega Cat Studios, these strategies enable us to render scenes that were unthinkable just a few years ago.
To us, rendering is basically magic: we can create anything we can imagine in real-time, but only after carefully studying the secret texts (Unity documentation, blog posts, YouTube tutorials). Luckily for us, we have some wizards on our staff who love nothing more than diving into that documentation and sharing the spoils with the team.
And of course, the ones who really benefit are our players. We hope fans will enjoy exploring Backyard Baseball without even stopping to consider how much blood, sweat, and magic went into making that world feel real.
Learn more
- Unity Profiling Guides
- Unity Documentation
- Unity 6 Features Announcement
- Official Unity Optimization tutorials
- Tarodev’s How To Render 2 Million Objects at 120 FPS
