feat(streaming): load full column bedrock->surface to fix terrain holes

This commit is contained in:
MDietermann
2026-06-03 08:01:38 +02:00
parent cba6b8c761
commit 225c6f7253
6 changed files with 87 additions and 87 deletions

View File

@@ -18,10 +18,11 @@ public sealed class ChunkStreamer : IDisposable
private readonly IWorld _world;
private readonly Func<Vector3i, VoxelChunk> _generate;
private readonly Func<int, int, int> _surfaceHeightAt;
// Per chunk-column (cx,cz) surface (min,max) height range, memoized so each
// column's samples are taken once and reused across ticks. Pruned to the
// horizontal window each recompute so it stays bounded.
private readonly Dictionary<(int cx, int cz), (int min, int max)> _surfaceRangeCache = new();
// Per chunk-column (cx,cz) surface-TOP height, memoized so each column's
// samples are taken once and reused across ticks. Pruned to the horizontal
// window each recompute so it stays bounded. Full-column loading only needs
// the top; the bottom is always bedrock (cy 0).
private readonly Dictionary<(int cx, int cz), int> _surfaceTopCache = new();
private readonly IChunkMesher _mesher;
private readonly BlockRegistry _registry;
private readonly IJobExecutor _executor;
@@ -156,42 +157,43 @@ public sealed class ChunkStreamer : IDisposable
this.PruneSurfaceCache(p);
}
// Drop cached surface ranges for columns outside the current horizontal
// Drop cached surface tops for columns outside the current horizontal
// window so the cache stays bounded as the player explores.
private void PruneSurfaceCache(Vector3i p)
{
int keep = this._cfg.RenderDistanceHorizontal + 1;
List<(int cx, int cz)>? stale = null;
foreach (var key in this._surfaceRangeCache.Keys)
foreach (var key in this._surfaceTopCache.Keys)
{
if (Math.Abs(key.cx - p.X) > keep || Math.Abs(key.cz - p.Z) > keep)
(stale ??= new()).Add(key);
}
if (stale != null)
foreach (var key in stale) this._surfaceRangeCache.Remove(key);
foreach (var key in stale) this._surfaceTopCache.Remove(key);
}
// Fills `into` with the chunk positions to load for a viewer at chunk
// `center`, over a square horizontal region of Chebyshev radius `radiusH`.
// Per column the loaded Y-range is the union of the surface shell and the
// player slab, clamped to the world's vertical bounds.
// Per column the full vertical stack from bedrock (cy 0) up to the
// surface-top chunk is loaded, plus any section above the surface holding
// player edits (e.g. a built tower).
private void CollectTargets(Vector3i center, int radiusH, HashSet<Vector3i> into)
{
int v = this._cfg.RenderDistanceVertical;
int skirt = this._cfg.SurfaceShellSkirtBelow;
for (int dx = -radiusH; dx <= radiusH; dx++)
for (int dz = -radiusH; dz <= radiusH; dz++)
{
int cx = center.X + dx;
int cz = center.Z + dz;
(int minH, int maxH) = this.SurfaceHeightRange(cx, cz);
int shellBot = ChunkMath.FloorDiv(minH, VoxelChunk.Size) - skirt;
int shellTop = ChunkMath.FloorDiv(maxH, VoxelChunk.Size);
ChunkStreamer.AddColumn(into, cx, cz, shellBot, shellTop);
int surfaceTop = ChunkMath.FloorDiv(this.SurfaceTopHeight(cx, cz), VoxelChunk.Size);
ChunkStreamer.AddColumn(into, cx, cz, 0, surfaceTop);
ChunkStreamer.AddColumn(into, cx, cz, center.Y - v, center.Y + v);
int clampedTop = Math.Min(WorldConstants.WorldHeightChunks - 1, surfaceTop);
for (int cy = clampedTop + 1; cy < WorldConstants.WorldHeightChunks; cy++)
{
var pos = new Vector3i(cx, cy, cz);
if (this._world.ModificationStore.HasDiff(pos)) into.Add(pos);
}
}
}
@@ -203,11 +205,13 @@ public sealed class ChunkStreamer : IDisposable
into.Add(new Vector3i(cx, cy, cz));
}
// Min/max terrain surface height across a chunk column's 16×16 footprint,
// sampled at the four corners and the centre (catches slope spread cheaply).
private (int min, int max) SurfaceHeightRange(int cx, int cz)
// Max terrain surface height across a chunk column's 16x16 footprint,
// sampled at the four corners and the centre (cheap bound on the column's
// top). Cached per column. The bottom of the loaded stack is always bedrock,
// so only the top is needed.
private int SurfaceTopHeight(int cx, int cz)
{
if (this._surfaceRangeCache.TryGetValue((cx, cz), out (int min, int max) cached))
if (this._surfaceTopCache.TryGetValue((cx, cz), out int cached))
return cached;
int x0 = cx * VoxelChunk.Size;
@@ -217,18 +221,13 @@ public sealed class ChunkStreamer : IDisposable
int xm = x0 + VoxelChunk.Size / 2;
int zm = z0 + VoxelChunk.Size / 2;
int h0 = this._surfaceHeightAt(x0, z0);
int h1 = this._surfaceHeightAt(x1, z0);
int h2 = this._surfaceHeightAt(x0, z1);
int h3 = this._surfaceHeightAt(x1, z1);
int h4 = this._surfaceHeightAt(xm, zm);
int max = Math.Max(
Math.Max(this._surfaceHeightAt(x0, z0), this._surfaceHeightAt(x1, z0)),
Math.Max(Math.Max(this._surfaceHeightAt(x0, z1), this._surfaceHeightAt(x1, z1)),
this._surfaceHeightAt(xm, zm)));
int min = Math.Min(Math.Min(h0, h1), Math.Min(Math.Min(h2, h3), h4));
int max = Math.Max(Math.Max(h0, h1), Math.Max(Math.Max(h2, h3), h4));
(int min, int max) range = (min, max);
this._surfaceRangeCache[(cx, cz)] = range;
return range;
this._surfaceTopCache[(cx, cz)] = max;
return max;
}
private void DrainCompleted()