using OpenTK.Mathematics; using VoxelEngine.Core; using VoxelEngine.World.Blocks; using VoxelChunk = VoxelEngine.World.Chunk.Chunk; namespace VoxelEngine.World.WorldGen; // Carves caves into an already-terrain-filled chunk. Stateless after // construction; the carve methods are pure functions of (chunk, chunkPos). // Safe to share across worker threads. public sealed class CaveCarver { private readonly int _seed; private readonly BlockRegistry _blocks; private readonly Configuration _cfg; private readonly FastNoiseLite _caveNoise; private readonly FastNoiseLite _wormDirNoise; public CaveCarver(int seed, BlockRegistry blocks, Configuration cfg) { this._seed = seed; this._blocks = blocks; this._cfg = cfg; this._caveNoise = new FastNoiseLite(seed + 100); this._caveNoise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); 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) { this.CarveNoiseCaves(chunk, chunkPos); this.CarveWormCaves(chunk, chunkPos); } // 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; if (worldY > this._cfg.MinCaveY) 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; } } } 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 = FloorDiv(chunkBaseX, cellSize); int chunkCellY = FloorDiv(chunkBaseY, cellSize); int chunkCellZ = 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; if (worldY > this._cfg.MinCaveY) 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); }