Contact Home

Procedurally Generating Wrapping World Maps in Unity C# – Part 4

Posted on: January 15th, 2016 by admin 19 Comments

Table of Contents

Check out Part 1, Part 2 and Part 3 of this series if you haven’t already. This is a continuation of those articles.


In the previous articles:

  1. Introduction
  2. Noise Generation
  3. Getting Started
  4. Generating the Height Map
  5. Wrapping the Map on One Axis
  6. Wrapping the Map on Both Axis
  7. Finding Neighbors
  8. Bitmasking
  9. Flood Filling
  10. Generating the Heat Map
  11. Generating the Moisture Map
  12. Generating Rivers

In this article (Part 4):

  1. Generating Biomes
  2. Generating Spherical Maps


Generating Biomes

Biomes are a way of classifying terrain types. Our biome generator will be based on the ever popular Whittaker’s model, where biomes are classified based on precipitation and temperature. Since we already generated a heat map and a moisture map for our world, determining our biomes will be pretty easy. Whittaker’s classification scheme is represented in the following diagram:

biomes1


We can identify different biome types based on a given temperature and moisture level. First, we can easily create a new enumeration that will store these biome types:

public enum BiomeType
{
	Desert,
	Savanna,
	TropicalRainforest,
	Grassland,
	Woodland,
	SeasonalForest,
	TemperateRainforest,
	BorealForest,
	Tundra,
	Ice
}

Then, we need to create a table that will tell us what biome type to use based on the temperature and humidity. We already have a HeatType, and a MoistureType. Each of these enumerations have 6 defined types. A table was created to match each of these types with Whittaker’s diagram, represented below:

biomestable


In order to easily look up this data in code, we can easily just recreate this table as a two-dimensional array. This looks like the following:

BiomeType[,] BiomeTable = new BiomeType[6,6] {   
	//COLDEST        //COLDER          //COLD                  //HOT                          //HOTTER                       //HOTTEST
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.Grassland,    BiomeType.Desert,              BiomeType.Desert,              BiomeType.Desert },              //DRYEST
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.Grassland,    BiomeType.Desert,              BiomeType.Desert,              BiomeType.Desert },              //DRYER
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.Woodland,     BiomeType.Woodland,            BiomeType.Savanna,             BiomeType.Savanna },             //DRY
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.Woodland,            BiomeType.Savanna,             BiomeType.Savanna },             //WET
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.SeasonalForest,      BiomeType.TropicalRainforest,  BiomeType.TropicalRainforest },  //WETTER
	{ BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.TemperateRainforest, BiomeType.TropicalRainforest,  BiomeType.TropicalRainforest }   //WETTEST
};

To make the lookup even easier, we will add a new function that will return the biome type of any tile. This part is quite simple, as each tile already has an associated heat and moisture type.

public BiomeType GetBiomeType(Tile tile)
{
    return BiomeTable [(int)tile.MoistureType, (int)tile.HeatType];
}

This check is done for every single tile, and assigns all of our map’s biome zones.

private void GenerateBiomeMap()
{
	for (var x = 0; x < Width; x++) {
		for (var y = 0; y < Height; y++) {
			
			if (!Tiles[x, y].Collidable) continue;
			
			Tile t = Tiles[x,y];
			t.BiomeType = GetBiomeType(t);
		}
	}
}

Great, so now all of the biomes are assigned. However, we have no way of seeing them yet. Our next step, is to assign a color for each Biome type. This will allow us to visually associate each biome region, so that we can represent them in an image. The colors I chose are as follows:

biomecolors


These color values are plugged into the TextureGenerator class, along with the Biome texture generation code:

	//biome map
	private static Color Ice = Color.white;
	private static Color Desert = new Color(238/255f, 218/255f, 130/255f, 1);
	private static Color Savanna = new Color(177/255f, 209/255f, 110/255f, 1);
	private static Color TropicalRainforest = new Color(66/255f, 123/255f, 25/255f, 1);
	private static Color Tundra = new Color(96/255f, 131/255f, 112/255f, 1);
	private static Color TemperateRainforest = new Color(29/255f, 73/255f, 40/255f, 1);
	private static Color Grassland = new Color(164/255f, 225/255f, 99/255f, 1);
	private static Color SeasonalForest = new Color(73/255f, 100/255f, 35/255f, 1);
	private static Color BorealForest = new Color(95/255f, 115/255f, 62/255f, 1);
	private static Color Woodland = new Color(139/255f, 175/255f, 90/255f, 1);


        public static Texture2D GetBiomeMapTexture(int width, int height, Tile[,] tiles, float coldest, float colder, float cold)
	{
		var texture = new Texture2D(width, height);
		var pixels = new Color[width * height];
		
		for (var x = 0; x < width; x++)
		{
			for (var y = 0; y < height; y++)
			{
				BiomeType value = tiles[x, y].BiomeType;
				
				switch(value){
				case BiomeType.Ice:
					pixels[x + y * width] = Ice;
					break;
				case BiomeType.BorealForest:
					pixels[x + y * width] = BorealForest;
					break;
				case BiomeType.Desert:
					pixels[x + y * width] = Desert;
					break;
				case BiomeType.Grassland:
					pixels[x + y * width] = Grassland;
					break;
				case BiomeType.SeasonalForest:
					pixels[x + y * width] = SeasonalForest;
					break;
				case BiomeType.Tundra:
					pixels[x + y * width] = Tundra;
					break;
				case BiomeType.Savanna:
					pixels[x + y * width] = Savanna;
					break;
				case BiomeType.TemperateRainforest:
					pixels[x + y * width] = TemperateRainforest;
					break;
				case BiomeType.TropicalRainforest:
					pixels[x + y * width] = TropicalRainforest;
					break;
				case BiomeType.Woodland:
					pixels[x + y * width] = Woodland;
					break;							
				}
				
				// Water tiles
				if (tiles[x,y].HeightType == HeightType.DeepWater) {
					pixels[x + y * width] = DeepColor;
				}
				else if (tiles[x,y].HeightType == HeightType.ShallowWater) {
					pixels[x + y * width] = ShallowColor;
				}

				// draw rivers
				if (tiles[x,y].HeightType == HeightType.River)
				{
					float heatValue = tiles[x,y].HeatValue;		

					if (tiles[x,y].HeatType == HeatType.Coldest)
						pixels[x + y * width] = Color.Lerp (IceWater, ColdWater, (heatValue) / (coldest));
					else if (tiles[x,y].HeatType == HeatType.Colder)
						pixels[x + y * width] = Color.Lerp (ColdWater, RiverWater, (heatValue - coldest) / (colder - coldest));
					else if (tiles[x,y].HeatType == HeatType.Cold)
						pixels[x + y * width] = Color.Lerp (RiverWater, ShallowColor, (heatValue - colder) / (cold - colder));
					else
						pixels[x + y * width] = ShallowColor;
				}


				// add a outline
				if (tiles[x,y].HeightType >= HeightType.Shore && tiles[x,y].HeightType != HeightType.River)
				{
					if (tiles[x,y].BiomeBitmask != 15)
						pixels[x + y * width] = Color.Lerp (pixels[x + y * width], Color.black, 0.35f);
				}
			}
		}
		
		texture.SetPixels(pixels);
		texture.wrapMode = TextureWrapMode.Clamp;
		texture.Apply();
		return texture;
	}


Rendering these biome maps, gives us these beautiful wrapping world maps.

biomemap1 biomemap2



Generating Spherical Maps

Up until this point, we have created worlds that wrap around the X and Y axis. These maps are great for games, as the data can easily be rendered as a game map.

If you wanted to project these wrappable textures onto a sphere, it would not look right. In order to make our world fit a sphere, we need to write a spherical texture generator. In this section, we will add this functionality to the worlds we have been generating.

The spherical generation is going to differ slightly from the wrappable generator, as it will require different noise patterns, and texture mapping. For this reason, we are going to branch off the generator class into two new sub classes, WrappableWorldGenerator and SphericalWorldGenerator, both will inherit from their base Generator class.

This will allow us to have shared core functionality, while providing custom extended features for each generator type.

The original Generator class will become abstract, as well as some of its functions:

    protected abstract void Initialize();
    protected abstract void GetData();

    protected abstract Tile GetTop(Tile tile);
    protected abstract Tile GetBottom(Tile tile);
    protected abstract Tile GetLeft(Tile tile);
    protected abstract Tile GetRight(Tile tile);

The Initialize() and GetData() functions that we currently have, are tailored for the Wrappable worlds, therefore, we are going to have to implement new ones for the Spherical generator. We are also going to have to create new Tile fetch classes, as we are only going to be wrapping on the x-axis with these spherical projections.

We initialize the noise similarly, however, with one main difference. The heat map in this particular generator is not going to be wrapping on the y-axis. Because of this, we cannot create a proper gradient that we can multiply. Instead, we will manually do this, while generating the data.

protected override void Initialize()
	{
		HeightMap = new ImplicitFractal (FractalType.MULTI, 
		                                 BasisType.SIMPLEX, 
		                                 InterpolationType.QUINTIC, 
		                                 TerrainOctaves, 
		                                 TerrainFrequency, 
		                                 Seed);		
		
		HeatMap = new ImplicitFractal(FractalType.MULTI, 
		                              BasisType.SIMPLEX, 
		                              InterpolationType.QUINTIC, 
		                              HeatOctaves, 
		                              HeatFrequency, 
		                              Seed);
		
		MoistureMap = new ImplicitFractal (FractalType.MULTI, 
		                                   BasisType.SIMPLEX, 
		                                   InterpolationType.QUINTIC, 
		                                   MoistureOctaves, 
		                                   MoistureFrequency, 
		                                   Seed);
	}

The GetData function is going to change dramatically. We are now going to go back to sampling 3D noise. The noise will be sampled with the help of a latitude and longitude coordinate system.

I looked at how libnoise did their spherical mapping, and applied the same concept here. The main code is the following, which converts the latitude and longitude coordinates, into 3D spherical cartesian map coordinates.

void LatLonToXYZ(float lat, float lon, ref float x, ref float y, ref float z)
{
	float r = Mathf.Cos (Mathf.Deg2Rad * lon);
	x = r * Mathf.Cos (Mathf.Deg2Rad * lat);
	y = Mathf.Sin (Mathf.Deg2Rad * lon);
	z = r * Mathf.Sin (Mathf.Deg2Rad * lat);
}

The GetData function will then loop through all coordinates, using this conversion method to generate the map data. We sample the heat, height and moisture data using this method. The biome map is generated the same way as before, from the resulting moisture and heat maps.

protected override void GetData()
{
	HeightData = new MapData (Width, Height);
	HeatData = new MapData (Width, Height);
	MoistureData = new MapData (Width, Height);

	// Define our map area in latitude/longitude
	float southLatBound = -180;
	float northLatBound = 180;
	float westLonBound = -90;
	float eastLonBound = 90; 
	
	float lonExtent = eastLonBound - westLonBound;
	float latExtent = northLatBound - southLatBound;
	
	float xDelta = lonExtent / (float)Width;
	float yDelta = latExtent / (float)Height;
	
	float curLon = westLonBound;
	float curLat = southLatBound;
	
	// Loop through each tile using its lat/long coordinates
	for (var x = 0; x < Width; x++) {
		
		curLon = westLonBound;
		
		for (var y = 0; y < Height; y++) {
			
			float x1 = 0, y1 = 0, z1 = 0;
			
			// Convert this lat/lon to x/y/z
			LatLonToXYZ (curLat, curLon, ref x1, ref y1, ref z1);

			// Heat data
			float sphereValue = (float)HeatMap.Get (x1, y1, z1);					
			if (sphereValue > HeatData.Max)
				HeatData.Max = sphereValue;
			if (sphereValue < HeatData.Min)
				HeatData.Min = sphereValue;				
			HeatData.Data [x, y] = sphereValue;
			
           // Adjust heat based on latitude
			float coldness = Mathf.Abs (curLon) / 90f;
			float heat = 1 - Mathf.Abs (curLon) / 90f;				
			HeatData.Data [x, y] += heat;
			HeatData.Data [x, y] -= coldness;
			
			// Height Data
			float heightValue = (float)HeightMap.Get (x1, y1, z1);
			if (heightValue > HeightData.Max)
				HeightData.Max = heightValue;
			if (heightValue < HeightData.Min)
				HeightData.Min = heightValue;				
			HeightData.Data [x, y] = heightValue;
			
			// Moisture Data
			float moistureValue = (float)MoistureMap.Get (x1, y1, z1);
			if (moistureValue > MoistureData.Max)
				MoistureData.Max = moistureValue;
			if (moistureValue < MoistureData.Min)
				MoistureData.Min = moistureValue;				
			MoistureData.Data [x, y] = moistureValue;

			curLon += xDelta;
		}			
		curLat += yDelta;
	}
}

Giving us our height map, heat map, moisture map, and biome map (respectively):

data


Notice that the maps curve near the corners. This is intentional, as it is how the spherical projection works. Let’s apply the biome texture onto a sphere and see how it looks:

globe


Not a bad start. Now, you have have noticed that our height map is now black and white. This was done on purpose, as we are going to be using it as the height map for our sphere’s shader. We are also going to need a bump map to provide some extra effect. In order to generate the bump map, we will first render a black and white texture that represents what we want our distortion to be. This texture will then be processed into the actual bump map with the following code:

    public static Texture2D CalculateBumpMap(Texture2D source, float strength)
    {
        Texture2D result;
        float xLeft, xRight;
        float yUp, yDown;
        float yDelta, xDelta;
        var pixels = new Color[source.width * source.height];
        strength = Mathf.Clamp(strength, 0.0F, 10.0F);        
        result = new Texture2D(source.width, source.height, TextureFormat.ARGB32, true);
        
        for (int by = 0; by < result.height; by++)
        {
            for (int bx = 0; bx < result.width; bx++)
            {
                xLeft = source.GetPixel(bx - 1, by).grayscale * strength;
                xRight = source.GetPixel(bx + 1, by).grayscale * strength;
                yUp = source.GetPixel(bx, by - 1).grayscale * strength;
                yDown = source.GetPixel(bx, by + 1).grayscale * strength;
                xDelta = ((xLeft - xRight) + 1) * 0.5f;
                yDelta = ((yUp - yDown) + 1) * 0.5f;

                pixels[bx + by * source.width] = new Color(xDelta, yDelta, 1.0f, yDelta);
            }
        }

        result.SetPixels(pixels);
        result.wrapMode = TextureWrapMode.Clamp;
        result.Apply();
        return result;
    }

Feeding this function the texture on the left, gives us our bump map, represented on the right:

bumpmap


Now, if we apply this bump map along with the height map, onto our sphere via the standard shader, we get the following:

globe2


For some extra effect, we are now going to add some cloud layers. We can generate clouds with noise very easily, so why not. We will use a billow noise module to represent our clouds.

We are going to add two cloud layers to give it some depth. The code for the cloud noise generator is:

        Cloud1Map = new ImplicitFractal(FractalType.BILLOW,
                                        BasisType.SIMPLEX,
                                        InterpolationType.QUINTIC,
                                        5,
                                        1.65f,
                                        Seed);

        Cloud2Map = new ImplicitFractal (FractalType.BILLOW, 
		                                BasisType.SIMPLEX, 
		                                InterpolationType.QUINTIC, 
		                                6, 
		                                1.75f, 
		                                Seed);

We grab the data the same way. The cloud texture generator is just a simple color lerp from white to transparent white. We cut off the clouds at a set value, making everything else transparent. The code for the cloud texture generation is:

public static Texture2D GetCloudTexture(int width, int height, Tile[,] tiles, float cutoff)
{
	var texture = new Texture2D(width, height);
	var pixels = new Color[width * height];
		
	for (var x = 0; x < width; x++)
	{
		for (var y = 0; y < height; y++)
		{                        
			if (tiles[x,y].CloudValue > cutoff)
				pixels[x + y * width] = Color.Lerp(new Color(1f, 1f, 1f, 0), Color.white, tiles[x,y].CloudValue);
			else
				pixels[x + y * width] = new Color(0,0,0,0);
		}
	}
		
	texture.SetPixels(pixels);
	texture.wrapMode = TextureWrapMode.Clamp;
	texture.Apply();
	return texture;
}

With this, we can generate two different cloud textures. Again, these textures were sampled to be spherical, which will warp near the corners:

clouds


Next, two new sphere meshes were added, that are slightly larger than the original sphere. Applying the cloud textures, to the standard shader with a fade effect, gives us some decent looking cloud coverage:

globe3


Finally, here is a screenshot of all the textures that were generated, and used to create the final rendering of the planet:

mapdata


That wraps it up for this tutorial series. You can get the full source code for this project on github.

19 Responses

Ross commented on January 31, 2016 at 4:53 am

Fantastic tutorial, Jon. Thanks so much for sharing this.

I’d love to see another tutorial series on how to use a generated world in a first person game, such as Space Engineers.

Anders commented on February 24, 2016 at 1:00 pm

Hi and thanks for a great tutorial! I see in the source that there is a heighttype called shore thats used when modifying the moisturemap but no tile is ever set to that heighttype, am I missing something or is it just an artifact from a feature that never was?

    admin commented on February 24, 2016 at 3:12 pm

    The Shore height type was a extra sub-division of the height map that I originally had and removed at one point. So yeah, it is just a remaining unused Type. You could, however, include it if you wanted.

Joonatan commented on March 26, 2016 at 5:36 pm

Very well explained. Joy to read. Might use the ideas discussed here on a game hackaton in the future 🙂

Thraka commented on April 3, 2016 at 10:42 pm

This is amazing! I just converted it to work with MonoGame and my Ascii game engine. Thanks so much!

It would be awesome to know how to alter the parameters to create different types of worlds. Kind of like how you can choose “Island” or “Big Continent” types of worlds in games.

Stephen commented on April 11, 2016 at 11:24 am

Thanks for this series.

Stefano commented on July 29, 2016 at 11:27 am

This is amazing tutorial!

Rick commented on September 24, 2016 at 11:25 pm

I came across this while looking for generation tools in Unity and I have to say, this tutorial was pretty cool! I’m currently trying to incorporate this into my terrain engine I wrote (it uses RAW data inputs), but I can’t find a good way to either output the tiles from your scripts or how to access the tiles straight from the instance.

If I can get this working I was hoping I could credit this script in my upcoming game. This was a great place to get a good start for climate models!

altair21 commented on December 18, 2016 at 6:56 am

Is a procedural planet, I can do play with a fps character, fly in the space into the planet?

Gerardo commented on September 4, 2017 at 10:15 pm

so this is like the world map on rimworlds.. ? we need then to make generate a new noise based map depending on the tile selected. right?

Peter commented on November 21, 2017 at 8:26 pm

Very well explained tutorial! I’ve been experimenting a bit with other methods of making spherical noise which might be useful for your terrain/world generation:

https://github.com/pec27/smerfs

Jonathan commented on June 22, 2018 at 12:15 pm

The base where you “print” all the height, biome data and the like.

It is a simple big tile[with, height] or multiple 1×1 square tiles which forms a plain?

    admin commented on June 22, 2018 at 12:19 pm

    It is just generating a png file in this case. Take a look at the TextureGenerator. It just takes the data, and generates a PNG representation of it.

Laki commented on October 26, 2018 at 3:58 am

The height of the world is not cyclical.

    admin commented on November 7, 2018 at 11:13 am

    Seems that there is some distortions happening when the world size is not the same width and height in current versions of Unity. I will need to investigate this issue.

Simon commented on December 1, 2018 at 5:37 am

It only works for width = 2 * height atm.

It’s due to the bugfix somebody posted on github that you merged, if you revert it it should work with W == H

Vadim Krasnobelmov commented on January 12, 2019 at 1:35 pm

Yes. In this pull-request https://github.com/jongallant/WorldGeneratorFinal/commit/df2dbfa3d53331f04da410533f14062b87fe292a have error:
float xDelta = latExtent / (float)Width;
float yDelta = lonExtent / (float)Height;
but lattitued specifies the north–south position, i..e Y and longitude specifies the west–east position, i.e. X, but in this bugfix polar coordinates are inverted.

Respond

Leave a Reply to Thraka

You must be logged in to post a comment.