Poly Coding

Marching Cubes Part 2: Generating a mesh with marching cubes

0. Introduction

1. Noise

2. Marching cubes mesh

3. Downloads

Marching Cubes Terrain

Terrain made with marching cubes

0. Introduction

In the first part we went over how the marching cubes algorithm creates a mesh given a 3D grid. In this part we will learn how to translate that understanding of the algorithm to an actual mesh in Unity. For constructing the actual mesh, we will use compute shaders as they speed up the process of calculating the configuration for each cube significantly. You can read more about compute shaders in this tutorial.

1. Noise

Before we start with generating a mesh, we first have to fill our grid with values. These values are responsable for telling us if the point is either underground or above ground.

Let’s start by creating a new Unity project. In this project, we wan’t to begin by creating a new static class called “GridMetrics”, this class will be used to store values that will be used all over the project, such as chunk size, number of threads per workgroup (for the compute shader), etc.

public static class GridMetrics 
{
	public const int NumThreads = 8;
	public const int PointsPerChunk = 8;
}

We will start with a very small chunk size for testing purposes, by the end of this guide, we will increase it.

In this tutorial series I will assume that each chunk size in our terrain will always be a multiple of 8, this makes sure that we will not have to spawn many threads on the GPU that are unused because of the arbitrary grid sizes.

1.1 Setup

To setup our grid, create a new C# script called “NoiseGenerator” (Right Click > Create > C# Script). Attach the script to a new object in the hierarchy.

File Structure

In this script, define a new compute buffer, this buffer is responsable for communicating the noise values generated by the GPU compute shader back to the CPU C# script.

...
public class NoiseGenerator : MonoBehaviour
{
    ComputeBuffer _weightsBuffer;
}    

To initialize our compute buffer, we need to know the size of our 3D noise grid, which we stored in our GridMetrics. We also need to know the size of a single element. Since we just want a single noise value for each point in our grid, we will fill our noise grid with floats. Create a function to create the buffer and another function to release it.

private void Awake() {
    CreateBuffers();
}
private void OnDestroy() {
    ReleaseBuffers();
}

void CreateBuffers() {
    _weightsBuffer = new ComputeBuffer(
        GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk, sizeof(float)
    );
}

void ReleaseBuffers() {
    _weightsBuffer.Release();
}

Next, create a function that generates an array of floats (this is a 1D array, as our compute shader will return a 1D array), which we will use to generate noise values for single chunk. Initialize the array with the size of our grid.

public float[] GetNoise() {
    float[] noiseValues = 
        new float[GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk];
    return noiseValues;
}

To execute our noise generation we first have to have a reference to the compute shader that will take care of generating the noise values. Create a new compute shader called “NoiseCompute” (Right Click > Create > Shader > Compute Shader) and add a variable field for it in the NoiseGenerator.

File Structure

public ComputeShader NoiseShader;

Now that we have our shader, we still need to set the buffer to be able to communicate between GPU and CPU.

public float[] GetNoise() {
    ...
    NoiseShader.SetBuffer(0, "_Weights", _weightsBuffer);

    return noiseValues;
}

Now, we dispatch the shader. We will only have single kernel in this shader, so use kernel id 0. We also need to tell the shader the size of our chunk/mesh and how many workgroups to create. Our chunk size is 8. Our number of threads per workgroup is 8. Therefore we have to have 1 workgroups for each dimension to create a grid of 8 by 8 by 8.

public float[] GetNoise() {
    ...
    NoiseShader.Dispatch(
        0, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads
    );

    return noiseValues;
}

Lastly, after dispatching and executing the compute shader, we expect to find the generated noise values on the buffer. Transfer the data on the buffer back to our noiseValues array.

public float[] GetNoise() {
    ...
    _weightsBuffer.GetData(noiseValues);

    return heights;
}

Now that we have executed noise generation, we also want to display the values on the screen. Let’s create a new script C# “NoiseVisual” to show us our output.

Attach this new scipt to a new object in the hierarchy.

File Structure

In this script, we want to have a reference to our noise generator and call the generate noise function on start.

public class NoiseVisual : MonoBehaviour
{
    public NoiseGenerator NoiseGenerator;

    float[] _weights;

    void Start()
    {
        _weights = NoiseGenerator.GetNoise();
    }
}

To display the weights, loop through them in the OnDrawGizmos function which allows us to draw shapes to the screen in editor.

As our noise array is a 1D array, but we want to display a 3D array, we need to convert the 3D index (xyz) to a 1D index with the following formula:

int index = x + GridMetricPointsPerChunk * (y + GridMetrics.PointsPerChunk z)

Before drawing our grid, make sure it actually contains values. Let’s also color our weights in such a way that if the noise value is 1, the point will be displayed as white, and when the value is 0, it will be black. We lerp between the colors with our noise value to get some gray colors too.

private void OnDrawGizmos() {
        if (_weights == null || _weights.Length == 0) {
            return;
        }
        for (int x = 0; x < GridMetrics.PointsPerChunk; x++) {
            for (int y = 0; y < GridMetrics.PointsPerChunk; y++) {
                for (int z = 0; z < GridMetrics.PointsPerChunk; z++) {
                    int index = x + GridMetrics.PointsPerChunk * (y + GridMetrics.PointsPerChunk * z);
                    float noiseValue = _weights[index];
                    Gizmos.color = Color.Lerp(Color.black, Color.white, noiseValue);
                    Gizmos.DrawCube(new Vector3(x, y, z), Vector3.one * .2f);
                }
            }
        }
    }

To make sure this works, let’s quickly populate our _weights with random values.

void Start()
{
    //_weights = NoiseGenerator.GetNoise();

    _weights = new float[GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk];
    for (int i = 0; i < _weights.Length; i++) {
        _weights[i] = Random.value;
    }
}

Running the game in editor should now give us a 8x8x8 grid. Don’t forget to turn on Gizmos!

Random noise

Random noise

1.2 Sampling Noise

While we can display noise values, we obviously don’t want them to be completely random. We want to make noise that somewhat reflects real terrain. For this we will use pseudo random noise (structured noise). This series is not a tutorial on how to generate noise from scratch though, so we will be using a noise library. I will be using a library called FastNoiseLite. Download the HLSL file and put it inside your project. I used the following file structure:

File Structure

My file structure

In the previous section we already created a compute shader called “NoiseCompute”. Open this shader and clear it of its boilerplate code, we won’t be needing it. In this compute script, we want to create a 3D array consisting of noise values. Since we use a library, let’s include this library file in our NoiseCompute. Don’t forget to use the right file extension! You may be using FastNoiseLite.hlsl instead of .compute.

#pragma kernel GenerateNoise

#include "Includes\FastNoiseLite.compute"

[numthreads(8,8,1)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
}

I also went ahead and changed our kernel from “CSMAIN” to “GenerateNoise”.

All of our numthreads will be the same, for clarity sake, let’s create a constant int for this nonetheless. These numthreads should always equal the defined numthreads in our static “GridMetrics” class.

static const uint numThreads = 8;

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
}

To store the values of our points in the grid, we need a RWStructuredBuffer, this however is a 1D array so we need to make sure when adding to it we use the 1D index, and not the 3D one. Let’s create a helper function for this.

RWStructuredBuffer<float> _Weights;

int indexFromCoord(int x, int y, int z)
{
    return x + _ChunkSize * (y + _ChunkSize * z);
}

To go from a 3D position (which is stored in our id of the thread within the workgroup of the compute shader) we require the size of our terrain chunk.

...
int _ChunkSize;

Now we can actually start to generate some noise. As with almost all noise, we want to have some control over how this noise is generated. We might want some control over what the base height is of our ground, how high the mountains reach, etc.

...
float _NoiseScale;
float _Amplitude;
float _Frequency;
int _Octaves;
float _GroundPercent;

The noise scale tells us how stretched out the noise will be. – Note (update 2023): this variable is not needed! The same effect can be achieved by only using frequency, I therefore recommend to not include this variable (or to set it to 1 at all times) as it could be an unnecessary added complexity –

The amplitude determines how high our terrain will reach, high amplitude means high mountains.

Frequency tells us where to sample noise, when we have a high frequency, we will sample the noise further away from eachother. This can give us quite chaotic noise as the sampling will be less coherent.

Octaves gives us details, when the noise is generated for a point, we will essentially regenerate it but at a smaller sample size and this for the amount of octaves.

Finally, the ground percent just tells us where along the height of the chunk we want to be above ground.

If what these variables exactly do isn’t all that clear, they will become a lot clearer once you can start playing with the variables of the terrain mesh by the end of this tutorial.

Next, we have to tell the noise library about our settings. I opted to use simplex noise with the ridged fractal type. The ridgid noise gives the noise a bit more of a natural feeling.

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
    fnl_state noise = fnlCreateState();
    noise.noise_type = FNL_NOISE_OPENSIMPLEX2;
    noise.fractal_type = FNL_FRACTAL_RIDGED;
    noise.frequency = _Frequency;
    noise.octaves = _Octaves;
}    

To sample and create the noise in such a way that it feels like a terrain, we first have to take into account the position to sample at.

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
    ...
    float3 pos = id * _NoiseScale;
}    

Once we have that, let’s define the height of our ground. This is done by taking the negative of our y position and adding some value to it (our base ground).

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
    ...
    float3 pos = id * _NoiseScale;.
    float ground = -pos.y + (_GroundPercent * _ChunkSize);
}    

Finally, sample the noise, add the ground and raise all of it with the amplitude.

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
    ...
    float3 pos = id * _NoiseScale;
    float ground = -pos.y + (_GroundPercent * _ChunkSize);
    float n = ground + fnlGetNoise3D(noise, pos.x, pos.y, pos.z) * _Amplitude;
}    

Let’s also not forget to actually put the noise in to our weights array.

[numthreads(numThreads, numThreads, numThreads)]
void GenerateNoise(uint3 id : SV_DispatchThreadID)
{
    ...
    _Weights[indexFromCoord(id.x, id.y, id.z)] = n;
}    

That should be it for our noise. Return to the NoiseGenerator and let’s set the noise paramaters on the shader, initialize them with some default values.

public class NoiseGenerator : MonoBehaviour
{
    [SerializeField] float noiseScale = 1f;
    [SerializeField] float amplitude = 5f;
    [SerializeField] float frequency = 0.005f;
    [SerializeField] int octaves = 8;
    [SerializeField, Range(0f, 1f)] float groundPercent = 0.2f;
}    
public class NoiseGenerator : MonoBehaviour
{
    public float[] GetNoise() {
        float[] noiseValues =
            new float[GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk];

        NoiseShader.SetBuffer(0, "_Weights", _weightsBuffer);

        NoiseShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk);
        NoiseShader.SetFloat("_NoiseScale", noiseScale);
        NoiseShader.SetFloat("_Amplitude", amplitude);
        NoiseShader.SetFloat("_Frequency", frequency);
        NoiseShader.SetInt("_Octaves", octaves);
        NoiseShader.SetFloat("_GroundPercent", groundPercent);


        NoiseShader.Dispatch(
            0, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads
        );

        _weightsBuffer.GetData(noiseValues);

        return noiseValues;
    }
}    

In case you haven’t already, return to the NoiseVisual and uncomment our GetNoise().

public class NoiseVisual : MonoBehaviour
{
    ...
    void Start() {
        _weights = NoiseGenerator.GetNoise();

        /*
        _weights = new float[GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk];
        for (int i = 0; i < _weights.Length; i++) {
        }
        */
    }
}

In case your computer can handle it, you can also set the PointsPerChunk to 16 (or higher).

public static class GridMetrics {
	public const int NumThreads = 8;
	public const int PointsPerChunk = 16;
}

Now, run the game.

Generated noise value

Generated noise value

Download: unity package with code

1. Marching cubes mesh

Since I have already explained before how the marching cubes algorithm works in the previous part, I won’t go over it again in detail.

Why didn't you include this part in the previous one? This question has two anwers:
  • Otherwise the guide would become quite long
  • Some people are more interested in an explanation of the algorithm, rather than a full tutorial on how to make meshes with it. The previous part was also not specific to Unity.
---

To start, we will rename our NoiseVisual to something more fitting. Since we will be creating terrain and terrains have many chunks, rename “NoiseVisual” to “Chunk”.

public class Chunk : MonoBehaviour
{
    ...
}

In here we need to call our marching cubes compute.

public class Chunk : MonoBehaviour
{
    public ComputeShader MarchingShader;
    ...
}

As mentioned in the previous part, we can use custom structs to pass data on the computeBuffer. We want to pass Triangles. A single triangle exists out of 3 vertices. Every triangle is therefore the size of 3 times 3 floats (xyz * 3).

public class Chunk : MonoBehaviour
{
    public ComputeShader MarchingShader;
    
    struct Triangle {
        public Vector3 a;
        public Vector3 b;
        public Vector3 c;

        public static int SizeOf => sizeof(float) * 3 * 3;
    }
    
    ...
}

Just as with our NoiseGenerator we will need buffers to transfer data between the CPU and the GPU. For marching cubes, we will need 3 buffers.

public class Chunk : MonoBehaviour
{
    ComputeBuffer _trianglesBuffer;
    ComputeBuffer _trianglesCountBuffer;
    ComputeBuffer _weightsBuffer;
}

The triangles buffer holds all of our triangle objects that will be generated by marching cubes

Since we can’t know how many triangles will be generated, we have to keep track of them

Our weights buffer contains the noise values generated in the first part of this tutorial

Just like before, create functions to create and release the buffers.

public class Chunk : MonoBehaviour
{
    ...

    private void Awake() {
        CreateBuffers();
    }

    private void OnDestroy() {
        ReleaseBuffers();
    }

    void CreateBuffers() {
        _trianglesBuffer = new ComputeBuffer(5 * (GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk), Triangle.SizeOf, ComputeBufferType.Append);
        _trianglesCountBuffer = new ComputeBuffer(1, sizeof(int), ComputeBufferType.Raw);
        _weightsBuffer = new ComputeBuffer(GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk * GridMetrics.PointsPerChunk, sizeof(float));
    }

    void ReleaseBuffers() {
        _trianglesBuffer.Release();
        _trianglesCountBuffer.Release();
        _weightsBuffer.Release();
    }
}

Going over the CreateBuffers function.

Since we have to tell the buffers the maximum amount of elements it can contain, we need to initialize the trianglesBuffer with 5 * the total grid size. 5 is derived from the fact that there can at most be 5 triangles per cube configuration.

We already defined the size of a single triangle which is the stride of the buffer. We also have to make our buffer of type append. Just as a regular buffer is similar to an array, the append buffer is similar to a list. Instead of having to use an index to add to the list, we can simply call list.Append(myElement)

The triangles count buffer will simply contain a single integer.

The weightsBuffer, just like the weightsBuffer in the NoiseGenerator, contains a single float value per point in the 3D grid.

Create a function ConstructMesh. In this function we want to first set our buffers.

Mesh ConstructMesh() {
    MarchingShader.SetBuffer(0, "_Triangles", _trianglesBuffer);
    MarchingShader.SetBuffer(0, "_Weights", _weightsBuffer);
}

Then, set some of the parameters like isoLevel, noise values, etc. used by marching cubes. Also set our trianglesBuffer counter to 0. So that we know for sure that the trianglesBuffer contains 0 elements.

Dispatch our marching cubes.

Mesh ConstructMesh() {
    MarchingShader.SetBuffer(0, "_Triangles", _trianglesBuffer);
    MarchingShader.SetBuffer(0, "_Weights", _weightsBuffer);

    MarchingShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk;
    MarchingShader.SetFloat("_IsoLevel", .5f);

    _weightsBuffer.SetData(_weights);
    _trianglesBuffer.SetCounterValue(0);

    MarchingShader.Dispatch(0, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads, GridMetrics.PointsPerChunk / GridMetrics.NumThreads);
}

Let’s now focus on the marching cubes compute shader. Create a MarchingCubesCompute compute shader (Create > Shader > Compute Shader).

Download the marching cubes lookup tables (HLSL). Put the lookup tables in the Assets/Scripts/Compute/Includes project folder.

Open the MarchingCubesCompute. Here we will rely on the code of the previous part. Start by including the lookup tables and renaming the kernel to “March”. Also add our const numThreads.

#pragma kernel March

#include "Includes\MarchingTable.hlsl"

static const uint numThreads = 8;

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
}

Add the parameters needed for the algorithm.

#pragma kernel March

#include "Includes\MarchingTable.compute"

...

RWStructuredBuffer<float> _Weights;

float _IsoLevel;
int _ChunkSize;

struct Triangle {
    float3 a, b, c;
};

AppendStructuredBuffer<Triangle> _Triangles;

...

To start, in our March function we need to first check whether we are inside of our grid. Since we are using cubes, we need to stop one before the end of our points per chunk, so that we won’t go out of bounds.

File Structure

Out of bounds example

In the image above, we are using 5 points on our x axis, as you can see, even though we have 5 points, we need to use 4 squares to fill up the grid. The same applies to our 3D grid.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    if (id.x >= _ChunkSize - 1 || id.y >= _ChunkSize - 1 || id.z >= _ChunkSize - 1)
    {
        return;
    }
}

Next, get the noise values at the corners of our cubes. Again, like our NoiseCompute, for this we need to know the index of the noise values. Create a new compute shader called “MetricsCompute”, put it into the Includes folder.

In this helperCompute, we will keep track of some shared functions and variables. Like so:

static const uint numThreads = 8;

int _ChunkSize;

int indexFromCoord(int x, int y, int z)
{
    return x + _ChunkSize * (y + _ChunkSize * z);
}

Inside of our NoiseCompute, remove the numThreads, chunkSize and indexFromCoord. Include the MetricsCompute.

Let’s return to our Marching Cubes shader and get the noise values of the corners of the cube.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...

    float cubeValues[8] = {
       _Weights[indexFromCoord(id.x, id.y, id.z + 1)],
       _Weights[indexFromCoord(id.x + 1, id.y, id.z + 1)],
       _Weights[indexFromCoord(id.x + 1, id.y, id.z)],
       _Weights[indexFromCoord(id.x, id.y, id.z)],
       _Weights[indexFromCoord(id.x, id.y + 1, id.z + 1)],
       _Weights[indexFromCoord(id.x + 1, id.y + 1, id.z + 1)],
       _Weights[indexFromCoord(id.x + 1, id.y + 1, id.z)],
       _Weights[indexFromCoord(id.x, id.y + 1, id.z)]
    };
}

The following should be familiar from the previous part.

Get the cube configuration.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...
    int cubeIndex = 0;
    if (cubeValues[0] < _IsoLevel) cubeIndex |= 1;
    if (cubeValues[1] < _IsoLevel) cubeIndex |= 2;
    if (cubeValues[2] < _IsoLevel) cubeIndex |= 4;
    if (cubeValues[3] < _IsoLevel) cubeIndex |= 8;
    if (cubeValues[4] < _IsoLevel) cubeIndex |= 16;
    if (cubeValues[5] < _IsoLevel) cubeIndex |= 32;
    if (cubeValues[6] < _IsoLevel) cubeIndex |= 64;
    if (cubeValues[7] < _IsoLevel) cubeIndex |= 128;

}

Get the triangle indexes.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...
    int edges[] = triTable[cubeIndex];
}

Loop through them to find the edges.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...
    for (int i = 0; edges[i] != -1; i += 3)
    {
        // First edge lies between vertex e00 and vertex e01
        int e00 = edgeConnections[edges[i]][0];
        int e01 = edgeConnections[edges[i]][1];

        // Second edge lies between vertex e10 and vertex e11
        int e10 = edgeConnections[edges[i + 1]][0];
        int e11 = edgeConnections[edges[i + 1]][1];
        
        // Third edge lies between vertex e20 and vertex e21
        int e20 = edgeConnections[edges[i + 2]][0];
        int e21 = edgeConnections[edges[i + 2]][1];
    }
}

Interpolate between the points on the edge to find the exact position for the vertices of the triangles.

float3 interp(float3 edgeVertex1, float valueAtVertex1, float3 edgeVertex2, float valueAtVertex2)
{
    return (edgeVertex1 + (_IsoLevel - valueAtVertex1) * (edgeVertex2 - edgeVertex1)  / (valueAtVertex2 - valueAtVertex1));
}


[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...
}

Finally, add our triangle to the list.

[numthreads(numThreads, numThreads, numThreads)]
void March(uint3 id : SV_DispatchThreadID)
{
    ...
    for (int i = 0; edges[i] != -1; i += 3)
    {
        ...

        Triangle tri;
        tri.a = interp(cornerOffsets[e00], cubeValues[e00], cornerOffsets[e01], cubeValues[e01]) + id;
        tri.b = interp(cornerOffsets[e10], cubeValues[e10], cornerOffsets[e11], cubeValues[e11]) + id;
        tri.c = interp(cornerOffsets[e20], cubeValues[e20], cornerOffsets[e21], cubeValues[e21]) + id;
        _Triangles.Append(tri);
    }
}
Why don't we just add the three vertices to the triangles list? When working with compute shaders, we can never be sure in which order elements are added to our list. This is because threads work in parallel, so many threads could be adding vertices at the same time causing the list to not be in the right order.
---

Great, that’s it for the compute shader.

Head over to our Chunk.cs.

Inside our Chunk.cs, let’s create a function to get the number of triangles we generated. Call it “ReadTriangleCount”. To do this, we have to create an array of ints, this array will simply contain a single element, our triangles count. We have to copy the count of triangles from the triangles buffer to the triangles count buffer.

In case you are wondering why we can’t just use _trianglesBuffer.count (which is an existing function), the .count gives us the max capacity of the buffer, not the actual length of the appendBuffer.

public class Chunk : MonoBehaviour
{
    int ReadTriangleCount() {
        int[] triCount = { 0 };
        ComputeBuffer.CopyCount(_trianglesBuffer, _trianglesCountBuffer, 0);
        _trianglesCountBuffer.GetData(triCount);
        return triCount[0];
    }
}

In the constructMesh, initialize an array with the count we just read from the buffer and transfer the triangles from the triangles buffer to our array.

public class Chunk : MonoBehaviour
{
    Mesh ConstructMesh() {
        ...
        Triangle[] triangles = new Triangle[ReadTriangleCount()];
        _trianglesBuffer.GetData(triangles);
    }
}

We have to now translate these triangles to actual mesh data, create a new function for this.

public class Chunk : MonoBehaviour
{
    Mesh CreateMeshFromTriangles(Triangle[] triangles) {

    }
}

Initialize the vertices and triangles list (this is for our mesh data). Every triangle contains 3 vertices so we will need triangles.Length * 3 as size of our vertices and triangles array.

Mesh CreateMeshFromTriangles(Triangle[] triangles) {
    Vector3[] verts = new Vector3[triangles.Length * 3];
    int[] tris = new int[triangles.Length * 3];
}

Loop through all triangles generated by marching cubes, and add them to our verts and tris.

Mesh CreateMeshFromTriangles(Triangle[] triangles) {
    Vector3[] verts = new Vector3[triangles.Length * 3];
    int[] tris = new int[triangles.Length * 3];

    for (int i = 0; i < triangles.Length; i++) {
        int startIndex = i * 3; 
        verts[startIndex] = triangles[i].a;
        verts[startIndex + 1] = triangles[i].b;
        verts[startIndex + 2] = triangles[i].c; 
        tris[startIndex] = startIndex;
        tris[startIndex + 1] = startIndex + 1;
        tris[startIndex + 2] = startIndex + 2;
    }
}

Create the mesh and return it.

Mesh CreateMeshFromTriangles(Triangle[] triangles) {
    ...

    Mesh mesh = new Mesh();
    mesh.vertices = verts;
    mesh.triangles = tris;
    mesh.RecalculateNormals();
    return mesh;
}

Also return it in our ConstructMesh() function.

Mesh ConstructMesh() {
    ...

    return CreateMeshFromTriangles(triangles);
}

Add a reference to a meshFilter. MeshFilters are responsible for showing the actual mesh.

public class Chunk : MonoBehaviour
{
    public MeshFilter MeshFilter;
}

Inside the start method, call contruct mesh and assign the mesh to the meshFilter.

public class Chunk : MonoBehaviour
{
    void Start() {
        _weights = NoiseGenerator.GetNoise();

        MeshFilter.sharedMesh = ConstructMesh();
    }
}

In Unity, make sure that there is a chunk object in the hierarchy, add a meshFilter and meshRenderer to it, also a new material.

The chunk also needs its references to the marching cubes shader, the mesh filter, and ofcourse the noise generator.

File Structure

We are finally ready to see our algorithm in action.

Result

Don’t forget to disable the Gizmos, they slow down your game by quite a lot.

Also, like mentioned in the beginning, you can now increase the size of your chunk (make sure Gizmos is off or removed entirely!)

Result

Noise with default values, frequency 0.02

In the next part we will make our small, static terrain a bit more exciting by allowing us to terraform.

3. Downloads

Final project