A downloadable game

Projects Github


Preface

Ever since my childhood. I've always been amazed with games that have seemingly endless worlds. The infinite terrain that is only yours to see, completely unique to you. I've always wanted to know how these games achieve this. So I wanted to create a small prototype where you can drive a train through a procedural landscape, across the endless oceans. 

Researching

While researching how these games achieved this, I found many different ways to generate infinite terrain

- Noised-based infinite terrain - Using mathematical functions to create an "ordered randomness" which can be used and layered to create hills. Games like Minecraft use this to create the terrain.

- Cellular Automata - A grid cell based terrain which has certain rules to dictate the state of the cells, This can end up creating complex patterns like rivers and caves. Games like Dwarf Fortress use this approach.

While researching these methods, I found the best method for generating hills and terrain would be noise-based. There are many different types of noise that can be used for generating infinite terrain. The main one that is used which is the one I will be using is called Perlin noise.

Implementing

For this project I will be working in c and using the library Raylib to easily create 3D worlds without much boilerplate. 

Looking at the Raylib cheatsheet, we can see Raylib has a function to generate this type of noise. 


This isn't quite exactly what we want though. As we want to be able to adjust different parameters for this noise. The Perlin noise function takes multiple arguments which we can adjust to create the exact noise we want for our use case

The usual arguments which we need are 

1. Scale - This determines the distance at which we are zoomed into the noise.

2. Octaves - This is the number of layers we want for the noise, these all get added to get a final noise image.

3. Lacunarity - This determines how detailed each octave level is.

4. Persistence - This determines how much detail each octave layer contributes to the final noise image.

Looking at how Raylib implements their Perlin noise function we can see they use the stb_perlin_fbm_noise3 function which allows us to pass in our lacunarity, gain (persistence) and octave arguments. so I adjusted the Raylib function to allow for these arguments.

Now we can generate many different types of noise, here are a few examples.

Using this we can now pass this into a function to generate a mesh, We can use the greyness level (The amount of grey at a pixel point) to determine the height of each vertex.

We will need to create a function to handle this mesh generation but for now we can use Raylib's

Passing our height maps into this function we get a mesh that looks like exactly what we want

You can try out my demo to run this for yourself, you can tweak the scale, persistence, lacunarity and octaves to see the terrain update in real time.

For generating a single chunk I made a GenerateChunk function, which takes in a chunk position and generates a model. The noise values given to the noise function are offset by the world position, creating seamless chunks.

This is how I generate my chunk models


Now to create a procedural world we want to tile these chunks together to create an endless world. We can generate these chunks around the player to create a seemingly endless terrain for the player.

To do this, we want to store an array of Chunks, Chunks will be a struct that will hold the `Model` and position of the `chunk`.

When the world is initialized we can calculate the chunk arrays size and initialize it with dummy data.

We can then check when the player moves to a different `chunk` by finding the players `chunk` position, and comparing it to their last `chunk` position. When the `chunk` positions are different we can call a function to update the chunks. We want to always update the chunks on the first time running this as a first pass.

Then to update the chunks we want to loop through each chunk in the array, if they are not empty we want to check their distance from the centre chunk and compare it to the render distance. If its further than the render distance we can unload this chunk and set it to empty.

We then want to go through each chunk around the player, check if the chunk is empty. If the chunk is empty we then want to generate a new chunk in that position.

We can then render this, by looping through the array each frame and and rendering it using the Raylib function `DrawModel(Model model, Vector position, float scale, Color color);`

After rendering these chunks we now get an infinite terrain we can move across. We can change the render distance easily by increasing the render distance constant.

Problems

With this approach we do come across a problem, the chunks don't align perfect together. This is because the vertices on the edges of the mesh are 2 separate values on the noise map. There are many different solutions to this problem. But the simple and most effective I found was to merge these edge vertices into one by getting the average of both vertices (or 4 vertices if corner) and setting both to the same value.

Solution

Here is how I implemented this solution,

We want the vertices on the edges of the mesh to share vertices with the other mesh, allowing the smooth transition between meshes. To do this by increasing the noise texture by 1, to account for the extra vertices we need to generate, and also decrease offset it by 1 to have overlapping vertices.

Inside of our GenerateIslandModel function, we change to noise function variables to account for the solution above.

Download

Download
ItchBuild.zip 592 kB

Leave a comment

Log in with itch.io to leave a comment.