In this step we will create the ground textures, also called „splat maps“.
Unity stores the information about ground textures in a three-dimensional float array. The first two dimensions are the coordinates (x, y), the third dimension is the index of the texture that is to be used at that position. The index refers to the textures that are defined as the ground textures to be used by the terrain object:
The first (grass) texture has index 0, the dirt texture has index 1 and so on. The array can be retrieved by calling GetAlphamaps() on the TerrainData object. As in the previous step we will create a class to hold the information about where to put which texture. We will define this by terrain height and steepness.
1 | Add the four textures from the standard terrain asset to the textures that Unity uses for the terrain. Add the Cliff texture twice – we will use it for steep terrain areas.
The textures should be in the following order: GoodDirt – Grass (Hill) – Grass&Rock – Cliff (Layered Rock) – Cliff (Layered Rock) |
||||||||||||||||||||||||||||||
2 | Create a new class and name it BiomDescription. Make it serializable by adding the Serializable attribute, so it can be used in the inspector. Add four float fields so we can define a height range and a steepness range:
using System; [Serializable] public class BiomDescription { public float HeightFrom = 0; public float HeightTo = 0; public float SteepnessFrom = 0; public float SteepnessTo = 0; } Assigning 0 to the variables enables us to find out which of those variables have been set. As long as they have that initial value we don’t care about them. Note: You could create more fields like Temperature, Humidity etc. or even some random (or Perlin noise) value, that define your biomes. You would of course have to provide the corresponding values for the map positions then. |
||||||||||||||||||||||||||||||
3 | Add a function named Fits() to the class that returns true when the parameters (height and steepness) fall into the range of the member fields:
using System; [Serializable] public class BiomDescription { // ... public bool Fits(float height, float steepness) { bool fits = true; if (HeightFrom != 0 || HeightTo != 0) { if (fits) { if (height >= HeightFrom && height <= HeightTo) { fits = true; } else fits = false; } } if (SteepnessFrom != 0 || SteepnessTo != 0) { if (fits) { if (steepness >= SteepnessFrom && steepness <= SteepnessTo) { fits = true; } else fits = false; } } if (fits) { return true; } return false; } }
|
||||||||||||||||||||||||||||||
4 | Define a public array of BiomDescription objects in the TerrainGenerator class:
public class TerrainGenerator : MonoBehaviour { public BiomDescription[] BiomDescriptions; public LayerDefinition[] LayerDefinitions; public float Level1Div = 1, Level2Div = 4; public float Level1Step = 200, Level2Step = 50; // ... } After this new array has been added you will see a new entry in the inspector of the TerrainGenerator script. It is the array that you have added and you can edit it in the inspector now: Add 5 new values to the array and set the values of the array items to the following values:
The first four entries describe the main textures to be used for the different heights; the steepness values are ignored. The fifth value is used to add the fifth texture when the steepness lies in the range from 45 to 90. Note: The descriptions can overlap and that means that ground textures can be mixed. For example: if you change the HeightTo value from the first array entry from 0.10 to 0.102 you will notice that the riverside areas are mixed with the grass around the lakes. This makes the terrain look a little more realistic. |
||||||||||||||||||||||||||||||
4 | Define a new function in the TerrainGenerator class and name it GenerateBiomMap. Call that function in Start() right after the height map has been set:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void GenerateBiomMap() { } private void Start() { _terrain = GetComponent<Terrain>(); _terrainData = _terrain.terrainData; _heights = new float[_terrainData.heightmapWidth, _terrainData.heightmapHeight]; GeneratePerlinTerrain(); PostProcessTerrain(); NormalizeHeightMap(); _terrainData.SetHeights(0, 0, _heights); GenerateBiomMap(); } } |
||||||||||||||||||||||||||||||
5 | Define another function in the TerrainGenerator class and name it GenerateAlphaMaps. Call that function in GenerateBiomMap():
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void GenerateBiomMap() { GenerateAlphaMaps(); } private void GenerateAlphaMaps() { } private void Start() { _terrain = GetComponent<Terrain>(); _terrainData = _terrain.terrainData; _heights = new float[_terrainData.heightmapWidth, _terrainData.heightmapHeight]; GeneratePerlinTerrain(); PostProcessTerrain(); NormalizeHeightMap(); _terrainData.SetHeights(0, 0, _heights); GenerateBiomMap(); } } |
||||||||||||||||||||||||||||||
6 | Define another function in the TerrainGenerator class and name it ClearAlphaMaps. This function is necessary because the alpha maps must be cleared from previous data before entering new data.:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void ClearAlphaMaps(float[, ,] map) { for (int x = 0; x < map.GetLength(0); ++x) { for (int y = 0; y < map.GetLength(1); ++y) { for (int z = 0; z < map.GetLength(2); ++z) { map[x, y, z] = 0; } } } } } |
||||||||||||||||||||||||||||||
7 | In the GenerateAlphaMaps() function read the alpha maps from the Unity terrain and call ClearAlphaMaps() to clear the map:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void GenerateAlphaMaps() { float[, ,] map = _terrainData.GetAlphamaps(0, 0, _terrainData.alphamapWidth, _terrainData.alphamapHeight); ClearAlphaMaps(map); } } |
||||||||||||||||||||||||||||||
8 | In the GenerateAlphaMaps() function now go through all map positions and check which of the biome descriptions fit. If a biome descriptions fits, write a 1 in the corresponding alpha map (which means that we want full texture at that position):
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void GenerateAlphaMaps() { // read alpha maps from Unity terrain float[, ,] map = _terrainData.GetAlphamaps(0, 0, _terrainData.alphamapWidth, _terrainData.alphamapHeight); // clear the maps ClearAlphaMaps(map); // store with and height of the alpha maps int alphaMapWidth = _terrainData.alphamapWidth; int alphaMapHeight = _terrainData.alphamapHeight; // for all positions in the alpha maps for (int y = 0; y < alphaMapHeight; ++y) { for (int x = 0; x < alphaMapWidth; ++x) { // calculate normalized position - that is a value between 0 and 1 float normX = (float)x / _terrainData.heightmapWidth; float normY = (float)y / _terrainData.heightmapHeight; // get steepness from Unity terrain at the current position float steepness = _terrainData.GetSteepness(normY, normX); // get height from Unity terrain at the current position. // NOTE: The alpha maps are rotated by 90 degrees compared to the // height map, so the coordinates for the call to GetHeight() // have to be swapped! // The height returned from GetHeight() represents the absolute // height, but since we need a value between 0 and 1 we divide it // by the total height of the terrain. float height = _terrainData.GetHeight(y, x) / _terrainData.heightmapScale.y; // Check for each biom description foreach (BiomDescription biom in BiomDescriptions) { // does it fit? if (biom.Fits(height, steepness)) { // Write a 1 into the alpha map using the GroundTextIndex // from the biom description map[x, y, biom.GroundTexIndex] = 1; } } } } } } |
||||||||||||||||||||||||||||||
9 | In an alpha map all values for a given map position have to add up to 1, otherwise you’ll discover overly bright areas where textures are mixed. Since we write a 1 in every alpha map that fits the biome description, it is quite likely that the sum of all values at one position exceeds a value of 1. Therefore we need to normalize the alpha maps.
Create a new function named NormalizeAlphaMaps() in the TerrainGenerator class: using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void NormalizeAlphaMaps(float[, ,] map) { // get number of positions vertically int yMax = map.GetLength(0); // get number of positions horizontally int xMax = map.GetLength(1); // get number of alpha maps per position int numOfMaps = map.GetLength(2); // go through all positions for (int y = 0; y < yMax; ++y) { for (int x = 0; x < xMax; ++x) { float sum = 0; // sum together all alpha map values for (int i = 0; i < numOfMaps; i++) { sum += map[x, y, i]; } // is the sum different from 1? if (sum != 1) { // go through all alpha maps again... for (int i = 0; i < numOfMaps; i++) { // ...and divide // every entry by the sum. // This makes the sum 1... map[x, y, i] /= sum; } } } } } } |
||||||||||||||||||||||||||||||
10 | Finally call NormalizeAlphaMaps() at the end of GenerateAlphaMaps() and write the maps back to Unity:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TerrainGenerator : MonoBehaviour { // ... private void GenerateAlphaMaps() { // read alpha maps from Unity terrain float[, ,] map = _terrainData.GetAlphamaps(0, 0, _terrainData.alphamapWidth, _terrainData.alphamapHeight); // clear the maps ClearAlphaMaps(map); // store with and height of the alpha maps int alphaMapWidth = _terrainData.alphamapWidth; int alphaMapHeight = _terrainData.alphamapHeight; // for all positions in the alpha maps for (int y = 0; y < alphaMapHeight; ++y) { for (int x = 0; x < alphaMapWidth; ++x) { // calculate normalized position - that is a value between 0 and 1 float normX = (float)x / _terrainData.heightmapWidth; float normY = (float)y / _terrainData.heightmapHeight; // get steepness from Unity terrain at the current position float steepness = _terrainData.GetSteepness(normY, normX); // get height from Unity terrain at the current position. // NOTE: The alpha maps are rotated by 90 degrees compared to the // height map, so the coordinates for the call to GetHeight() // have to be swapped! // The height returned from GetHeight() represents the absolute // height, but since we need a value between 0 and 1 we divide it // by the total height of the terrain. float height = _terrainData.GetHeight(y, x) / _terrainData.heightmapScale.y; // Check for each biom description foreach (BiomDescription biom in BiomDescriptions) { // does it fit? if (biom.Fits(height, steepness)) { // Write a 1 into the alpha map using the GroundTextIndex // from the biom description map[x, y, biom.GroundTexIndex] = 1; } } } } // normalize alpha maps NormalizeAlphaMaps(map); // hand alpha maps back to Unity _terrainData.SetAlphamaps(0, 0, map); } } |