feat(streaming): load surface shell + player slab instead of vertical box
This commit is contained in:
@@ -68,6 +68,7 @@ public sealed class Game : GameWindow
|
||||
this._cfg,
|
||||
this._world,
|
||||
this._generator.Generate,
|
||||
this._generator.SurfaceHeightAt,
|
||||
mesher,
|
||||
this._registry,
|
||||
new ThreadPoolJobExecutor(),
|
||||
|
||||
@@ -44,7 +44,8 @@ internal sealed class EmptyMesher : IChunkMesher
|
||||
|
||||
public class ChunkStreamerTests
|
||||
{
|
||||
private static (ChunkStreamer streamer, World.World world, List<FakeRenderer> created) CreateStreamer(Configuration cfg)
|
||||
private static (ChunkStreamer streamer, World.World world, List<FakeRenderer> created) CreateStreamer(
|
||||
Configuration cfg, Func<int, int, int>? surfaceHeightAt = null)
|
||||
{
|
||||
var registry = new BlockRegistry();
|
||||
var store = new InMemoryChunkModificationStore();
|
||||
@@ -59,7 +60,8 @@ public class ChunkStreamerTests
|
||||
return r;
|
||||
};
|
||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
Func<int, int, int> height = surfaceHeightAt ?? ((_, _) => 8);
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, height, mesher, registry, executor, factory);
|
||||
return (streamer, world, created);
|
||||
}
|
||||
|
||||
@@ -186,7 +188,7 @@ public class ChunkStreamerTests
|
||||
int localIndex = (5 * VoxelChunk.Size + 6) * VoxelChunk.Size + 7;
|
||||
store.RecordEdit(pos, localIndex, registry.Stone.Id);
|
||||
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, (_, _) => 8, mesher, registry, executor, factory);
|
||||
streamer.Tick(pos);
|
||||
|
||||
Assert.Equal(registry.Stone.Id, world.GetBlock(5, 6, 7).Id);
|
||||
@@ -232,7 +234,7 @@ public class ChunkStreamerTests
|
||||
var executor = new SynchronousJobExecutor();
|
||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, (_, _) => 8, mesher, registry, executor, factory);
|
||||
|
||||
var pos = new Vector3i(0, 0, 0);
|
||||
streamer.Tick(pos);
|
||||
@@ -348,7 +350,7 @@ public class ChunkStreamerTests
|
||||
};
|
||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
||||
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, (_, _) => 8, mesher, registry, executor, factory);
|
||||
|
||||
streamer.Tick(new Vector3i(0, 0, 0));
|
||||
Assert.Equal(1, generateCalls);
|
||||
@@ -382,7 +384,7 @@ public class ChunkStreamerTests
|
||||
};
|
||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
||||
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory);
|
||||
var streamer = new ChunkStreamer(cfg, world, generate, (_, _) => 8, mesher, registry, executor, factory);
|
||||
var pos = new Vector3i(0, 0, 0);
|
||||
streamer.Tick(pos);
|
||||
|
||||
@@ -419,4 +421,28 @@ public class ChunkStreamerTests
|
||||
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0)));
|
||||
Assert.Null(world.GetChunk(new Vector3i(0, 1, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_LoadsSurfaceShell_AndExcludesAirAboveSurface()
|
||||
{
|
||||
var cfg = new Configuration
|
||||
{
|
||||
RenderDistanceHorizontal = 0, // single column at the player
|
||||
RenderDistanceVertical = 0, // slab is just the player's chunk
|
||||
SurfaceShellSkirtBelow = 1,
|
||||
MaxGenerateJobsInFlight = 100,
|
||||
MaxMeshUploadsPerFrame = 100,
|
||||
};
|
||||
// Surface at worldY 40 → chunk Y 2. Shell = [2-1, 2] = {1, 2}. Slab at
|
||||
// player Y=0 = {0}. Union = {0, 1, 2}. Chunk Y 3+ is air above surface.
|
||||
var (streamer, world, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => 40);
|
||||
|
||||
streamer.Tick(new Vector3i(0, 0, 0));
|
||||
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 0, 0))); // slab
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 1, 0))); // shell skirt
|
||||
Assert.NotNull(world.GetChunk(new Vector3i(0, 2, 0))); // shell surface
|
||||
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0))); // air above surface — not loaded
|
||||
Assert.Null(world.GetChunk(new Vector3i(0, 5, 0))); // air above surface — not loaded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class StreamingIntegrationTests
|
||||
var mesher = new ChunkMesher(atlasWidthPixels: 128, atlasHeightPixels: 32);
|
||||
var executor = new SynchronousJobExecutor();
|
||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
||||
var streamer = new ChunkStreamer(cfg, world, gen.Generate, mesher, registry, executor, factory);
|
||||
var streamer = new ChunkStreamer(cfg, world, gen.Generate, gen.SurfaceHeightAt, mesher, registry, executor, factory);
|
||||
return (streamer, world, store);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
private readonly Configuration _cfg;
|
||||
private readonly IWorld _world;
|
||||
private readonly Func<Vector3i, VoxelChunk> _generate;
|
||||
private readonly Func<int, int, int> _surfaceHeightAt;
|
||||
private readonly IChunkMesher _mesher;
|
||||
private readonly BlockRegistry _registry;
|
||||
private readonly IJobExecutor _executor;
|
||||
@@ -51,6 +52,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
Configuration cfg,
|
||||
IWorld world,
|
||||
Func<Vector3i, VoxelChunk> generate,
|
||||
Func<int, int, int> surfaceHeightAt,
|
||||
IChunkMesher mesher,
|
||||
BlockRegistry registry,
|
||||
IJobExecutor executor,
|
||||
@@ -59,6 +61,7 @@ public sealed class ChunkStreamer : IDisposable
|
||||
this._cfg = cfg;
|
||||
this._world = world;
|
||||
this._generate = generate;
|
||||
this._surfaceHeightAt = surfaceHeightAt;
|
||||
this._mesher = mesher;
|
||||
this._registry = registry;
|
||||
this._executor = executor;
|
||||
@@ -104,18 +107,8 @@ public sealed class ChunkStreamer : IDisposable
|
||||
this._spawnAnchorSet = true;
|
||||
}
|
||||
|
||||
int h = this._cfg.RenderDistanceHorizontal;
|
||||
int v = this._cfg.RenderDistanceVertical;
|
||||
|
||||
var target = new HashSet<Vector3i>();
|
||||
for (int dx = -h; dx <= h; dx++)
|
||||
for (int dy = -v; dy <= v; dy++)
|
||||
for (int dz = -h; dz <= h; dz++)
|
||||
{
|
||||
int cy = p.Y + dy;
|
||||
if (cy < 0 || cy >= WorldConstants.WorldHeightChunks) continue;
|
||||
target.Add(new Vector3i(p.X + dx, cy, p.Z + dz));
|
||||
}
|
||||
this.CollectTargets(p, this._cfg.RenderDistanceHorizontal, target);
|
||||
|
||||
foreach (var pos in target)
|
||||
{
|
||||
@@ -145,6 +138,60 @@ public sealed class ChunkStreamer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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);
|
||||
|
||||
ChunkStreamer.AddColumn(into, cx, cz, center.Y - v, center.Y + v);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddColumn(HashSet<Vector3i> into, int cx, int cz, int loCy, int hiCy)
|
||||
{
|
||||
int lo = Math.Max(0, loCy);
|
||||
int hi = Math.Min(WorldConstants.WorldHeightChunks - 1, hiCy);
|
||||
for (int cy = lo; cy <= hi; cy++)
|
||||
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)
|
||||
{
|
||||
int x0 = cx * VoxelChunk.Size;
|
||||
int z0 = cz * VoxelChunk.Size;
|
||||
int x1 = x0 + VoxelChunk.Size - 1;
|
||||
int z1 = z0 + VoxelChunk.Size - 1;
|
||||
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 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));
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
private void DrainCompleted()
|
||||
{
|
||||
int budget = this._cfg.MaxMeshUploadsPerFrame;
|
||||
@@ -176,20 +223,17 @@ public sealed class ChunkStreamer : IDisposable
|
||||
{
|
||||
if (this._spawnReady || !this._spawnAnchorSet) return;
|
||||
|
||||
int hs = this._cfg.SpawnReadyRadiusHorizontal;
|
||||
int v = this._cfg.RenderDistanceVertical;
|
||||
Vector3i a = this._spawnAnchor;
|
||||
var spawnTargets = new HashSet<Vector3i>();
|
||||
this.CollectTargets(this._spawnAnchor, this._cfg.SpawnReadyRadiusHorizontal, spawnTargets);
|
||||
|
||||
for (int dx = -hs; dx <= hs; dx++)
|
||||
for (int dy = -v; dy <= v; dy++)
|
||||
for (int dz = -hs; dz <= hs; dz++)
|
||||
foreach (var pos in spawnTargets)
|
||||
{
|
||||
int cy = a.Y + dy;
|
||||
if (cy < 0 || cy >= WorldConstants.WorldHeightChunks) continue;
|
||||
if (!this._ready.Contains(new Vector3i(a.X + dx, cy, a.Z + dz))) return;
|
||||
if (this._failed.Contains(pos)) continue;
|
||||
if (!this._ready.Contains(pos)) return;
|
||||
}
|
||||
|
||||
this._spawnReady = true;
|
||||
Log.Information("Spawn area ready at anchor {Anchor}", a);
|
||||
Log.Information("Spawn area ready at anchor {Anchor}", this._spawnAnchor);
|
||||
}
|
||||
|
||||
private void InstallResult(ChunkJobResult result)
|
||||
|
||||
Reference in New Issue
Block a user