diff --git a/Core/Game.cs b/Core/Game.cs index 8672de5..2bb24ba 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -29,6 +29,7 @@ public sealed class Game : GameWindow private WorldGenerator _generator = null!; private ChunkStreamer _streamer = null!; private Player _player = null!; + private double _logAccumSeconds; public Game(Configuration cfg, GameWindowSettings gameSettings, NativeWindowSettings nativeSettings) : base(gameSettings, nativeSettings) @@ -127,12 +128,30 @@ public sealed class Game : GameWindow this._shader.SetVector3("u_skyColor", this._cfg.SkyColor); this._texture.Use(TextureUnit.Texture0); - this._streamer.DrawAll((pos, renderer) => + Matrix4 viewProj = this._player.Camera.GetViewMatrix() * this._player.Camera.GetProjectionMatrix(); + Frustum frustum = Frustum.FromViewProjection(viewProj); + + int drawn = this._streamer.DrawVisible( + pos => + { + Vector3 min = new(pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size); + Vector3 max = min + new Vector3(Chunk.Size); + return frustum.Intersects(min, max); + }, + (pos, renderer) => + { + this._shader.SetMatrix4("model", Matrix4.CreateTranslation( + pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size)); + renderer.Draw(); + }); + + this._logAccumSeconds += args.Time; + if (this._logAccumSeconds >= 1.0) { - this._shader.SetMatrix4("model", Matrix4.CreateTranslation( - pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size)); - renderer.Draw(); - }); + this._logAccumSeconds = 0; + Log.Information("Rendered {Drawn} / {Renderers} chunks ({Tracked} loaded)", + drawn, this._streamer.RendererCount, this._streamer.TrackedCount); + } this.SwapBuffers(); } diff --git a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs index 9b75a5e..abe7476 100644 --- a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs +++ b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using OpenTK.Mathematics; using VoxelEngine.Core; using VoxelEngine.World; @@ -445,4 +446,25 @@ public class ChunkStreamerTests 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 } + + [Fact] + public void DrawVisible_DrawsOnlyChunksPassingPredicate_AndReturnsCount() + { + var cfg = new Configuration + { + RenderDistanceHorizontal = 1, + RenderDistanceVertical = 0, + MaxGenerateJobsInFlight = 100, + MaxMeshUploadsPerFrame = 100, + }; + var (streamer, _, created) = CreateStreamer(cfg); + streamer.Tick(new Vector3i(0, 0, 0)); // 9 columns, all chunk Y 0, non-empty meshes + + int drawn = streamer.DrawVisible( + pos => pos == new Vector3i(0, 0, 0), // only the centre chunk "visible" + (_, r) => r.Draw()); + + Assert.Equal(1, drawn); + Assert.Equal(1, created.Sum(r => r.DrawCount)); + } } diff --git a/World/Streaming/ChunkStreamer.cs b/World/Streaming/ChunkStreamer.cs index f050830..f44620b 100644 --- a/World/Streaming/ChunkStreamer.cs +++ b/World/Streaming/ChunkStreamer.cs @@ -39,6 +39,7 @@ public sealed class ChunkStreamer : IDisposable private Vector3i _spawnAnchor; public int RendererCount => this._renderers.Count; + public int TrackedCount => this._tracked.Count; public bool IsReady(Vector3i pos) => this._ready.Contains(pos); public bool SpawnReady => this._spawnReady; @@ -88,9 +89,20 @@ public sealed class ChunkStreamer : IDisposable this.UpdateSpawnReady(); } - public void DrawAll(Action drawCallback) + // Draws every loaded chunk for which `isChunkVisible` returns true. Returns + // the number of chunks actually drawn (passed the visibility test). The + // visibility predicate lives with the caller so the streamer stays free of + // any rendering/Frustum dependency. + public int DrawVisible(Func isChunkVisible, Action draw) { - foreach (var kvp in this._renderers) drawCallback(kvp.Key, kvp.Value); + int drawn = 0; + foreach (var kvp in this._renderers) + { + if (!isChunkVisible(kvp.Key)) continue; + draw(kvp.Key, kvp.Value); + drawn++; + } + return drawn; } public void Dispose() @@ -258,7 +270,7 @@ public sealed class ChunkStreamer : IDisposable } // Skip renderer allocation for empty meshes (pure-air chunks above // terrain, or chunks whose every face culls). The chunk still counts - // as ready — DrawAll's iteration over _renderers naturally no-ops. + // as ready — DrawVisible's iteration over _renderers naturally no-ops. if (result.Mesh.Length > 0) { this._renderers[result.Pos] = this._rendererFactory(result.Mesh);