Culling large amount of dynamic objects at runtime?

So… I’m trying to create a Minecraft-clone (just for learning)… and I want to hide all the cubes that the camera cannot see and unhide them when they are in view.

Since the landscape is generated at runtime I can’t bake the OC. I tried to set the Renderer enabled to false when they are not in the camera view and turning them back on when they are in view… However, this caused some major performance issues. It was better not using it than using it…

I did this in the Update function, which I guess isn’t the way to go - I believe that’s what causing the perfomance issues; checking all the thousands of cubes once every frame.

How would you go about something like this?

Are you using voxels?

No, I’m only using Prefabs… (Cubes with different materials)

Then you’re never going to get the performance you need for your goals.

Break, and look at voxels.

To elaborate a little more: when you have a bunch of different cubes, each of these is a mesh. Every mesh you send to the video card (what we term a “draw call”) has a certain amount of overhead. The way to render lots more things is to combine them into a single mesh. This is why you can have 10,000 particles with smooth performance, but if you have 10,000 cubes, your framerate will chug: The particle effect has all 10,000 of these on a single mesh and a single draw call, while 10,000 cubes takes 10,000 draw calls.

Unity has realtime “dynamic batching” which can merge cubes together to reduce this, but the batching process itself takes a fair amount of CPU, and it’s limited to a certain number of vertices. It’s far better to have the objects be created from the get-go as a merged mesh.

There are other inefficiencies to the “bunch of cubes” method, all of which worsen the whole problem.

  1. At least half of each cube is invisible at any given time (the back faces). While this isn’t a too big a deal for your graphics card on any given cube, it’s a BIG problem for the dynamic batching process, which has to handle all the triangles you can’t see.

  2. I say “at least half” because many cubes will have more faces that are impossible to see (e.g. the ones that are against other cubes). That’s more vertices/triangles that the batcher has to deal with that you can’t see.

  3. A lot of cubes will be getting obscured entirely because they’re underground.

  4. A lot of cubes are behind the camera.

While #1 is only a big deal for the batcher, #2 and #3 are a problem for both the batcher AND the video card thanks to fillrate: It draws all the pixels required to render a cube, and then it has to draw all the pixels of a cube in front of it. This is called “overdraw”, and you need to minimize it (though realistically you can’t eliminate it entirely). You’ll find that the two biggest obstacles to fast frame rates, in terms of graphics, are draw calls and fillrate. If your cube world is five cubes deep and they’re all visible, the video card has to draw the entire screen five times, and since you only see the top layer, that’s HUGELY wasteful.

Believe it or not, #4 is actually not much of a problem at all, because Unity pretty efficiently takes care of it with something called frustum culling. Every camera has a “frustum” - that trapezoid-ish shape representing everything the camera can see - and it’s relatively cheap computationally to determine what things are outside that space.


So one thing we can do right off the bat is decide *when* to update your cubes' visibility, because this logic will actually be more or less unchanged even when you're generating your own cube mesh (which you definitely need to do). As you've discovered, Update() is the wrong place - it gets executed so often that your rendering savings are just eaten up. But if you remove the "behind the camera" logic, because view frustum culling is pretty efficient, you can now start thinking about something interesting: *when is this list going to change?* The answer is, almost never (in terms of game time): pretty much only when a cube is mined or added. And even then, if you have your data stored in a solid data structure, you will only need to modify the visibility of the cubes that directly surround the one that got added or removed, so you need to run the "am I visible?" logic for probably just 7 cubes (4 cardinal direction, up, down, and the affected cube itself).

Once you have gotten this logic done, and it's being executed at the correct time and with the correct scope, you can check your performance. Depending on your use case, these improvements might be enough to get your game to an acceptable framerate, but once you get these done, you'll be in a good position to start writing a mesh generator script to merge the cubes into one mesh.
3 Likes