Poly Coding

Marching Cubes Part 5: Level of detail switching with marching cubes

0. Introduction

1. Switching Level of detail

2. Downloads

Marching Cubes Terrain

The same terrain at a different level of detail

0. Introduction

This is the fifth part of a series on marching cubes. This time we will make it possible to switch between the level of detail while ensuring that previous terrain edits are kept and displayed.

1. Switching Level of detail

In the last part we made it possible to select a resolution for our terrain, however there are 2 problems:

  • When switching between LOD, the mesh does not take into account previous edits.

  • When a new LOD is selected, we regenerate the entire chunks noise.

In this part we will fix these issues by generating the maximum amount of noise for the chunk at the start and reuse this generated noise for all other LODs.

1.1 Reusing noise

Before being able to reuse noise, we need to settle on a number for the maximum amount of noise within our chunk. The amount of points within our last LOD seems like the most appropriate choice.

Add a variable to GridMetrics to access the last lod easily.

public static class GridMetrics {
    public static int LastLod = LODs.Length - 1;
}

In the chunk class we now need to give the noise generator the last LOD, since we want the maximum amount of noise. Let’s also make sure this noise is generated just once.

public class Chunk : MonoBehaviour
{
    void Create() {
        ...
        if (_weights == null) {
            _weights = NoiseGenerator.GetNoise(GridMetrics.LastLod);
        }
        ...
    }
}

Of course, if we have a fixed amount of noise returned regardless of selected LOD, we must also initialize the weights buffer with this fixed number.

public class Chunk : MonoBehaviour
{
    void CreateBuffers() {
        ...
        _weightsBuffer = new ComputeBuffer(GridMetrics.PointsPerChunk(GridMetrics.LastLod) * GridMetrics.PointsPerChunk(GridMetrics.LastLod) * GridMetrics.PointsPerChunk(GridMetrics.LastLod), sizeof(float));
    }
}

The count of triangles will remain the same, as we still use a variable amount of triangles given a LOD.

When in playmode, we now get noise similar to following:

File Structure

When the last LOD is selected in the inspector everthing still works as normal, the problem is that currently we assume that the chunk size is equal to the size of our selected LOD, causing the noise values that are sampled within the marching cubes algorithm to be all over the place, as the noise array within the marching cubes compute has a size of the last LOD.

So, when constructing our mesh, we need to tell the maching cubes compute that the chunk size is always the same.

public class Chunk : MonoBehaviour
{
    Mesh ConstructMesh() {
        ...
        // MarchingShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk(LOD));
        MarchingShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk(GridMetrics.LastLod));
        ...
    }
}

Again in playmode, we now see that while we generate the right amount of triangles given our LOD, we do not scale the mesh correctly nor do we sample the noise at the right locations.

1.2 Scale

Let’s start by scaling correctly. For this we need to know what the chunk size given an LOD is once again. Call this the “LODSize”.

public class Chunk : MonoBehaviour
{
    Mesh ConstructMesh() {
        ...
        MarchingShader.SetInt("_LODSize", GridMetrics.PointsPerChunk(LOD));
        ...
    }
}

Now, in the marching cubes compute, create the appropriate variable.

int _LODSize;

And instead of scaling the mesh relative to the chunk size (which at this point represents the maximum possible chunk size). Scale it relative to the LODSize.

void March(uint3 id : SV_DispatchThreadID) {
    ...

    for (int i = 0; edges[i] != -1; i += 3) {
        ...
        tri.a = (interp(cornerOffsets[e00], cubeValues[e00], cornerOffsets[e01], cubeValues[e01]) + id) / (_LODSize - 1) * _Scale;
        tri.b = (interp(cornerOffsets[e10], cubeValues[e10], cornerOffsets[e11], cubeValues[e11]) + id) / (_LODSize - 1) * _Scale;
        tri.c = (interp(cornerOffsets[e20], cubeValues[e20], cornerOffsets[e21], cubeValues[e21]) + id) / (_LODSize - 1) * _Scale;
        ...
    }
}

1.2 Sampling noise

At this point we again scale our mesh correctly. However our scaled mesh is not actually using the right noise values. When using LOD 0 (Points Per Chunk = 8), we only use the first 8 noise values in any given dimension (xyz) out of the entire noise grid.

To sample the right noise values, we must scale these points so that they take up the entire grid size. For example: When using LOD 0 (Points Per Chunk = 8), we must scale all points within the 3D grid relative to the last LOD (Points Per Chunk = 40). This means that the point (1,1,1) must actually represent point (5,5,5). To get this new point, we use the following formula:

Scale factor = Chunk size / lod size
New point = original point * scale factor

Let’s define the scale factor in the Chunk script.

public class Chunk : MonoBehaviour
{
    Mesh ConstructMesh() {
        ...
        float lodScaleFactor = ((float)GridMetrics.PointsPerChunk(GridMetrics.LastLod) + 1) / (float)GridMetrics.PointsPerChunk(LOD);

        MarchingShader.SetFloat("_LodScaleFactor", lodScaleFactor);
        ...
    }
}

Back in marching cubes compute we now have to apply the scale factor to our id (where the id represents a position between (0,0,0) and (lod size, lod size,lod size)).

Let’s also not forget to change the bounds of the id to LOD Size instead of chunk size.

float _LodScaleFactor;

void March(uint3 id : SV_DispatchThreadID) {
    if (id.x >= _LODSize - 1 || id.y >= _LODSize - 1 || id.z >= _LODSize - 1)
    {
        return;
    }

    ...
}

Adjust the cube values so they sample the right position.

void March(uint3 id : SV_DispatchThreadID) {
    ...

    float3 samplePos = id * _LodScaleFactor;

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

In game we now find that our mesh contains many gaps.

The problem is that an addition of 1 no longers samples the next cube corner, we now also need to scale the 1.

void March(uint3 id : SV_DispatchThreadID) {
    ...

    float unit = 1 * _LodScaleFactor;

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

1.3 Fixing terrain editing

Finally, we need to fix the terrain editing. When the last LOD is selected in the inspector, there seems to be no problem, we can edit the terrain just fine. When in any other LOD, we get either no edits at all or very strange edits.

The solution is to always use the last lod when editing weights.

public class Chunk : MonoBehaviour
{
    Mesh EditWeights(Vector3 hitPosition, float brushSize, bool add) {
        ...
    
        MarchingShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk(GridMetrics.LastLod));

        ...

        MarchingShader.Dispatch(kernel, GridMetrics.ThreadGroups(GridMetrics.LastLod), GridMetrics.ThreadGroups(GridMetrics.LastLod), GridMetrics.ThreadGroups(GridMetrics.LastLod));

        ...
    }
}

3. Downloads

Final project