In this post, I’ll give a broad overview on the height map generation process used so far in Simurillion. The approach is based on the resources I’ve listed in the last blog post, so you might want to check that as well.



I’m using Kurt Spencer’s OpenSimplex noise as the base function. As far as I’m concerned the function is a black box that returns a smooth value between -1 and 1 for a set of input coordinates.

Let’s have a look at the raw output of the noise function first. For each point in the height map, a two-dimensional array of floating point values, I retrieve the noise value v by dividing the input coordinates by the texture size and by scaling v from range [-1, 1] to range [0, 1] by adding 1 to the value and dividing the result by 2.

v = openSimplex(x/size, y/size);

heightmap[x, y] = (v+1) * 0.5;

The output image is a color representation of the height map where the color at a certain point is the RGB color (v, v, v), so a value of 0 is black and a value of 1 is white:

In order to create a more interesting output image we can stack the output of multiple instances of the same noise function on top of each other, while changing the scale of the input coordinates and the output value in a process called fractional Brownian motion or fBm for short.

The input scaling factor is called the frequency (f), where

v = noise(f*x, f*y);

the output scaling factor is called the amplitude (a) that we apply by multiplying the return value so the base function becomes

v = a * noise(f*x, f*y);

This process is repeated for several iterations and the results of each iteration, called an octave (0), is added to a total noise sum. After each octave we increase the frequency by a factor, called lacunarity, and decrease the amplitude by another factor, called gain. To normalize the result to the noise function to range [0, 1] we record the minimum and maximum range of the noise function [-amplitude, +amplitude], add up these ranges and normalize the resulting value to this range. The result of this process is a smoother, cloud-like image, which depending on the initial frequency and the values for gain and lacunarity might look like this:



The implementation looks something like this:

max = 0.0;

min = 0.0;

sum = 0.0;

frequency = scale;

amplitude = 1.0; for (o = 0; o < octaves; o++)

x = x * frequency;

y = y * frequency;

v = noise(x, y); sum += v * amplitude;

max += amplitude;

min -= amplitude; frequency *= lacunarity;

amplitude *= gain; value = (sum-min)/(max-min);

The above image uses the output of the noise function as is. Different results can be achieved by slightly modifying the output of the noise function for each octave. Here’s a billowed pattern we get when taking the absolute value of the noise function

v = abs(noise(x, y))

Using

v = 1 - abs(noise(x, y))

yields a ridged pattern.

While SimplexNoise returns a value in range [-1, 1], applying the abs function changes this range to [0, 1]. Strictly speaking we should change the calculation of min/max for the last two cases to [0, amplitude], but right is what looks right, so after forgetting about this during implementation, I decided to just go with it, when I thought about it.

Even more interesting patterns can be produced by taking the output of one fBm turbulence function and using it to modify the input coordinates into another. Here’s the result of perturbing the input coordinates into the ridged function by the result of a billowed function times some constant factor:



dx = x/size;

dy = y/size;

v1 = perturbNoise(dx, dy);

// scale from [0, 1] to [-1, 1]

v1 = (v1 * 2 - 1);

v1 *= perturbationFactor;

dx += v1;

dz += v1;

v = baseNoise(dx, dy);

This pattern is the result of a lot of fiddling with the different noise functions and their parameters. I’d encourage you to just keep playing with the parameters until you are satisfied with the result as this process helps exploring the potential output space and the limitations of these kinds of functions and might lead to insights and happy accidents that are hard to fabricate otherwise.

For the map generation process, I wanted to end up with a continent/island like shape, meaning the height values should fall off towards 0 at the edges. To achieve this, I apply a circular gradient by subtracting the distance to the center from the height value at a certain pair of coordinates:

By cutting off values lower than a certain threshold (sealevel height) I get an image like this:

This is a bit too circular for my taste, so instead of just calculating the gradient based on the linear distance, I raise the distance to a constant power factor.

gradient = pow(distance, exp)

Voilà!

That’s it for now. I’ll return to the map generation process in a later post, talking about biomes, mesh generation and color sampling for the output texture.

The image at the top is a higher resolution image with added rivers and lakes and biomes rendered on a terrain mesh generated from this heightmap.