feat(world-gen): larger biomes, smooth height blending, capped mountains; centralize tunables in Configuration
Terrain feel: biome regions ~10x larger (Voronoi cell size 96->960); terrain height now eases across biome borders via a Voronoi distance blend (smooth, and ~25x cheaper than the old 169-sample box blur); mountains capped to ~2/3 world height and gentled (ridged amplitude/frequency lowered), snow line lowered to match. Centralization: ~27 world-gen/render tuning constants (biome amplitudes & frequencies, climate-noise frequencies/octaves, biome-selection thresholds, blend width, cell size, filler depth, snow line, camera near plane, spawn offset, overlay scale) hoisted from scattered literals into a region-organized, fully-commented Configuration. Configuration is threaded into NoiseSource, the biomes, BiomeRegistry, BiomeMap (+Classify), TerrainSampler, WorldGenerator (+BlockAt filler depth), and Camera via Player. Defaults equal the prior values, so generation is unchanged; worldgen tests updated for the new constructor signatures.
This commit is contained in:
@@ -4,58 +4,205 @@ using OpenTK.Mathematics;
|
||||
|
||||
public sealed class Configuration
|
||||
{
|
||||
#region -- World --
|
||||
|
||||
// World generation seed. null → a fresh random seed each run; set a value for
|
||||
// a reproducible world.
|
||||
public int? Seed { get; set; } = null;
|
||||
|
||||
// Blocks above the terrain surface at (0,0) where the player spawns.
|
||||
public float SpawnHeightOffset { get; set; } = 2f;
|
||||
|
||||
#endregion -- World --
|
||||
|
||||
#region -- Rendering & View --
|
||||
|
||||
// Horizontal view radius in chunks (Chebyshev). Drives how far terrain loads
|
||||
// and the fog / far-plane distance. Higher = see further, much heavier load.
|
||||
public int RenderDistanceHorizontal { get; set; } = 8;
|
||||
|
||||
// Camera vertical field of view, in degrees.
|
||||
public float Fov { get; set; } = 60f;
|
||||
|
||||
// Near clip plane distance (world units); anything nearer is not drawn.
|
||||
// (The far plane is derived from RenderDistanceHorizontal over in Player.)
|
||||
public float CameraNearPlane { get; set; } = 0.1f;
|
||||
|
||||
// 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.
|
||||
// Sky / clear colour, also the distance-fog target colour (RGB, 0–1).
|
||||
public Vector3 SkyColor { get; set; } = new(0.45f, 0.65f, 0.95f);
|
||||
public int SpawnReadyRadiusHorizontal { get; set; } = 1;
|
||||
|
||||
public int MaxGenerateJobsInFlight { get; set; } = 8;
|
||||
public int MaxMeshUploadsPerFrame { get; set; } = 4;
|
||||
public int WorkerThreadCount { get; set; } = Math.Max(1, Environment.ProcessorCount / 2);
|
||||
#endregion -- Rendering & View --
|
||||
|
||||
// Multiplier applied to (dy)^2 in chunk-load priority. >1 prioritizes
|
||||
// chunks at the player's Y over equally-distant chunks above/below, so the
|
||||
// visible surface fills in before far-underground / overhead chunks.
|
||||
public int VerticalPriorityWeight { get; set; } = 4;
|
||||
|
||||
public float MoveSpeed { get; set; } = 8f;
|
||||
public float MouseSensitivity { get; set; } = 0.15f;
|
||||
public float Fov { get; set; } = 60f;
|
||||
#region -- Window --
|
||||
|
||||
// Initial window size in pixels.
|
||||
public int WindowWidth { get; set; } = 1280;
|
||||
public int WindowHeight { get; set; } = 720;
|
||||
|
||||
public int? Seed { get; set; } = null;
|
||||
#endregion -- Window --
|
||||
|
||||
// -- Cave generation tuning --
|
||||
#region -- Chunk Streaming --
|
||||
|
||||
// Horizontal radius (chunks) around spawn that must finish loading before the
|
||||
// loading screen clears.
|
||||
public int SpawnReadyRadiusHorizontal { get; set; } = 1;
|
||||
|
||||
// Max chunk generate/mesh jobs running on worker threads at once.
|
||||
public int MaxGenerateJobsInFlight { get; set; } = 8;
|
||||
|
||||
// Max finished chunk meshes uploaded to the GPU per frame (caps per-frame hitches).
|
||||
public int MaxMeshUploadsPerFrame { get; set; } = 4;
|
||||
|
||||
// Background worker threads used for chunk generation/meshing.
|
||||
public int WorkerThreadCount { get; set; } = Math.Max(1, Environment.ProcessorCount / 2);
|
||||
|
||||
// Multiplier applied to (dy)² in chunk-load priority. >1 prioritizes chunks at
|
||||
// the player's Y over equally-distant chunks above/below, so the visible
|
||||
// surface fills in before far-underground / overhead chunks.
|
||||
public int VerticalPriorityWeight { get; set; } = 4;
|
||||
|
||||
#endregion -- Chunk Streaming --
|
||||
|
||||
#region -- Player & Controls --
|
||||
|
||||
// Movement speed, in blocks per second.
|
||||
public float MoveSpeed { get; set; } = 8f;
|
||||
|
||||
// Mouse-look sensitivity (degrees of rotation per pixel of mouse movement).
|
||||
public float MouseSensitivity { get; set; } = 0.15f;
|
||||
|
||||
#endregion -- Player & Controls --
|
||||
|
||||
#region -- World Gen: Terrain Shape --
|
||||
|
||||
// Average width in blocks of one biome region (Voronoi cell). Larger = bigger,
|
||||
// fewer biomes.
|
||||
public float BiomeCellSize { get; set; } = 960f;
|
||||
|
||||
// Blocks over which terrain height eases from one biome to its neighbour across
|
||||
// their shared border. Larger = gentler, wider transitions.
|
||||
public float BiomeBlendWidth { get; set; } = 128f;
|
||||
|
||||
// Filler layers (dirt/sand) placed directly below the surface block before the
|
||||
// biome's solid under-material (usually stone) begins.
|
||||
public int SurfaceFillerDepth { get; set; } = 3;
|
||||
|
||||
#endregion -- World Gen: Terrain Shape --
|
||||
|
||||
#region -- World Gen: Climate Noise --
|
||||
// Low-frequency fields that decide biome placement. Lower frequency = larger,
|
||||
// smoother climate regions; more octaves = more fine variation.
|
||||
|
||||
// Elevation field — high values become Mountains, low values lowland.
|
||||
public float ContinentFrequency { get; set; } = 0.0035f;
|
||||
public int ContinentOctaves { get; set; } = 4;
|
||||
|
||||
// Temperature field — high values become Desert.
|
||||
public float TemperatureFrequency { get; set; } = 0.0015f;
|
||||
public int TemperatureOctaves { get; set; } = 3;
|
||||
|
||||
// Forest-density field — high values become Forest (else Plains).
|
||||
public float ForestFrequency { get; set; } = 0.02f;
|
||||
|
||||
#endregion -- World Gen: Climate Noise --
|
||||
|
||||
#region -- World Gen: Biome Selection --
|
||||
// Thresholds on the climate fields (values ≈ -1..1), tested in order:
|
||||
// mountains → desert → forest → plains.
|
||||
|
||||
// Elevation at/above which a cell is Mountains.
|
||||
public float MountainsElevationThreshold { get; set; } = 0.25f;
|
||||
|
||||
// Temperature above which a non-mountain cell is Desert.
|
||||
public float DesertTemperatureThreshold { get; set; } = 0.33f;
|
||||
|
||||
// Minimum temperature for Forest (colder than this → Plains).
|
||||
public float ForestTemperatureThreshold { get; set; } = -0.33f;
|
||||
|
||||
// Minimum forest-density for Forest (below this → Plains).
|
||||
public float ForestDensityThreshold { get; set; } = 0f;
|
||||
|
||||
#endregion -- World Gen: Biome Selection --
|
||||
|
||||
#region -- World Gen: Biome Surface Heights --
|
||||
// Per biome: surfaceHeight = SeaLevel + ContinentAmplitude·continentNoise
|
||||
// + DetailAmplitude·detailNoise. Amplitudes are in blocks; detail frequency
|
||||
// sets the bumpiness scale (higher = smaller, choppier features).
|
||||
|
||||
// Plains — gentle rolling grassland.
|
||||
public float PlainsContinentAmplitude { get; set; } = 24f;
|
||||
public float PlainsDetailAmplitude { get; set; } = 8f;
|
||||
public float PlainsDetailFrequency { get; set; } = 0.01f;
|
||||
|
||||
// Forest — like plains, a touch hillier.
|
||||
public float ForestContinentAmplitude { get; set; } = 24f;
|
||||
public float ForestDetailAmplitude { get; set; } = 16f;
|
||||
public float ForestDetailFrequency { get; set; } = 0.01f;
|
||||
|
||||
// Desert — flat with low dunes.
|
||||
public float DesertContinentAmplitude { get; set; } = 16f;
|
||||
public float DesertDetailAmplitude { get; set; } = 6f;
|
||||
public float DesertDetailFrequency { get; set; } = 0.008f;
|
||||
|
||||
// Mountains — tall ridged peaks (uses ridged noise instead of a smooth detail
|
||||
// field). Peaks roughly cap at SeaLevel + ContinentAmplitude + RidgedAmplitude.
|
||||
public float MountainsContinentAmplitude { get; set; } = 40f;
|
||||
public float MountainsRidgedAmplitude { get; set; } = 45f;
|
||||
public float MountainsRidgedFrequency { get; set; } = 0.005f;
|
||||
public int MountainsRidgedOctaves { get; set; } = 4;
|
||||
// World-Y at/above which mountain tops render as snow.
|
||||
public int MountainsSnowLine { get; set; } = 270;
|
||||
|
||||
#endregion -- World Gen: Biome Surface Heights --
|
||||
|
||||
#region -- Cave Generation --
|
||||
|
||||
// Spatial frequency of the 3D simplex cave-density field; higher = smaller,
|
||||
// more frequent pockets.
|
||||
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;
|
||||
|
||||
// Edge length (blocks) of a worm-spawn grid cell; one worm candidate per cell.
|
||||
public int WormCellSize { get; set; } = 64;
|
||||
|
||||
// Per-cell chance out of 1000 that a worm spawns; 300 = 30%.
|
||||
public int WormSpawnProbabilityPermille { get; set; } = 300;
|
||||
|
||||
// Number of trace steps (≈ blocks) a worm travels.
|
||||
public int WormMaxLength { get; set; } = 80;
|
||||
// Worm tunnel radius bounds; interpolated + sine-modulated along the path
|
||||
// to form bulges/chambers.
|
||||
|
||||
// Worm tunnel radius bounds; interpolated + sine-modulated along the path to
|
||||
// form bulges/chambers.
|
||||
public float WormMinRadius { get; set; } = 1.5f;
|
||||
public float WormMaxRadius { get; set; } = 3.5f;
|
||||
|
||||
// Ceiling: caves carve only at or below this world-Y ("no caves above").
|
||||
// Default 256 = SeaLevel → caves stay below sea level, keeping the taller
|
||||
// surface terrain solid. Raise toward WorldHeight-1 to allow surface caves.
|
||||
public int MinCaveY { get; set; } = 256;
|
||||
|
||||
// -- Cave lighting (fog-distance darkness) --
|
||||
#endregion -- Cave Generation --
|
||||
|
||||
#region -- Cave Lighting --
|
||||
// Underground darkening: brightness fades from CaveDarkStartY down to
|
||||
// CaveDarkEndY, never dropping below CaveMinBrightness.
|
||||
|
||||
public float CaveDarkStartY { get; set; } = 264f; // WorldConstants.SeaLevel + 8
|
||||
public float CaveDarkEndY { get; set; } = 224f; // WorldConstants.SeaLevel - 32
|
||||
public float CaveMinBrightness { get; set; } = 0.15f;
|
||||
public float CaveMinBrightness { get; set; } = 0.15f; // floor brightness deep underground (0–1)
|
||||
|
||||
#endregion -- Cave Lighting --
|
||||
|
||||
#region -- Debug Overlay --
|
||||
|
||||
// Pixel size of each font "pixel" in the on-screen debug text (biome + coords).
|
||||
public int DebugOverlayTextScale { get; set; } = 3;
|
||||
|
||||
#endregion -- Debug Overlay --
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class Player
|
||||
this.Camera = new Camera(spawnPosition, aspectRatio)
|
||||
{
|
||||
Fov = cfg.Fov,
|
||||
NearPlane = cfg.CameraNearPlane,
|
||||
FarPlane = (cfg.RenderDistanceHorizontal + 1) * VoxelChunk.Size,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class Camera
|
||||
public float Yaw { get; private set; } = -90f; // degrees; -90 looks down -Z
|
||||
public float Pitch { get; private set; } // degrees
|
||||
public float Fov { get; set; } = 60f; // degrees
|
||||
public float NearPlane { get; set; } = 0.1f; // world units
|
||||
public float FarPlane { get; set; } = 100f; // world units
|
||||
public float AspectRatio { get; set; }
|
||||
|
||||
@@ -37,7 +38,7 @@ public sealed class Camera
|
||||
|
||||
public Matrix4 GetProjectionMatrix() =>
|
||||
Matrix4.CreatePerspectiveFieldOfView(
|
||||
MathHelper.DegreesToRadians(this.Fov), this.AspectRatio, 0.1f, this.FarPlane);
|
||||
MathHelper.DegreesToRadians(this.Fov), this.AspectRatio, this.NearPlane, this.FarPlane);
|
||||
|
||||
private void UpdateVectors()
|
||||
{
|
||||
@@ -49,7 +50,7 @@ public sealed class Camera
|
||||
MathF.Sin(pitch),
|
||||
MathF.Sin(yaw) * MathF.Cos(pitch)));
|
||||
|
||||
this.Right = Vector3.Normalize(Vector3.Cross(this.Front, Camera.WorldUp));
|
||||
this.Right = Vector3.Normalize(Vector3.Cross(this.Front, WorldUp));
|
||||
this.Up = Vector3.Normalize(Vector3.Cross(this.Right, this.Front));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
using VoxelEngine.World.WorldGen;
|
||||
using Xunit;
|
||||
@@ -9,38 +10,38 @@ public class BiomeMapTests
|
||||
private static (BiomeMap map, BiomeRegistry biomes) Build(int seed = 42)
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(seed, blocks);
|
||||
var noise = new NoiseSource(seed);
|
||||
var biomes = new BiomeRegistry(seed, blocks, new Configuration());
|
||||
var noise = new NoiseSource(seed, new Configuration());
|
||||
var cells = new VoronoiCells(seed, 96f);
|
||||
return (new BiomeMap(noise, cells, biomes), biomes);
|
||||
return (new BiomeMap(noise, cells, biomes, new Configuration()), biomes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_HighElevation_IsMountains()
|
||||
{
|
||||
var (_, biomes) = Build();
|
||||
Assert.Same(biomes.Mountains, BiomeMap.Classify(0f, 0.5f, 0f, biomes));
|
||||
Assert.Same(biomes.Mountains, BiomeMap.Classify(0f, 0.5f, 0f, biomes, new Configuration()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_HotLowElevation_IsDesert()
|
||||
{
|
||||
var (_, biomes) = Build();
|
||||
Assert.Same(biomes.Desert, BiomeMap.Classify(0.5f, 0f, 0f, biomes));
|
||||
Assert.Same(biomes.Desert, BiomeMap.Classify(0.5f, 0f, 0f, biomes, new Configuration()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_TemperateWooded_IsForest()
|
||||
{
|
||||
var (_, biomes) = Build();
|
||||
Assert.Same(biomes.Forest, BiomeMap.Classify(0f, 0f, 0.5f, biomes));
|
||||
Assert.Same(biomes.Forest, BiomeMap.Classify(0f, 0f, 0.5f, biomes, new Configuration()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_ColdLow_IsPlains()
|
||||
{
|
||||
var (_, biomes) = Build();
|
||||
Assert.Same(biomes.Plains, BiomeMap.Classify(-0.9f, 0f, 0f, biomes));
|
||||
Assert.Same(biomes.Plains, BiomeMap.Classify(-0.9f, 0f, 0f, biomes, new Configuration()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
using VoxelEngine.World.WorldGen;
|
||||
using Xunit;
|
||||
@@ -10,7 +11,7 @@ public class BiomeTests
|
||||
public void Desert_TopAndFiller_AreSand()
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(42, blocks);
|
||||
var biomes = new BiomeRegistry(42, blocks, new Configuration());
|
||||
Assert.Equal("Sand", biomes.Desert.Surface.Top.Name);
|
||||
Assert.Equal("Sand", biomes.Desert.Surface.Filler.Name);
|
||||
}
|
||||
@@ -19,7 +20,7 @@ public class BiomeTests
|
||||
public void Mountains_UseSnowAboveSnowLine_StoneBelow()
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(42, blocks);
|
||||
var biomes = new BiomeRegistry(42, blocks, new Configuration());
|
||||
Assert.Equal("Stone", biomes.Mountains.TopBlock(200).Name); // below snow line
|
||||
Assert.Equal("Snow Block", biomes.Mountains.TopBlock(420).Name); // above snow line (384)
|
||||
}
|
||||
@@ -28,7 +29,7 @@ public class BiomeTests
|
||||
public void Plains_TopBlock_IsGrass_RegardlessOfHeight()
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(42, blocks);
|
||||
var biomes = new BiomeRegistry(42, blocks, new Configuration());
|
||||
Assert.Equal("Grass", biomes.Plains.TopBlock(40).Name);
|
||||
Assert.Equal("Grass", biomes.Plains.TopBlock(120).Name);
|
||||
}
|
||||
@@ -37,8 +38,8 @@ public class BiomeTests
|
||||
public void SampleHeight_IsDeterministic()
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(42, blocks);
|
||||
var noise = new NoiseSource(42);
|
||||
var biomes = new BiomeRegistry(42, blocks, new Configuration());
|
||||
var noise = new NoiseSource(42, new Configuration());
|
||||
float h1 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
||||
float h2 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
||||
Assert.Equal(h1, h2);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.WorldGen;
|
||||
using Xunit;
|
||||
|
||||
@@ -8,8 +9,8 @@ public class NoiseSourceTests
|
||||
[Fact]
|
||||
public void SameSeed_SameCoord_IsDeterministic()
|
||||
{
|
||||
var a = new NoiseSource(42);
|
||||
var b = new NoiseSource(42);
|
||||
var a = new NoiseSource(42, new Configuration());
|
||||
var b = new NoiseSource(42, new Configuration());
|
||||
Assert.Equal(a.Continent(10f, 20f), b.Continent(10f, 20f));
|
||||
Assert.Equal(a.Temperature(10f, 20f), b.Temperature(10f, 20f));
|
||||
}
|
||||
@@ -17,7 +18,7 @@ public class NoiseSourceTests
|
||||
[Fact]
|
||||
public void Output_InMinusOneToOneRange()
|
||||
{
|
||||
var n = new NoiseSource(7);
|
||||
var n = new NoiseSource(7, new Configuration());
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
float c = n.Continent(i * 3.1f, i * 1.7f);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
using VoxelEngine.World.WorldGen;
|
||||
using Xunit;
|
||||
@@ -9,11 +10,11 @@ public class TerrainSamplerTests
|
||||
private static TerrainSampler Build(int seed = 42)
|
||||
{
|
||||
var blocks = new BlockRegistry();
|
||||
var biomes = new BiomeRegistry(seed, blocks);
|
||||
var noise = new NoiseSource(seed);
|
||||
var biomes = new BiomeRegistry(seed, blocks, new Configuration());
|
||||
var noise = new NoiseSource(seed, new Configuration());
|
||||
var cells = new VoronoiCells(seed, 96f);
|
||||
var map = new BiomeMap(noise, cells, biomes);
|
||||
return new TerrainSampler(noise, map);
|
||||
var map = new BiomeMap(noise, cells, biomes, new Configuration());
|
||||
return new TerrainSampler(noise, map, new Configuration());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -15,8 +15,8 @@ public class WorldGeneratorTests
|
||||
public void BlockAt_AboveTerrainAndSea_IsAir()
|
||||
{
|
||||
var blocks = Blocks;
|
||||
var biomes = new BiomeRegistry(1, blocks);
|
||||
ushort id = WorldGenerator.BlockAt(300, surfaceHeight: 290, biomes.Plains, blocks);
|
||||
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||
ushort id = WorldGenerator.BlockAt(300, surfaceHeight: 290, biomes.Plains, blocks, fillerDepth: 3);
|
||||
Assert.Equal(blocks.Air.Id, id); // 300 > 290 (above surface) and 300 > SeaLevel(256)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ public class WorldGeneratorTests
|
||||
public void BlockAt_AboveTerrainBelowSea_IsWater()
|
||||
{
|
||||
var blocks = Blocks;
|
||||
var biomes = new BiomeRegistry(1, blocks);
|
||||
ushort id = WorldGenerator.BlockAt(60, surfaceHeight: 50, biomes.Plains, blocks);
|
||||
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||
ushort id = WorldGenerator.BlockAt(60, surfaceHeight: 50, biomes.Plains, blocks, fillerDepth: 3);
|
||||
Assert.Equal(blocks.Water.Id, id); // 60 > 50 but 60 <= SeaLevel(64)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ public class WorldGeneratorTests
|
||||
public void BlockAt_Surface_IsBiomeTop()
|
||||
{
|
||||
var blocks = Blocks;
|
||||
var biomes = new BiomeRegistry(1, blocks);
|
||||
ushort id = WorldGenerator.BlockAt(70, surfaceHeight: 70, biomes.Desert, blocks);
|
||||
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||
ushort id = WorldGenerator.BlockAt(70, surfaceHeight: 70, biomes.Desert, blocks, fillerDepth: 3);
|
||||
Assert.Equal(blocks.Sand.Id, id);
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ public class WorldGeneratorTests
|
||||
public void BlockAt_FillerBand_IsBiomeFiller()
|
||||
{
|
||||
var blocks = Blocks;
|
||||
var biomes = new BiomeRegistry(1, blocks);
|
||||
ushort id = WorldGenerator.BlockAt(68, surfaceHeight: 70, biomes.Plains, blocks);
|
||||
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||
ushort id = WorldGenerator.BlockAt(68, surfaceHeight: 70, biomes.Plains, blocks, fillerDepth: 3);
|
||||
Assert.Equal(blocks.Dirt.Id, id); // within 3 of surface
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ public class WorldGeneratorTests
|
||||
public void BlockAt_Deep_IsStone()
|
||||
{
|
||||
var blocks = Blocks;
|
||||
var biomes = new BiomeRegistry(1, blocks);
|
||||
ushort id = WorldGenerator.BlockAt(40, surfaceHeight: 70, biomes.Plains, blocks);
|
||||
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||
ushort id = WorldGenerator.BlockAt(40, surfaceHeight: 70, biomes.Plains, blocks, fillerDepth: 3);
|
||||
Assert.Equal(blocks.Stone.Id, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using VoxelEngine.Core;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen;
|
||||
|
||||
public sealed class BiomeMap
|
||||
@@ -5,32 +7,51 @@ public sealed class BiomeMap
|
||||
private readonly NoiseSource _noise;
|
||||
private readonly VoronoiCells _cells;
|
||||
private readonly BiomeRegistry _biomes;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public BiomeMap(NoiseSource noise, VoronoiCells cells, BiomeRegistry biomes)
|
||||
public BiomeMap(NoiseSource noise, VoronoiCells cells, BiomeRegistry biomes, Configuration cfg)
|
||||
{
|
||||
this._noise = noise;
|
||||
this._cells = cells;
|
||||
this._biomes = biomes;
|
||||
this._cfg = cfg;
|
||||
}
|
||||
|
||||
public int CellId(float worldX, float worldZ) => this._cells.Nearest(worldX, worldZ).Id;
|
||||
|
||||
public IBiome BiomeAt(float worldX, float worldZ)
|
||||
=> this.BiomeForCell(this._cells.Nearest(worldX, worldZ));
|
||||
|
||||
// Climate -> biome, sampled at the cell centre so the biome is constant
|
||||
// across the cell.
|
||||
public IBiome BiomeForCell(VoronoiCells.Cell cell)
|
||||
{
|
||||
VoronoiCells.Cell cell = this._cells.Nearest(worldX, worldZ);
|
||||
// Sample climate at the cell center so the biome is constant across the cell.
|
||||
float temperature = this._noise.Temperature(cell.CenterX, cell.CenterZ);
|
||||
float elevation = this._noise.Continent(cell.CenterX, cell.CenterZ);
|
||||
float forest = this._noise.Forest(cell.CenterX, cell.CenterZ);
|
||||
return BiomeMap.Classify(temperature, elevation, forest, this._biomes);
|
||||
return Classify(temperature, elevation, forest, this._biomes, this._cfg);
|
||||
}
|
||||
|
||||
// The biomes of the two nearest cells plus how far inside the nearer cell the
|
||||
// point sits (SecondDist - FirstDist, in blocks; 0 exactly on the border).
|
||||
// TerrainSampler uses this to ease height across biome borders.
|
||||
public readonly record struct BiomeBlend(IBiome Near, IBiome Far, float BorderDistance);
|
||||
|
||||
public BiomeBlend NearestTwo(float worldX, float worldZ)
|
||||
{
|
||||
VoronoiCells.NearestPair p = this._cells.NearestTwo(worldX, worldZ);
|
||||
return new BiomeBlend(
|
||||
this.BiomeForCell(p.First),
|
||||
this.BiomeForCell(p.Second),
|
||||
p.SecondDist - p.FirstDist);
|
||||
}
|
||||
|
||||
// Pure climate -> biome lookup. Thresholds in noise units (~[-1,1]).
|
||||
public static IBiome Classify(float temperature, float elevation, float forest, BiomeRegistry biomes)
|
||||
public static IBiome Classify(float temperature, float elevation, float forest, BiomeRegistry biomes, Configuration cfg)
|
||||
{
|
||||
if (elevation > 0.25f) return biomes.Mountains; // high ground
|
||||
if (temperature > 0.33f) return biomes.Desert; // hot, low
|
||||
if (temperature > -0.33f && forest > 0f) return biomes.Forest; // temperate, wooded
|
||||
if (elevation > cfg.MountainsElevationThreshold) return biomes.Mountains; // high ground
|
||||
if (temperature > cfg.DesertTemperatureThreshold) return biomes.Desert; // hot, low
|
||||
if (temperature > cfg.ForestTemperatureThreshold && forest > cfg.ForestDensityThreshold) return biomes.Forest; // temperate, wooded
|
||||
return biomes.Plains; // temperate/cold, low
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
using VoxelEngine.World.WorldGen.Biomes;
|
||||
|
||||
@@ -10,11 +11,11 @@ public sealed class BiomeRegistry
|
||||
public IBiome Mountains { get; }
|
||||
public IBiome Forest { get; }
|
||||
|
||||
public BiomeRegistry(int seed, BlockRegistry blocks)
|
||||
public BiomeRegistry(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this.Plains = new PlainsBiome(seed, blocks);
|
||||
this.Desert = new DesertBiome(seed, blocks);
|
||||
this.Mountains = new MountainsBiome(seed, blocks);
|
||||
this.Forest = new ForestBiome(seed, blocks);
|
||||
this.Plains = new PlainsBiome(seed, blocks, cfg);
|
||||
this.Desert = new DesertBiome(seed, blocks, cfg);
|
||||
this.Mountains = new MountainsBiome(seed, blocks, cfg);
|
||||
this.Forest = new ForestBiome(seed, blocks, cfg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
public sealed class DesertBiome : IBiome
|
||||
{
|
||||
private readonly FastNoiseLite _detail;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public string Name => "Desert";
|
||||
public SurfacePalette Surface { get; }
|
||||
|
||||
public DesertBiome(int seed, BlockRegistry blocks)
|
||||
public DesertBiome(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this._cfg = cfg;
|
||||
this._detail = new FastNoiseLite(seed + 101);
|
||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._detail.SetFrequency(0.015f);
|
||||
this._detail.SetFrequency(cfg.DesertDetailFrequency);
|
||||
this.Surface = new SurfacePalette(blocks.Sand, blocks.Sand, blocks.Stone);
|
||||
}
|
||||
|
||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||
=> WorldConstants.SeaLevel + 16f * noise.Continent(worldX, worldZ)
|
||||
+ 6f * this._detail.GetNoise(worldX, worldZ);
|
||||
=> WorldConstants.SeaLevel + this._cfg.DesertContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||
+ this._cfg.DesertDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||
|
||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
public sealed class ForestBiome : IBiome
|
||||
{
|
||||
private readonly FastNoiseLite _detail;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public string Name => "Forest";
|
||||
public SurfacePalette Surface { get; }
|
||||
|
||||
public ForestBiome(int seed, BlockRegistry blocks)
|
||||
public ForestBiome(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this._cfg = cfg;
|
||||
this._detail = new FastNoiseLite(seed + 103);
|
||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._detail.SetFrequency(0.03f);
|
||||
this._detail.SetFrequency(cfg.ForestDetailFrequency);
|
||||
this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone);
|
||||
}
|
||||
|
||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||
=> WorldConstants.SeaLevel + 24f * noise.Continent(worldX, worldZ)
|
||||
+ 16f * this._detail.GetNoise(worldX, worldZ);
|
||||
=> WorldConstants.SeaLevel + this._cfg.ForestContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||
+ this._cfg.ForestDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||
|
||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
|
||||
public sealed class MountainsBiome : IBiome
|
||||
{
|
||||
private const int SnowLine = 384;
|
||||
private readonly FastNoiseLite _ridged;
|
||||
private readonly IBlock _stone;
|
||||
private readonly IBlock _snow;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public string Name => "Mountains";
|
||||
public SurfacePalette Surface { get; }
|
||||
|
||||
public MountainsBiome(int seed, BlockRegistry blocks)
|
||||
public MountainsBiome(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this._cfg = cfg;
|
||||
this._ridged = new FastNoiseLite(seed + 102);
|
||||
this._ridged.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._ridged.SetFractalType(FastNoiseLite.FractalType.Ridged);
|
||||
this._ridged.SetFractalOctaves(4);
|
||||
this._ridged.SetFrequency(0.01f);
|
||||
this._ridged.SetFractalOctaves(cfg.MountainsRidgedOctaves);
|
||||
this._ridged.SetFrequency(cfg.MountainsRidgedFrequency);
|
||||
this._stone = blocks.Stone;
|
||||
this._snow = blocks.Snow;
|
||||
this.Surface = new SurfacePalette(blocks.Stone, blocks.Stone, blocks.Stone);
|
||||
}
|
||||
|
||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||
=> WorldConstants.SeaLevel + 40f * noise.Continent(worldX, worldZ)
|
||||
+ 200f * this._ridged.GetNoise(worldX, worldZ);
|
||||
=> WorldConstants.SeaLevel + this._cfg.MountainsContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||
+ this._cfg.MountainsRidgedAmplitude * this._ridged.GetNoise(worldX, worldZ);
|
||||
|
||||
public IBlock TopBlock(int worldY) => worldY >= MountainsBiome.SnowLine ? this._snow : this._stone;
|
||||
public IBlock TopBlock(int worldY) => worldY >= this._cfg.MountainsSnowLine ? this._snow : this._stone;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using VoxelEngine.Core;
|
||||
using VoxelEngine.World.Blocks;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
||||
public sealed class PlainsBiome : IBiome
|
||||
{
|
||||
private readonly FastNoiseLite _detail;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public string Name => "Plains";
|
||||
public SurfacePalette Surface { get; }
|
||||
|
||||
public PlainsBiome(int seed, BlockRegistry blocks)
|
||||
public PlainsBiome(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this._cfg = cfg;
|
||||
this._detail = new FastNoiseLite(seed + 100);
|
||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._detail.SetFrequency(0.02f);
|
||||
this._detail.SetFrequency(cfg.PlainsDetailFrequency);
|
||||
this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone);
|
||||
}
|
||||
|
||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||
=> WorldConstants.SeaLevel + 24f * noise.Continent(worldX, worldZ)
|
||||
+ 8f * this._detail.GetNoise(worldX, worldZ);
|
||||
=> WorldConstants.SeaLevel + this._cfg.PlainsContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||
+ this._cfg.PlainsDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||
|
||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using VoxelEngine.Core;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen;
|
||||
|
||||
// Seeded wrapper over FastNoiseLite. All Get* methods return roughly [-1, 1].
|
||||
@@ -7,23 +9,23 @@ public sealed class NoiseSource
|
||||
private readonly FastNoiseLite _temperature;
|
||||
private readonly FastNoiseLite _forest;
|
||||
|
||||
public NoiseSource(int seed)
|
||||
public NoiseSource(int seed, Configuration cfg)
|
||||
{
|
||||
this._continent = new FastNoiseLite(seed);
|
||||
this._continent.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._continent.SetFractalType(FastNoiseLite.FractalType.FBm);
|
||||
this._continent.SetFractalOctaves(4);
|
||||
this._continent.SetFrequency(0.0035f);
|
||||
this._continent.SetFractalOctaves(cfg.ContinentOctaves);
|
||||
this._continent.SetFrequency(cfg.ContinentFrequency);
|
||||
|
||||
this._temperature = new FastNoiseLite(seed + 1);
|
||||
this._temperature.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._temperature.SetFractalType(FastNoiseLite.FractalType.FBm);
|
||||
this._temperature.SetFractalOctaves(3);
|
||||
this._temperature.SetFrequency(0.0015f);
|
||||
this._temperature.SetFractalOctaves(cfg.TemperatureOctaves);
|
||||
this._temperature.SetFrequency(cfg.TemperatureFrequency);
|
||||
|
||||
this._forest = new FastNoiseLite(seed + 2);
|
||||
this._forest.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||
this._forest.SetFrequency(0.02f);
|
||||
this._forest.SetFrequency(cfg.ForestFrequency);
|
||||
}
|
||||
|
||||
public float Continent(float x, float z) => this._continent.GetNoise(x, z);
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
using VoxelEngine.Core;
|
||||
|
||||
namespace VoxelEngine.World.WorldGen;
|
||||
|
||||
public sealed class TerrainSampler
|
||||
{
|
||||
// Box-blur radius (in blocks) applied to the biome-height field to smooth
|
||||
// biome borders. A full (2R+1)x(2R+1) kernel at 1-block spacing bounds the
|
||||
// per-block height change to ~maxBiomeHeightDiff / (2R+1), preventing cliffs.
|
||||
// A sparse kernel does NOT bound it (axis-aligned borders flip several samples
|
||||
// at once), so the kernel must be dense.
|
||||
private const int BlendRadius = 6;
|
||||
|
||||
private readonly NoiseSource _noise;
|
||||
private readonly BiomeMap _biomes;
|
||||
private readonly Configuration _cfg;
|
||||
|
||||
public TerrainSampler(NoiseSource noise, BiomeMap biomes)
|
||||
public TerrainSampler(NoiseSource noise, BiomeMap biomes, Configuration cfg)
|
||||
{
|
||||
this._noise = noise;
|
||||
this._biomes = biomes;
|
||||
this._cfg = cfg;
|
||||
}
|
||||
|
||||
public int HeightAt(float worldX, float worldZ)
|
||||
{
|
||||
// For each kernel point find the biome THERE, but evaluate its height AT the
|
||||
// column. Averaging blurs the hard biome borders into smooth transitions.
|
||||
float sum = 0f;
|
||||
int count = 0;
|
||||
for (int dz = -TerrainSampler.BlendRadius; dz <= TerrainSampler.BlendRadius; dz++)
|
||||
for (int dx = -TerrainSampler.BlendRadius; dx <= TerrainSampler.BlendRadius; dx++)
|
||||
// Blend the two nearest biomes' heights (each evaluated AT this column) by
|
||||
// how deep inside the nearer cell we are: exactly on the border they mix
|
||||
// 50/50, easing to 100% of the nearer biome over BiomeBlendWidth blocks.
|
||||
BiomeMap.BiomeBlend blend = this._biomes.NearestTwo(worldX, worldZ);
|
||||
float hNear = blend.Near.SampleHeight(this._noise, worldX, worldZ);
|
||||
float hFar = blend.Far.SampleHeight(this._noise, worldX, worldZ);
|
||||
|
||||
float fracFar = 0.5f * (1f - SmoothStep01(blend.BorderDistance / this._cfg.BiomeBlendWidth));
|
||||
float avg = hNear * (1f - fracFar) + hFar * fracFar;
|
||||
|
||||
int h = (int)MathF.Round(avg);
|
||||
return Math.Clamp(h, 0, WorldConstants.WorldHeight - 1);
|
||||
}
|
||||
|
||||
// Hermite smoothstep on [0,1].
|
||||
private static float SmoothStep01(float t)
|
||||
{
|
||||
IBiome biome = this._biomes.BiomeAt(worldX + dx, worldZ + dz);
|
||||
sum += biome.SampleHeight(this._noise, worldX, worldZ);
|
||||
count++;
|
||||
}
|
||||
float avg = sum / count;
|
||||
int h = (int)System.MathF.Round(avg);
|
||||
return System.Math.Clamp(h, 0, WorldConstants.WorldHeight - 1);
|
||||
t = Math.Clamp(t, 0f, 1f);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ public sealed class VoronoiCells
|
||||
|
||||
public Cell Nearest(float x, float z)
|
||||
{
|
||||
int gx = (int)System.MathF.Floor(x / this._cellSize);
|
||||
int gz = (int)System.MathF.Floor(z / this._cellSize);
|
||||
int gx = (int)MathF.Floor(x / this._cellSize);
|
||||
int gz = (int)MathF.Floor(z / this._cellSize);
|
||||
|
||||
float bestDist = float.MaxValue;
|
||||
int bestId = 0;
|
||||
@@ -28,8 +28,8 @@ public sealed class VoronoiCells
|
||||
{
|
||||
int cgx = gx + ox;
|
||||
int cgz = gz + oz;
|
||||
float jitterX = VoronoiCells.Jitter(cgx, cgz, this._seed);
|
||||
float jitterZ = VoronoiCells.Jitter(cgx, cgz, this._seed + 9871);
|
||||
float jitterX = Jitter(cgx, cgz, this._seed);
|
||||
float jitterZ = Jitter(cgx, cgz, this._seed + 9871);
|
||||
float cx = (cgx + jitterX) * this._cellSize;
|
||||
float cz = (cgz + jitterZ) * this._cellSize;
|
||||
float dx = cx - x, dz = cz - z;
|
||||
@@ -37,7 +37,7 @@ public sealed class VoronoiCells
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestId = (int)VoronoiCells.Hash(cgx, cgz, this._seed);
|
||||
bestId = (int)Hash(cgx, cgz, this._seed);
|
||||
bestCx = cx;
|
||||
bestCz = cz;
|
||||
}
|
||||
@@ -45,8 +45,48 @@ public sealed class VoronoiCells
|
||||
return new Cell(bestId, bestCx, bestCz);
|
||||
}
|
||||
|
||||
public readonly record struct NearestPair(Cell First, Cell Second, float FirstDist, float SecondDist);
|
||||
|
||||
// The two nearest cells to (x,z) with their (Euclidean) distances. Used to
|
||||
// ease terrain height across the border between adjacent biome cells. Same
|
||||
// 3x3 jittered-grid search as Nearest — the two closest centres always fall
|
||||
// within that neighbourhood for jitter in [0,1).
|
||||
public NearestPair NearestTwo(float x, float z)
|
||||
{
|
||||
int gx = (int)MathF.Floor(x / this._cellSize);
|
||||
int gz = (int)MathF.Floor(z / this._cellSize);
|
||||
|
||||
float best = float.MaxValue, second = float.MaxValue;
|
||||
Cell bestCell = default, secondCell = default;
|
||||
|
||||
for (int oz = -1; oz <= 1; oz++)
|
||||
for (int ox = -1; ox <= 1; ox++)
|
||||
{
|
||||
int cgx = gx + ox;
|
||||
int cgz = gz + oz;
|
||||
float jitterX = Jitter(cgx, cgz, this._seed);
|
||||
float jitterZ = Jitter(cgx, cgz, this._seed + 9871);
|
||||
float cx = (cgx + jitterX) * this._cellSize;
|
||||
float cz = (cgz + jitterZ) * this._cellSize;
|
||||
float dx = cx - x, dz = cz - z;
|
||||
float dist = dx * dx + dz * dz;
|
||||
Cell cell = new((int)Hash(cgx, cgz, this._seed), cx, cz);
|
||||
if (dist < best)
|
||||
{
|
||||
second = best; secondCell = bestCell;
|
||||
best = dist; bestCell = cell;
|
||||
}
|
||||
else if (dist < second)
|
||||
{
|
||||
second = dist; secondCell = cell;
|
||||
}
|
||||
}
|
||||
return new NearestPair(bestCell, secondCell,
|
||||
MathF.Sqrt(best), MathF.Sqrt(second));
|
||||
}
|
||||
|
||||
private static float Jitter(int x, int z, int seed)
|
||||
=> (VoronoiCells.Hash(x, z, seed) & 0xFFFFFF) / (float)0x1000000; // [0,1)
|
||||
=> (Hash(x, z, seed) & 0xFFFFFF) / (float)0x1000000; // [0,1)
|
||||
|
||||
private static uint Hash(int x, int z, int seed)
|
||||
{
|
||||
|
||||
@@ -7,9 +7,7 @@ namespace VoxelEngine.World.WorldGen;
|
||||
|
||||
public sealed class WorldGenerator
|
||||
{
|
||||
private const int FillerDepth = 3;
|
||||
private const float CellSize = 96f;
|
||||
|
||||
private readonly Configuration _cfg;
|
||||
private readonly BlockRegistry _blocks;
|
||||
private readonly BiomeMap _biomeMap;
|
||||
private readonly TerrainSampler _terrain;
|
||||
@@ -17,17 +15,20 @@ public sealed class WorldGenerator
|
||||
|
||||
public WorldGenerator(int seed, BlockRegistry blocks, Configuration cfg)
|
||||
{
|
||||
this._cfg = cfg;
|
||||
this._blocks = blocks;
|
||||
NoiseSource noise = new(seed);
|
||||
VoronoiCells cells = new(seed, WorldGenerator.CellSize);
|
||||
BiomeRegistry biomes = new(seed, blocks);
|
||||
this._biomeMap = new BiomeMap(noise, cells, biomes);
|
||||
this._terrain = new TerrainSampler(noise, this._biomeMap);
|
||||
NoiseSource noise = new(seed, cfg);
|
||||
VoronoiCells cells = new(seed, cfg.BiomeCellSize);
|
||||
BiomeRegistry biomes = new(seed, blocks, cfg);
|
||||
this._biomeMap = new BiomeMap(noise, cells, biomes, cfg);
|
||||
this._terrain = new TerrainSampler(noise, this._biomeMap, cfg);
|
||||
this._caves = new CaveCarver(seed, blocks, cfg);
|
||||
}
|
||||
|
||||
public int SurfaceHeightAt(int worldX, int worldZ) => this._terrain.HeightAt(worldX, worldZ);
|
||||
|
||||
public string BiomeNameAt(int worldX, int worldZ) => this._biomeMap.BiomeAt(worldX, worldZ).Name;
|
||||
|
||||
public VoxelChunk Generate(Vector3i chunkPos)
|
||||
{
|
||||
VoxelChunk chunk = new();
|
||||
@@ -46,7 +47,7 @@ public sealed class WorldGenerator
|
||||
for (int y = 0; y < VoxelChunk.Size; y++)
|
||||
{
|
||||
int worldY = baseY + y;
|
||||
chunk[x, y, z] = WorldGenerator.BlockAt(worldY, height, biome, this._blocks);
|
||||
chunk[x, y, z] = BlockAt(worldY, height, biome, this._blocks, this._cfg.SurfaceFillerDepth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +55,13 @@ public sealed class WorldGenerator
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public static ushort BlockAt(int worldY, int surfaceHeight, IBiome biome, BlockRegistry blocks)
|
||||
public static ushort BlockAt(int worldY, int surfaceHeight, IBiome biome, BlockRegistry blocks, int fillerDepth)
|
||||
{
|
||||
if (worldY > surfaceHeight)
|
||||
return worldY <= WorldConstants.SeaLevel ? blocks.Water.Id : blocks.Air.Id;
|
||||
if (worldY == surfaceHeight)
|
||||
return biome.TopBlock(worldY).Id;
|
||||
if (worldY >= surfaceHeight - WorldGenerator.FillerDepth)
|
||||
if (worldY >= surfaceHeight - fillerDepth)
|
||||
return biome.Surface.Filler.Id;
|
||||
return biome.Surface.Under.Id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user