Issue with marching cubes, anyone willing to have a peek at this?

My mesh doesn’t appear to be generating and I’m thinking it’s something with the way I’m creating triangles, but I can’t figure out where I’m screwing up.

This is the output. Definitely not what I was expecting:

LogTemp: Warning: Creating mesh for chunk : (8192) vert, (-908209720) triangles.

This is the main cpp:

#include "WorldChunk.h"
#include "Kismet/KismetMathLibrary.h"

// Sets default values for this component's properties
AWorldChunk::AWorldChunk()
{
    PrimaryActorTick.bCanEverTick = false;
    if (!ProcMesh)
    {
        ProcMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProcMeshComponent"));
        ProcMesh->SetupAttachment(RootComponent);
        ProcMesh->SetVisibility(true);
    }

}


// Called when the game starts
void AWorldChunk::BeginPlay()
{
	Super::BeginPlay();

    GenerateTerrain();
}

void AWorldChunk::GenerateTerrain()
{
    UE_LOG(LogTemp, Warning, TEXT("Loading chunk..."));

    // Create a grid of vertices
    TArray<FVector> vertices;
    for (int32 z = 0; z < gridSizeZ; z++)
    {
        for (int32 y = 0; y < gridSizeY; y++)
        {
            for (int32 x = 0; x < gridSizeX; x++)
            {
                // Calculate the world position of each vertex
                FVector worldPos = GetActorLocation() + FVector(x * voxelSize, y * voxelSize, z * voxelSize);
                float density = CalculateDensity(worldPos);
                vertices.Add(worldPos + density * normal);
            }
        }
    }

    // Create triangles using Marching Cubes algorithm
    TArray<int32> triangles;
    for (int32 z = 0; z < gridSizeZ - 1; z++)
    {
        for (int32 y = 0; y < gridSizeY - 1; y++)
        {
            for (int32 x = 0; x < gridSizeX - 1; x++)
            {
                // Create a byte code to represent the density values of the corners
                uint8 cubeIndex = 0;
                for (int32 i = 0; i < 8; i++)
                {
                    FVector cornerPos = vertices[GetIndex(x + edgeVertices[i][0],
                        y + edgeVertices[i][1],
                        z + edgeVertices[i][2])];
                    if (CalculateDensity(cornerPos) > 0)
                        cubeIndex |= (1 << i);
                }

                // Generate triangles for the current cube
                for (int32 i = 0; triTable[cubeIndex][i] != -1; i += 3)
                {
                    triangles.Add(GetIndex(x + triTable[cubeIndex][i],
                        y + triTable[cubeIndex][i + 1],
                        z + triTable[cubeIndex][i + 2]));
                }
            }
        }
    }

    // Create mesh from generated vertices and triangles
    if (ProcMesh && terrainMaterial)
    {
        FString vertcount = FString::FromInt(vertices.Num());
        FString tricount = FString::FromInt(triangles.Num());

        UE_LOG(LogTemp, Warning, TEXT("Creating mesh for chunk : (%s) vert, (%i) triangles."), *vertcount, *tricount);
        ProcMesh->CreateMeshSection(0, vertices, triangles, TArray<FVector>(), TArray<FVector2D>(), TArray<FColor>(), TArray<FProcMeshTangent>(), true);
        ProcMesh->SetMaterial(0, terrainMaterial);
    }
}

int32 AWorldChunk::GetIndex(int32 x, int32 y, int32 z) const
{
    return x + gridSizeX * (y + gridSizeY * z);
}

float AWorldChunk::CalculateDensity(const FVector& worldPos)
{
    noiseScale = 0.1f; // Adjust the noise scale to control the level of detail
    float noiseValue = FMath::PerlinNoise3D(worldPos * noiseScale);

    threshold = 0.5f; // Adjust the threshold to control the terrain shape
    return noiseValue - threshold;
}

const int32 AWorldChunk::edgeVertices[12][3] = {
    {0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {0, 0, 0},
    {0, 1, 1}, {1, 1, 1}, {1, 0, 1}, {0, 0, 1},
    {0, 0, 0}, {0, 1, 0}, {1, 1, 0}, {1, 0, 0}
};

const int32 AWorldChunk::triTable[256][16] = {

// Marching cubes table here... really big so I left this out for help post.

};

the .h file:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ProceduralMeshComponent.h"
#include "WorldChunk.generated.h"


UENUM(BlueprintType)
enum eBiomeType
{
	Invalid,
	Dirt,
	Grass,
	Water,
};

UCLASS(Abstract)
class WORLDGEN_API AWorldChunk : public AActor
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	AWorldChunk();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	

    void GenerateTerrain();

    int32 GetIndex(int32 x, int32 y, int32 z) const;

	float CalculateDensity(const FVector& worldPos);// const;

    static const int32 edgeVertices[12][3];
    static const int32 triTable[256][16];

	UPROPERTY(BlueprintReadOnly, Category = "Terrain")
		UProceduralMeshComponent* ProcMesh;
	UPROPERTY(EditAnywhere, Category = "Terrain")
		UMaterialInterface* terrainMaterial; // The material used to render the terrain

	UPROPERTY(EditAnywhere, Category = "Terrain")
    int gridSizeZ = 32;
	UPROPERTY(EditAnywhere, Category = "Terrain")
    int gridSizeY = 16;
	UPROPERTY(EditAnywhere, Category = "Terrain")
    int gridSizeX = 16;
	UPROPERTY(EditAnywhere, Category = "Terrain")
	float threshold = 0.5f; // Adjust the threshold to control the terrain shape
	UPROPERTY(EditAnywhere, Category = "Terrain")
	float noiseScale = 0.1f; // Adjust the noise scale to control the level of detail

	UPROPERTY(EditAnywhere, Category = "Terrain")
		float voxelSize = 100.0f; // Set the default voxel size to 100 units
	UPROPERTY(EditAnywhere, Category = "Terrain")
		FVector normal = FVector(0.0f, 0.0f, 1.0f); // Set the default normal vector to (0, 0, 1)
};

This looks odd. FMath::PerlinNoise3D returns in the range -1, 1 and you are subtracting 0.5 to make the range -1.5, 0.5, so 3/4 of your density values are going to be negative - is that what you intend?

As I understood from what I’ve been reading, yes. I could be off in my understanding though, to be honest. Trying to understand how to create voxel terrain has been a heck of a ride for me.

This is where I initially was reading from: https://courses.cs.ut.ee/student_projects/download/138.pdf

Do you have any suggestions on how to better calculate/use density? Am I just not understanding it properly?

If the surface is where the density value is zero, the subtracting a threshold value is just going to move the whole surface up by that value.

Have you read https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu and Polygonising a scalar field (Marching Cubes) both of which include source code.

If I was doing this I’d make a small surface which was a 3x3 grid and step through the code in the debugger and see if the values match manually calculated ones