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...

Alpha-Beta Pruning Minimax Chess Bot

I've written a chess bot that uses the minimax algorithm to play chess, even though I barely know how to play. What could possibly  go wrong? Minimax is a decision rule in game theory that seeks to minimize the possible loss of a situation, or in this case, to prevent the AI from being at a "disadvantage" with the player. The bot plays through a bunch of moves, determines how much of a loss they incur, and chooses a move with the lowest disadvantage. My implementation is in Python and uses the python-chess   library to create a chessboard. So, let's look at the code. First, I set up the board as follows: board = chess . Board() values = {chess . PAWN: 1 , chess . KNIGHT: 3 , chess . BISHOP: 3 , chess . ROOK: 5 , chess . QUEEN: 9 , chess . KING: 0 } To calculate advantage, I iterate through all of the pieces on the chess board and add or subtract their values from a running count depending on their color. By weighing more valuable pieces, you could be...