/factory-td

Worley cell based procedural terrain

Primary LanguageC#

Worley cell based procedural terrain

Design

Procedural terrain with gameplay before visual appearance

Procedurally generate deterministic terrain, with an emphasis on limited/controlled traversal to encourage interesting logistical/tactical situations.

Terrain is broken up by un-pathable cliffs separating different heights. Slopes provide limited/choked access between areas, like an RTS map.

Terrain generation uses Worley (Cellular) noise. All noise generation code is based on (sometimes edited) parts of FastNoise.cs.

Worley Cells

Cellular noise (below) is generated using an even grid of points, where a pixel's cell is the closest point in the grid. Each cell has a unique grid index (int2) and unique value noise (float between 0 and 1). Below:

  • Pixels are coloured using their cell value noise where 0=black and 1=white
  • Cell shapes change between the left and right images, as the grid of points are scattered randomly.

Cellular noise with increasing scatter from left to right, coloured by cell value.

The images above were generated using FastNoise Preview. A more in-depth explanation of Worley/Cellular noise implementation can be found here.


Worley Terrain

Cellular noise, like Perlin or Simplex, is deterministic but can be randomised using a seed. It is possible to generate the following (amongst other) information for any point in world space:

  • Cell containing the point: Index, value noise

  • Closest adjacent cell to point: Index, value noise

  • Distance from the point to edge of the cell, in the direction of the closest adjacent cell (distance-to-edge)

Index is an int2 and value noise is a float between 0 and 1. Both are unique to each individual cell.

To determine cell height the cell index x and y values are used as the input for a 2D Simplex noise function. The Simplex output chooses the cell height. This causes more natural, gradual height transitions between cells due to the wave shape generated by simplex noise.

Terrain cells with different heights and connecting slopes

Scatter can be addedto make the terrain appear more natural. The grid will no longer be distinguishable and some cells may be lost, but each cell still has the same unique int2 index and value noise.

Animation showing the effect of increased scatter on terrain cells.


Slopes

Cell value noise is used to decide if a slope exists between two neighbouring cells. Using the value of two cells, a third deterministic value can be created and used to choose which adjacent cell is connected. Slopes traverse both cells, with each cell owning half of the slope.

float cellPairValue = (cellValue * adjacentValue);
int2 slopedEdge = GetSlopeConnectionBasedOnValue(cellPairValue);

The connection is an int2 describing the direction of the connected adjacent cell. For the cell with the lowest value of the two, this int2 is 'flipped' to the direction of the opposite adjacent cell.

//  Slope edge is currently pointing right - int2(1, 0)
if(cellValue < adjacentValue)   //  This will always return true for the same one of any two cells
    slopeEdge = flipDirection(slopeEdge);
    //  Now slopeEdge points left - int2(-1, 0)

This results in two cells with half a slope on 'opposite' sides (the side at which each cell meets the other) and so connected. Each cell can be generated independently of any adjacent cells and slopes will always connect properly.

Slopes generated using Cellular distance-to-edge noise.

The distance-to-edge value will equal ~0 near the edge of the cell and ~1 near the center. It can be used with linear interpolation to 'smooth' any value between two cells. In this case, interpolating between a cell's height and it's neighbour's height creates a slope. (The interpolation is actually to the half way point between the cells, as each cell generates half the slope and meets the other in the middle.)

The distance-to-edge value is used to create an interpolator which is interpolates between the cell height and half way to the adjacent cell's height.

float slopeLength = 0.5f;
float interpolator = math.unlerp(0, slopeLength, point.distanceToEdge);

float halfWayHeight = (point.cellHeight + point.adjacentHeight) / 2;
float cellHeight = point.cellHeight;

float terrainHeight = math.lerp(halfWayHeight, cellHeight, interpolator);

Distance to edge noise visualised using FastNoise Preview.


Video

https://streamable.com/rkrb3