2
\$\begingroup\$

I'm creating voxel planets using Marching Cubes (MC). To handle nearby (high-resolution) and distant (low-resolution) blocks, I use an Octree data structure.

NOTE: The implementation uses compute shaders.

Problem: When two adjacent blocks have different LODs, holes appear along the edges.

Example of a crack

(The high-resolution block is on the left, and the lower one is on the right. As you can see, there are holes between them; this is a crack.)

I considered using skirts to cover the cracks, but extending the skirts toward the center of the planet only works on open terrain (plains, mountains, hills, etc.). In caves or tunnels, especially from above, extending the skirts toward the center of the planet creates triangles that cause very unsightly protrusions.

Skirts should always extend in the opposite direction to the vertex normal, calculated from the normalized sum of the normals of the triangles containing the given vertex. However, with the classic MC implementation:

Each triangle has independent vertices, so vertex normals cannot be shared between adjacent triangles.

This makes it difficult to correctly calculate the position of skirts on chunk edges.

So, how can I solve this problem? I need to share vertices between triangles, but I don't know how...

Currently, my chunks are 8x8x8 voxels, processed by a single set of threads.

How the code works: In the CalculateNoiseValues kernel the compute shader calculate the density field for each point, since the voxels are 888 then the number of points will be 99*9, after that it stores the values in the noiseValuesBuffer and then they are used in the ComputeMarchingCubes kernel which create the vertices and triangles.

This code is quiet simple for now, it is for test, but the concept is this.*

#include "Assets/Shader Helpers/MCTriangulation.hlsl"

#define NOISE_VALUES_STRIDE_PER_CHUNK 729 

struct Chunk
{
    float3 position;
    uint celestialBodyIndex;
};
#pragma kernel CalculateNoiseValues
#pragma kernel ComputeMarchingCubes


RWStructuredBuffer<float> noiseValuesBuffer;
RWStructuredBuffer<Chunk> chunksInfoBuffer;

[numthreads(9,9,9)]
void CalculateNoiseValues (uint3 id : SV_GroupThreadID, uint linearID : SV_GroupIndex, uint3 chunkIndex : SV_GroupID)
{
    Chunk chunk = chunksInfoBuffer[chunkIndex.x];
    float3 pointPos = (float3)float3(id.x + chunk.position.x, id.y + chunk.position.z, id.z + chunk.position.y) - 4;
    
    float noise = SimplexNoise(pointPos * 0.01);
    
    int storeIndex = linearID + chunkIndex.x * NOISE_VALUES_STRIDE_PER_CHUNK;
    noiseValuesBuffer[storeIndex] = noise;
}

RWStructuredBuffer<float3> verticesBuffer;
RWStructuredBuffer<int> verticesPerChunk;

void GetNoiseValues(uint id, out float values[8])
{
    values[0] = noiseValuesBuffer[id];
    values[1] = noiseValuesBuffer[id + 1];
    values[3] = noiseValuesBuffer[id + 9];
    values[2] = noiseValuesBuffer[id + 10];
    values[4] = noiseValuesBuffer[id + 81];
    values[5] = noiseValuesBuffer[id + 82];
    values[7] = noiseValuesBuffer[id + 90];
    values[6] = noiseValuesBuffer[id + 91];
}

void CalculateVertexPos(float values[8])
{
    
}

#define BLOCK_SIZE 512
#define CHUNK_STRIDE BLOCK_SIZE * 15
groupshared int sharedData[BLOCK_SIZE];
[numthreads(8,8,8)]
void ComputeMarchingCubes (uint3 id : SV_GroupThreadID, uint linearID : SV_GroupIndex, uint3 chunkIndex : SV_GroupID)
{
    //For each voxel
    Chunk chunk = chunksInfoBuffer[chunkIndex.x];
    
    float values[8];
    uint baseID = id.y * 81 + id.z * 9 + id.x + ( chunkIndex.x * NOISE_VALUES_STRIDE_PER_CHUNK );
    GetNoiseValues(baseID, values);

    int combination = 0;
    for (int i = 0; i < 8; i++)
    {
        if (values[i] > 0)
        {
            combination = combination | (1 << i);
        }
    }

    int combinationOffset = combination * 16;
    int numberOfTriangles = triangleTable[combinationOffset + 15];

    #pragma region Prefix Sum
    //Prefix-sum
    sharedData[linearID] = numberOfTriangles;
    GroupMemoryBarrierWithGroupSync();

    float offset = 1;

    //Up-Sweep
    for (int threadNeeded = BLOCK_SIZE / 2; threadNeeded > 0; threadNeeded /= 2)
    {
        if (linearID < threadNeeded)
        {
            uint indexA = offset * (2 * linearID + 1) - 1;
            uint indexB = offset * (2 * linearID + 2) - 1;

            sharedData[indexB] += sharedData[indexA];
        }

        offset *= 2;

        GroupMemoryBarrierWithGroupSync();
    }

    if(linearID == 0)
    {
        verticesPerChunk[chunkIndex.x] = sharedData[511];
        sharedData[511] = 0;
    }
    offset = BLOCK_SIZE / 2;
    
    GroupMemoryBarrierWithGroupSync();

    //Down-Sweep
    for (int threadNeeded = 1; threadNeeded <= BLOCK_SIZE / 2; threadNeeded *= 2)
    {
        if (linearID < threadNeeded)
        {
            uint indexA = offset * (2 * linearID + 1) - 1;
            uint indexB = offset * (2 * linearID + 2) - 1;

            int temp = sharedData[indexA];
            sharedData[indexA] = sharedData[indexB];
            sharedData[indexB] += temp;
        }

        offset /= 2;

        GroupMemoryBarrierWithGroupSync();
    }
    #pragma endregion

    int storeOffset = sharedData[linearID] + chunkIndex.x * CHUNK_STRIDE;
    for (int i = 0; i < numberOfTriangles; i+=3)
    {
        int edge1 = triangleTable[combinationOffset + i];
        int edge2 = triangleTable[combinationOffset + i + 1];
        int edge3 = triangleTable[combinationOffset + i + 2];
        
        int2 cornerIndices1 = edgeToVertex[edge1];
        int2 cornerIndices2 = edgeToVertex[edge2];
        int2 cornerIndices3 = edgeToVertex[edge3];

        float3 cornerA1 = cornerPositions[cornerIndices1.x];
        float3 cornerA2 = cornerPositions[cornerIndices1.y];
        float3 cornerB1 = cornerPositions[cornerIndices2.x];
        float3 cornerB2 = cornerPositions[cornerIndices2.y];
        float3 cornerC1 = cornerPositions[cornerIndices3.x];
        float3 cornerC2 = cornerPositions[cornerIndices3.y];
        
        float3 pos1 = (cornerA1 + cornerA2) * 0.5f;
        float3 pos2 = (cornerB1 + cornerB2) * 0.5f;
        float3 pos3 = (cornerC1 + cornerC2) * 0.5f;
        
        verticesBuffer[storeOffset + i] = pos1 + ((float3)id + chunk.position);
        verticesBuffer[storeOffset + i + 1] = pos2 + ((float3)id + chunk.position);
        verticesBuffer[storeOffset + i + 2] = pos3 + ((float3)id + chunk.position);
        
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Have you checked out the "Transvoxel" approach to this? \$\endgroup\$ Commented Sep 25 at 0:05
  • \$\begingroup\$ I read the post you sent, but I'm not sure about opting for the transvoxel, because as you can see I'm working on a GPU with compute shaders, and placing these adaptability cells where the LOD changes seems difficult. It would be very feasible to add these cells around each piece, then consider whether to use classic marching cubes or these transition cells. Or maybe that's what's already done? \$\endgroup\$ Commented Sep 25 at 11:36

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.