Procedural Environment

Procedural Environment

By Chris Huider

Introduction

Hi, my name is Chris Huider. I’m a 19-year-old student at the Amsterdam University of Applied Sciences. In the second semester of my third year I decided to follow Gameplay Engineering (GPE).
During the first block of that semester, I worked on a research & development project.

For this project I chose procedurally generated environment as my research subject. To show you an example of what that means, take a look at Gaia.
Gaia is a Unity asset which can procedurally generate vast landscapes with natural vegetation and geology.

Now that I’m entering the final phase of my project, I’ll be showing you the progress I made along the way.


What I’ll be showing you

  • Unity’s Terrain
    • Researching how it works
    • Deciding I couldn’t be bothered
  • Creating my own terrain
    • Generating the mesh
    • Height- and Slopemap
    • Data formatting
      • The problem
      • The fix
    • Spawning trees
    • Smoothing the terrain
    • Creating chunks
    • Texturing the terrain
  • End words
  • Sources


Unity’s Terrain

When I first picked my research subject, I imagined being able to use Unity’s built-in Terrain object to get off to a quick start. I wanted to focus on procedurally generating the height- and texturemaps, loading them onto the Terrain and it just working. However, when I started my research I quickly ran into a problem, which would give me a hard reality check.

Researching how it works

I started by researching how Unity’s built-in Terrain object, stores its data. I quickly learned it stored all of its data in a ScriptableObject which housed the heightmap texture, splatmap textures (more on these later), length, width and much more.

I made a basic Terrain which i’d use for testing and started checking the stored textures by placing them onto a plane. This showed me how the Terrain was represented in the height- and splatmap data. This gave me the following result.

As you can see, in the left-bottom corner we have the Terrain, in the right-bottom corner we have the heightmap texture and in both top corners we have the, currently two, splatmap textures.

The heightmap texture has a R16 textureformat and a resolution of 513×513, off which each pixel represents a height value for a vertex of the Terrain mesh.

The splatmap textures both have a RGBA32 textureformat and a resolution of 512×512, off which each pixel represents which texture should be rendered on the quad of the mesh that pixel is connected to. Each splatmap can hold four terrain textures in its R, G, B and A data. If R is 1 it renders texture 1, if G is 1 it renders texture 2, etc. If R, G, B and A are all 0 it checks the next splatmap.

Deciding I couldn’t be bothered

Now armed with some information about Unity’s built-in Terrain I started trying to load my own heightmap texture onto the Terrain which had the following result.

To the left is the heightmap I created in Paint.net and to the right is the result of loading it onto the Terrain. Now, this looks good but it wasn’t as I wanted. As an example of why, take a look at this.

To the left is what I wanted and to the right is what I got. The left is the result of Unity’s own Terrain importing tool and the right is my own script, which was actually based on that importing tool. After about a week of struggling I decided to call it, I didn’t start this research project to struggle with understanding Unity’s own Terrain object, so I decided to scrap my current progress and start over making my own terrain generation.



Creating my own terrain

Now that I decided to make my own terrain, basically rendering the first week of progress useless, I was a bit behind schedule. Luckily I’ll be able to scavenge some of the knowledge I gained from my first week of research and put it to use in my own terrain generation.

Generating the mesh

First things first, for there to be terrain, there needs to be a mesh. I started out making a grid mesh with variable width and height. I first made it myself, but I’m going to foreshadow a little, it wasn’t very performance friendly because it was using Lists instead or Arrays. To fix this I chose the easy way of “borrowing” someone else’s code (Catlike Coding) which was using Arrays, but was otherwise the same as mine.*1 Seeing as I already understood the principles of the process I didn’t mind just copying the code as this saved me the time and effort of making it myself, keep in mind, I was already behind schedule.

Now skipping ahead into the future a little bit, when loading in a terrain bigger than 256×256 vertices a problem occurs. It took a little while to figure out what it was, but it ended up being the mesh hitting the vertex/triangle limit. This limit lies at 65.535 vertices, and 256×256 is 65.536 vertices. I’m not sure how 256×256 was still working, but let’s put that aside.

Now to visualize the problem a bit, take a look at these examples.

Simply said, Unity hit its vertex/triangle limit and finished off the mesh in whatever way it thought looked worst.

To fix this I had to make terrain chunks, but more on that further on.

Height- and Slopemap

Next up is the height- and slopemap textures, so, let me explain what they are and why I needed them.

I think the heightmap is relatively self explanatory, so I’ll keep it short. The heightmap stores the height value for each vertex in the terrain mesh.

On the other hand, why do I need a slopemap? Well, looking into the future, I want to texture my terrain. When I do that, I want to add a grass texture to places where there isn’t a big slope and add a rock texture where there is a large slope, for example the side of a mountain. So to do this, I need the slopemap texture to store data of the verticality of the terrain.

Next up is creating the slopemap texture. The slopemap texture is procedurally generated using data from the heightmap texture. The slopemap values are calculated by going over each pixel on the heightmap texture and cross checking it’s neighbours’ values.

In this example, the white pixel is the one we want to calculate the slope value for, and the colored ones are colorcoded to represent which pixels wil be cross checked with each other.
For each neighbour cross-pair I calculate the difference in height value and divide it by 2. Then once I have the four cross checked values, I check which one is the highest and assign that as my slope value.

But, what about the edges of the texture, where the pixel doesn’t have certain neighbours? I got that covered.
Instead of cross checking the neighbours, I check every neighbour with the pixel I want to calculate the slope value for. This time I don’t divide it by 2, which is essentially the same as simulating the slope continuing with the same amount to the pixel outside the texture.
Finally I end up with anywhere between three and five values, where I again, assign the highest as my slope value.

For anyone interested in how the algorithm looks, here it is.


for (int y = 0; y < heightMapData.Height; y++)
        {
            for (int x = 0; x < heightMapData.Width; x++)
            {
                bool completeDataSet = true;
                Color[] colors = new Color[9];
                
                /*
                 0 = left top
                 1 = middle top
                 2 = right top
                 
                 3 = left middle
                 4 = middle middle
                 5 = right middle
                 
                 6 = left bottom
                 7 = middle bottom
                 8 = right bottom
                 */

                try { colors[0] = heightMapData.Pixels[x - 1, y + 1]; }catch { completeDataSet = false; }
                try { colors[1] = heightMapData.Pixels[x    , y + 1]; }catch { completeDataSet = false; }
                try { colors[2] = heightMapData.Pixels[x + 1, y + 1]; }catch { completeDataSet = false; }
                
                try { colors[3] = heightMapData.Pixels[x - 1, y    ]; }catch { completeDataSet = false; }
                try { colors[4] = heightMapData.Pixels[x    , y    ]; }catch { completeDataSet = false; }
                try { colors[5] = heightMapData.Pixels[x + 1, y    ]; }catch { completeDataSet = false; }
                
                try { colors[6] = heightMapData.Pixels[x - 1, y - 1]; }catch { completeDataSet = false; }
                try { colors[7] = heightMapData.Pixels[x    , y - 1]; }catch { completeDataSet = false; }
                try { colors[8] = heightMapData.Pixels[x + 1, y - 1]; }catch { completeDataSet = false; }

                List<float> values = new List<float>();

                if (completeDataSet)
                {
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[0].r, colors[8].r) / 2f);
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[1].r, colors[7].r) / 2f);
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[2].r, colors[6].r) / 2f);
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[3].r, colors[5].r) / 2f);
                }
                else
                {
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[0].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[1].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[2].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[3].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[4].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[5].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[6].r, colors[4].r));
                    values.Add(MathExtensions.BiggestMinusSmallest(colors[7].r, colors[4].r));
                }

                var highest = MathExtensions.MaxValue(values.ToArray());
                slopeMapPixels[x, y] = new Color(highest*10, 0, 0, 0);
            }
        }

It doesn’t win the beauty peagant, seeing as I’m using try{}catch{} but it works and it works well.

Data formatting

Now onto something I wasn’t sure about talking about, seeing as it isn’t really used in the end product, but for anyone interested, here it is.

In the beginning of creating my own terrain, I wanted to store my data in the same way Unity’s built-in Terrain object does, using a ScriptableObject.

The problem

Storing textures in a ScriptableObject isn’t really a big deal, but, if you want to modify or create entirely new textures and store those, it becomes a different story. If you want to create entirely new textures and store those in a ScriptableObject, Unity requires you to make them an asset. You could just make it a new asset and save it in the same folder as the ScriptableObject, but this will cause problems if you start moving them to different folders. So, what you have to do, is make them into a child asset with the ScriptableObject as its parent. This makes the file path consistent as it’s the same as the ScriptableObject.

Like before, making child assets itself isn’t really a big deal, but what I wanted, was to be able to assign them in the inspector aswell. This was the cause of a lot of problems and a lot of struggle. Assigning it in the inspector, copying it to the child asset and then modifying it, would also modify the original asset which I assigned in the inspector, which is a big problem. Let’s get into the child asset creation and the fix to this problem as that’s the actual interesting part.

The fix

First of all, the child asset creation. I struggled a bit trying to do it myself, before crumbling under time constraints and opting to “borrow” someone else’s code (StackOverflow “derHugo”)*2. First I just used this code in my ScriptableObject, but after having to store three child assets, the script became too long, with too much duplicate code, so I created my own child asset (and parent asset) script.

Basically all it did was store the valuetype you wanted to store and create a child asset for it. But for it to work properly it did need a parent asset script and subscribe to its Awake(), OnValidate(), Reset() and Destroy() methods.

While we’re on this topic, this also caused some problems as Unity removes these subscriptions each time the editor reloads. I fixed this in a suboptimal way by resetting the subscriptions each time the parent asset calls it’s OnValidate() method.

I also fixed the problem where the original asset was being overwritten by not actually storing the original as my child asset. When I assign it in the inspector I copy that asset to my child asset and not store any reference to both, which means, it won’t have a reference to the original which fixes the problem.

If you’re interested in the code, here it is.


#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public class ChildAsset<T> where T : Object
{
    private bool _initiated = false;

    [HideInInspector] public ParentScriptableObjectAsset Parent;
    
    [HideInInspector] public string AssetName;
    
    public T Asset{
        get
        {
            var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(Parent));
            var foundAsset = assets.FirstOrDefault(a => a.name == AssetName) as T;
            if (!foundAsset && _initiated) throw new Exception($"{AssetName}: Asset not found!");
            return foundAsset;
        }
        set
        {
            var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(Parent));
            var asset = assets.FirstOrDefault(a => a.name == AssetName);

            if (!asset)
            {
                Debug.LogError($"Cannot find '{AssetName}' asset!");
                return;
            }
            
            if (value != null)
            {
                SetAsset(value, asset);
            }
            else
            {
                SetAssetIfValueIsNull(asset);
            }
        }
    }

    [SerializeField] private T _asset;

    public ChildAsset(string assetName, ParentScriptableObjectAsset parent)
    {
        this.AssetName = assetName;
        this.Parent = parent;

        Init();
        Subscribe(parent);
        Refresh();
    }

    protected virtual void SetAsset(T value, Object asset)
    {
        value.name = AssetName;
        EditorUtility.CopySerialized(value, asset);
    }
    
    protected virtual void SetAssetIfValueIsNull(Object asset)
    {
        EditorUtility.CopySerialized(new Texture2D(0, 0){name = AssetName}, asset);
    }

    private void Validate()
    {
        Init(); 
        Asset = _asset;
    }

    public void Subscribe(ParentScriptableObjectAsset parent)
    {
        parent.OnValidateEvent += Init;
        parent.OnValidateEvent += Validate;
        parent.OnResetEvent += Init;
        parent.OnDestroyEvent += Destroy;
    }
    
    private void Destroy()
    {
        EditorApplication.update -= DelayedInit;
        Parent.OnAwakeEvent -= Init;
        Parent.OnValidateEvent -= Validate;
        Parent.OnResetEvent -= Init;
        Parent.OnDestroyEvent -= Destroy;
    }

    protected virtual void Create()
    {
        var asset = ObjectFactory.CreateInstance(typeof(T));
        asset.name = AssetName;
        AssetDatabase.AddObjectToAsset(asset, Parent);
    }

    public void Refresh()
    {
        _asset = Asset;
    }

    private void Init()
    {
        if (Asset) { return; }
        
        if (AssetDatabase.Contains(Parent))
        {
            DelayedInit();
        }
        else
        {
            EditorApplication.update -= DelayedInit;
            EditorApplication.update += DelayedInit;
        }
    }

    private void DelayedInit()
    {
        if (!AssetDatabase.Contains(Parent)) { return; }
        
        EditorApplication.update -= DelayedInit;

        if (!Asset)
        {
            Create();
            _asset = Asset;
            _initiated = true;
        }
        
        EditorUtility.SetDirty(Parent);
        AssetDatabase.SaveAssets();
    }
}

Then finally, how does the resulting ScriptableObject look in the project files? Like this.

Spawning trees

Spawning trees, or actually any environment object is done in a very simple way, by picking a random point on the slopemap texture, checking if that point’s and its eight neighbours’ slope values are lower than a variable amount, checking if the heightmap texture’s value on that point is above the variable sea level, and if that’s all fine spawning a random environment object from the prefab List. It does this a variable amount of times untill that amount of environment objects is reached.

I could’ve used a fancy algorithm to not make them spawn too close to each other, but I didn’t have time for that, keep in mind, I’ve been behind schedule from the beginning.

Smoothing the terrain

In the first week of my project, where I was still researching Unity’s built-in Terrain object, you could see a difference between Unity’s import tool and mine, that difference being how smooth the terrain ended up. In my current terrain, I’m still having the same issue, where you can still see obvious pixels (due to Paint.net brush size).

To fix this I created a method to smooth out any texture I want. Obviously this would be mainly used for the heightmap texture, but it’s nice to know it works on any texture. But, how does this work and what do I mean by “smoothing”? Simply said, I set every pixel to the average value of itself and its neighbours. This makes any places where there is a large difference between pixel values, a smaller difference.

I can run this algorithm as many times as I want, but the more you do it, the “flatter” the heightmap becomes. By running the algorithm three times, the result looks like this.

On the left is the non-smoothed heightmap and to the right is the smoothed heightmap.

For anyone interested in the code, here you go.


for (int x = 0; x < mapData.Width; x++)
            {
                for (int y = 0; y < mapData.Height; y++)
                {
                    List<float> redValues = new List<float>();

                    try { redValues.Add(mapData.Pixels[x - 1, y + 1].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x + 1, y + 1].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x    , y + 1].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x - 1, y    ].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x + 1, y    ].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x    , y    ].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x - 1, y - 1].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x + 1, y - 1].r); }catch{ /*ignored*/ }
                    try { redValues.Add(mapData.Pixels[x    , y - 1].r); }catch{ /*ignored*/ }

                    float averageValue = MathExtensions.AverageValue(redValues.ToArray());
                    
                    newValues[x, y] = new Color(averageValue, 0, 0, 0);
                }
            }

Creating chunks

Now here is something interesting. When I told you about the mesh creation, I said I fixed the problem by creating terrain chunks, so, here is the full explanation of how I did that.

The theory of chunking a mesh isn’t quite difficult to understand, you take a mesh, chop it up into pieces and voila, you have chunks. However, when you have to chunk it before creating the original, it gets a little more difficult.

I create my terrain by using a heightmap, and for this example, let’s say that texture is 512×512 pixels. I can’t just simply split it into four chunks of 256×256. If you were to do that you would end up with gaps in between, because the meshes don’t connect to each other.

The simple fix is to create overlap. You want the chunks to also hold the information about the first layer of the next chunk so you can connect them together. Makes sense? Ok then now onto my struggle.

When trying to correctly overlap these chunks and line up the pixels of the original texture, onto the new ones all hell broke loose. I might’ve just not thought about it enough, maybe I was just stupid, but aligning the original pixels on the new textures, making sure they overlap correctly and adjust to different sizes (middle chunks are aligned differently from ones on the outside) was hell and took hours.

About that part where the middle chunks are aligned differently from the outside ones, I’ll explain. The middle textures also hold the data for the first layer of the connected chunk, but, the chunks on the outside don’t have any connected chunks and thus, don’t have any data on at least two of the outside layers of the texture.

Eventually I got everything to work and here is the result.

You can see the selected chunk in the middle. It’s size is currently 128×128, because the texture is actually 129×129 seeing as 128×128 quads means 129×129 vertices, and as we discussed before, Unity can only handle chunks of maximum 256×256. So if I would make the chunk that size, the texture, and thus the vertex/triangle count would become 257×257, exceeding the vertex/triangle limit.

For anyone who doesn’t want to go through my pain, here is the entire method’s code.


public static Texture2D[] NewChunkTexture(TextureMapData textureMapData, out Vector3[] chunkOffsets)
    {
        #region Create Chunk Grid
        
        int maxChunkWidth = 128;
        int maxChunkHeight = 128;
        
        int originalWidth = textureMapData.Width;
        int originalHeight = textureMapData.Height;
        
        bool isWidthFound = false;
        bool isHeightFound = false;

        Texture2D[,] chunkGrid;
        int chunkGridXSize = 1;
        int chunkGridYSize = 1;
        int chunkTotalAmount;

        Vector3[,] offsetGrid;
        
        while (!isWidthFound && !isHeightFound)
        {
            if (originalWidth / chunkGridXSize <= maxChunkWidth) isWidthFound = true;
            else chunkGridXSize++;

            if (originalHeight / chunkGridYSize <= maxChunkHeight) isHeightFound = true;
            else chunkGridYSize++;
        }

        chunkTotalAmount = chunkGridXSize * chunkGridYSize;
        chunkGrid = new Texture2D[chunkGridXSize, chunkGridYSize];
        offsetGrid = new Vector3[chunkGridXSize, chunkGridYSize];
        
        #endregion

        #region Create Chunks
        
        int newWidth = textureMapData.Width / chunkGridXSize;
        int newHeight = textureMapData.Height / chunkGridYSize;

        int newExtendedWidth = newWidth + 1;
        int newExtendedHeight = newHeight + 1; //<--------- increase for increase overlap? 2 overlaps by 1

        TextureFormat textureFormat = textureMapData.Texture2D.format;
        int mipCount = -1;
        bool linear = false;
        TextureWrapMode wrapMode = textureMapData.Texture2D.wrapMode;

        for (int chunkGridYIndex = 0; chunkGridYIndex < chunkGridYSize; chunkGridYIndex++)
        {
            for (int chunkGridXIndex = 0; chunkGridXIndex < chunkGridXSize; chunkGridXIndex++)
            {
                Texture2D chunk = new Texture2D(newExtendedWidth, newExtendedHeight, textureFormat, mipCount, linear){name = $"Chunk: {chunkGridXIndex},{chunkGridYIndex}", wrapMode = wrapMode};
                FinalTextureGenerator.SetTextureToColor(chunk, Color.black);
                TextureMapData chunkMapData = new TextureMapData(chunk);

                Color[,] newPixels = new Color[chunkMapData.Width, chunkMapData.Height];

                int xLength = newExtendedWidth;
                int yLength = newExtendedHeight;

                if (chunkGridXIndex == 0 || chunkGridXIndex == chunkGridXSize - 1) xLength--;
                if (chunkGridYIndex == 0 || chunkGridYIndex == chunkGridYSize - 1) yLength--;

                for (int y = 0; y < yLength; y++)
                {
                    for (int x = 0; x < xLength; x++)
                    {
                        int chunkStartX = x;
                        int chunkStartY = y;
                        
                        int oriStartX = x + (chunkGridXIndex * newWidth);
                        int oriStartY = y + (chunkGridYIndex * newHeight);

                        if (chunkGridXIndex == 0) chunkStartX++;
                        if (chunkGridYIndex == 0) chunkStartY++;
                        
                        if (chunkGridXIndex != 0) oriStartX--;
                        if (chunkGridYIndex != 0) oriStartY--;

                        newPixels[chunkStartX, chunkStartY] = textureMapData.Pixels[oriStartX, oriStartY];
                    }
                }

                chunkMapData.Pixels = newPixels;
                chunkGrid[chunkGridXIndex, chunkGridYIndex] = chunkMapData.Texture2D;
                offsetGrid[chunkGridXIndex, chunkGridYIndex] = new Vector3(chunkGridXIndex * newWidth, 0, chunkGridYIndex * newHeight);
            }
        }
        
        #endregion

        #region Convert Grids

        Texture2D[] chunkArray = new Texture2D[chunkTotalAmount];
        Vector3[] offsetArray = new Vector3[chunkTotalAmount];
        
        for (int y = 0; y < chunkGridYSize; y++)
        {
            for (int x = 0; x < chunkGridXSize; x++)
            {
                int listIndex = x + (y * chunkGridYSize);

                chunkArray[listIndex] = chunkGrid[x, y];
                offsetArray[listIndex] = offsetGrid[x, y];
            }
        }

        #endregion

        chunkOffsets = offsetArray;
        return chunkArray;
    }

I haven’t tried to translate this method to logical names because I can’t be bothered to. If you really want to use it, you’ll be able to figure it out.

Texturing the terrain

This blog post has become pretty long so, unless I add anything after writing this, this will be the last subject I will talk about.

At the beginning I used a very simple way to texture my terrain. I would create a ground texture with green where the slope was near 0 and grey where it wasn’t. This created a simple but effective ground texture. Take a look.

However, I wanted something better. So I decided to convert this into a blend map and use it to blend multiple triplanar textures like grass, rock, dirt and pebbles.

I thought this would be an easy thing to do but once again, I was wrong. I had to create a shader that would do this for me. Honestly, my research project now includes multiple categories, procedural generation, performance and shaders, where am I suppose to publish this under. Anyway, I decided to “borrow” someone else’s code because I really didn’t feel like struggling with shaders aswell. Actually I borrowed code from two people, bgolus (Triplanar texture mapping with normals and occulusion) and mouurusai (Blending textures using a blend map)*3-4.

I struggled a bit because my blending wasn’t working, and then it was but the blend map wasn’t overlayed properly, but it came down to me not really knowing alot about shaders and only sort off knowing what I was doing, so I’ll count that as my own shortcomings.

I fixed the overlay of the blend map by scaling it to the width and height in Unity scene measurements and it was fixed.

I created a default material where the textures and their offsets and scales where set and assigned that to every chunk with the correct scaling and blendmap. The result looks like this.

I’m not putting the shader code in here because: 1. The script is 500 lines, 2. I’m not proud of it because it’s really cobbled together, and 3. I can’t really explain most of what it does.


End words

Well, all in all, I learned quite a bit, mainly that five weeks to do something of any big scale is a very short time. I’m proud of what I achieved and the end product. It isn’t what I had imagined when I chose this research subject, but hey, it was a good experience.


Sources

Leave a Reply

Your email address will not be published. Required fields are marked *