feat(world-gen): implement worm cave pass in CaveCarver
This commit is contained in:
@@ -115,4 +115,72 @@ public class CaveCarverTests
|
|||||||
Assert.True(airAtSeamC1 > 0, "c1 seam column had no air — carve too sparse?");
|
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?");
|
Assert.True(airAtSeamC2 > 0, "c2 seam column had no air — carve too sparse?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WormCaves_IsDeterministic_ForSameSeedAndChunkPos()
|
||||||
|
{
|
||||||
|
var c1 = new CaveCarver(seed: 999, Blocks, Cfg);
|
||||||
|
var c2 = new CaveCarver(seed: 999, Blocks, Cfg);
|
||||||
|
Chunk a = MakeStoneChunk();
|
||||||
|
Chunk b = MakeStoneChunk();
|
||||||
|
|
||||||
|
c1.CarveWormCaves(a, new Vector3i(0, 4, 0));
|
||||||
|
c2.CarveWormCaves(b, new Vector3i(0, 4, 0));
|
||||||
|
|
||||||
|
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 WormCaves_OnlyConvertsSolidToAir_NeverWater()
|
||||||
|
{
|
||||||
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
||||||
|
Chunk c = MakeStoneChunk();
|
||||||
|
// Set every voxel along a diagonal to water — must survive.
|
||||||
|
for (int i = 0; i < Chunk.Size; i++)
|
||||||
|
c[i, i, i] = Blocks.Water.Id;
|
||||||
|
|
||||||
|
carver.CarveWormCaves(c, new Vector3i(0, 4, 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < Chunk.Size; i++)
|
||||||
|
Assert.Equal(Blocks.Water.Id, c[i, i, i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WormCaves_RespectsBedrockFloor()
|
||||||
|
{
|
||||||
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
||||||
|
Chunk c = MakeStoneChunk();
|
||||||
|
|
||||||
|
carver.CarveWormCaves(c, new Vector3i(0, 0, 0)); // worldY = 0..15
|
||||||
|
|
||||||
|
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]); // bedrock untouched
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WormCaves_SeamContinuity_VoxelAtSharedWorldCoord_IsSame()
|
||||||
|
{
|
||||||
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
||||||
|
Chunk c1 = MakeStoneChunk();
|
||||||
|
Chunk c2 = MakeStoneChunk();
|
||||||
|
|
||||||
|
carver.CarveWormCaves(c1, new Vector3i(0, 4, 0));
|
||||||
|
carver.CarveWormCaves(c2, new Vector3i(1, 4, 0));
|
||||||
|
|
||||||
|
int airC1Seam = 0, airC2Seam = 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) airC1Seam++;
|
||||||
|
if (c2[0, y, z] == Blocks.Air.Id) airC2Seam++;
|
||||||
|
}
|
||||||
|
bool bothEmpty = airC1Seam == 0 && airC2Seam == 0;
|
||||||
|
bool bothCarved = airC1Seam > 0 && airC2Seam > 0;
|
||||||
|
Assert.True(bothEmpty || bothCarved,
|
||||||
|
$"seam mismatch: c1={airC1Seam}, c2={airC2Seam}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class CaveCarver
|
|||||||
private readonly BlockRegistry _blocks;
|
private readonly BlockRegistry _blocks;
|
||||||
private readonly Configuration _cfg;
|
private readonly Configuration _cfg;
|
||||||
private readonly FastNoiseLite _caveNoise;
|
private readonly FastNoiseLite _caveNoise;
|
||||||
|
private readonly FastNoiseLite _wormDirNoise;
|
||||||
|
|
||||||
public CaveCarver(int seed, BlockRegistry blocks, Configuration cfg)
|
public CaveCarver(int seed, BlockRegistry blocks, Configuration cfg)
|
||||||
{
|
{
|
||||||
@@ -24,12 +25,16 @@ public sealed class CaveCarver
|
|||||||
this._caveNoise = new FastNoiseLite(seed + 100);
|
this._caveNoise = new FastNoiseLite(seed + 100);
|
||||||
this._caveNoise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
this._caveNoise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||||
this._caveNoise.SetFrequency(cfg.CaveNoiseFrequency);
|
this._caveNoise.SetFrequency(cfg.CaveNoiseFrequency);
|
||||||
|
|
||||||
|
this._wormDirNoise = new FastNoiseLite(seed + 200);
|
||||||
|
this._wormDirNoise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||||
|
this._wormDirNoise.SetFrequency(0.05f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Carve(VoxelChunk chunk, Vector3i chunkPos)
|
public void Carve(VoxelChunk chunk, Vector3i chunkPos)
|
||||||
{
|
{
|
||||||
this.CarveNoiseCaves(chunk, chunkPos);
|
this.CarveNoiseCaves(chunk, chunkPos);
|
||||||
// Worm pass added in Task 6.
|
this.CarveWormCaves(chunk, chunkPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-voxel 3D simplex threshold. Public for direct testability,
|
// Per-voxel 3D simplex threshold. Public for direct testability,
|
||||||
@@ -58,4 +63,116 @@ public sealed class CaveCarver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CarveWormCaves(VoxelChunk chunk, Vector3i chunkPos)
|
||||||
|
{
|
||||||
|
int cellSize = this._cfg.WormCellSize;
|
||||||
|
int radius = (int)Math.Ceiling((double)this._cfg.WormMaxLength / cellSize) + 1;
|
||||||
|
|
||||||
|
int chunkBaseX = chunkPos.X * VoxelChunk.Size;
|
||||||
|
int chunkBaseY = chunkPos.Y * VoxelChunk.Size;
|
||||||
|
int chunkBaseZ = chunkPos.Z * VoxelChunk.Size;
|
||||||
|
|
||||||
|
int chunkCellX = CaveCarver.FloorDiv(chunkBaseX, cellSize);
|
||||||
|
int chunkCellY = CaveCarver.FloorDiv(chunkBaseY, cellSize);
|
||||||
|
int chunkCellZ = CaveCarver.FloorDiv(chunkBaseZ, cellSize);
|
||||||
|
|
||||||
|
for (int cdx = -radius; cdx <= radius; cdx++)
|
||||||
|
for (int cdy = -radius; cdy <= radius; cdy++)
|
||||||
|
for (int cdz = -radius; cdz <= radius; cdz++)
|
||||||
|
{
|
||||||
|
int cellX = chunkCellX + cdx;
|
||||||
|
int cellY = chunkCellY + cdy;
|
||||||
|
int cellZ = chunkCellZ + cdz;
|
||||||
|
|
||||||
|
uint h = WormHash.Hash(cellX, cellY, cellZ, this._seed);
|
||||||
|
if ((h % 1000) >= (uint)this._cfg.WormSpawnProbabilityPermille) continue;
|
||||||
|
|
||||||
|
// Hash bit layout: [ox:6][oy:6][oz:6][theta:8][phi:6] = 32 bits
|
||||||
|
float ox = (h & 0x3F) / 64f * cellSize;
|
||||||
|
float oy = ((h >> 6) & 0x3F) / 64f * cellSize;
|
||||||
|
float oz = ((h >> 12) & 0x3F) / 64f * cellSize;
|
||||||
|
Vector3 pos = new(cellX * cellSize + ox,
|
||||||
|
cellY * cellSize + oy,
|
||||||
|
cellZ * cellSize + oz);
|
||||||
|
|
||||||
|
float theta = ((h >> 18) & 0xFF) / 255f * MathF.Tau;
|
||||||
|
float phi = (((h >> 26) & 0x3F) / 63f * 2f - 1f) * 0.3f * MathF.PI;
|
||||||
|
Vector3 dir = new(
|
||||||
|
MathF.Cos(theta) * MathF.Cos(phi),
|
||||||
|
MathF.Sin(phi),
|
||||||
|
MathF.Sin(theta) * MathF.Cos(phi));
|
||||||
|
dir = Vector3.Normalize(dir);
|
||||||
|
|
||||||
|
this.TraceAndCarveWorm(chunk, chunkPos, pos, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TraceAndCarveWorm(VoxelChunk chunk, Vector3i chunkPos,
|
||||||
|
Vector3 startPos, Vector3 startDir)
|
||||||
|
{
|
||||||
|
int maxLen = this._cfg.WormMaxLength;
|
||||||
|
float minR = this._cfg.WormMinRadius;
|
||||||
|
float maxR = this._cfg.WormMaxRadius;
|
||||||
|
|
||||||
|
Vector3 pos = startPos;
|
||||||
|
Vector3 dir = startDir;
|
||||||
|
|
||||||
|
for (int step = 0; step < maxLen; step++)
|
||||||
|
{
|
||||||
|
float t = step / (float)maxLen;
|
||||||
|
float bulgeT = 0.5f + 0.5f * MathF.Sin(t * MathF.Tau * 3f);
|
||||||
|
float r = minR + (maxR - minR) * bulgeT;
|
||||||
|
|
||||||
|
this.CarveSphere(chunk, chunkPos, pos, r);
|
||||||
|
|
||||||
|
float px = this._wormDirNoise.GetNoise(pos.X, pos.Y, pos.Z);
|
||||||
|
float py = this._wormDirNoise.GetNoise(pos.X + 100f, pos.Y, pos.Z);
|
||||||
|
float pz = this._wormDirNoise.GetNoise(pos.X, pos.Y, pos.Z + 100f);
|
||||||
|
Vector3 perturbation = new(px, py, pz);
|
||||||
|
dir = Vector3.Normalize(dir + perturbation * 0.3f);
|
||||||
|
|
||||||
|
dir = new Vector3(dir.X, dir.Y * 0.7f, dir.Z);
|
||||||
|
dir = Vector3.Normalize(dir);
|
||||||
|
|
||||||
|
pos += dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CarveSphere(VoxelChunk chunk, Vector3i chunkPos, Vector3 center, float r)
|
||||||
|
{
|
||||||
|
int baseX = chunkPos.X * VoxelChunk.Size;
|
||||||
|
int baseY = chunkPos.Y * VoxelChunk.Size;
|
||||||
|
int baseZ = chunkPos.Z * VoxelChunk.Size;
|
||||||
|
|
||||||
|
int xMin = Math.Max(0, (int)MathF.Floor(center.X - r) - baseX);
|
||||||
|
int xMax = Math.Min(VoxelChunk.Size - 1, (int)MathF.Ceiling(center.X + r) - baseX);
|
||||||
|
int yMin = Math.Max(0, (int)MathF.Floor(center.Y - r) - baseY);
|
||||||
|
int yMax = Math.Min(VoxelChunk.Size - 1, (int)MathF.Ceiling(center.Y + r) - baseY);
|
||||||
|
int zMin = Math.Max(0, (int)MathF.Floor(center.Z - r) - baseZ);
|
||||||
|
int zMax = Math.Min(VoxelChunk.Size - 1, (int)MathF.Ceiling(center.Z + r) - baseZ);
|
||||||
|
|
||||||
|
float r2 = r * r;
|
||||||
|
|
||||||
|
for (int x = xMin; x <= xMax; x++)
|
||||||
|
for (int y = yMin; y <= yMax; y++)
|
||||||
|
for (int z = zMin; z <= zMax; z++)
|
||||||
|
{
|
||||||
|
int worldY = baseY + y;
|
||||||
|
if (worldY <= 0) continue;
|
||||||
|
|
||||||
|
float vx = baseX + x + 0.5f;
|
||||||
|
float vy = baseY + y + 0.5f;
|
||||||
|
float vz = baseZ + z + 0.5f;
|
||||||
|
float dx = vx - center.X, dy = vy - center.Y, dz = vz - center.Z;
|
||||||
|
if (dx * dx + dy * dy + dz * dz > r2) continue;
|
||||||
|
|
||||||
|
ushort id = chunk[x, y, z];
|
||||||
|
if (id == this._blocks.Air.Id) continue;
|
||||||
|
if (id == this._blocks.Water.Id) continue;
|
||||||
|
chunk[x, y, z] = this._blocks.Air.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FloorDiv(int a, int b) => a >= 0 ? a / b : -(((-a) + b - 1) / b);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user