feat(world-gen): implement noise-threshold cave pass

This commit is contained in:
2026-06-01 15:28:11 +02:00
parent 093b3deabf
commit 070bdfeac1
2 changed files with 109 additions and 1 deletions

View File

@@ -35,4 +35,84 @@ public class CaveCarverTests
for (int z = 0; z < Chunk.Size; z++)
Assert.Equal(Blocks.Air.Id, c[x, y, z]);
}
[Fact]
public void NoiseCaves_OnlyConvertsSolidToAir_NeverWaterNeverOtherBlocks()
{
var carver = new CaveCarver(seed: 42, Blocks, Cfg);
Chunk c = MakeStoneChunk();
// Plant some water and air voxels we expect untouched.
c[1, 1, 1] = Blocks.Water.Id;
c[2, 2, 2] = Blocks.Air.Id;
carver.CarveNoiseCaves(c, new Vector3i(0, 4, 0));
Assert.Equal(Blocks.Water.Id, c[1, 1, 1]); // water survived
Assert.Equal(Blocks.Air.Id, c[2, 2, 2]); // air survived
// Spot-check that everywhere else is either Stone or Air (no other blocks).
for (int x = 0; x < Chunk.Size; x++)
for (int y = 0; y < Chunk.Size; y++)
for (int z = 0; z < Chunk.Size; z++)
{
ushort id = c[x, y, z];
Assert.True(
id == Blocks.Stone.Id || id == Blocks.Air.Id || id == Blocks.Water.Id,
$"unexpected block id {id} at ({x},{y},{z})");
}
}
[Fact]
public void NoiseCaves_IsDeterministic_ForSameSeedAndChunkPos()
{
var carver1 = new CaveCarver(seed: 1234, Blocks, Cfg);
var carver2 = new CaveCarver(seed: 1234, Blocks, Cfg);
Chunk a = MakeStoneChunk();
Chunk b = MakeStoneChunk();
carver1.CarveNoiseCaves(a, new Vector3i(2, 4, 3));
carver2.CarveNoiseCaves(b, new Vector3i(2, 4, 3));
for (int x = 0; x < Chunk.Size; x++)
for (int y = 0; y < Chunk.Size; y++)
for (int z = 0; z < Chunk.Size; z++)
Assert.Equal(a[x, y, z], b[x, y, z]);
}
[Fact]
public void NoiseCaves_RespectsBedrockFloor_DoesNotCarveBelowY1()
{
// ChunkPos.Y = 0 means worldY ranges 0..15. We require Y=0 stays solid.
var carver = new CaveCarver(seed: 42, Blocks, Cfg);
Chunk c = MakeStoneChunk();
carver.CarveNoiseCaves(c, new Vector3i(0, 0, 0));
for (int x = 0; x < Chunk.Size; x++)
for (int z = 0; z < Chunk.Size; z++)
Assert.Equal(Blocks.Stone.Id, c[x, 0, z]); // worldY = 0 is bedrock
}
[Fact]
public void NoiseCaves_SeamContinuity_AdjacentChunksAgreeAtBoundary()
{
var carver = new CaveCarver(seed: 7, Blocks, Cfg);
Chunk c1 = MakeStoneChunk();
Chunk c2 = MakeStoneChunk();
carver.CarveNoiseCaves(c1, new Vector3i(0, 4, 0));
carver.CarveNoiseCaves(c2, new Vector3i(1, 4, 0));
int airAtSeamC1 = 0, airAtSeamC2 = 0;
for (int y = 1; y < Chunk.Size; y++)
for (int z = 0; z < Chunk.Size; z++)
{
if (c1[15, y, z] == Blocks.Air.Id) airAtSeamC1++;
if (c2[0, y, z] == Blocks.Air.Id) airAtSeamC2++;
}
// Sanity: noise threshold = -0.3 yields some carved cells in each.
Assert.True(airAtSeamC1 > 0, "c1 seam column had no air — carve too sparse?");
Assert.True(airAtSeamC2 > 0, "c2 seam column had no air — carve too sparse?");
}
}

View File

@@ -28,6 +28,34 @@ public sealed class CaveCarver
public void Carve(VoxelChunk chunk, Vector3i chunkPos)
{
// Implemented in Tasks 5 and 6.
this.CarveNoiseCaves(chunk, chunkPos);
// Worm pass added in Task 6.
}
// Per-voxel 3D simplex threshold. Public for direct testability,
// matching the pattern of WorldGenerator.BlockAt.
public void CarveNoiseCaves(VoxelChunk chunk, Vector3i chunkPos)
{
int baseX = chunkPos.X * VoxelChunk.Size;
int baseY = chunkPos.Y * VoxelChunk.Size;
int baseZ = chunkPos.Z * VoxelChunk.Size;
for (int x = 0; x < VoxelChunk.Size; x++)
for (int y = 0; y < VoxelChunk.Size; y++)
for (int z = 0; z < VoxelChunk.Size; z++)
{
int worldY = baseY + y;
if (worldY <= 0) continue;
ushort id = chunk[x, y, z];
if (id == this._blocks.Air.Id) continue;
if (id == this._blocks.Water.Id) continue;
float density = this._caveNoise.GetNoise(baseX + x, worldY, baseZ + z);
if (density < this._cfg.CaveNoiseThreshold)
{
chunk[x, y, z] = this._blocks.Air.Id;
}
}
}
}