In this quick tutorial I will go over a simple method to generate the data needed for representing an island for a game or graphic application.
Islands are the natural extension to simple terrain generation. Islands can be of many different sizes with their own internal biomes and ecosystems and the surrounding water creates a natural game border limiting the playfield of the player without the use of fake borders and error messages that tend to break immersion.
Before we start, let's quickly go over what is needed to create an island:
The terrain height map will basically tell us the Y value of each vertex of our terrain. The island mask will be multiplied against the height map and fade the terrain into an island shape creating the final island height map. A moisture map will tell us how much water is in the ground which will then be used together with the final height data to create a biome map.
The height map is the most basic component that controls the height of each tile or vertex of the terrain. It can be a greyscale bitmap or higher percision for more scale and is commonly generated using Perlin or Simplex noise. If you aren't familiar with noise functions I highly recommend Amit Patel's post on noise functions.
A downside to Simplex and Perlin is that they are relatively noisy and aren't very well suited to creating wide open areas like plains. For that reason I personally like to use Exponentially Distributed Noise as it creates much more uniform terrain, unlike Perlin and Simplex which create a distribution much better for cliffs and ridges.
However when making a large world map it is nice to have a variety of features and combining the two at different weights may give just that.
Now that we have a height map, let's understand how it is used.
Basically each pixel represents the height of our terrain. It is easy to see some areas naturally become valleys, hills and mountains and if we want to add lakes then we can simply decide that everything beneath a certain value is water.
When rendering, simply multiply the Y value by the pixel value. The first thing you might want to do is to scale the maximum and minimum values somewhat otherwise the terrain bounds will be [0,1] or [0,255] depending if you are using a byte or a float for your terrain data.
I also suggest taking the minimum level slightly underneath the water if you are planning to add a custom water renderer later.
When generating the Simplex noise I like using these parameters to give a nice distribution of the height data with a good percentage of lakes and mountains:
// A value that gives a nice distribution for 1024px bitmap const float scale_1024 = 0.0033f; // Fit the scale value for our bitmap size const float scale = (1024.0f / (float)island_size) * scale_1024; SimplexNoise noise; noise.SetOctaves( 16.0f ); noise.SetPersistence( 0.5f ); noise.SetScale( scale ); noise.SetBounds( -0.25f, 1.0f );
Note that these values are subjective and you should play with them until results that you like. I personally having a chance for lakes which is why my minimum bound is [-0.25].
The scale_1024 variable is just a number that I found which gives a good noise distribution when used as the scale/step value for Simplex when creating a bitmap of 1024x1024 pixels. I scale it against the size of the map relative to 1024 so I can keep that distribution.
Now you should have a nice looking height map and maybe even some lakes in some area, but you might have noticed that this is not an island. Luckily making it into an island is quite simple by the use of a mask.
So far our island isn't looking very island-like. It is fair to say a criteria to being an island is being surrounded by a body of water. This is where a mask can be used to neatly lower the terrain below sea level. This can be anything from a generic radial or square functions to a pre-determined shape. Anything will work as long as it will create an island like shape either on its own or when multiplied with the height map.
The simplest mask I like to use is a quadratic distance function from the center of the island.
float distance_x = fabs(x - island_size * 0.5f); float distance_y = fabs(y - island_size * 0.5f); float distance = sqrt(distance_x*distance_x + distance_y*distance_y); // circular mask float max_width = island_size * 0.5f - 10.0f; float delta = distance / max_width; float gradient = delta * delta; noise *= fmax(0.0f, 1.0f - gradient);
When combined with the height map it creates a neat island shape.
If you want to make your island look like the treasure maps from old cartoons you can consider using a square mask instead.
float distance_x = fabs(x - island_size * 0.5f); float distance_y = fabs(y - island_size * 0.5f); float distance = fmax(distance_x, distance_y); // square mask float max_width = island_size * 0.5f - 10.0f; float delta = distance / max_width; float gradient = delta * delta; noise *= fmax(0.0f, 1.0f - gradient);
And now we have our island! Time to decide where are forests, plains and deserts are going to be at by sectioning areas into biomes. But first, the moisture map.
The moisture map represents the water in the ground and is basically just a helper map for generating the biomes in the next step. This can be just another noise bitmap using Simplex or Perlin functions. I also like to add moisture next to fresh water such as lakes generated by the height map as it encourages vegetation.
Note that I also apply the moisture map the same mask I apply to the height map. Lowering the moisture level near the sea encourages the generation of beaches and sand.
Now that we have the basic shape of our island and some knowledge of water, we need to add some properties like forests, beaches and snowy mountains. These biomes can all be decided by the combination of moisture and height. An easy way to visualize that is by creating a table of height and moisture level, for example:
|Height <= 0.25||Height <= 0.50||Height <= 0.75||Height <= 1.00|
|Moisture <= 0.25||Subtropical Desert||Grassland||Tropical Seasonal Forest||Tropical Rain Forest|
|Moisture <= 0.50||Temperate Desert||Grassland||Temperate Decidous Forest||Temperate Rain Forest|
|Moisture <= 0.75||Temperate Desert||Shrubland||Taiga||Taiga|
|Moisture <= 1.00||Scorched land||Bare Land||Tundra||Snow|
I also automatically assign unique "Salt water" biome to water connected to the edge of the map and "Fresh Water" to closed bodies of water such as lakes.
While I will not be going over it in this tutorial, I thought it is useful to note an A* map or a navigation mesh can be created for AI and deciding where a player can go.
An A* map can be easily created by checking slope between points in the final height data. If the slope between two vertices is below a threshold, they become connected nodes in an A* graph, otherwise there is no direct passage between the two.
The final step is to get this lovely island to the screen! There is definitely more than one way you can go about it and it doesn't have to be in 3D.
The easiest way to render the island is by creating a simple square grid and assigning with the Y value assigned directly from the heightmap or multiplied by some scale. You can also assign a color to be associated with each biome which will give you a representation of beaches, grasslands, forests and everything else.
This is just a simple representation and there is a lot more you can do from here, including adding textures, using a level of detail algorithm to greatly scale the island and adding more features.
Ok, rendering the island was not really the final step. There is much you can do from here including adding vegetation, wildlife and simply improving the renderer. I will leave these as ideas for you to toy with and possible for me to follow up in a future post.
Did you like this post? Please share it on social networks! Also feel free to let me know your thoughts! :)