Improving ProcGen Performance

Hi everyone, I previously had some low-performance monobehaviour code for some procedural generation. However since then, I have optimised and used the following tools:

  • ECS (Entity Component System)
  • Burst Compiler
  • Unity parallel jobs system
  • Batching

Is there any other technologies anyone would recommend I try?

Here is the revised code btw, free for anyone to use:

using System.Collections.Generic;
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using Material = UnityEngine.Material;
using Random = UnityEngine.Random;

public class ImprovedProcGen : MonoBehaviour
{
    public GenerationSettings ChunkGen;
}

[System.Serializable]
public struct GenerationSettings : IComponentData
{
    [Range(1, 254)]
    public int MeshDetail;
    public int ChunkSize, ViewRange;
    public float NoiseScale, TesselationFactor, HeightMultiplier;
    public bool InvertPlane;
    public Color TerrainColour;
}

class ImrpovedProcGenBaker : Baker<ImprovedProcGen>
{
    public override void Bake(ImprovedProcGen authoring)
    {
        Entity Base = GetEntity(flags: TransformUsageFlags.None);
        AddComponent(Base, authoring.ChunkGen);
        Debug.Log("Baked");
    }
}

public partial class MySystem : SystemBase
{
    GenerationSettings CS;
    Mesh TerrainMesh;
    int[] TriData;
    Material TerrainMat;
    float2 SeedOffset;
    float2 PrevPlayerPos = float2.zero;
    Transform PlaTra;
    EntityManager EM;

    Dictionary<float2, CombineInstance> Meshes = new Dictionary<float2, CombineInstance>();
    ChunkConfig CC;

    struct ChunkConfig
    {
        public int LOD, verticesPerSide, totalVertices, totalTriangles;
        public float2 SeedOffset;
    }

    protected override void OnStartRunning()
    {
        base.OnStartRunning();
        CS = SystemAPI.GetSingleton<GenerationSettings>();
        PlaTra = GameObject.FindFirstObjectByType<Player>().transform;

        int NLOD = math.max(CS.MeshDetail, 1);
        int VPS = NLOD + 1;
        int TV = VPS * VPS;
        int TT = NLOD * NLOD * 6;
        Random.InitState((int)(Random.value * math.pow(2, 32)));
        SeedOffset = new float2(Random.Range(-10000, 10000), Random.Range(-10000, 10000));
        CC = new ChunkConfig()
        {
            LOD = NLOD,
            verticesPerSide = VPS,
            totalVertices = TV,
            totalTriangles = TT,
            SeedOffset = new float2(Random.Range(-10000, 10000), Random.Range(-10000, 10000))
        };
        GenerateTrianglesJob TriJob = new GenerateTrianglesJob()
        {
            LOD = CC.LOD,
            verticesPerSide = CC.LOD + 1,
            triangles = new NativeArray<int>(CC.totalTriangles, Allocator.TempJob),
            F = CS.InvertPlane
        };
        JobHandle TriJobHandle = TriJob.Schedule(CC.totalTriangles, CS.MeshDetail + 1);

        TriJobHandle.Complete();

        TriData = TriJob.triangles.ToArray();

        TerrainMat = TesselatedTerrrain();

        TerrainMesh = new Mesh();
        CreateInitialChunks();
        GenerateMesh();
    }

    void CheckIfMovement()
    {
        float2 CurPos = new float2(PlaTra.position.x, PlaTra.position.z);
        int Spacing = CS.ChunkSize;
        if (CurPos.x - PrevPlayerPos.x > Spacing)
        {
            PrevPlayerPos.x += Spacing;
            GenerateChunks(true, false);
            Debug.Log("Working");
        }
        if (CurPos.x - PrevPlayerPos.x < -Spacing)
        {
            PrevPlayerPos.x -= Spacing;
            GenerateChunks(true, true);
            Debug.Log("Working");
        }
        if (CurPos.y - PrevPlayerPos.y > Spacing)
        {
            PrevPlayerPos.y += Spacing;
            GenerateChunks(false, false);
            Debug.Log("Working");
        }
        if (CurPos.y - PrevPlayerPos.y < -Spacing)
        {
            PrevPlayerPos.y -= Spacing;
            GenerateChunks(false, true);
            Debug.Log("Working");
        }
    }

    void GenerateChunks(bool XAxis, bool Negative)
    {
        int R = CS.ViewRange;
        float S = CS.ChunkSize;
        float SR = S * R * ((Negative) ? -1 : 1);
        if (XAxis)
        {
            float PX = PrevPlayerPos.x + SR;
            float NX = PrevPlayerPos.x - SR;
            for (int i = (int)-math.floor(R); i < math.floor(R); i++)
            {
                float Y = PrevPlayerPos.y + (i * S);

                CreateChunk(new float2(PX, Y));
                Meshes.Remove(new float2(NX, Y));
            }
        }
        else
        {
            float PY = PrevPlayerPos.y + SR;
            float NY = PrevPlayerPos.y - SR;
            for (int i = (int)-math.floor(R); i < math.floor(R); i++)
            {
                float X = PrevPlayerPos.x + (i * S);

                CreateChunk(new float2(X, PY));
                Meshes.Remove(new float2(X, NY));
            }
        }
        GenerateMesh();
        EM = World.DefaultGameObjectInjectionWorld.EntityManager;

        CreateColliderEntity();
        CreateCollider();
    }

    void GenerateMesh()
    {
        TerrainMesh.CombineMeshes(Meshes.Values.ToArray());
        TerrainMesh.RecalculateNormals();
        TerrainMesh.RecalculateTangents();
    }

    void CreateColliderEntity()
    {
        GameObject.CreatePrimitive(PrimitiveType.Cube);
        //ColEnt.AddComponent<UnityEngine.MeshCollider>();
    }

    void CreateCollider()
    {
        //ColEnt.GetComponent<UnityEngine.MeshCollider>().sharedMesh = TerrainMesh;
    }
    
    void CreateInitialChunks()
    {
        int ViewRange = CS.ViewRange;
        for (int x = (int) math.floor(-ViewRange); x < (int)math.floor(ViewRange); x++)
        {
            for (int y = (int)math.floor(-ViewRange); y < (int)math.floor(ViewRange); y++)
            {
                CreateChunk(new float2(x * CS.ChunkSize, y * CS.ChunkSize));
            }
        }
    }

    protected override void OnUpdate()
    {
        CheckIfMovement();
        Graphics.DrawMesh(TerrainMesh, Vector3.zero, Quaternion.identity, TerrainMat, 0);
    }

    Material TesselatedTerrrain()
    {
        Material Result = new Material(Shader.Find("Shader Graphs/Terrain"));
        Result.SetColor("_TerrainColour", CS.TerrainColour);
        Result.SetFloat("_TesselationFactor", CS.TesselationFactor);
        return Result;
    }

    CombineInstance CreateChunk(float2 pos)
    {
        LocalTransform NT = new LocalTransform()
        {
            Position = float3.zero,
            Rotation = quaternion.identity,
            Scale = 1
        };
        CombineInstance Instance = new CombineInstance()
        {
            mesh = GeneratePlane(pos), transform = NT.ToMatrix()
        };
        if (Instance.mesh != null)
        {
            Meshes.Add(pos, Instance);
        }
        return Instance;
    }

    public Mesh GeneratePlane(float2 pos)
    {
        int TVs = CC.totalVertices;
        int VPSs = CC.verticesPerSide;
        GetVerticesJob VertexJob = new GetVerticesJob()
        {
            VerticesPerSide = VPSs,
            Steps = CS.ChunkSize / (VPSs - 1f),
            NewVertices = new NativeArray<float3>(TVs, Allocator.TempJob),
            ChunkSize = CS.ChunkSize / VPSs,
            ChunkOffset = pos,
            SeedOffset = SeedOffset,
            NoiseScale = CS.NoiseScale,
            HeightMultiplier = CS.HeightMultiplier
        };
        JobHandle VertexJobHandle = VertexJob.Schedule(TVs, VPSs);
        VertexJobHandle.Complete();

        Mesh Result = new Mesh();
        Result.vertices = ConvertToVector3Array(VertexJob.NewVertices);
        VertexJob.NewVertices.Dispose();

        Result.triangles = TriData;

        return Result;
    }

    [BurstCompile]
    public struct GetVerticesJob : IJobParallelFor
    {
        public int VerticesPerSide;
        public float ChunkSize, Steps, NoiseScale, HeightMultiplier;
        public float2 ChunkOffset, SeedOffset;
        public NativeArray<float3> NewVertices;

        public void Execute(int index)
        {
            int x = index % VerticesPerSide;
            int y = index / VerticesPerSide;
            float2 Pos = new float2(x, y) * Steps;
            float2 GlobalPos = Pos + ChunkOffset;
            float2 NoisePos = GlobalPos * NoiseScale + SeedOffset;
            float Height = Mathf.PerlinNoise(NoisePos.x, NoisePos.y) * HeightMultiplier;
            NewVertices[index] = new float3(GlobalPos.x, Height, GlobalPos.y);
        }
    }

    public static Vector3[] ConvertToVector3Array(NativeArray<float3> float3Array)
    {
        int length = float3Array.Length;
        Vector3[] vector3Array = new Vector3[length];

        for (int i = 0; i < length; i++)
        {
            vector3Array[i] = new Vector3(float3Array[i].x, float3Array[i].y, float3Array[i].z);
        }

        return vector3Array;
    }

    [BurstCompile]
    public struct GenerateTrianglesJob : IJobParallelFor
    {
        [ReadOnly] public int LOD, verticesPerSide;
        [ReadOnly] public bool F; //Flipped triangles?

        [NativeDisableParallelForRestriction, WriteOnly] public NativeArray<int> triangles;

        public void Execute(int y)
        {
            int triIndex = y * LOD * 6; // Each row of quads generates LOD * 6 triangle indices (2 triangles per quad, 3 vertices per triangle)

            for (int x = 0; x < LOD; x++)
            {
                // Get the indices of the square's vertices
                int bottomLeft = y * verticesPerSide + x;
                int bottomRight = bottomLeft + 1;
                int topLeft = bottomLeft + verticesPerSide;
                int topRight = topLeft + 1;

                // First triangle (bottom-left to top-left to top-right)
                triangles[triIndex++] = bottomLeft;
                triangles[triIndex++] = (F) ? topRight : topLeft;
                triangles[triIndex++] = (F) ? topLeft : topRight;

                // Second triangle (bottom-left to top-right to bottom-right)
                triangles[triIndex++] = bottomLeft;
                triangles[triIndex++] = (F) ? bottomRight : topRight;
                triangles[triIndex++] = (F) ? topRight : bottomRight;
            }
        }
    }
}

1 Like

I’d suggest using some of the newer Mesh APIs that take NativeArrays instead of managed arrays. And you can also look into the low-level MeshData API.

As in this?

1 Like

Yes. That. Though I would at least try the NativeArray APIs first.

Haven’t I used NativeArray in my jobs?
Also curious question, blank URP projects seem to not be able to get 1000fps.

Yes, but it looks like you are still converting them to managed arrays when assigning the data to your Mesh. You don’t need to do that.