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.
(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);
}
}
