I am part of The Game Assembly’s internship program. As per the agreement between the Games Industry and The Game Assembly, neither student nor company may be in contact with one another regarding internships before April 15th. Any internship offers can be made on April 27th, at the earliest.

Specialization

Making a Hydraulic Erosion Simulator for terrain.

Introduction

  • For the first couple of weeks when developing Catstronaut: Mission Impawssible I mostly worked on importing landscapes from Unreal Engine into BOB, our custom game engine. This led to me thinking a lot about landscapes and terrain and what one could do to make them look nicer.
  • When our specialization course at TGA began and we had to choose a project, I started researching and quickly stumbled upon terrain erosion. After some further research I thought that hydraulic erosion would be fun to implement and decided to choose it as my specialization project.

Features

  • Terrain Generation using perlin noise
  • Hydraulic erosion simulation on GPU
  • Heightmap export as DDS and raw data
  • Heightmap import from png, DDS, raw and .r16 (Unreal's file format)

Project Setup

I chose to use our BOB engine for this project because I felt that it would allow me to work the most efficiently as I have been working with it for the past 6 months. I also felt that the control offered by a custom engine would be a major advantage, since this project involved a lot of graphics programming that would likely have been much harder in a commercially available engine.


The project builds off the Modelviewer for the BOB engine, made by Svante Jakobsson, although it is heavily modified to fit my purposes. Even the engine itself is very stripped down to its core, as to not bloat the project with unnecessary game-related code.

Erosion debug view

Modified Modelviewer

Terrain Generation

Different layers of noise.

The very first thing i did was to make the program capable of generating terrain based on perlin and fractal noise.

Fractal noise is a kind of noise that is created by using multiple layers of noise (octaves) on top of each other, where each layer's frequency is doubled and amplitude halved.


This way of generating terrain is commonly used as it is really simple to implement and it looks pretty good on its own.

Unfortunately this technique does not generate realistic looking terrain.

Erosion

Erosion is one technique one can use to get realistic looking terrain. There are several types of erosion that you can simulate, but I chose hydraulic erosion (water). When simulation hydraulic erosion there are three main approaches: Water as particles, water as feilds, and waterflow as a directed graph. I chose to simulate water as particles.


In my simulation, for every iteration one water particle spawn on a random point on the terrain. Then, using bilinear interpolation, the particle calculates the slope of the terrain on that point and start "flowing" down. When a particle flows downward it pick up sediment from the ground. Later, if it encounters a hill or a flat part and stops going down, it instead deposits some if its sediment on the ground.


This works really well, but by this point I'm doing all calculations on the CPU. When doing 5-10 million iterations of this, the performance starts to really go down.

Erosion debug view

Prog-art illustration of a particles path

Optimization

The compute time for 5 million iterations of erosion on the CPU was about 1 minute, which was not acceptable. To fix this I decided to move all my erosion calculations to the GPU, in the form of Compute Shaders.

I initially hadn't planned to do this, so fitting it in the tight schedule was hard. Fortunately, Svante came to the rescue again, and gave me a crash course in compute shaders. Turns out, they were not as complicated as I thought they would be. The actual porting of the code was really easy. The hard part was designing how to get all the data from the CPU to the GPU in a nice way.

The actual terrain height data was easy to pass along to the GPU in a single structured buffer. Were I ran into some problems was with my erosion brush.

The erosion brush is a precomputed set of values, telling the particles from which gridcells on the terrain they could erode from. This had to be recalculated every time the user changes the radius of this. When doing this on the CPU I just had two 2D vectors containing all the data, but that was impossible to do on the GPU. Instead I had to pack all the data into three separate structured buffers. One containing all indices, one containing all brush weights and one offset-buffer containing the start and end indices for the other two buffers

After getting all of this to work I got the compute time down from 1 minute to about 2-3 seconds. This did sadly break by ability to export heightmaps, because the eroded terrain never lives ob the CPU. I could have fixed this by doing a render pass of the heightmap into a texture, and then exporting it, but I unfortunately didn't have time for that.

Conclusion

If I were to keep working on this I will firstly fix the heightmap exporting of course, but I would also like to add different types of erosion. Like wind erosion, to be able to make cool desert-like landscapes.

Another thing I would like to add is terrain sculpting, to give a little bit more control over how the landscape looks. I could even reuse my already existing erosion brush for that!

Overall I'm really happy how this turned out! Seeing the erosion work for the first time was such a cool feeling, and I also liked learning more about compute shaders and structured buffers! :D

Below are some images and a video showing the final product!

Before / After Erosion Comparison

Top View - Before
Top view before erosion
Top View - After
Top view after erosion
Angle View - Before
Angle view before erosion
Angle View - After
Angle view after erosion

Showcase video