0

I'm working on a unity project involving deformable terrain based on marching-cubes. It works by generating a map of density over the 3-dimensional coordinates of a terrain chunk and using that data to create a mesh representing the surface of the terrain. It has been working, however the process is very slow. I'm attempting to introduce multithreading to improve performance, but I've run into a problem that's left me scratching my head.

When I run CreateMeshData() and try to pass my density map terrainMap into the MarchCubeJob struct, it recognizes it as a reference type, not a value type. I've seemed to whittle down the errors to this one, but I've tried to introduce the data in every way I know how and I'm stumped. I thought passing a reference like this was supposed to create a copy of the data disconnected from the reference, but my understanding must be flawed. My goal is to pass each marchingcube cube into a job and have them run concurrently.

I'm brand new to multithreading, so I've probably made some newbie mistakes here and I'd appreciate if someone would help me out with a second look. Cheers!

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

public class Chunk
{

    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();

    public GameObject chunkObject;
    MeshFilter meshFilter;
    MeshCollider meshCollider;
    MeshRenderer meshRenderer;

    Vector3Int chunkPosition;

    public float[,,] terrainMap;
    
    // Job system
    NativeList<Vector3> marchVerts;
    NativeList<Vector3> marchTris;
    MarchCubeJob instanceMarchCube;
    JobHandle instanceJobHandle;


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

    public Chunk (Vector3Int _position){ // Constructor

        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";
        terrainMap = new float[width + 1, height + 1, width + 1]; // Weight of each point
        meshRenderer.material = Resources.Load<Material>("Materials/Terrain");
        
        // Generate chunk
        PopulateTerrainMap();
        CreateMeshData();
    }

    void PopulateTerrainMap(){
        ...
    }

    void CreateMeshData(){

        ClearMeshData();

        vertices = new List<Vector3>();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                for (int z = 0; z < width; z++) {

                    Debug.Log(x + ", " + y + ", " + z + ", begin");

                    Vector3Int position = new Vector3Int(x, y, z);

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

                    // Split marchcube into jobs by cube
                    instanceMarchCube = new MarchCubeJob(){
                        position = position,
                        marchVerts = marchVerts,
                        marchTris = marchTris,
                        mapSample = terrainMap
                    };

                    // Run job for each cube in a chunk
                    instanceJobHandle = instanceMarchCube.Schedule();
                    instanceJobHandle.Complete();

                    // Copy data from job to mesh data
                    //instanceMarchCube.marchVerts.CopyTo(vertices);
                    vertices.AddRange(marchVerts);
                    triangles.AddRange(marchTris);

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

                    Debug.Log(x + ", " + y + ", " + z + ", end");
                }
            }
        }

        BuildMesh();

    }

    public void PlaceTerrain (Vector3 pos, int radius, float speed){

        ...

        CreateMeshData();

    }

    public void RemoveTerrain (Vector3 pos, int radius, float speed){

        ...

        CreateMeshData();

    }

    void ClearMeshData(){

        vertices.Clear();
        triangles.Clear();

    }

    void BuildMesh(){

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

    }

    private void OnDestroy(){
        marchVerts.Dispose();
        marchTris.Dispose();
    }

}

// Build a cube as a job
[BurstCompile]
public struct MarchCubeJob: IJob{

    static float terrainSurface { get { return Terrain_Data.terrainSurface;}}

    public Vector3Int position;

    public NativeList<Vector3> marchVerts;
    public NativeList<int> marchTris;
    public float[,,] mapSample;

    public void Execute(){

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

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

        }

        int configIndex = GetCubeConfiguration(cube);

        // If done (-1 means there are no more vertices)
        if (configIndex == 0 || configIndex == 255){
            return;
        }

        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){
                    return;
                }

                // 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 SampleTerrain(Vector3Int point){

        return mapSample[point.x, point.y, point.z];

    }

}
  • 3
    _"Why does the array I pass to my multithreading job struct act as a reference type?"_ -- why shouldn't it? Arrays _are_ reference types, after all. _"I thought passing a reference like this was supposed to create a copy of the data disconnected from the reference"_ -- that _never_ happens in C#. References are always passed as references; copies are _never_ implicitly made. – Peter Duniho Apr 12 '21 at 02:44
  • @PeterDuniho Thanks for the reply, so if I were to copy my float[,,] terrainMap manually via .clone() and pass that into mapSample, would that make it a value type? I'm not sure I quite grasp what's going on here. – Seth McCann Apr 12 '21 at 02:53
  • _"if I were to copy my float[,,] terrainMap manually via .clone() and pass that into mapSample, would that make it a value type?"_ -- no, types are inherently reference or value types. You can't change that after the fact. But, copying the array would probably have the result you seem to want, nevertheless. The `float` type _is_ a value type, so a shallow copy of the array as you suggest will duplicate those values, severing any connection to the original array. – Peter Duniho Apr 12 '21 at 03:05
  • (cloning the array constantly would probably be a bad idea from a performance perspective, note) – Marc Gravell Apr 12 '21 at 06:51
  • Also what is the use of creating jobs and then use `Complete` so the calling method freezes until the job is finished...? Then you can as well directly do it synchronous – derHugo Apr 12 '21 at 10:33
  • I was planning on adding a delay after I got the ball rolling, but I've got to improve my understanding of it first and get something working. I'm thinking of copying the array once at the start of the method and passing that in, then delaying the completion of all jobs until they're required in **BuildMesh()**. – Seth McCann Apr 12 '21 at 20:06

0 Answers0