Skip to main content

Multithreaded Infinite Procedural Terrain with LODs in Unity


Infinitely procedurally generated terrain can be a lot of fun in games. I can't tell you how much fun I've had exploring worlds in games like Minecraft. Unfortunately, as so many overly-ambitious indie developers will tell you, it takes a lot of work to make procedural worlds performant without a lot of work. Thankfully, there's a solution: multithreading.

Wait, don't leave! Multithreading is a lot simpler than you might think, and in this post, I'll show you how to create infinite, procedural terrain in Unity with just a few lines of code that's fast, beautiful, and even has LODs.

The code is below:

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Linq;
using UnityEngine;

public class TerrainGenerator : MonoBehaviour {
    public Material material;
    public int chunkSize = 64;
    public int rangeMultiplier = 2;
    public NoiseFilter noiseFilter;
    public float[] LODDistances = {
        64f,
        128f,
        256f,
        512f,
        1024f
    };

    public Transform player;
    public float heightThreshold = 128f;

    private bool isGenerating = true;

    private int range = 1;
    private Dictionary<Vector3, Geometry> chunkCache;
    private Dictionary<Vector3, GameObject> chunkMap;
    private List<Thread> terrainThreads;
    private volatile Mesh mesh;
    private Vector3 playerPos;

    private struct Geometry {
        public Vector3 position;
        public Vector3[] vertices;
        public int[] triangles;
        public Vector2[] uvs;
        public int LOD;
    }

    private void Start() {
        chunkCache = new Dictionary<Vector3, Geometry>();
        chunkMap = new Dictionary<Vector3, GameObject>();
        terrainThreads = new List<Thread>();

        LoadChunkMap();
        UnloadOldChunks();

        StartCoroutine(UpdateChunks());
    }

    private IEnumerator UpdateChunks() {
        while (isGenerating) {
            if (playerPos != player.position) {
                playerPos = player.position.y > heightThreshold ? player.position : new Vector3(player.position.x, 0, player.position.z);
            }

            LoadChunkMap();
            UnloadOldChunks();

            range = (int)(rangeMultiplier + Mathf.Abs(playerPos.y * 2 / chunkSize));

            yield return new WaitForSeconds(0.5f);
        }
    }

    private void LoadChunkMap() {
        for (int x = (int)playerPos.x / chunkSize - range; x <= (int)playerPos.x / chunkSize + range; x++) {
            for (int z = (int)playerPos.z / chunkSize - range; z <= (int)playerPos.z / chunkSize + range; z++) {
                Vector3 chunkPos = new Vector3(x * chunkSize, 0, z * chunkSize);

                int LOD = GetLOD(chunkPos);

                if (!chunkCache.ContainsKey(chunkPos) || chunkCache[chunkPos].LOD != LOD) {
                    Debug.Log("Generating chunk at " + chunkPos.ToString() + " with LOD " + LOD.ToString() + ".");
                    try {
                        chunkCache.Remove(chunkPos);
                        Destroy(chunkMap[chunkPos]);
                        chunkMap.Remove(chunkPos);
                    } catch {}

                    Geometry chunk = new Geometry();
                    chunk.position = chunkPos;

                    Thread thread = new Thread(() => GenerateChunk(ref chunk, LOD));

                    thread.Start();
                    terrainThreads.Add(thread);

                    foreach (Thread t in terrainThreads) {
                        t.Join();
                    }

                    chunkCache.Add(chunkPos, chunk);
                    LoadChunk(chunk);
                }
            }
        }
    }

    private void UnloadOldChunks() {
        for (int i = 0; i < chunkMap.Count; i++) {
            Vector3 chunkPos = chunkMap.ElementAt(i).Key;

            if (Vector3.Distance(chunkPos, playerPos) > (range + 1) * chunkSize) {
                Debug.Log("Unloading chunk at " + chunkPos.ToString() + ".");
                Destroy(chunkMap[chunkPos]);
                chunkMap.Remove(chunkPos);
            }
        }
    }

    private void LoadChunk(Geometry chunk) {
        if (chunk.vertices != null && !chunkMap.ContainsKey(chunk.position)) {
            mesh = new Mesh();
            mesh.vertices = chunk.vertices;
            mesh.triangles = chunk.triangles;
            mesh.uv = chunk.uvs;

            mesh.RecalculateNormals();
            mesh.RecalculateTangents();

            GameObject chunkObject = new GameObject("Chunk" + chunk.position.ToString());
            chunkObject.transform.position = chunk.position;

            MeshRenderer meshRenderer = chunkObject.AddComponent<MeshRenderer>();
            meshRenderer.material = material;

            MeshFilter meshFilter = chunkObject.AddComponent<MeshFilter>();
            meshFilter.mesh = mesh;

            chunkMap.Add(chunk.position, chunkObject);
        }
    }

    private void GenerateChunk(ref Geometry chunk, int LOD) {
        int verticesPerSide = (int)Mathf.Pow(2, LOD) + 1;
        chunk.vertices = new Vector3[(verticesPerSide + 1) * (verticesPerSide + 1)];
        chunk.triangles = new int[verticesPerSide * verticesPerSide * 6];
        chunk.uvs = new Vector2[chunk.vertices.Length];
        chunk.LOD = LOD;

        for (int x = 0, v = 0; x <= verticesPerSide; x++) {
            for (int z = 0; z <= verticesPerSide; z++, v++) {
                float xPos = x * chunkSize / (float)verticesPerSide - (chunkSize * 0.5f);
                float zPos = z * chunkSize / (float)verticesPerSide - (chunkSize * 0.5f);

                float height = noiseFilter.Evaluate(xPos + chunk.position.x, zPos + chunk.position.z);

                chunk.vertices[v] = new Vector3(xPos, height, zPos);
                chunk.uvs[v] = new Vector2((float)x / verticesPerSide, (float)z / verticesPerSide);
            }
        }

        for (int x = 0, t = 0, v = 0; x < verticesPerSide; x++, v++) {
            for (int z = 0; z < verticesPerSide; z++, v++, t += 6) {
                chunk.triangles[t] = v + verticesPerSide + 1;
                chunk.triangles[t + 1] = v;
                chunk.triangles[t + 2] = v + 1;
                chunk.triangles[t + 3] = v + verticesPerSide + 1;
                chunk.triangles[t + 4] = v + 1;
                chunk.triangles[t + 5] = v + verticesPerSide + 2;
            }
        }
    }

    private int GetLOD(Vector3 chunkPos) {
        float distance = Vector3.Distance(playerPos, chunkPos);

        for (int i = 0; i < LODDistances.Length; i++) {
            if (distance < LODDistances[i]) {
                return LODDistances.Length - i - 1;
            }
        }

        return 0;
    }
}

Comments

Popular posts from this blog

Emotion Classification NN with Keras Transformers and TensorFlow

  In this post, I discuss an emotional classification model I created and trained for the Congressional App Challenge last month. It's trained on the Google GoEmotions dataset and can detect the emotional qualities of a text. First, create a training script and initialize the following variables. checkpoint = 'distilbert-base-uncased' #model to fine-tune weights_path = 'weights/' #where weights are saved batch_size = 16 num_epochs = 5 Next, import the dataset with the Hugging Face datasets library. dataset = ds . load_dataset( 'go_emotions' , 'simplified' ) Now, we can create train and test splits for our data. def generate_split(split): text = dataset[split] . to_pandas()[ 'text' ] . to_list() labels = [ int (a[ 0 ]) for a in dataset[split] . to_pandas()[ 'labels' ] . to_list()] return (text, labels) (x_text, x_labels) = generate_split( 'train' ) (y_text, y_labels) =...

Pure Pursuit Robot Navigation Following Interpolated Cubic Splines

I've been working to improve my school VEX team's autonomous code for my robotics class, and have created a pure pursuit robotics PID that I thought I would share. The code here is in Python, and I'm only using matplotlib as an output to visualize the robot's movement. However, I do hope to rewrite this code in C++ soon, getting a movement vector output which will then be applied to a VEX robot. First is the spline class. I'm currently using a simple parametric cubic spline class. Keep in mind that this is a really  bad way to implement splines, as it demands increasing x-values along the domain which isn't ideal for a robot's path. I am definitely going to rewrite all of this in the future to have a periodic domain, but I thought I would share what I have right now anyways because it might be usef A spline is defined as a piecewise function of polynomials, and in the case of a cubic spline, the polynomials of choice are cubic polynomials. Therefore, the fir...

Exploring Active Ragdoll Systems

  Active ragdolls is the name given to wobbly, physics-based character controllers which apply forces to ragdolls. You may have seen them implemented in popular games such as Human Fall Flat  and Fall Guys . This post introduces a technique I developed to create active ragdolls for a personal project, implemented in Unity. The system I will demonstrate is surprisingly simple and only requires a small amount of code. Unity has these beautiful things called Configurable Joints , which are joints that can, as the name suggests, be configured, with simulated motors on the X and YZ axes providing force to the joints. What we can do with this is map the motions of a regular  game character with an Animation Controller (an "animator clone") to our active ragdoll. Doing this means we only have to animate the animator clone for the active ragdoll to automatically be animated with it! Firstly, I created a ragdoll from a rigged character. (Side note: Mixamo is a great tool to q...