Procedural Floating Islands Generator: Heightmap Generator | 2020


Procedural Floating Islands Generator: Heightmap Generator

The first thing we have to do is a tool that procedurally generates heightmaps that we will later interpret to build our floating islands. In case you wonder if we could make the images from some image design tool, the answer would be yes. However, in this way you can automate the formation of islands if that is what you want.

Important: This program is going to be used as a tool so it can only be used inside the Unity editor and not in the final compilation.

The first thing we are going to do is a folder and call it "Editor". Everything inside this folder will not be included in the compilation then we create our C# script and write the following code:

using UnityEngine;
using UnityEditor;

public class Generator : EditorWindow
{
    [MenuItem("Experimental/Generator")]
    public static void OnInit()
    {
        Generator window = EditorWindow.GetWindow<Generator>();
        window.Show();
    }

    private void OnGUI()
    {
        //Paint here
    }
}

This will allow us to create a window to show a preview of our heightmap and different parameters to create plenty of styles of islands.

Before we can accommodate all our parameters and show the preview of the image we have to write a function that returns a black image. For this proposal we will use the following code:

    private Texture2D CreateBlackTexture(int resolution)
    {
        var aux = new Texture2D(resolution, resolution);

        for (int x = 0; x < resolution; x++)
        {
            for (int y = 0; y < resolution; y++)
            {
                aux.SetPixel(x,y, Color.black);
            }
        }

        aux.Apply();

        return aux;
    }

This method receives a resolution and returns a black square image. Now we create all our variables and paint it inside the OnGUI method:

    private Texture2D map;
    Vector2 rectPos;

    private enum PixelCountAlgorithm
    {
        EIGHT_CONNNECTED, FOUR_CONNECTED
    }

    //Map settings
    private int resolution = 512;
    private int seed;
    private float landArea = 4;
    private float landType = 0.5f;
    private int erosion = 0;
    private int octaves = 1;
    private AnimationCurve landFalloff = AnimationCurve.Linear(0, 1, 1, 0);
    private Gradient landHeightColor = new Gradient();
    private PixelCountAlgorithm algorithm = PixelCountAlgorithm.EIGHT_CONNNECTED;

    private void OnGUI()
    {
        if (map == null)
            map = CreateBlackTexture(resolution);

        EditorGUILayout.BeginHorizontal(GUILayout.Width(position.width), GUILayout.ExpandWidth(true));

        rectPos = EditorGUILayout.BeginScrollView(rectPos);
        GUILayout.Label(map);
        EditorGUILayout.EndScrollView();

        EditorGUILayout.BeginHorizontal("box", GUILayout.ExpandHeight(true), GUILayout.Width(position.width/2));

        EditorGUILayout.BeginVertical();

        resolution = EditorGUILayout.IntSlider("Resolution", resolution, 64, 2048);
        resolution = Mathf.ClosestPowerOfTwo(resolution);

        seed = EditorGUILayout.IntField("Seed", seed);

        Random.InitState(seed);

        octaves = EditorGUILayout.IntSlider("Octaves", octaves, 1, 8);

        erosion = EditorGUILayout.IntSlider("Erosion", erosion, 0, 5);

        DrawLabeledSlider("Land Area", "Few", "Many", ref landArea, 0.12f, 1f);
        DrawLabeledSlider("Land Type", "Archipielago", "Continent", ref landType, 0.5f, 1f);

        landFalloff = EditorGUILayout.CurveField("Land Falloff", landFalloff);

        landHeightColor = EditorGUILayout.GradientField("Land Heightcolor", landHeightColor);

        algorithm = (PixelCountAlgorithm)EditorGUILayout.EnumPopup("Count Pixel Algorithm", algorithm);

        if (GUILayout.Button("Generate"))
        {
            //GenerateMap();
        }

        if (GUILayout.Button("Save"))
        {
            //Save();
        }

        EditorGUILayout.EndVertical();
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.EndHorizontal();
    }

This code uses an excellent function that I found in a forum to be able to add texts to the sliders as seen in the image:


You will need to add the following method:

    private void DrawLabeledSlider(string mainLabel, string leftLabel, string rightLabel, ref float value, float minValue, float maxValue)
    {
        Rect position = EditorGUILayout.GetControlRect(false, 2 * EditorGUIUtility.singleLineHeight); // Get two lines for the control
        position.height *= 0.5f;
        value = EditorGUI.Slider(position, mainLabel, value, minValue, maxValue);
        // Set the rect for the sub-labels
        position.y += position.height;
        position.x += EditorGUIUtility.labelWidth;
        position.width -= EditorGUIUtility.labelWidth + 54; //54 seems to be the width of the slider's float field
                                                            //sub-labels
        GUIStyle style = GUI.skin.label;
        style.alignment = TextAnchor.UpperLeft; EditorGUI.LabelField(position, leftLabel, style);
        style.alignment = TextAnchor.UpperRight; EditorGUI.LabelField(position, rightLabel, style);
    }

If in the Unity tools you open this window a black image will appear and to the right all the parameters that we will later use to modify the image. To end with the initialization of variables we must add two constants and one formula that will be used later.

    //Constants
    public const float DOTS_AMOUNT_FACTOR = 4f; //Only POW values!
    public const float DOTS_RADIUS_MARGIN = 3; 

With this formula we can calculate the number of islands according to the resolution of the image, and make it look uniform when the resolution changes:

    private int GetDots_Formula(float clamp01Value)
    {
        return (int)((resolution / DOTS_AMOUNT_FACTOR) * clamp01Value);
    }

Now we are ready to create the method "GenerateMap", this method is executed when the user presses the button to generate a map:

    private void GenerateMap()
    {
        //Generate Noise ===================================
        #region

        map = CreateBlackTexture(resolution);

        for (int o = 0; o < octaves; o++)
        {
            for (int i = 0; i < GetDots_Formula(landArea); i++)
            {
                var halfResolution = resolution / 2;
                var margin = (resolution / DOTS_RADIUS_MARGIN);

                var posInsideCircle = (Random.insideUnitCircle * margin) + Vector2.one * halfResolution;

                var radius = (int)(halfResolution * 0.35f * landType);

                for (int x = -radius; x < radius; x++)
                {
                    for (int y = -radius; y < radius; y++)
                    {
                        var radialPos = posInsideCircle + new Vector2(x, y);

                        float distanceToCenter = Vector2.Distance(radialPos, posInsideCircle);

                        var normalizedDistance = Mathf.InverseLerp(0, radius, distanceToCenter);

                        if (Random.Range(0f, 1f) < landFalloff.Evaluate(normalizedDistance))
                        {
                            map.SetPixel((int)radialPos.x, (int)radialPos.y, Color.white);
                        }
                    }
                }
            }
        }
        #endregion

        //Generate Shape
        #region

        int pixelCount = 0;

        for (int i = 0; i < erosion; i++)
        {
            var auxMap = new Texture2D(resolution, resolution);
            Graphics.CopyTexture(map, auxMap);

            for (int x = 0; x < resolution; x++)
            {
                for (int y = 0; y < resolution; y++)
                {
                    pixelCount++;

                    float alphaAmount = 0;
                    float result = 0;

                    if (algorithm == PixelCountAlgorithm.EIGHT_CONNNECTED)
                    {
                        for (int dx = -1; dx <= 1; dx++)
                        {
                            for (int dy = -1; dy <= 1; dy++)
                            {
                                if (x + dx > 0 && x + dx < resolution - 1 && y + dy > 0 && y + dy < resolution - 1)
                                {
                                    alphaAmount += map.GetPixel(x + dx, y + dy).grayscale;
                                }
                            }
                        }

                        result = alphaAmount / 9;
                    }

                    if (algorithm == PixelCountAlgorithm.FOUR_CONNECTED)
                    {
                        if (x < resolution - 1)
                            alphaAmount += map.GetPixel(x + 1, y).grayscale;

                        if (x > 0)
                            alphaAmount += map.GetPixel(x - 1, y).grayscale;

                        if (y < resolution - 1)
                            alphaAmount += map.GetPixel(x, y + 1).grayscale;

                        if (y > 0)
                            alphaAmount += map.GetPixel(x, y - 1).grayscale;

                        result = alphaAmount / 4;
                    }

                    auxMap.SetPixel(x, y, landHeightColor.Evaluate(result));

                    int index = i + 1;

                    EditorUtility.DisplayProgressBar("Creating Land", "Rasterizing " + index + "/" + erosion , pixelCount / (Mathf.Pow(resolution, 2)*erosion));
                }
            }

            Graphics.CopyTexture(auxMap, map);
            map.Apply();
        }

        EditorUtility.ClearProgressBar();
        #endregion

        TextureScale.Point(map, 512, 512);
    }


This code is somewhat complex so I will explain it in parts.
First, a black texture is created then the number of islands is related to the result of the formula. The octaves are the times that this process is repeated.

        map = CreateBlackTexture(resolution);

        for (int o = 0; o < octaves; o++)
        {
            for (int i = 0; i < GetDots_Formula(landArea); i++)
            {
                var halfResolution = resolution / 2;
                var margin = (resolution / DOTS_RADIUS_MARGIN);

                var posInsideCircle = (Random.insideUnitCircle * margin) + Vector2.one * halfResolution;

                var radius = (int)(halfResolution * 0.35f * landType);

                for (int x = -radius; x < radius; x++)
                {
                    for (int y = -radius; y < radius; y++)
                    {
                        var radialPos = posInsideCircle + new Vector2(x, y);

                        float distanceToCenter = Vector2.Distance(radialPos, posInsideCircle);

                        var normalizedDistance = Mathf.InverseLerp(0, radius, distanceToCenter);

                        if (Random.Range(0f, 1f) < landFalloff.Evaluate(normalizedDistance))
                        {
                            map.SetPixel((int)radialPos.x, (int)radialPos.y, Color.white);
                        }
                    }
                }
            }
        }


To have a clearer picture of this code, first, it is responsible for generating random points within a radius


Then spawn dots around these initial points and according to a random value and its normalized distance, the point may or may not be placed.





In the second part of the method, an algorithm widely used in procedural map generation is applied, consists of counting its neighbors for each pixel and comparing it with a threshold to know if it paints it black or white. There are several ways to counting neighbors each way generate different results, the ones used in this project are two:  4-connected and 8-connected.

        //Generate Shape
        #region

        int pixelCount = 0;

        for (int i = 0; i < erosion; i++)
        {
            var auxMap = new Texture2D(resolution, resolution);
            Graphics.CopyTexture(map, auxMap);

            for (int x = 0; x < resolution; x++)
            {
                for (int y = 0; y < resolution; y++)
                {
                    pixelCount++;

                    float alphaAmount = 0;
                    float result = 0;

                    if (algorithm == PixelCountAlgorithm.EIGHT_CONNNECTED)
                    {
                        for (int dx = -1; dx <= 1; dx++)
                        {
                            for (int dy = -1; dy <= 1; dy++)
                            {
                                if (x + dx > 0 && x + dx < resolution - 1 && y + dy > 0 && y + dy < resolution - 1)
                                {
                                    alphaAmount += map.GetPixel(x + dx, y + dy).grayscale;
                                }
                            }
                        }

                        result = alphaAmount / 9;
                    }

                    if (algorithm == PixelCountAlgorithm.FOUR_CONNECTED)
                    {
                        if (x < resolution - 1)
                            alphaAmount += map.GetPixel(x + 1, y).grayscale;

                        if (x > 0)
                            alphaAmount += map.GetPixel(x - 1, y).grayscale;

                        if (y < resolution - 1)
                            alphaAmount += map.GetPixel(x, y + 1).grayscale;

                        if (y > 0)
                            alphaAmount += map.GetPixel(x, y - 1).grayscale;

                        result = alphaAmount / 4;
                    }

                    auxMap.SetPixel(x, y, landHeightColor.Evaluate(result));

                    int index = i + 1;

                    EditorUtility.DisplayProgressBar("Creating Land", "Rasterizing " + index + "/" + erosion , pixelCount / (Mathf.Pow(resolution, 2)*erosion));
                }
            }

            Graphics.CopyTexture(auxMap, map);
            map.Apply();
        }

        EditorUtility.ClearProgressBar();
        #endregion

        TextureScale.Point(map, 512, 512);

In the last line, the image is resized with this code.

You should add TextureScale.cs into your project to use that method.

Finally, we will add the "Save" method to save our image on our computer:

    private void Save()
    {
        string path = EditorUtility.SaveFilePanel("Save texture as PNG", "", map.name + ".png", "png");

        byte[] bytes = map.EncodeToPNG();

        File.WriteAllBytes(path, bytes);
    }

We will use this tool to generate heightmaps which will be interpreted by another system to build the blocks of the island. See you in the next part!


Complete code:

Generator.cs
using UnityEngine;
using UnityEditor;
using System.IO;

public class Generator : EditorWindow
{
    [MenuItem("Experimental/Generator")]
    public static void OnInit()
    {
        Generator window = EditorWindow.GetWindow<Generator>();
        window.seed = Random.Range(1000000, 999999);
        window.Show();
    }

    private Texture2D map;
    Vector2 rectPos;

    private enum PixelCountAlgorithm
    {
        EIGHT_CONNNECTED, FOUR_CONNECTED
    }

    //Constants
    public const float DOTS_AMOUNT_FACTOR = 4f; //Only POW values!
    public const float DOTS_RADIUS_MARGIN = 3; 

    //Map settings
    private int resolution = 512;
    private int seed;
    private float landArea = 4;
    private float landType = 0.5f;
    private int erosion = 0;
    private int octaves = 1;
    private AnimationCurve landFalloff = AnimationCurve.Linear(0, 1, 1, 0);
    private Gradient landHeightColor = new Gradient();
    private PixelCountAlgorithm algorithm = PixelCountAlgorithm.EIGHT_CONNNECTED;

    private void OnGUI()
    {
        if (map == null)
            map = CreateBlackTexture(resolution);

        EditorGUILayout.BeginHorizontal(GUILayout.Width(position.width), GUILayout.ExpandWidth(true));

        rectPos = EditorGUILayout.BeginScrollView(rectPos);
        GUILayout.Label(map);
        EditorGUILayout.EndScrollView();

        EditorGUILayout.BeginHorizontal("box", GUILayout.ExpandHeight(true), GUILayout.Width(position.width/2));

        EditorGUILayout.BeginVertical();

        resolution = EditorGUILayout.IntSlider("Resolution", resolution, 64, 2048);
        resolution = Mathf.ClosestPowerOfTwo(resolution);

        seed = EditorGUILayout.IntField("Seed", seed);

        Random.InitState(seed);

        octaves = EditorGUILayout.IntSlider("Octaves", octaves, 1, 8);

        erosion = EditorGUILayout.IntSlider("Erosion", erosion, 0, 5);

        DrawLabeledSlider("Land Area", "Few", "Many", ref landArea, 0.12f, 1f);
        DrawLabeledSlider("Land Type", "Archipielago", "Continent", ref landType, 0.5f, 1f);

        landFalloff = EditorGUILayout.CurveField("Land Falloff", landFalloff);

        landHeightColor = EditorGUILayout.GradientField("Land Heightcolor", landHeightColor);

        algorithm = (PixelCountAlgorithm)EditorGUILayout.EnumPopup("Count Pixel Algorithm", algorithm);

        if (GUILayout.Button("Generate"))
        {
            GenerateMap();
        }

        if (GUILayout.Button("Save"))
        {
            Save();
        }

        EditorGUILayout.EndVertical();
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.EndHorizontal();
    }

    private void Save()
    {
        string path = EditorUtility.SaveFilePanel("Save texture as PNG", "", map.name + ".png", "png");

        byte[] bytes = map.EncodeToPNG();

        File.WriteAllBytes(path, bytes);
    }

    private void DrawLabeledSlider(string mainLabel, string leftLabel, string rightLabel, ref float value, float minValue, float maxValue)
    {
        Rect position = EditorGUILayout.GetControlRect(false, 2 * EditorGUIUtility.singleLineHeight); // Get two lines for the control
        position.height *= 0.5f;
        value = EditorGUI.Slider(position, mainLabel, value, minValue, maxValue);
        // Set the rect for the sub-labels
        position.y += position.height;
        position.x += EditorGUIUtility.labelWidth;
        position.width -= EditorGUIUtility.labelWidth + 54; //54 seems to be the width of the slider's float field
                                                            //sub-labels
        GUIStyle style = GUI.skin.label;
        style.alignment = TextAnchor.UpperLeft; EditorGUI.LabelField(position, leftLabel, style);
        style.alignment = TextAnchor.UpperRight; EditorGUI.LabelField(position, rightLabel, style);
    }

    private Texture2D CreateBlackTexture(int resolution)
    {
        var aux = new Texture2D(resolution, resolution);

        for (int x = 0; x < resolution; x++)
        {
            for (int y = 0; y < resolution; y++)
            {
                aux.SetPixel(x,y, Color.black);
            }
        }

        aux.Apply();

        return aux;
    }

    private void GenerateMap()
    {
        //Generate Noise ===================================
        #region

        map = CreateBlackTexture(resolution);

        for (int o = 0; o < octaves; o++)
        {
            for (int i = 0; i < GetDots_Formula(landArea); i++)
            {
                var halfResolution = resolution / 2;
                var margin = (resolution / DOTS_RADIUS_MARGIN);

                var posInsideCircle = (Random.insideUnitCircle * margin) + Vector2.one * halfResolution;

                var radius = (int)(halfResolution * 0.35f * landType);

                for (int x = -radius; x < radius; x++)
                {
                    for (int y = -radius; y < radius; y++)
                    {
                        var radialPos = posInsideCircle + new Vector2(x, y);

                        float distanceToCenter = Vector2.Distance(radialPos, posInsideCircle);

                        var normalizedDistance = Mathf.InverseLerp(0, radius, distanceToCenter);

                        if (Random.Range(0f, 1f) < landFalloff.Evaluate(normalizedDistance))
                        {
                            map.SetPixel((int)radialPos.x, (int)radialPos.y, Color.white);
                        }
                    }
                }
            }
        }
        #endregion

        //Generate Shape
        #region

        int pixelCount = 0;

        for (int i = 0; i < erosion; i++)
        {
            var auxMap = new Texture2D(resolution, resolution);
            Graphics.CopyTexture(map, auxMap);

            for (int x = 0; x < resolution; x++)
            {
                for (int y = 0; y < resolution; y++)
                {
                    pixelCount++;

                    float alphaAmount = 0;
                    float result = 0;

                    if (algorithm == PixelCountAlgorithm.EIGHT_CONNNECTED)
                    {
                        for (int dx = -1; dx <= 1; dx++)
                        {
                            for (int dy = -1; dy <= 1; dy++)
                            {
                                if (x + dx > 0 && x + dx < resolution - 1 && y + dy > 0 && y + dy < resolution - 1)
                                {
                                    alphaAmount += map.GetPixel(x + dx, y + dy).grayscale;
                                }
                            }
                        }

                        result = alphaAmount / 9;
                    }

                    if (algorithm == PixelCountAlgorithm.FOUR_CONNECTED)
                    {
                        if (x < resolution - 1)
                            alphaAmount += map.GetPixel(x + 1, y).grayscale;

                        if (x > 0)
                            alphaAmount += map.GetPixel(x - 1, y).grayscale;

                        if (y < resolution - 1)
                            alphaAmount += map.GetPixel(x, y + 1).grayscale;

                        if (y > 0)
                            alphaAmount += map.GetPixel(x, y - 1).grayscale;

                        result = alphaAmount / 4;
                    }

                    auxMap.SetPixel(x, y, landHeightColor.Evaluate(result));

                    int index = i + 1;

                    EditorUtility.DisplayProgressBar("Creating Land", "Rasterizing " + index + "/" + erosion , pixelCount / (Mathf.Pow(resolution, 2)*erosion));
                }
            }

            Graphics.CopyTexture(auxMap, map);
            map.Apply();
        }

        EditorUtility.ClearProgressBar();
        #endregion

        TextureScale.Point(map, 512, 512);
    }

    private int GetDots_Formula(float clamp01Value)
    {
        return (int)((resolution / DOTS_AMOUNT_FACTOR) * clamp01Value);
    }
}

No hay comentarios:

Publicar un comentario

Pages