Search This Blog

Procedural 2D Terrain - Part 4: Making Some Noise

So far I've not covered precise noise generation techniques I used for the terrain, mostly because I've not really settled on any one 'perfect' technique. I have finally begun to refine it, however, particularly as I'm now adding detail textures to the generation so I thought I'd share what I have so far.

A Quick Recap
If you've not seen how I'm currently rendering the terrain, it's probably worth reading part 1 of this series first, as I'll be referring to some of the rendering methods used. I'm currently using a noise generation library named FastNoiseSIMD which is highly optimised for x86/x64 processors, as well as being wonderfullly lightweight, requiring only a few source files to be added to the project. My old 2.1Ghz Core2Duo chomps through chunk generation with no problems at all as a testiment to the quality of the library.
Noise generation for each chunk is performed in a series of steps using a variety noise patterns. I'll cover the settings of each as they currently are at the time of writing - although the source code may well have changed significantly by the time anyone reads this. I shall also point out that there is currently no generation of rivers or roads (although I plan to work on at least roads at some point), just everlasting terrain interspersed by large bodies of water. If you're on Windows and using FastNoise there is a binary in the releases section of the github repository, which allows previewing the different noise types. This has been incredibly useful while testing different noise parameters for quick visual feedback. While the FastNoise library itself is cross platform (at least, I have it working in xubuntu) the preview executable appears to be Windows only unfortunately, due to its dependence on .net. Perhaps if you're sadistic enough you might get it running with Mono...

The Data Structure
The underlying data array of each chunk has been modified since I wrote part 1. In that part I stated I use a 64x64 array of std::uint16_t, each value holding a bitmask of parameters which are sent to the shader. I've now increased the data size to std::uint32_t, so that more information my be included per value (each value represents a tile within the chunk). I could probably change the texture type used to store the array to RGBA 8-bit so that each channel is accessable in the shader without having to use bitwise operations to extract the data GPU side, and I may still do that, but for now it remains a single unsigned red channel. The format of each value looks like this (where F represents 4 used bits):

0x000000FF - Tile ID. This is used by the shader to look up the index of a tile.

0x00000F00 - BiomeID. There are 9 biomes, which fits nicely in 4 bits

0x0000F000 - Depth value. This isn't the same as the height value, rather it is used by the shader to darken tiles to give some illusion of 'depth'. This is most noticeable in the water.

0x00FF0000 - ID of a detail tile. Details are drawn on top of existing tiles, and exist within each biome tile set, so that each biome has a unique set of details

The top byte is not currently used, but will probably be involved in rendering the road systems.

Creating Tile Indices
To calculate the tile ID I use two maps. The first is a simplex fractal set, using the default FastNoise settings. The value is normalised then multiplied by 4 to provide 4 variations of tiles, where 0 is water and 1-3 are land varieties. This creates a 'lumpy' lanscape with pools of water and patches of hills. Then I set the the frequency of the noise generator to a much lower 0.003 to create a simplex set (note no fractals). This is used to create large open areas of water, such as seas and oceans by adding it to the first map before it has been normalised. The low frequency means that large areas of 0 value IDs are created during normalisation, which are interpreted as large bodies of water. It also produces nice looking coast lines, and islands in shallower areas.

Potential real estate for a harbour?

These two maps are actually produced one tile larger in width and height than the chunk size - 65x65 values, starting at the position of the current chunk. This is so that once the values are normalised and converted to the 0-3 range they can be passed through the marching squares algorithm I talked about in the last post. Each tile needs to consider its neighbour, which is why the noise sets have the extra values. Once the final tile ID has been calculated it is OR'd into the bottom byte of the chunk's data array.

Biome Generation
To calculate the biome ID I use three noise maps:

Rainfall, which is a simplex fractal set with 3 octaves and a frequency of 0.005

Average temperature - a gradient fractal set with a RidgedMulti fractal type

Height data, created from a value fractal set with 2 octaves and a frequency of 0.002

I also create a 'latitudinal variance' which is a linear gradient in the Y direction which decreases with distance. This is used to decrease the average temperature the further north or south the chunk appears, and also slightly increase the rainfall value. The height map is also applied to the rainfall and temperature maps, decreasing temperature linearly with height, and reducing the average rainfall to near zero after a certain limit. The rainfall and temperture values are then used as x/y coordinates in the biome lookup table, detailed in the last post, to find the biome ID. This is then shifted and OR'd into the lower half of the second byte of the chunk data array.

Biomes by the sea...

Depth data is also calculated at this point, by taking the terrain data map, normalising it, and multiplying it by 15, to fit it in to the top half of the second byte.

Adding Detail
Finally the values of detail tiles are calculated, although not with a noise map. Details, in this case, are tiles which may contain non-interactive vegetation, stones/rocks or items floating in the water, and are scattered as evenly as possible. To get a good distrubution I use a poisson disc function (based on this article, and added to xygine) which scatters the locations nicely. The side effect of not being noise based is that the result of the function is non-deterministic and varies every time it is run as it is not seed based - but for non-interactive details such as these I don't regard it as a real problem, particularly as chunks are written to disk after generation anyway, rendering the positions fixed. Details are based on the depth value of the current tile, of which the deepest value over open water is ignored. The remaining values are divided by 4 to determine which of the four tile types the current tile is a member of, before then being randomised between one of the 3 variations of detail for that type. These are stored in order in the biome tile set texture starting at index 90: 0A, 0B, 0C, 1A, 1B, 1C... and so on, where the number is the tile type, and letter the detail variation.

That pretty much sums up the noise generation as it stands - there is still a lot to do including laying out roads and adding cities and other interesting places for the player to visit and explore, but I am pleased with the progress so far.