Week 4 - Voxels and Optimisation Purgatory


Week 4 - Voxels and Optimisation Purgatory

This week I was determined to tackle the biggest challenge of this project, implementing procedural level generation and deterioration. Going into it I wasn't quite sure how best to do this, so I started looking at games that implement some form of terrain deformation and many of them are using voxels. Of course, the quintessential example is Minecraft, but there are plenty of  small voxel games like Tuxedo Labs' Tear Down that use this approach. Even No Mans Sky uses voxels for it's terrain deformation, despite not having a cubic art style. I knew a little about what voxels were and how they worked, and I also knew that they are incredibly resource intensive to use, so after deciding on this approach I spent most of last week reading about the graphics rendering pipeline and optimisation techniques so I could keep performance high enough for deployment on WebGL.

Even for a simple level I was going to need tens if not hundreds of thousands of voxels to have enough for a good deterioration effect. This meant I would have to use GPU instancing to reduce the number of draw calls I was sending to the GPU each frame.  Unity has an option to enable GPU instancing on the standard material shader -  so for a first attempt, I wrote a script that iterates through nested loops to create a square (later a circle) platform, instantiating game objects that all share the same mesh and material, so they would all be batch rendered.

This worked (kind of) but was far from optimal. Using individual game objects, each with their own mesh renderer, creates way too much overhead. Even if they are batch rendered, Unity has to loop through each object every frame. I was capped at around 10,000 voxels before dropping below 60 fps (without anything else running in the game).

To improve this, I would have to manually send the GPU an array of matrices (essentially points in world space) as well as the mesh and material to use to draw the voxels. I had to read up on how exactly to implement this, and made the necessary changes to my platform generator script, but this literally increased my performance over 10 times, with over 100,000 voxels rendering at 70 - 80 fps.

Of course, this approach has it's own problems. First, because I am directly telling the GPU to render the voxels, they have no associated game objects, and therefore no colliders or rigid bodies. The platform is essentially a phantom to the rest of the game, and I cant have it break down realistically. Second, it still isn't even close to as optimised as it could be - as I am still sending the entire array of matrices to be drawn over and over again every frame, even if nothing changes.

I could (theoretically) fix both of these problems using indirect GPU instancing, where I send the GPU a compute buffer containing the mesh and material and instructions on how to render it once, and doing all the calculations on the graphics card from then on. Unfortunately, WebGL doesn't support compute shaders, so the holy grail of rendering optimisation isn't an option for this project.

So, I figured the best way to get the platform to deteriorate would be to combine both of the previous methods. I would have a single script that would render all the voxels on the GPU, and for each rendered voxel I would create a game object that only had colliders enabled. This obviously had a performance impact, but it was surprisingly little given the huge number of game objects and colliders in the scene. Now, to have the platform deteriorate I would have to first sort all the matrices and game objects from the outside in, so both the arrays were in the same order. Then I would gradually iterate through the arrays, removing a voxel from the matrix and enabling the renderer (and adding a rigid body) to it's game object counterpart so it could fall - and discard it when off screen.

If I've lost you by now, don't worry - all that matters is it works, it looks cool, and it runs (reasonably) well.

Of course, there are still countless ways this could be optimised and improved further. For instance, I looked into adding occlusion culling, so I wasn't rendering every voxel on the inside of the platform - but again, because I am GPU instancing them, I need to use a compute shader to execute culling instructions on the GPU.

Still, there's something really satisfying about it, and I liked it enough to shift the aesthetic of the game to using voxels throughout. The obvious next step was to adapt the level generator code to create a bomb explosion out of voxels. This was simple enough, and after a little (well a lot) of trial and error, I managed to get an explosion that generates voxels and rapidly expands outwards. This was cool enough, but I topped it off by instantiating a bunch of voxel gameobjects at the end so it explodes like fireworks, with all the particles slowly falling down like embers.

I also wanted to add a cool death effect where the player shatters into pieces when they fall of the platform, but when trying to implement it I ended up finding a stack of bugs that I had just created by doing all this, so I've been preoccupied fixing them before the testing session (but I'll save that for the next devlog). Hopefully I can get it added before the final pitch though!

- Cody.