### Isometric maps generation with Perlin Noise. Part 2

Tulenber 22 May, 2020 ⸱ Intermediate ⸱ 16 min ⸱ 2019.3.14f1 ⸱

That is our second article about isometric maps based on Perlin Noise in the cycle about procedural generation.

The procedural generation of space implies the usage of mechanisms that create its elements independently of each other. In the future, to describe these elements, we will use the term - sector. Last time, we used the previously prepared Perlin Noise generation algorithm to simulate the earth’s surface and convert it into an isometric tilemap. The next steps will be the division of the map into sectors, their separate generation, and subsequent merging into one united object.

### Goal

Generate data for the sector with Noise Perlin and combine several sectors into a united map covering the camera view.

### Isometric view

Previously we use standard Unity mechanisms for isometric maps. Working with coordinates did not go beyond the usual positioning of tiles one after another and did not require a deep understanding of the process. However, our further actions will need a basic knowledge of isometric space, so let’s take a little look at the theory. As always, I’ll try not to go too deep and consider only the important for the current article elements. For more information, you can always turn to the Internet, as an option, start with Wikipedia.

Isometric projection is a method for visually representing three-dimensional objects in two dimensions with different distortion coefficients along the coordinate axes. Last time, we chose the isometric map where positioning uses the `X` and `Y` axes, and the height set with the `Z-axis`. Accordingly, we would like to note our transition to three-dimensional space and define terms for determining the direction. On the `X-axis`, we will count the length; on the `Y-axis` - the width and along the `Z-axis` - the height. Height is currently not a significant factor for us, so we will omit its presence and focus on the `X` and `Y` axes.

The simplest way to representing space is to display it on a plane, for example, the coordinates of tiles in two-dimensional space:

By default, the coordinate axes in Unity isometric tilemaps directed upward (relative to the display on the screen), respectively, this is the isometric projection of our space:

### Sector

Sector - is a group of tiles with the same length and width (the height, as we agreed earlier, is omitted). I note right away that sectors behave in space in the same way as individual tiles so that we can ignore what objects we are currently dealing with. Usually, we will operate with sectors.

The sector in two-dimensional space:

The sector in isometric space:

### Display isometric space on the screen

I think it is clear that changing the direction of the coordinate axes changes the application of length and width to determine the number of rows and columns needed to fill the entire screen. Direct usage of length and width will either leave the corners of the screen unfilled or will place many of the sectors beyond the boundaries of the visible region.

The most efficient sector allocation looks like that:

The number of columns can be calculated using the formula `int sectorCountX = Mathf.CeilToInt (cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;` additional sector closes the extreme values.
Accordingly, the formula for the number of rows is `int sectorCountY = 2 * (Mathf.CeilToInt (cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);` where an additional sector also closes the extreme values, and a doubled size is taken due to the shift of the lines relative to each other only half the height.

This formula will not give us perfect placement and will contain an equal number of sectors in each row. Still, now we will step aside resource optimization to reduce code complexity.

### Screen filling

The cherry on the cake will be an algorithm for this “effective” sector allocation.

To offset along the rows, we use a sequential increase of the `X` and `Y` coordinates. For offset along with the columns, according to the isometric coordinate grid, it is necessary to increase `X` and to decrease `Y` by one.

### Implementation

• Calculation of bias for generating Perlin Noise
• Calculation of the number of sectors needed to fill the screen
• Calculation of the position of sectors in space
• Sector placement for screen-filling
##### Noise generator refactoring

The noise generator settings move to a separate structure; this will simplify their transfer between different objects.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 `````` ``````using System; using UnityEngine; [Serializable] public struct GeneratorSettings { public float scale; public int octaves; public float persistence; public float lacunarity; public int seed; public Vector2 offset; } ``````

Accordingly, we add an interface to work with settings from such a structure to the noise generator.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 `````` ``````using UnityEngine; public static class NoiseMapGenerator { public static float[] GenerateNoiseMap(GeneratorSettings settings, Vector2 offset) { // Offset from settings Vector2 generationOffset = offset + settings.offset; // The sizes of the generated zone are equal to the size of the sector by default return GenerateNoiseMap(TileMapSector.SectorSizeInTiles, TileMapSector.SectorSizeInTiles, settings.seed, settings.scale, settings.octaves, settings.persistence, settings.lacunarity, generationOffset); } public static float[] GenerateNoiseMap(int width, int height, int seed, float scale, int octaves, float persistence, float lacunarity, Vector2 offset) { // An array of vertex data, a one-dimensional view will help get rid of unnecessary cycles later float[] noiseMap = new float[width*height]; // Seed element System.Random rand = new System.Random(seed); // Octave shift to get a more interesting picture with overlapping Vector2[] octavesOffset = new Vector2[octaves]; for (int i = 0; i < octaves; i++) { // Also use external position shift float xOffset = rand.Next(-100000, 100000) + offset.x; float yOffset = rand.Next(-100000, 100000) + offset.y; octavesOffset[i] = new Vector2(xOffset / width, yOffset / height); } if (scale < 0) { scale = 0.0001f; } // For a more visually pleasant zoom shift our view to the center float halfWidth = width / 2f; float halfHeight = height / 2f; // Generate points for a heightmap for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Set the values for the first octave float amplitude = 1; float frequency = 1; float noiseHeight = 0; float superpositionCompensation = 0; // Octave Overlay Processing for (int i = 0; i < octaves; i++) { // Calculate the coordinates to get from Noise Perlin float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency; float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency; // Obtaining Altitude from the PRNG float generatedValue = Mathf.PerlinNoise(xResult, yResult); // Octave overlay noiseHeight += generatedValue * amplitude; // Compensate octave overlay to stay in a range [0,1] noiseHeight -= superpositionCompensation; // Calculation of amplitude, frequency and superposition compensation for the next octave amplitude *= persistence; frequency *= lacunarity; superpositionCompensation = amplitude / 2; } // Save heightmap point // Due to the superposition of octaves, there is a chance of going out of the range [0,1] noiseMap[y * width + x] = Mathf.Clamp01(noiseHeight); } } return noiseMap; } } ``````

##### Refactoring of the test displaying of the generated space

Test rendering is separated from the main class responsible for the generation of isometric maps and repeats the algorithm for generating isometric structures, but for a two-dimensional texture.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 `````` ``````using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class NoiseMapRenderer : MonoBehaviour { // Structure with the dependence of the pixel color to the height of the map [Serializable] public struct TerrainLevel { public float height; public Color color; } [SerializeField] public List terrainLevel = new List(); // Function for generating a test texture with the parameters specified for the noise generator // Used from the editor extension public void GenerateMap() { // Translation of tile settings into the structure of the dependence of pixel color on height TileMapHandler handler = FindObjectOfType(); List tileList = handler.tileList; terrainLevel = new List(); float heightOffset = 1.0f / tileList.Count; for (int i = 0; i < tileList.Count; i++) { Color color = tileList[i].sprite.texture.GetPixel(tileList[i].sprite.texture.width / 2, tileList[i].sprite.texture.height / 2); TerrainLevel lev = new TerrainLevel(); lev.color = color; lev.height = Mathf.InverseLerp(0, tileList.Count, i) + heightOffset; terrainLevel.Add(lev); } // Render a test display of space for our camera Vector2Int sectorSize = handler.GetScreenSizeInSectors(); RenderMap(sectorSize.x, sectorSize.y, handler.generatorSettings); } // Create and draw a noise-based texture public void RenderMap(int sectorLength, int sectorWidth, GeneratorSettings settings) { // Delete previously generated textures for editor mode for (int i = transform.childCount; i > 0; --i) DestroyImmediate(transform.GetChild(0).gameObject); // Initial parameters for sectors traversing int startX = -sectorLength; int startY = 0; int currentSectorX = startX; int currentSectorY = startY; bool increaseX = true; // We shift the index by one to optimize the use of the loop with the remainder of the division int lastSectorIndex = sectorLength * sectorWidth + 1; for (int sectorIndex = 1; sectorIndex < lastSectorIndex; sectorIndex++) { // Generate the current sector GenerateSector(new Vector2Int(currentSectorX, currentSectorY), settings); // Move to the next column currentSectorX++; currentSectorY--; // Go to the next line if (sectorIndex % sectorLength == 0) { // Go to the next line, alternating between increasing X and Y if (increaseX) { currentSectorX = ++startX; currentSectorY = startY; increaseX = false; } else { currentSectorX = startX; currentSectorY = ++startY; increaseX = true; } } } } private void GenerateSector(Vector2Int sectorCoordinates, GeneratorSettings settings) { // Calculate offset of the sector for the noise generator int sectorSize = TileMapSector.SectorSizeInTiles; float sectorOffsetX = sectorSize * sectorCoordinates.x * sectorSize / settings.scale; float sectorOffsetY = sectorSize * sectorCoordinates.y * sectorSize / settings.scale; Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY); // Noise generation float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(settings, sectorOffset); // Generate and position the sector texture GameObject sectorSprite = new GameObject(); SpriteRenderer spriteRenderer = sectorSprite.AddComponent(); Texture2D texture = new Texture2D(sectorSize, sectorSize) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Point }; texture.SetPixels(GenerateColorMap(noiseMap)); texture.Apply(); spriteRenderer.sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100.0f); sectorSprite.transform.SetParent(transform, false); sectorSprite.name = "" + sectorCoordinates.x + "_" + sectorCoordinates.y; float positionOffsetMultiplier = sectorSize * transform.localScale.x / spriteRenderer.sprite.pixelsPerUnit; sectorSprite.transform.Translate(new Vector3(sectorCoordinates.x * positionOffsetMultiplier, sectorCoordinates.y * positionOffsetMultiplier), Space.Self); } // Convert noise to color to render the texture private Color[] GenerateColorMap(float[] noiseMap) { Color[] colorMap = new Color[noiseMap.Length]; for (int i = 0; i < noiseMap.Length; i++) { // Basic is the color with the highest level colorMap[i] = terrainLevel[terrainLevel.Count-1].color; foreach (var level in terrainLevel) { // Choose a color depending on the noise level if (noiseMap[i] < level.height) { colorMap[i] = level.color; break; } } } return colorMap; } } ``````

##### Sector

Responsible for the location of tiles on the map.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 `````` ``````using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public static class TileMapSector { public static int SectorSizeInTiles = 10; public static void GenerateSector(float[] noiseMap, List tileList, Tilemap tileMap) { // Sizes of the sector int length = SectorSizeInTiles; int width = SectorSizeInTiles; // Clear previous values tileMap.ClearAllTiles(); // Bypass all sector tiles for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { // Noise level for the current tile float noiseHeight = noiseMap[length*y + x]; // The levels of the generated surface are evenly distributed on the noise scale // "Stretch" the noise scale to the size of the array of tiles float colorHeight = noiseHeight * tileList.Count; // Select a tile below the resulting value int colorIndex = Mathf.FloorToInt(colorHeight); // We consider addressing in arrays for maximum noise values if (colorIndex == tileList.Count) { colorIndex = tileList.Count-1; } // Tile assets allow you to use a height of 2z // Therefore, we "stretch" the noise scale more than two times with color float tileHeight = noiseHeight * tileList.Count * 2; int tileHeightIndex = Mathf.FloorToInt(tileHeight); // We shift the obtained height to align the tiles with water and the first level of sand tileHeightIndex -= 4; if (tileHeightIndex < 0) { tileHeightIndex = 0; } // Take an asset tile depending on the converted noise level Tile tile = tileList[colorIndex]; // Set the tile height depending on the converted noise level Vector3Int p = new Vector3Int(x - length / 2, y - width / 2, tileHeightIndex); tileMap.SetTile(p, tile); } } } } ``````

##### Calculation of bias for generating Perlin Noise

In our implementation of the generator, accounting for coordinate offsets to obtain noise looks like this:

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 `````` ``````// Octave shift to get a more interesting picture with overlapping Vector2[] octavesOffset = new Vector2[octaves]; for (int i = 0; i < octaves; i++) { // Also use external position shift float xOffset = rand.Next(-100000, 100000) + offset.x; float yOffset = rand.Next(-100000, 100000) + offset.y; octavesOffset[i] = new Vector2(xOffset / width, yOffset / height); } // For a more visually pleasant zoom shift our view to the center float halfWidth = width / 2f; float halfHeight = height / 2f; // Generate points for a heightmap for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Octave Overlay Processing for (int i = 0; i < octaves; i++) { // Calculate the coordinates to get from Perlin Noise float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency; float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency; } } } ``````

Accordingly, for the correct calculation of the displacement of sectors, the following formula is used:

 ``````1 2 3 `````` ``````int sectorSize = TileMapSector.SectorSizeInTiles; float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale; float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale; ``````

##### Calculation of the number of sectors required for tiling the screen

As mentioned above:

 ``````1 2 `````` ``````int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1; int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1); ``````

##### Calculation of the position of sectors in space

It takes into account the displacement of the tiles relative to each other by half the size, as well as the projection of the coordinate axes.

 ``````1 2 3 4 5 `````` ``````var tileSize = grid.cellSize; float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f; float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f; float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX; float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY; ``````

##### Full code of map generation class
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 `````` ``````using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class TileMapHandler : MonoBehaviour { // Prefab of the tilemap public GameObject tileMapPrefab = null; // Grid for displaying generated tilemaps public Grid grid = null; // List of tiles public List tileList = new List(); // Generator settings [SerializeField] public GeneratorSettings generatorSettings; // List of generated maps private readonly List _tileMaps = new List(); // Start is called before the first frame update void Start() { // Hide the test texture NoiseMapRenderer mapRenderer = FindObjectOfType(); mapRenderer.gameObject.SetActive(false); // Screen size in sectors Vector2Int screenSizeInSectors = GetScreenSizeInSectors(); // Parameters for positioning sectors in space var tileSize = grid.cellSize; float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f; float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f; // Parameters for traversing tiles by row columns int startX = -screenSizeInSectors.x; int startY = 0; int currentSectorX = startX; int currentSectorY = startY; bool increaseX = true; // We shift the index by one to optimize the use of the loop with the remainder of the division int lastSectorIndex = screenSizeInSectors.x * screenSizeInSectors.y + 1; for (int currentSectorIndex = 1; currentSectorIndex < lastSectorIndex; currentSectorIndex++) { // Sector object Transform sector = GetTileMapSector(new Vector2Int(currentSectorX, currentSectorY)); sector.SetParent(grid.transform); // Calculate the position of the sector in space float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX; float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY; sector.Translate(new Vector3(positionX, positionY, 0), Space.Self); sector.name = "" + currentSectorX + "," + currentSectorY; // Save the sector _tileMaps.Add(sector); // Move to the next column currentSectorX++; currentSectorY--; // Go to the next line if (currentSectorIndex % screenSizeInSectors.x == 0) { if (increaseX) { currentSectorX = ++startX; currentSectorY = startY; increaseX = false; } else { currentSectorX = startX; currentSectorY = ++startY; increaseX = true; } } } } // Screen size in sectors public Vector2Int GetScreenSizeInSectors() { // Camera height float cameraHeight = Camera.main.orthographicSize * 2; // Camera width equals camera height * aspect ratio float screenAspect = Camera.main.aspect; float cameraWidth = cameraHeight * screenAspect; // Calculation of the number of tiles for tiling the screen var cellSize = grid.cellSize; int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1; int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1); return new Vector2Int(sectorCountX, sectorCountY); } // Sector generation private Transform GetTileMapSector(Vector2Int sector) { // Tile map object GameObject sectorGameObject = Instantiate(tileMapPrefab); // Calculate the sector offset for the noise generator int sectorSize = TileMapSector.SectorSizeInTiles; float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale; float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale; Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY); Tilemap sectorTileMap = sectorGameObject.GetComponent(); // Tile map sector generation TileMapSector.GenerateSector(NoiseMapGenerator.GenerateNoiseMap(generatorSettings, sectorOffset), tileList, sectorTileMap); return sectorGameObject.transform; } } ``````

### Result

Test texture:

The result of generating an isometric map divided into sectors:

### Conclusion

The addition of sectors is a small step for our example of procedural generation. Still, it solved such critical tasks as positioning the generation area of Perlin Noise, underlying mechanisms for working with an isometric coordinate system and dividing space into sectors. These three tasks are preparation for dynamically sector loading with movement and actual creation of infinite space. But we will deal with this in the next article of our series. See you next time! =)