How we created a GPU based semi-procedural terrain and vegetation system with Unity to fit the need of our openworld game
As a premise, we are an indie studio of ~10 people, we are trying to make a very ambitious turn based RPG game with a huge world named Edge Of Eternity, and this is about how the world is created that i'm going to talk about in this article.
At the beginning of the project we were using the Unity Terrain but we quickly had two big issues, first was creating that kind of world by hand is extremely time consuming and not possible for a studio of our scale, and the second one was that the Unity Terrain is not made to be able to handle very large maps (> 256km²).
So we decided to start from scratch a new terrain system with specific constraints in mind:
The terrain system must be able to handle arbitrary large map size without increasing the memory consumption or lowering the performance, also it should use a "LOD" system for all resources (including heightmaps) used in it to minimize the memory consumption
Asset density must be very high the closer to the player it is and high assets density must be visible from afar to have breathtaking scenery
The terrain system must be able to use SpeedTree assets as well as the Unity Terrain (Wind + Touch Bending)
Assets spawning must be procedural fully realtime with nearly no performance hit and controlled by artist rules and a biome system must be created to setup different areas with completely different rules, also the artists must be able to clip assets at a specific place to spawn manually instead for specific area setups
Terrain texturing must also be procedural and based on ruleset defined by artists (layer based), also the number of different textures possible to use on the texture must not be limited and must not cause noticeable impact on the performances
Assets must be interactibles to allow gathering some of them for crafting system purpose
No native dependencies and it should use APIs that are available also on latest generation consoles
Pathfinding must be done using the NavMesh realtime baking API to generate navigation tiles that can be reused at later plays
Drawcalls must be very low (< 1000) to leave space for the artists for the remaining assets
We took a few weeks to build a prototype from it that will later become the terrain system of Edge Of Eternity.
First we tried a pure CPU approach to the procedural generation and multiple problems arose, it was very problematic to get a huge asset density generated, process a lot of asset matrices in realtime with a decent framerate otherwise we would had to bake a part of these data but we didn't want to have heavy memory footprint or latencies during the generation, also constructing huge render lists to pass to the Graphics.DrawInstanced method was creating lot of issues like insane garbage collection when rebuilding periodically the render list to match the generation, huge copy time etc...
So finally we switched to compute shader based generation (GPU), we had the idea when we heard about the new low level feature of Unity Graphics.DrawMeshInstancedIndirect, allowing an instanced rendering with the render list coming from a compute buffer so we started to think about putting everything on the GPU and just reading back nearby collisions and terrain mesh collisions. The use of Graphics.DrawMeshInstancedIndirect allowed to have only 1 drawcall per asset type and LOD level, we also optimized this part and put a timer on asset visibility, when an asset is not visible for a few frames the drawcall is not made anymore saving even more drawcalls, we applied also compute shader based frustrum culling (and we are currently working on a non baked occlusion culling gpu based solution) and finally when an object is not spawned anymore the meshes and the textures of it are unloaded, all our optimizations has made all the screenshots that you are able to see on this post as low as 600-700 drawcalls with everything included.
The key idea was to make the generation based on the player position, compute shaders are extremely fast so it was possible to generate the world while the player is moving along it, using just a seed to have a predictable generation for consistency, when the player move around the generation is biased by the distance to the player, these factors are tweakable and diminish the asset spawn rate based on the distance and since the generation is based on the player position everything that is not spawned doesn't exist anymore so the memory footprint of the algorithm is fixed and doesn't grow as the map scale increase.
Our biome system allow very different areas with noise blended transitions between biomes, the system autodetect where biomes overlapse and increase the rendering cost only on the pixels affected by the biome transition
The heightmap handling took us a lot of time, we tried a few different solutions, we did choose for some time to use a SparseTexture to be able to handle partially present areas and mips of the texture so a distant part of the terrain would be uploaded on the SparseTexture as Mip 2 or 3 but a close area would be Mip 0 or 1, we stored the currently loaded Mip on a lookup texture written by the CPU. Finally we had the info that SparseTexture was not supported everywhere so we created our home made SparseTexture module in the old school way, using a texture as a buffer for storing tile datas and a lookup texture for indirection, but we are quite eager to try switching to the new mip streaming feature of Unity 2018.2!
Texturing is procedurally done by rules with different parameters like terrain normal slope, noise, blending and height, assets are spawned ontop of textures, meaning that for example you place bushes ontop of the texture grass with distance, spread and noise rules.
Assets are also fully interactible, with touch bending when any flagged entity pass by, react to Unity Wind and can be "looted" with a custom animation for crafting.
Terrain geometry is handled in the form of instanced geometry patches (one for each terrain LOD geometry quality level) that are displaced by the sparse heightmap on the vertex shader but we plan to move to Graphics.DrawMeshProceduralIndirect with a vertex generator compute shader to have more control over the LOD geometry transitions to do it in a smoother way.
We created our generation algorithm directly as a compute shader with biome and generation parameters tweakable from the editor with a custom editor window
All modifications done by the tools are instantly applied ingame causing a dispatch of the generation compute shader.
You can also live paint biomes and how they mix with other biomes with the currently work in progress biome painter
It was very challenging for us and took a lot of time to reach this result but we are very happy with it and it now allow us to build Edge Of Eternity a lot more quickly and artists can now focus on creating awesome areas in minutes instead of spending hours painting every batch of grass.
We are considering putting the terrain system on the Asset Store when it will reach the level of polish required for the Asset Store.
In the next articles i will go in technical details (C# and shader sides) on how we did each modules of the terrain system