Compare commits
7 Commits
22531c6ee1
...
9c28bf152f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c28bf152f | ||
|
|
94930faccb | ||
|
|
43527e617b | ||
|
|
9daa9f8267 | ||
|
|
89ac70f1ab | ||
|
|
262933238c | ||
|
|
6c808b507b |
@@ -2,6 +2,7 @@
|
|||||||
in vec2 vUv;
|
in vec2 vUv;
|
||||||
in float vShade;
|
in float vShade;
|
||||||
in float vWorldY;
|
in float vWorldY;
|
||||||
|
in vec3 vWorldPos;
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
@@ -10,11 +11,25 @@ uniform float u_caveDarkStartY;
|
|||||||
uniform float u_caveDarkEndY;
|
uniform float u_caveDarkEndY;
|
||||||
uniform float u_caveMinBrightness;
|
uniform float u_caveMinBrightness;
|
||||||
|
|
||||||
|
uniform vec3 u_cameraPos;
|
||||||
|
uniform float u_fogStart;
|
||||||
|
uniform float u_fogEnd;
|
||||||
|
uniform vec3 u_skyColor;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 texel = texture(atlas, vUv);
|
vec4 texel = texture(atlas, vUv);
|
||||||
|
|
||||||
|
// Y-based cave darkness.
|
||||||
float t = clamp((u_caveDarkStartY - vWorldY) /
|
float t = clamp((u_caveDarkStartY - vWorldY) /
|
||||||
(u_caveDarkStartY - u_caveDarkEndY), 0.0, 1.0);
|
(u_caveDarkStartY - u_caveDarkEndY), 0.0, 1.0);
|
||||||
float brightness = mix(1.0, u_caveMinBrightness, t);
|
float brightness = mix(1.0, u_caveMinBrightness, t);
|
||||||
FragColor = vec4(texel.rgb * vShade * brightness, 1.0);
|
vec3 color = texel.rgb * vShade * brightness;
|
||||||
|
|
||||||
|
// Distance fog to the sky color, applied after cave darkness.
|
||||||
|
float dist = length(vWorldPos - u_cameraPos);
|
||||||
|
float fog = clamp((dist - u_fogStart) / (u_fogEnd - u_fogStart), 0.0, 1.0);
|
||||||
|
color = mix(color, u_skyColor, fog);
|
||||||
|
|
||||||
|
FragColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ layout(location = 2) in float aShade;
|
|||||||
out vec2 vUv;
|
out vec2 vUv;
|
||||||
out float vShade;
|
out float vShade;
|
||||||
out float vWorldY;
|
out float vWorldY;
|
||||||
|
out vec3 vWorldPos;
|
||||||
|
|
||||||
uniform mat4 model;
|
uniform mat4 model;
|
||||||
uniform mat4 view;
|
uniform mat4 view;
|
||||||
@@ -17,6 +18,7 @@ void main()
|
|||||||
vShade = aShade;
|
vShade = aShade;
|
||||||
// World-space position uses row-vector convention (matches gl_Position below).
|
// World-space position uses row-vector convention (matches gl_Position below).
|
||||||
vec4 worldPos = vec4(aPos, 1.0) * model;
|
vec4 worldPos = vec4(aPos, 1.0) * model;
|
||||||
|
vWorldPos = worldPos.xyz;
|
||||||
vWorldY = worldPos.y;
|
vWorldY = worldPos.y;
|
||||||
gl_Position = worldPos * view * projection;
|
gl_Position = worldPos * view * projection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
namespace VoxelEngine.Core;
|
namespace VoxelEngine.Core;
|
||||||
|
|
||||||
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
public sealed class Configuration
|
public sealed class Configuration
|
||||||
{
|
{
|
||||||
public int RenderDistanceHorizontal { get; set; } = 8;
|
public int RenderDistanceHorizontal { get; set; } = 8;
|
||||||
|
// Player-slab half-height in chunks. NOTE: repurposed by the visibility
|
||||||
|
// rework — this is no longer a global vertical load-box. It is how many
|
||||||
|
// chunks load above and below the player's chunk across the whole
|
||||||
|
// horizontal region, so caves at the player's depth render to the full
|
||||||
|
// horizontal distance.
|
||||||
public int RenderDistanceVertical { get; set; } = 2;
|
public int RenderDistanceVertical { get; set; } = 2;
|
||||||
|
|
||||||
|
// Chunks loaded below the lowest surface chunk of each column. Gives the
|
||||||
|
// surface chunk a solid neighbor below (so its buried bottom faces cull)
|
||||||
|
// and covers cave mouths just under the surface.
|
||||||
|
public int SurfaceShellSkirtBelow { get; set; } = 1;
|
||||||
|
|
||||||
|
// Distance fog begins at this fraction of the fog-end distance (fog end is
|
||||||
|
// derived from RenderDistanceHorizontal). 0.7 → fog starts at 70% of the edge.
|
||||||
|
public float FogStartFraction { get; set; } = 0.7f;
|
||||||
|
|
||||||
|
// Sky / clear color, also the distance-fog target color.
|
||||||
|
public Vector3 SkyColor { get; set; } = new(0.45f, 0.65f, 0.95f);
|
||||||
public int SpawnReadyRadiusHorizontal { get; set; } = 1;
|
public int SpawnReadyRadiusHorizontal { get; set; } = 1;
|
||||||
|
|
||||||
public int MaxGenerateJobsInFlight { get; set; } = 8;
|
public int MaxGenerateJobsInFlight { get; set; } = 8;
|
||||||
@@ -25,13 +44,26 @@ public sealed class Configuration
|
|||||||
public int? Seed { get; set; } = null;
|
public int? Seed { get; set; } = null;
|
||||||
|
|
||||||
// -- Cave generation tuning --
|
// -- Cave generation tuning --
|
||||||
|
|
||||||
|
// Spatial frequency of the 3D simplex cave-density field; higher = smaller,
|
||||||
|
// more frequent pockets.
|
||||||
public float CaveNoiseFrequency { get; set; } = 0.03f;
|
public float CaveNoiseFrequency { get; set; } = 0.03f;
|
||||||
|
// Density cutoff below which a solid voxel becomes air; higher = more cave.
|
||||||
public float CaveNoiseThreshold { get; set; } = -0.3f;
|
public float CaveNoiseThreshold { get; set; } = -0.3f;
|
||||||
|
// Edge length (blocks) of a worm-spawn grid cell; one worm candidate per cell.
|
||||||
public int WormCellSize { get; set; } = 64;
|
public int WormCellSize { get; set; } = 64;
|
||||||
|
// Per-cell chance out of 1000 that a worm spawns; 300 = 30%.
|
||||||
public int WormSpawnProbabilityPermille { get; set; } = 300;
|
public int WormSpawnProbabilityPermille { get; set; } = 300;
|
||||||
|
// Number of trace steps (≈ blocks) a worm travels.
|
||||||
public int WormMaxLength { get; set; } = 80;
|
public int WormMaxLength { get; set; } = 80;
|
||||||
|
// Worm tunnel radius bounds; interpolated + sine-modulated along the path
|
||||||
|
// to form bulges/chambers.
|
||||||
public float WormMinRadius { get; set; } = 1.5f;
|
public float WormMinRadius { get; set; } = 1.5f;
|
||||||
public float WormMaxRadius { get; set; } = 3.5f;
|
public float WormMaxRadius { get; set; } = 3.5f;
|
||||||
|
// Ceiling: caves carve only at or below this world-Y ("no caves above").
|
||||||
|
// Default 127 = world top (WorldHeight - 1) → no effective ceiling. Lower it
|
||||||
|
// to keep caves away from the surface (removes surface cave mouths).
|
||||||
|
public int MinCaveY { get; set; } = 127;
|
||||||
|
|
||||||
// -- Cave lighting (fog-distance darkness) --
|
// -- Cave lighting (fog-distance darkness) --
|
||||||
public float CaveDarkStartY { get; set; } = 72f; // WorldConstants.SeaLevel + 8
|
public float CaveDarkStartY { get; set; } = 72f; // WorldConstants.SeaLevel + 8
|
||||||
|
|||||||
29
Core/Game.cs
29
Core/Game.cs
@@ -29,6 +29,7 @@ public sealed class Game : GameWindow
|
|||||||
private WorldGenerator _generator = null!;
|
private WorldGenerator _generator = null!;
|
||||||
private ChunkStreamer _streamer = null!;
|
private ChunkStreamer _streamer = null!;
|
||||||
private Player _player = null!;
|
private Player _player = null!;
|
||||||
|
private double _logAccumSeconds;
|
||||||
|
|
||||||
public Game(Configuration cfg, GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
|
public Game(Configuration cfg, GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
|
||||||
: base(gameSettings, nativeSettings)
|
: base(gameSettings, nativeSettings)
|
||||||
@@ -43,7 +44,7 @@ public sealed class Game : GameWindow
|
|||||||
Log.Information("Game starting with seed={Seed} renderDistanceH={H} renderDistanceV={V}",
|
Log.Information("Game starting with seed={Seed} renderDistanceH={H} renderDistanceV={V}",
|
||||||
this._cfg.Seed, this._cfg.RenderDistanceHorizontal, this._cfg.RenderDistanceVertical);
|
this._cfg.Seed, this._cfg.RenderDistanceHorizontal, this._cfg.RenderDistanceVertical);
|
||||||
|
|
||||||
GL.ClearColor(0.45f, 0.65f, 0.95f, 1.0f);
|
GL.ClearColor(this._cfg.SkyColor.X, this._cfg.SkyColor.Y, this._cfg.SkyColor.Z, 1.0f);
|
||||||
GL.Enable(EnableCap.DepthTest);
|
GL.Enable(EnableCap.DepthTest);
|
||||||
GL.Enable(EnableCap.CullFace);
|
GL.Enable(EnableCap.CullFace);
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ public sealed class Game : GameWindow
|
|||||||
this._cfg,
|
this._cfg,
|
||||||
this._world,
|
this._world,
|
||||||
this._generator.Generate,
|
this._generator.Generate,
|
||||||
|
this._generator.SurfaceHeightAt,
|
||||||
mesher,
|
mesher,
|
||||||
this._registry,
|
this._registry,
|
||||||
new ThreadPoolJobExecutor(),
|
new ThreadPoolJobExecutor(),
|
||||||
@@ -119,15 +121,38 @@ public sealed class Game : GameWindow
|
|||||||
this._shader.SetFloat("u_caveDarkStartY", this._cfg.CaveDarkStartY);
|
this._shader.SetFloat("u_caveDarkStartY", this._cfg.CaveDarkStartY);
|
||||||
this._shader.SetFloat("u_caveDarkEndY", this._cfg.CaveDarkEndY);
|
this._shader.SetFloat("u_caveDarkEndY", this._cfg.CaveDarkEndY);
|
||||||
this._shader.SetFloat("u_caveMinBrightness", this._cfg.CaveMinBrightness);
|
this._shader.SetFloat("u_caveMinBrightness", this._cfg.CaveMinBrightness);
|
||||||
|
float fogEnd = this._cfg.RenderDistanceHorizontal * Chunk.Size;
|
||||||
|
this._shader.SetFloat("u_fogEnd", fogEnd);
|
||||||
|
this._shader.SetFloat("u_fogStart", fogEnd * this._cfg.FogStartFraction);
|
||||||
|
this._shader.SetVector3("u_cameraPos", this._player.Camera.Position);
|
||||||
|
this._shader.SetVector3("u_skyColor", this._cfg.SkyColor);
|
||||||
this._texture.Use(TextureUnit.Texture0);
|
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(
|
this._shader.SetMatrix4("model", Matrix4.CreateTranslation(
|
||||||
pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size));
|
pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size));
|
||||||
renderer.Draw();
|
renderer.Draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._logAccumSeconds += args.Time;
|
||||||
|
if (this._logAccumSeconds >= 1.0)
|
||||||
|
{
|
||||||
|
this._logAccumSeconds = 0;
|
||||||
|
Log.Information("Rendered {Drawn} / {Renderers} chunks ({Tracked} loaded)",
|
||||||
|
drawn, this._streamer.RendererCount, this._streamer.TrackedCount);
|
||||||
|
}
|
||||||
|
|
||||||
this.SwapBuffers();
|
this.SwapBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ public sealed class Player
|
|||||||
public Player(Configuration cfg, Vector3 spawnPosition, float aspectRatio)
|
public Player(Configuration cfg, Vector3 spawnPosition, float aspectRatio)
|
||||||
{
|
{
|
||||||
this._cfg = cfg;
|
this._cfg = cfg;
|
||||||
this.Camera = new Camera(spawnPosition, aspectRatio) { Fov = cfg.Fov };
|
this.Camera = new Camera(spawnPosition, aspectRatio)
|
||||||
|
{
|
||||||
|
Fov = cfg.Fov,
|
||||||
|
FarPlane = (cfg.RenderDistanceHorizontal + 1) * VoxelChunk.Size,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Move(Vector3 delta) => this.Camera.Move(delta);
|
public void Move(Vector3 delta) => this.Camera.Move(delta);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public sealed class Camera
|
|||||||
public float Yaw { get; private set; } = -90f; // degrees; -90 looks down -Z
|
public float Yaw { get; private set; } = -90f; // degrees; -90 looks down -Z
|
||||||
public float Pitch { get; private set; } // degrees
|
public float Pitch { get; private set; } // degrees
|
||||||
public float Fov { get; set; } = 60f; // degrees
|
public float Fov { get; set; } = 60f; // degrees
|
||||||
|
public float FarPlane { get; set; } = 100f; // world units
|
||||||
public float AspectRatio { get; set; }
|
public float AspectRatio { get; set; }
|
||||||
|
|
||||||
public Vector3 Front { get; private set; } = -Vector3.UnitZ;
|
public Vector3 Front { get; private set; } = -Vector3.UnitZ;
|
||||||
@@ -36,7 +37,7 @@ public sealed class Camera
|
|||||||
|
|
||||||
public Matrix4 GetProjectionMatrix() =>
|
public Matrix4 GetProjectionMatrix() =>
|
||||||
Matrix4.CreatePerspectiveFieldOfView(
|
Matrix4.CreatePerspectiveFieldOfView(
|
||||||
MathHelper.DegreesToRadians(this.Fov), this.AspectRatio, 0.1f, 100f);
|
MathHelper.DegreesToRadians(this.Fov), this.AspectRatio, 0.1f, this.FarPlane);
|
||||||
|
|
||||||
private void UpdateVectors()
|
private void UpdateVectors()
|
||||||
{
|
{
|
||||||
|
|||||||
61
Rendering/Frustum.cs
Normal file
61
Rendering/Frustum.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using OpenTK.Mathematics;
|
||||||
|
|
||||||
|
namespace VoxelEngine.Rendering;
|
||||||
|
|
||||||
|
// View-frustum for culling chunk AABBs. Built from a world→clip matrix in this
|
||||||
|
// engine's row-vector convention (clip = worldPos * view * projection, matching
|
||||||
|
// the chunk vertex shader). Planes point inward; a point is inside when it is on
|
||||||
|
// the positive side of all six.
|
||||||
|
public readonly struct Frustum
|
||||||
|
{
|
||||||
|
// Each Vector4 is (a, b, c, d) for plane a*x + b*y + c*z + d = 0, normalized.
|
||||||
|
private readonly Vector4 _left, _right, _bottom, _top, _near, _far;
|
||||||
|
|
||||||
|
private Frustum(Vector4 l, Vector4 r, Vector4 b, Vector4 t, Vector4 n, Vector4 f)
|
||||||
|
{
|
||||||
|
this._left = l; this._right = r; this._bottom = b;
|
||||||
|
this._top = t; this._near = n; this._far = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `vp` maps a world-space row vector p to clip space via p * vp. In that
|
||||||
|
// convention clip.x = p · column0, clip.w = p · column3, etc., so each plane
|
||||||
|
// is a combination of vp's columns (Gribb–Hartmann, row-vector form).
|
||||||
|
public static Frustum FromViewProjection(Matrix4 vp)
|
||||||
|
{
|
||||||
|
Vector4 c0 = vp.Column0, c1 = vp.Column1, c2 = vp.Column2, c3 = vp.Column3;
|
||||||
|
return new Frustum(
|
||||||
|
Normalize(c3 + c0), // left: clip.w + clip.x >= 0
|
||||||
|
Normalize(c3 - c0), // right: clip.w - clip.x >= 0
|
||||||
|
Normalize(c3 + c1), // bottom: clip.w + clip.y >= 0
|
||||||
|
Normalize(c3 - c1), // top: clip.w - clip.y >= 0
|
||||||
|
Normalize(c3 + c2), // near: clip.w + clip.z >= 0 (GL NDC z in [-1,1])
|
||||||
|
Normalize(c3 - c2)); // far: clip.w - clip.z >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the axis-aligned box [min,max] is at least partially inside.
|
||||||
|
public bool Intersects(Vector3 min, Vector3 max)
|
||||||
|
{
|
||||||
|
return InsideOrCrossing(this._left, min, max)
|
||||||
|
&& InsideOrCrossing(this._right, min, max)
|
||||||
|
&& InsideOrCrossing(this._bottom, min, max)
|
||||||
|
&& InsideOrCrossing(this._top, min, max)
|
||||||
|
&& InsideOrCrossing(this._near, min, max)
|
||||||
|
&& InsideOrCrossing(this._far, min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The box is fully outside a plane only if its most-positive vertex (the
|
||||||
|
// corner farthest along the plane normal) is still behind the plane.
|
||||||
|
private static bool InsideOrCrossing(Vector4 plane, Vector3 min, Vector3 max)
|
||||||
|
{
|
||||||
|
float px = plane.X >= 0 ? max.X : min.X;
|
||||||
|
float py = plane.Y >= 0 ? max.Y : min.Y;
|
||||||
|
float pz = plane.Z >= 0 ? max.Z : min.Z;
|
||||||
|
return plane.X * px + plane.Y * py + plane.Z * pz + plane.W >= 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 Normalize(Vector4 p)
|
||||||
|
{
|
||||||
|
float len = new Vector3(p.X, p.Y, p.Z).Length;
|
||||||
|
return len > 0f ? p / len : p;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,9 @@ public sealed class Shader : IDisposable
|
|||||||
public void SetFloat(string name, float value) =>
|
public void SetFloat(string name, float value) =>
|
||||||
GL.Uniform1(this.Location(name), value);
|
GL.Uniform1(this.Location(name), value);
|
||||||
|
|
||||||
|
public void SetVector3(string name, Vector3 value) =>
|
||||||
|
GL.Uniform3(this.Location(name), value.X, value.Y, value.Z);
|
||||||
|
|
||||||
private int Location(string name)
|
private int Location(string name)
|
||||||
{
|
{
|
||||||
if (this._uniforms.TryGetValue(name, out int location)) return location;
|
if (this._uniforms.TryGetValue(name, out int location)) return location;
|
||||||
|
|||||||
@@ -40,4 +40,12 @@ public class CameraTests
|
|||||||
var camera = new Camera(Vector3.Zero, 1.5f);
|
var camera = new Camera(Vector3.Zero, 1.5f);
|
||||||
Assert.NotEqual(Matrix4.Identity, camera.GetProjectionMatrix());
|
Assert.NotEqual(Matrix4.Identity, camera.GetProjectionMatrix());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FarPlane_ChangesProjection()
|
||||||
|
{
|
||||||
|
var near = new Camera(Vector3.Zero, 1f) { FarPlane = 100f };
|
||||||
|
var far = new Camera(Vector3.Zero, 1f) { FarPlane = 200f };
|
||||||
|
Assert.NotEqual(near.GetProjectionMatrix(), far.GetProjectionMatrix());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,4 +74,15 @@ public class ConfigurationTests
|
|||||||
Assert.Equal(32f, c.CaveDarkEndY); // SeaLevel (64) - 32
|
Assert.Equal(32f, c.CaveDarkEndY); // SeaLevel (64) - 32
|
||||||
Assert.Equal(0.15f, c.CaveMinBrightness);
|
Assert.Equal(0.15f, c.CaveMinBrightness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Defaults_HaveVisibilityAndCaveCeilingConstants()
|
||||||
|
{
|
||||||
|
var c = new Configuration();
|
||||||
|
|
||||||
|
Assert.Equal(1, c.SurfaceShellSkirtBelow);
|
||||||
|
Assert.Equal(0.7f, c.FogStartFraction);
|
||||||
|
Assert.Equal(new OpenTK.Mathematics.Vector3(0.45f, 0.65f, 0.95f), c.SkyColor);
|
||||||
|
Assert.Equal(127, c.MinCaveY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
VoxelEngine.Tests/FrustumTests.cs
Normal file
44
VoxelEngine.Tests/FrustumTests.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using OpenTK.Mathematics;
|
||||||
|
using VoxelEngine.Rendering;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace VoxelEngine.Tests;
|
||||||
|
|
||||||
|
public class FrustumTests
|
||||||
|
{
|
||||||
|
// Camera at the origin, default yaw -90° → looks down -Z.
|
||||||
|
private static Frustum MakeFrustum()
|
||||||
|
{
|
||||||
|
var cam = new Camera(new Vector3(0, 0, 0), 16f / 9f);
|
||||||
|
return Frustum.FromViewProjection(cam.GetViewMatrix() * cam.GetProjectionMatrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AABBInFront_IsVisible()
|
||||||
|
{
|
||||||
|
var f = MakeFrustum();
|
||||||
|
Assert.True(f.Intersects(new Vector3(-2, -2, -22), new Vector3(2, 2, -18)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AABBBehindCamera_IsCulled()
|
||||||
|
{
|
||||||
|
var f = MakeFrustum();
|
||||||
|
Assert.False(f.Intersects(new Vector3(-2, -2, 18), new Vector3(2, 2, 22)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AABBStraddlingCamera_Intersects()
|
||||||
|
{
|
||||||
|
var f = MakeFrustum();
|
||||||
|
Assert.True(f.Intersects(new Vector3(-2, -2, -5), new Vector3(2, 2, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AABBFarToTheSide_IsCulled()
|
||||||
|
{
|
||||||
|
var f = MakeFrustum();
|
||||||
|
// Far along +X at a depth in front of the camera — outside horizontal FOV.
|
||||||
|
Assert.False(f.Intersects(new Vector3(1000, -2, -5), new Vector3(1004, 2, -1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using OpenTK.Mathematics;
|
using OpenTK.Mathematics;
|
||||||
using VoxelEngine.Core;
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World;
|
using VoxelEngine.World;
|
||||||
@@ -44,7 +45,8 @@ internal sealed class EmptyMesher : IChunkMesher
|
|||||||
|
|
||||||
public class ChunkStreamerTests
|
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 registry = new BlockRegistry();
|
||||||
var store = new InMemoryChunkModificationStore();
|
var store = new InMemoryChunkModificationStore();
|
||||||
@@ -59,7 +61,8 @@ public class ChunkStreamerTests
|
|||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
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);
|
return (streamer, world, created);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +189,7 @@ public class ChunkStreamerTests
|
|||||||
int localIndex = (5 * VoxelChunk.Size + 6) * VoxelChunk.Size + 7;
|
int localIndex = (5 * VoxelChunk.Size + 6) * VoxelChunk.Size + 7;
|
||||||
store.RecordEdit(pos, localIndex, registry.Stone.Id);
|
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);
|
streamer.Tick(pos);
|
||||||
|
|
||||||
Assert.Equal(registry.Stone.Id, world.GetBlock(5, 6, 7).Id);
|
Assert.Equal(registry.Stone.Id, world.GetBlock(5, 6, 7).Id);
|
||||||
@@ -232,7 +235,7 @@ public class ChunkStreamerTests
|
|||||||
var executor = new SynchronousJobExecutor();
|
var executor = new SynchronousJobExecutor();
|
||||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
||||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
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);
|
var pos = new Vector3i(0, 0, 0);
|
||||||
streamer.Tick(pos);
|
streamer.Tick(pos);
|
||||||
@@ -348,7 +351,7 @@ public class ChunkStreamerTests
|
|||||||
};
|
};
|
||||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
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));
|
streamer.Tick(new Vector3i(0, 0, 0));
|
||||||
Assert.Equal(1, generateCalls);
|
Assert.Equal(1, generateCalls);
|
||||||
@@ -382,7 +385,7 @@ public class ChunkStreamerTests
|
|||||||
};
|
};
|
||||||
Func<Vector3i, VoxelChunk> generate = _ => new VoxelChunk();
|
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);
|
var pos = new Vector3i(0, 0, 0);
|
||||||
streamer.Tick(pos);
|
streamer.Tick(pos);
|
||||||
|
|
||||||
@@ -419,4 +422,72 @@ public class ChunkStreamerTests
|
|||||||
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0)));
|
Assert.Null(world.GetChunk(new Vector3i(0, 3, 0)));
|
||||||
Assert.Null(world.GetChunk(new Vector3i(0, 1, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeTargetSet_CachesSurfaceHeights_OnlySamplesNewColumns()
|
||||||
|
{
|
||||||
|
var cfg = new Configuration
|
||||||
|
{
|
||||||
|
RenderDistanceHorizontal = 1,
|
||||||
|
RenderDistanceVertical = 0,
|
||||||
|
SpawnReadyRadiusHorizontal = 1,
|
||||||
|
MaxGenerateJobsInFlight = 100,
|
||||||
|
MaxMeshUploadsPerFrame = 100,
|
||||||
|
};
|
||||||
|
int calls = 0;
|
||||||
|
var (streamer, _, _) = CreateStreamer(cfg, surfaceHeightAt: (_, _) => { calls++; return 8; });
|
||||||
|
|
||||||
|
streamer.Tick(new Vector3i(0, 0, 0));
|
||||||
|
int afterFirst = calls; // 9 columns × 5 samples = 45 (spawn-check reuses cache)
|
||||||
|
streamer.Tick(new Vector3i(1, 0, 0)); // overlaps 6 columns; only the cx=2 strip is new
|
||||||
|
int newCalls = calls - afterFirst;
|
||||||
|
|
||||||
|
Assert.Equal(45, afterFirst);
|
||||||
|
Assert.Equal(15, newCalls); // 3 new columns × 5 — cached columns are not re-sampled
|
||||||
|
}
|
||||||
|
|
||||||
|
[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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class StreamingIntegrationTests
|
|||||||
var mesher = new ChunkMesher(atlasWidthPixels: 128, atlasHeightPixels: 32);
|
var mesher = new ChunkMesher(atlasWidthPixels: 128, atlasHeightPixels: 32);
|
||||||
var executor = new SynchronousJobExecutor();
|
var executor = new SynchronousJobExecutor();
|
||||||
Func<float[], IChunkRenderer> factory = m => new FakeRenderer(m);
|
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);
|
return (streamer, world, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,4 +183,20 @@ public class CaveCarverTests
|
|||||||
Assert.True(bothEmpty || bothCarved,
|
Assert.True(bothEmpty || bothCarved,
|
||||||
$"seam mismatch: c1={airC1Seam}, c2={airC2Seam}");
|
$"seam mismatch: c1={airC1Seam}, c2={airC2Seam}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Carve_RespectsMinCaveY_NoCarvingAboveCeiling()
|
||||||
|
{
|
||||||
|
var cfg = new Configuration { MinCaveY = 20 };
|
||||||
|
var carver = new CaveCarver(seed: 42, Blocks, cfg);
|
||||||
|
Chunk c = MakeStoneChunk();
|
||||||
|
|
||||||
|
// Chunk Y=2 → worldY 32..47, all above MinCaveY=20 → must stay solid.
|
||||||
|
carver.Carve(c, new Vector3i(0, 2, 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(Blocks.Stone.Id, c[x, y, z]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
private readonly Configuration _cfg;
|
private readonly Configuration _cfg;
|
||||||
private readonly IWorld _world;
|
private readonly IWorld _world;
|
||||||
private readonly Func<Vector3i, VoxelChunk> _generate;
|
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();
|
||||||
private readonly IChunkMesher _mesher;
|
private readonly IChunkMesher _mesher;
|
||||||
private readonly BlockRegistry _registry;
|
private readonly BlockRegistry _registry;
|
||||||
private readonly IJobExecutor _executor;
|
private readonly IJobExecutor _executor;
|
||||||
@@ -38,6 +43,7 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
private Vector3i _spawnAnchor;
|
private Vector3i _spawnAnchor;
|
||||||
|
|
||||||
public int RendererCount => this._renderers.Count;
|
public int RendererCount => this._renderers.Count;
|
||||||
|
public int TrackedCount => this._tracked.Count;
|
||||||
public bool IsReady(Vector3i pos) => this._ready.Contains(pos);
|
public bool IsReady(Vector3i pos) => this._ready.Contains(pos);
|
||||||
public bool SpawnReady => this._spawnReady;
|
public bool SpawnReady => this._spawnReady;
|
||||||
|
|
||||||
@@ -51,6 +57,7 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
Configuration cfg,
|
Configuration cfg,
|
||||||
IWorld world,
|
IWorld world,
|
||||||
Func<Vector3i, VoxelChunk> generate,
|
Func<Vector3i, VoxelChunk> generate,
|
||||||
|
Func<int, int, int> surfaceHeightAt,
|
||||||
IChunkMesher mesher,
|
IChunkMesher mesher,
|
||||||
BlockRegistry registry,
|
BlockRegistry registry,
|
||||||
IJobExecutor executor,
|
IJobExecutor executor,
|
||||||
@@ -59,6 +66,7 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
this._cfg = cfg;
|
this._cfg = cfg;
|
||||||
this._world = world;
|
this._world = world;
|
||||||
this._generate = generate;
|
this._generate = generate;
|
||||||
|
this._surfaceHeightAt = surfaceHeightAt;
|
||||||
this._mesher = mesher;
|
this._mesher = mesher;
|
||||||
this._registry = registry;
|
this._registry = registry;
|
||||||
this._executor = executor;
|
this._executor = executor;
|
||||||
@@ -85,9 +93,20 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
this.UpdateSpawnReady();
|
this.UpdateSpawnReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawAll(Action<Vector3i, IChunkRenderer> 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<Vector3i, bool> isChunkVisible, Action<Vector3i, IChunkRenderer> 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()
|
public void Dispose()
|
||||||
@@ -104,18 +123,8 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
this._spawnAnchorSet = true;
|
this._spawnAnchorSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int h = this._cfg.RenderDistanceHorizontal;
|
|
||||||
int v = this._cfg.RenderDistanceVertical;
|
|
||||||
|
|
||||||
var target = new HashSet<Vector3i>();
|
var target = new HashSet<Vector3i>();
|
||||||
for (int dx = -h; dx <= h; dx++)
|
this.CollectTargets(p, this._cfg.RenderDistanceHorizontal, target);
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var pos in target)
|
foreach (var pos in target)
|
||||||
{
|
{
|
||||||
@@ -143,6 +152,83 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
this._tracked.Remove(pos);
|
this._tracked.Remove(pos);
|
||||||
this._pendingUnload.Enqueue(pos);
|
this._pendingUnload.Enqueue(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.PruneSurfaceCache(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop cached surface ranges 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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
if (this._surfaceRangeCache.TryGetValue((cx, cz), out (int min, int max) cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
(int min, int max) range = (min, max);
|
||||||
|
this._surfaceRangeCache[(cx, cz)] = range;
|
||||||
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrainCompleted()
|
private void DrainCompleted()
|
||||||
@@ -176,20 +262,17 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
{
|
{
|
||||||
if (this._spawnReady || !this._spawnAnchorSet) return;
|
if (this._spawnReady || !this._spawnAnchorSet) return;
|
||||||
|
|
||||||
int hs = this._cfg.SpawnReadyRadiusHorizontal;
|
var spawnTargets = new HashSet<Vector3i>();
|
||||||
int v = this._cfg.RenderDistanceVertical;
|
this.CollectTargets(this._spawnAnchor, this._cfg.SpawnReadyRadiusHorizontal, spawnTargets);
|
||||||
Vector3i a = this._spawnAnchor;
|
|
||||||
|
|
||||||
for (int dx = -hs; dx <= hs; dx++)
|
foreach (var pos in spawnTargets)
|
||||||
for (int dy = -v; dy <= v; dy++)
|
|
||||||
for (int dz = -hs; dz <= hs; dz++)
|
|
||||||
{
|
{
|
||||||
int cy = a.Y + dy;
|
if (this._failed.Contains(pos)) continue;
|
||||||
if (cy < 0 || cy >= WorldConstants.WorldHeightChunks) continue;
|
if (!this._ready.Contains(pos)) return;
|
||||||
if (!this._ready.Contains(new Vector3i(a.X + dx, cy, a.Z + dz))) return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._spawnReady = true;
|
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)
|
private void InstallResult(ChunkJobResult result)
|
||||||
@@ -214,7 +297,7 @@ public sealed class ChunkStreamer : IDisposable
|
|||||||
}
|
}
|
||||||
// Skip renderer allocation for empty meshes (pure-air chunks above
|
// Skip renderer allocation for empty meshes (pure-air chunks above
|
||||||
// terrain, or chunks whose every face culls). The chunk still counts
|
// 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)
|
if (result.Mesh.Length > 0)
|
||||||
{
|
{
|
||||||
this._renderers[result.Pos] = this._rendererFactory(result.Mesh);
|
this._renderers[result.Pos] = this._rendererFactory(result.Mesh);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public sealed class CaveCarver
|
|||||||
{
|
{
|
||||||
int worldY = baseY + y;
|
int worldY = baseY + y;
|
||||||
if (worldY <= 0) continue;
|
if (worldY <= 0) continue;
|
||||||
|
if (worldY > this._cfg.MinCaveY) continue;
|
||||||
|
|
||||||
ushort id = chunk[x, y, z];
|
ushort id = chunk[x, y, z];
|
||||||
if (id == this._blocks.Air.Id) continue;
|
if (id == this._blocks.Air.Id) continue;
|
||||||
@@ -160,6 +161,7 @@ public sealed class CaveCarver
|
|||||||
{
|
{
|
||||||
int worldY = baseY + y;
|
int worldY = baseY + y;
|
||||||
if (worldY <= 0) continue;
|
if (worldY <= 0) continue;
|
||||||
|
if (worldY > this._cfg.MinCaveY) continue;
|
||||||
|
|
||||||
float vx = baseX + x + 0.5f;
|
float vx = baseX + x + 0.5f;
|
||||||
float vy = baseY + y + 0.5f;
|
float vy = baseY + y + 0.5f;
|
||||||
|
|||||||
Reference in New Issue
Block a user