3
\$\begingroup\$

I'm making a terrain-deformation system in Unity that utilizes the common marching-cubes algorithm. So far I've gotten the system working, employed Unity's job system and burst compiler, and managed to cut down frame calculation time from ~100ms per job to ~25ms. That's great, and I'm getting around 20 FPS during the worst performance drops when two jobs have to run end-to-end, so I'm super proud of that. However 20 FPS is still pretty choppy and I would like to cut it down further, but I've been looking at this so long I'm probably missing some easy performance gains. Does anyone have any insight into wasteful practices I'm overlooking in my mesh deformation script?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;

public class Chunk
{

    // Chunk data
    public GameObject chunkObject;
    MeshFilter meshFilter;
    MeshCollider meshCollider;
    MeshRenderer meshRenderer;
    Vector3Int chunkPosition;



    // Job system data
    private NativeList<Vector3> marchVerts;
    private NativeList<int> marchTris;
    MarchCubeJob instanceMarchCube;
    public JobHandle jobHandler;
    
    // Density map of terrain
    // [x + (chunkWidth+1) * (y + (ChunkHeight+1) * z)] is the formula to retrieve [x, y, z]
    private NativeArray<float> passMap;



    // World settings
    int chunkWidth { get { return Terrain_Data.chunkWidth;}}
    int ChunkHeight { get { return Terrain_Data.chunkHeight;}}
    static float terrainSurface { get { return Terrain_Data.terrainSurface;}}



    // Constructor
    public Chunk (Vector3Int _position){

        passMap = new NativeArray<float>((chunkWidth+1)*(chunkWidth+1)*(ChunkHeight+1), Allocator.Persistent);
        chunkObject = new GameObject();
        chunkObject.name = string.Format("Chunk x{0}, y{1}, z{2}", _position.x, _position.y, _position.z);
        chunkPosition = _position;
        chunkObject.transform.position = chunkPosition;
        meshRenderer = chunkObject.AddComponent<MeshRenderer>();
        meshFilter = chunkObject.AddComponent<MeshFilter>();
        meshCollider = chunkObject.AddComponent<MeshCollider>();
        chunkObject.transform.tag = "Terrain";
        meshRenderer.material = Resources.Load<Material>("Materials/Terrain");
        
        // Generate chunk
        PopulateTerrainMap();
        CreateMeshData();
        FinishMesh();
    }



    // Get natural layout of terrainMap based on random generation
    void PopulateTerrainMap(){

        for (int x = 0; x < chunkWidth + 1; x++) {
            for (int y = 0; y < ChunkHeight + 1; y++) {
                for (int z = 0; z < chunkWidth + 1; z++) {
                    
                    passMap[x + (chunkWidth+1) * (y + (ChunkHeight+1) * z)] = Terrain_Data.GetTerrainPoint(x + chunkPosition.x, y + chunkPosition.y, z + chunkPosition.z);

                }
            }
        }

    }



    // Setup job system for marching cubes to get vertex data
    void CreateMeshData(){

        // Set up memory pointers
        marchVerts = new NativeList<Vector3>(1,  Allocator.TempJob);
        marchTris = new NativeList<int>(1,  Allocator.TempJob);

        // Call marchcubes job
        instanceMarchCube = new MarchCubeJob(){
            marchVerts = marchVerts,
            marchTris = marchTris,
            mapSample = passMap
        };

        // Run job for this chunk
        jobHandler = instanceMarchCube.Schedule();

    }



    // Finish meshes on affected chunks
    public void FinishMesh(){
        
        // Make sure chunk job is complete
        jobHandler.Complete();

        // Build mesh for this chunk
        BuildMesh();

        // Dispose of memory pointers
        marchVerts.Dispose();
        marchTris.Dispose();
    }



    // Increase/decrease terrain density in a sphere
    public void ModifyTerrain (Vector3 pos, int radius, float speed){

        Vector3Int v3Int = new Vector3Int(Mathf.RoundToInt(pos.x), Mathf.RoundToInt(pos.y), Mathf.RoundToInt(pos.z));
        v3Int -= chunkPosition;

        // Set bounds
        int xlow = Mathf.Clamp(v3Int.x - radius, 0, chunkWidth);
        int ylow = Mathf.Clamp(v3Int.y - radius, 0, ChunkHeight);
        int zlow = Mathf.Clamp(v3Int.z - radius, 0, chunkWidth);
        
        int xhigh = Mathf.Clamp(v3Int.x + radius, 0, chunkWidth + 1);
        int yhigh = Mathf.Clamp(v3Int.y + radius, 0, ChunkHeight + 1);
        int zhigh = Mathf.Clamp(v3Int.z + radius, 0, chunkWidth + 1);

        // Set new terrainMap values
        for (int x = xlow; x < xhigh; x++){
            for (int y = ylow; y < yhigh; y++){
                for (int z = zlow; z < zhigh; z++){
                    float dist = Vector3.Distance(new Vector3(x, y, z), v3Int);
                    if (dist < radius){
                        passMap[x + (chunkWidth+1) * (y + (ChunkHeight+1) * z)] += speed;
                    }
                }
            }
        }

        // Call marchcubes and draw mesh
        CreateMeshData();

    }



    // Draw mesh using vertex values
    void BuildMesh(){

        Mesh mesh = new Mesh();
        mesh.vertices = instanceMarchCube.marchVerts.ToArray();
        mesh.triangles = instanceMarchCube.marchTris.ToArray();
        mesh.RecalculateNormals();
        meshFilter.mesh = mesh;
        meshCollider.sharedMesh = mesh;

    }



    // Dispose native data on destroy
    private void OnDestroy(){
        marchVerts.Dispose();
        marchTris.Dispose();
        passMap.Dispose();
    }



    // Build a chunk using marching cubes as a job
    [BurstCompile]
    public struct MarchCubeJob: IJob{

        int chunkWidth { get { return Terrain_Data.chunkWidth;}}
        int ChunkHeight { get { return Terrain_Data.chunkHeight;}}
        static float terrainSurface { get { return Terrain_Data.terrainSurface;}}

        public NativeList<Vector3> marchVerts;
        [WriteOnly] public NativeList<int> marchTris;
        [ReadOnly] public NativeArray<float> mapSample;



        public void Execute(){

            for (int x = 0; x < chunkWidth; x++) {
                for (int y = 0; y < ChunkHeight; y++) {
                    for (int z = 0; z < chunkWidth; z++) {

                        // Instantiate position var
                        Vector3Int position = new Vector3Int(x, y, z);


                        //Sample terrain values to get density at each corner of cube
                        float[] cube = new float[8];
                        for (int i = 0; i < 8; i++){

                            cube[i] = SampleTerrainMap(position + Terrain_Data.CornerTable[i]);

                        }

                        // Get configuration index of cube by comparing corner density to terrainSurface value
                        int configIndex = GetCubeConfiguration(cube);

                        // If cube is empty (-1 in all vertices means there is no cube here)
                        if (configIndex == 0 || configIndex == 255){
                            continue;
                        }

                        // Start mesh vertex calculations
                        int edgeIndex = 0;
                        for (int i = 0; i < 5; i++){ // Triangles
                            for (int p = 0; p < 3; p++){ // Tri Vertices

                                int indice = Terrain_Data.TriangleTable[configIndex, edgeIndex];

                                if (indice == -1){
                                    continue;
                                }

                                // Get 2 points of edge
                                Vector3 vert1 = position + Terrain_Data.CornerTable[Terrain_Data.EdgeIndexes[indice, 0]];
                                Vector3 vert2 = position + Terrain_Data.CornerTable[Terrain_Data.EdgeIndexes[indice, 1]];

                                Vector3 vertPosition;

                                // Smooth terrain
                                // Sample terrain values at either end of current edge
                                float vert1Sample = cube[Terrain_Data.EdgeIndexes[indice, 0]];
                                float vert2Sample = cube[Terrain_Data.EdgeIndexes[indice, 1]];

                                // Calculate difference between terrain values
                                float difference = vert2Sample - vert1Sample;

                                if (difference == 0){
                                    difference = terrainSurface;
                                }
                                else{
                                    difference = (terrainSurface - vert1Sample) / difference;
                                }

                                vertPosition = vert1 + ((vert2 - vert1) * difference);

                                marchVerts.Add(vertPosition);
                                marchTris.Add(marchVerts.Length - 1);
                                edgeIndex++;
                            }
                        }
                    }
                }
            }
        }



        static int GetCubeConfiguration(float[] cube){

            int configurationIndex = 0;
            for (int i = 0; i < 8; i++){

                if (cube[i] > terrainSurface){
                    configurationIndex |= 1 << i;
                }

            }

            return configurationIndex;

        }



        public float SampleTerrainMap(Vector3Int point){

            return mapSample[point.x + (chunkWidth+1) * (point.y + (ChunkHeight+1) * point.z)];

        }

    }

}
             

A short summary of the execution order:

It starts by having a separate object call ModifyTerrain() for each affected chunk in the gameworld.

ModifyTerrain() then scrolls through each point in the chunk and updates the NativeArray(float) passMap, a representation of the density of the ground at that point.

It then calls CreateMeshData(), which allocates the NativeList(Vector3) marchVerts and NativeList(int) marchTris before creating a MarchCubeJob() job to calculate the chunk mesh.

The same outside object then calls FinishMesh() in LateUpdate() for the same affected chunks, which completes all jobs as a priority, copies the marchVerts and marchTris NativeLists into the mesh construction method BuildMesh().

If you have any useful critique on my code I'd be happy to hear it! I don't have much experience with optimization so every little bit helps. Thanks!

\$\endgroup\$
3
  • \$\begingroup\$ Welcome to the Code Review Community. The question would be better if the title just stated what the code does, and you addressed your concerns about the code within the body of the question. \$\endgroup\$ – pacmaninbw Apr 20 at 3:48
  • \$\begingroup\$ Thanks @pacmaninbw, I changed my title to the main topic I'm trying to get information on - that is to say optimizing c# procedural mesh generation. Do you have any insight into the matter? I would guess eliminating for loops would be the biggest time save, but I'm not sure that's possible with how I'm executing the methods as of now. Perhaps how to make marchVerts write-only? I'm kinda stumped. \$\endgroup\$ – Seth McCann Apr 20 at 5:01
  • \$\begingroup\$ I've since made a big improvement by moving the position and cube allocators to the beginning of the job execution and instead modifying their values. Job timing is down to ~8ms. \$\endgroup\$ – Seth McCann Apr 20 at 17:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.