📜 ⬆️ ⬇️

Procedurally generated world maps on Unity C #, Part 4 (traffic)

image

This is the last article in a series on procedurally generated world maps using Unity and C #. Carefully, under the cut 7 MB images.

Content

Part 1 :
')
Introduction
Noise generation
Beginning of work
Generate elevation map

Part 2 :

Map folding on one axis
Map folding on both axes
Neighbor Search
Bit masks
Fill

Part 3 :

Heat map generation
Humidity Map Generation
River generation

Part 4 (this article):

Biome generation
Spherical map generation

Biome generation

Biomes are a way to classify terrestrial types. Our biome generator will be based on the popular Whittaker model, in which biomes are classified by rainfall and temperature. We have already generated a heat map and a moisture map for our world, so the definition of biomes will be quite simple. The Whittaker classification scheme is presented in the following illustration:

image

We can separate different types of biomes by a given temperature and humidity level. First, create a new enumeration in which these types of biomes will be stored:

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

Then you need to create a table that will help us determine the type of biome based on temperature and humidity. We already have HeatType and MoistureType. Each of these listings contains 6 specific types. The following table was created to compare each of these types with the Whittaker scheme:

image

To make it easier to find this data in the code, we will transform the table into a two-dimensional array. It will be like this:

 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 further simplify the search, add a new function that returns the type of biome of any tile. This part is quite simple, because each tile has already been assigned a type of heat and humidity.

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

This check is performed for each tile and sets the biome areas for the entire map.

 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, we've identified all the biomes. However, while we can not visualize them. The next step is to assign colors to each type. This will allow us to visualize the area of ​​each biome in the image. I chose the following colors:

image

The color values ​​are inserted into the TextureGenerator class along with the biome texture generation code:

 //  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; } //   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; } //   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; } //   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; } 

When rendering biome maps, it turns out beautiful folding maps of the world.

imageimage

Spherical map generation

Up to this point, we created worlds that collapse along the X and Y axes. Such maps are great for games, because the data is easily rendered into a game map.

If you try to design such collapsible textures onto a sphere, they will look strange. In order for our world to overlap with a sphere, it is necessary to write a generator of spherical textures. In this part we will add such a function for the worlds we have generated.

Spherical generation is slightly different from generating rolled maps, because it requires other noise schemes and texture mapping. For this reason, we divide the generator class into two branches of subclasses: WrappableWorldGenerator and SphericalWorldGenerator. Each of them will inherit the base class Generator.

This will allow us to have a common functional core, providing advanced features to each type of generator.

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

 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 functions Initialize () and GetData () that we have were created for collapsing worlds, so we need to write new ones for the spherical generator. We will also create new classes for obtaining tiles, because the folding will take place on the X axis with spherical projection.

Initialization of noise occurs in a manner similar to that previously described, with the exception of one major difference. The heat map in the new generator will not roll up on the Y axis. Therefore, we cannot create the correct gradient for multiplication. We will have to do it manually during data generation.

 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 will change significantly. We will return to the three-dimensional noise sampling. The noise will be sampled based on a coordinate system with latitude and longitude.

I looked at how spherical projection into libnoise was performed , and used the same concept. The main code that converts the latitude and longitude coordinates to Cartesian coordinates of a three-dimensional spherical map will be as follows:

 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 cycles through all coordinates using this conversion method to generate map data. With this method we create heat, altitude and humidity data. The biome map is generated in the same way as before - from the final heat map and the humidity map.

 protected override void GetData() { HeightData = new MapData (Width, Height); HeatData = new MapData (Width, Height); MoistureData = new MapData (Width, Height); //      / 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; //         / for (var x = 0; x < Width; x++) { curLon = westLonBound; for (var y = 0; y < Height; y++) { float x1 = 0, y1 = 0, z1 = 0; //      x, y, z LatLonToXYZ (curLat, curLon, ref x1, ref y1, ref z1); //   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; //      float coldness = Mathf.Abs (curLon) / 90f; float heat = 1 - Mathf.Abs (curLon) / 90f; HeatData.Data [x, y] += heat; HeatData.Data [x, y] -= coldness; //   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; //   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; } } 

We obtain, respectively, a height map, a heat map, a humidity map and a biome map:

image

Notice that the cards bend near the corners. This is done on purpose, this is how spherical projection works. Let's apply the biome texture to the sphere and see what happens:

image

Not a bad start. Please note, our elevation map has become black and white. We did this in order to use the height map as a shader sphere. For the best effect, we need a relief texture, so we will first render a black and white texture that reflects the displacements we need. This texture will then be converted into a relief texture 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; } 

By transferring the left texture to this function, we obtain the relief texture shown on the right:

image

Now, if we apply this relief map along with a height map through a standard shader to our sphere, we will get the following:

image

To further enhance the image, we will add a couple of cloud layers. Generating clouds using noise is very simple, so why not. We use a billow noise module to create clouds.

Add two layers of clouds to give them depth. The cloud noise generator code is shown below:

 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 use data in the same way. The cloud texture generator is a simple linear interpolator (lerp) from white to transparent white. We cut the clouds to the set value, making everything else transparent. The cloud texture generator code is as follows:

 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; } 

Let's create with it two different cloud textures. These textures are also created for spherical projection, therefore they have bends along the edges:

image

Now add two spherical meshes a bit larger than the original sphere. By applying cloud textures to the standard fade shader, we get beautiful looking cloudiness:

image

At the end, I provide a screenshot of all the generated textures used to create the final render of the planet:

image

This concludes the series of articles. The source code of the entire project on github: World Generator Final .

Source: https://habr.com/ru/post/276551/


All Articles