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
|
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;
|
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
|
// 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.
|
// derived from RenderDistanceHorizontal). 0.7 → fog starts at 70% of the edge.
|
||||||
public float FogStartFraction { get; set; } = 0.7f;
|
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 Vector3 SkyColor { get; set; } = new(0.45f, 0.65f, 0.95f);
|
||||||
public int SpawnReadyRadiusHorizontal { get; set; } = 1;
|
|
||||||
|
|
||||||
public int MaxGenerateJobsInFlight { get; set; } = 8;
|
#endregion -- Rendering & View --
|
||||||
public int MaxMeshUploadsPerFrame { get; set; } = 4;
|
|
||||||
public int WorkerThreadCount { get; set; } = Math.Max(1, Environment.ProcessorCount / 2);
|
|
||||||
|
|
||||||
// Multiplier applied to (dy)^2 in chunk-load priority. >1 prioritizes
|
#region -- Window --
|
||||||
// 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;
|
|
||||||
|
|
||||||
|
// Initial window size in pixels.
|
||||||
public int WindowWidth { get; set; } = 1280;
|
public int WindowWidth { get; set; } = 1280;
|
||||||
public int WindowHeight { get; set; } = 720;
|
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,
|
// Spatial frequency of the 3D simplex cave-density field; higher = smaller,
|
||||||
// more frequent pockets.
|
// 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.
|
// 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.
|
// 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%.
|
// 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.
|
// 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.
|
// 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").
|
// Ceiling: caves carve only at or below this world-Y ("no caves above").
|
||||||
// Default 256 = SeaLevel → caves stay below sea level, keeping the taller
|
// Default 256 = SeaLevel → caves stay below sea level, keeping the taller
|
||||||
// surface terrain solid. Raise toward WorldHeight-1 to allow surface caves.
|
// surface terrain solid. Raise toward WorldHeight-1 to allow surface caves.
|
||||||
public int MinCaveY { get; set; } = 256;
|
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 CaveDarkStartY { get; set; } = 264f; // WorldConstants.SeaLevel + 8
|
||||||
public float CaveDarkEndY { get; set; } = 224f; // WorldConstants.SeaLevel - 32
|
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)
|
this.Camera = new Camera(spawnPosition, aspectRatio)
|
||||||
{
|
{
|
||||||
Fov = cfg.Fov,
|
Fov = cfg.Fov,
|
||||||
|
NearPlane = cfg.CameraNearPlane,
|
||||||
FarPlane = (cfg.RenderDistanceHorizontal + 1) * VoxelChunk.Size,
|
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 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 NearPlane { get; set; } = 0.1f; // world units
|
||||||
public float FarPlane { get; set; } = 100f; // world units
|
public float FarPlane { get; set; } = 100f; // world units
|
||||||
public float AspectRatio { get; set; }
|
public float AspectRatio { get; set; }
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ public sealed class Camera
|
|||||||
|
|
||||||
public Matrix4 GetProjectionMatrix() =>
|
public Matrix4 GetProjectionMatrix() =>
|
||||||
Matrix4.CreatePerspectiveFieldOfView(
|
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()
|
private void UpdateVectors()
|
||||||
{
|
{
|
||||||
@@ -49,7 +50,7 @@ public sealed class Camera
|
|||||||
MathF.Sin(pitch),
|
MathF.Sin(pitch),
|
||||||
MathF.Sin(yaw) * MathF.Cos(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));
|
this.Up = Vector3.Normalize(Vector3.Cross(this.Right, this.Front));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
using VoxelEngine.World.WorldGen;
|
using VoxelEngine.World.WorldGen;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -9,38 +10,38 @@ public class BiomeMapTests
|
|||||||
private static (BiomeMap map, BiomeRegistry biomes) Build(int seed = 42)
|
private static (BiomeMap map, BiomeRegistry biomes) Build(int seed = 42)
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
var blocks = new BlockRegistry();
|
||||||
var biomes = new BiomeRegistry(seed, blocks);
|
var biomes = new BiomeRegistry(seed, blocks, new Configuration());
|
||||||
var noise = new NoiseSource(seed);
|
var noise = new NoiseSource(seed, new Configuration());
|
||||||
var cells = new VoronoiCells(seed, 96f);
|
var cells = new VoronoiCells(seed, 96f);
|
||||||
return (new BiomeMap(noise, cells, biomes), biomes);
|
return (new BiomeMap(noise, cells, biomes, new Configuration()), biomes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Classify_HighElevation_IsMountains()
|
public void Classify_HighElevation_IsMountains()
|
||||||
{
|
{
|
||||||
var (_, biomes) = Build();
|
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]
|
[Fact]
|
||||||
public void Classify_HotLowElevation_IsDesert()
|
public void Classify_HotLowElevation_IsDesert()
|
||||||
{
|
{
|
||||||
var (_, biomes) = Build();
|
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]
|
[Fact]
|
||||||
public void Classify_TemperateWooded_IsForest()
|
public void Classify_TemperateWooded_IsForest()
|
||||||
{
|
{
|
||||||
var (_, biomes) = Build();
|
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]
|
[Fact]
|
||||||
public void Classify_ColdLow_IsPlains()
|
public void Classify_ColdLow_IsPlains()
|
||||||
{
|
{
|
||||||
var (_, biomes) = Build();
|
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]
|
[Fact]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
using VoxelEngine.World.WorldGen;
|
using VoxelEngine.World.WorldGen;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -10,7 +11,7 @@ public class BiomeTests
|
|||||||
public void Desert_TopAndFiller_AreSand()
|
public void Desert_TopAndFiller_AreSand()
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
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.Top.Name);
|
||||||
Assert.Equal("Sand", biomes.Desert.Surface.Filler.Name);
|
Assert.Equal("Sand", biomes.Desert.Surface.Filler.Name);
|
||||||
}
|
}
|
||||||
@@ -19,7 +20,7 @@ public class BiomeTests
|
|||||||
public void Mountains_UseSnowAboveSnowLine_StoneBelow()
|
public void Mountains_UseSnowAboveSnowLine_StoneBelow()
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
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("Stone", biomes.Mountains.TopBlock(200).Name); // below snow line
|
||||||
Assert.Equal("Snow Block", biomes.Mountains.TopBlock(420).Name); // above snow line (384)
|
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()
|
public void Plains_TopBlock_IsGrass_RegardlessOfHeight()
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
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(40).Name);
|
||||||
Assert.Equal("Grass", biomes.Plains.TopBlock(120).Name);
|
Assert.Equal("Grass", biomes.Plains.TopBlock(120).Name);
|
||||||
}
|
}
|
||||||
@@ -37,8 +38,8 @@ public class BiomeTests
|
|||||||
public void SampleHeight_IsDeterministic()
|
public void SampleHeight_IsDeterministic()
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
var blocks = new BlockRegistry();
|
||||||
var biomes = new BiomeRegistry(42, blocks);
|
var biomes = new BiomeRegistry(42, blocks, new Configuration());
|
||||||
var noise = new NoiseSource(42);
|
var noise = new NoiseSource(42, new Configuration());
|
||||||
float h1 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
float h1 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
||||||
float h2 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
float h2 = biomes.Mountains.SampleHeight(noise, 100f, 200f);
|
||||||
Assert.Equal(h1, h2);
|
Assert.Equal(h1, h2);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.WorldGen;
|
using VoxelEngine.World.WorldGen;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -8,8 +9,8 @@ public class NoiseSourceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SameSeed_SameCoord_IsDeterministic()
|
public void SameSeed_SameCoord_IsDeterministic()
|
||||||
{
|
{
|
||||||
var a = new NoiseSource(42);
|
var a = new NoiseSource(42, new Configuration());
|
||||||
var b = new NoiseSource(42);
|
var b = new NoiseSource(42, new Configuration());
|
||||||
Assert.Equal(a.Continent(10f, 20f), b.Continent(10f, 20f));
|
Assert.Equal(a.Continent(10f, 20f), b.Continent(10f, 20f));
|
||||||
Assert.Equal(a.Temperature(10f, 20f), b.Temperature(10f, 20f));
|
Assert.Equal(a.Temperature(10f, 20f), b.Temperature(10f, 20f));
|
||||||
}
|
}
|
||||||
@@ -17,7 +18,7 @@ public class NoiseSourceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Output_InMinusOneToOneRange()
|
public void Output_InMinusOneToOneRange()
|
||||||
{
|
{
|
||||||
var n = new NoiseSource(7);
|
var n = new NoiseSource(7, new Configuration());
|
||||||
for (int i = 0; i < 200; i++)
|
for (int i = 0; i < 200; i++)
|
||||||
{
|
{
|
||||||
float c = n.Continent(i * 3.1f, i * 1.7f);
|
float c = n.Continent(i * 3.1f, i * 1.7f);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
using VoxelEngine.World.WorldGen;
|
using VoxelEngine.World.WorldGen;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -9,11 +10,11 @@ public class TerrainSamplerTests
|
|||||||
private static TerrainSampler Build(int seed = 42)
|
private static TerrainSampler Build(int seed = 42)
|
||||||
{
|
{
|
||||||
var blocks = new BlockRegistry();
|
var blocks = new BlockRegistry();
|
||||||
var biomes = new BiomeRegistry(seed, blocks);
|
var biomes = new BiomeRegistry(seed, blocks, new Configuration());
|
||||||
var noise = new NoiseSource(seed);
|
var noise = new NoiseSource(seed, new Configuration());
|
||||||
var cells = new VoronoiCells(seed, 96f);
|
var cells = new VoronoiCells(seed, 96f);
|
||||||
var map = new BiomeMap(noise, cells, biomes);
|
var map = new BiomeMap(noise, cells, biomes, new Configuration());
|
||||||
return new TerrainSampler(noise, map);
|
return new TerrainSampler(noise, map, new Configuration());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ public class WorldGeneratorTests
|
|||||||
public void BlockAt_AboveTerrainAndSea_IsAir()
|
public void BlockAt_AboveTerrainAndSea_IsAir()
|
||||||
{
|
{
|
||||||
var blocks = Blocks;
|
var blocks = Blocks;
|
||||||
var biomes = new BiomeRegistry(1, blocks);
|
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||||
ushort id = WorldGenerator.BlockAt(300, surfaceHeight: 290, biomes.Plains, blocks);
|
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)
|
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()
|
public void BlockAt_AboveTerrainBelowSea_IsWater()
|
||||||
{
|
{
|
||||||
var blocks = Blocks;
|
var blocks = Blocks;
|
||||||
var biomes = new BiomeRegistry(1, blocks);
|
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||||
ushort id = WorldGenerator.BlockAt(60, surfaceHeight: 50, biomes.Plains, blocks);
|
ushort id = WorldGenerator.BlockAt(60, surfaceHeight: 50, biomes.Plains, blocks, fillerDepth: 3);
|
||||||
Assert.Equal(blocks.Water.Id, id); // 60 > 50 but 60 <= SeaLevel(64)
|
Assert.Equal(blocks.Water.Id, id); // 60 > 50 but 60 <= SeaLevel(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ public class WorldGeneratorTests
|
|||||||
public void BlockAt_Surface_IsBiomeTop()
|
public void BlockAt_Surface_IsBiomeTop()
|
||||||
{
|
{
|
||||||
var blocks = Blocks;
|
var blocks = Blocks;
|
||||||
var biomes = new BiomeRegistry(1, blocks);
|
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||||
ushort id = WorldGenerator.BlockAt(70, surfaceHeight: 70, biomes.Desert, blocks);
|
ushort id = WorldGenerator.BlockAt(70, surfaceHeight: 70, biomes.Desert, blocks, fillerDepth: 3);
|
||||||
Assert.Equal(blocks.Sand.Id, id);
|
Assert.Equal(blocks.Sand.Id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ public class WorldGeneratorTests
|
|||||||
public void BlockAt_FillerBand_IsBiomeFiller()
|
public void BlockAt_FillerBand_IsBiomeFiller()
|
||||||
{
|
{
|
||||||
var blocks = Blocks;
|
var blocks = Blocks;
|
||||||
var biomes = new BiomeRegistry(1, blocks);
|
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||||
ushort id = WorldGenerator.BlockAt(68, surfaceHeight: 70, biomes.Plains, blocks);
|
ushort id = WorldGenerator.BlockAt(68, surfaceHeight: 70, biomes.Plains, blocks, fillerDepth: 3);
|
||||||
Assert.Equal(blocks.Dirt.Id, id); // within 3 of surface
|
Assert.Equal(blocks.Dirt.Id, id); // within 3 of surface
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ public class WorldGeneratorTests
|
|||||||
public void BlockAt_Deep_IsStone()
|
public void BlockAt_Deep_IsStone()
|
||||||
{
|
{
|
||||||
var blocks = Blocks;
|
var blocks = Blocks;
|
||||||
var biomes = new BiomeRegistry(1, blocks);
|
var biomes = new BiomeRegistry(1, blocks, new Configuration());
|
||||||
ushort id = WorldGenerator.BlockAt(40, surfaceHeight: 70, biomes.Plains, blocks);
|
ushort id = WorldGenerator.BlockAt(40, surfaceHeight: 70, biomes.Plains, blocks, fillerDepth: 3);
|
||||||
Assert.Equal(blocks.Stone.Id, id);
|
Assert.Equal(blocks.Stone.Id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen;
|
namespace VoxelEngine.World.WorldGen;
|
||||||
|
|
||||||
public sealed class BiomeMap
|
public sealed class BiomeMap
|
||||||
@@ -5,32 +7,51 @@ public sealed class BiomeMap
|
|||||||
private readonly NoiseSource _noise;
|
private readonly NoiseSource _noise;
|
||||||
private readonly VoronoiCells _cells;
|
private readonly VoronoiCells _cells;
|
||||||
private readonly BiomeRegistry _biomes;
|
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._noise = noise;
|
||||||
this._cells = cells;
|
this._cells = cells;
|
||||||
this._biomes = biomes;
|
this._biomes = biomes;
|
||||||
|
this._cfg = cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CellId(float worldX, float worldZ) => this._cells.Nearest(worldX, worldZ).Id;
|
public int CellId(float worldX, float worldZ) => this._cells.Nearest(worldX, worldZ).Id;
|
||||||
|
|
||||||
public IBiome BiomeAt(float worldX, float worldZ)
|
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 temperature = this._noise.Temperature(cell.CenterX, cell.CenterZ);
|
||||||
float elevation = this._noise.Continent(cell.CenterX, cell.CenterZ);
|
float elevation = this._noise.Continent(cell.CenterX, cell.CenterZ);
|
||||||
float forest = this._noise.Forest(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]).
|
// 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 (elevation > cfg.MountainsElevationThreshold) return biomes.Mountains; // high ground
|
||||||
if (temperature > 0.33f) return biomes.Desert; // hot, low
|
if (temperature > cfg.DesertTemperatureThreshold) return biomes.Desert; // hot, low
|
||||||
if (temperature > -0.33f && forest > 0f) return biomes.Forest; // temperate, wooded
|
if (temperature > cfg.ForestTemperatureThreshold && forest > cfg.ForestDensityThreshold) return biomes.Forest; // temperate, wooded
|
||||||
return biomes.Plains; // temperate/cold, low
|
return biomes.Plains; // temperate/cold, low
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
using VoxelEngine.World.WorldGen.Biomes;
|
using VoxelEngine.World.WorldGen.Biomes;
|
||||||
|
|
||||||
@@ -10,11 +11,11 @@ public sealed class BiomeRegistry
|
|||||||
public IBiome Mountains { get; }
|
public IBiome Mountains { get; }
|
||||||
public IBiome Forest { 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.Plains = new PlainsBiome(seed, blocks, cfg);
|
||||||
this.Desert = new DesertBiome(seed, blocks);
|
this.Desert = new DesertBiome(seed, blocks, cfg);
|
||||||
this.Mountains = new MountainsBiome(seed, blocks);
|
this.Mountains = new MountainsBiome(seed, blocks, cfg);
|
||||||
this.Forest = new ForestBiome(seed, blocks);
|
this.Forest = new ForestBiome(seed, blocks, cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
|||||||
public sealed class DesertBiome : IBiome
|
public sealed class DesertBiome : IBiome
|
||||||
{
|
{
|
||||||
private readonly FastNoiseLite _detail;
|
private readonly FastNoiseLite _detail;
|
||||||
|
private readonly Configuration _cfg;
|
||||||
|
|
||||||
public string Name => "Desert";
|
public string Name => "Desert";
|
||||||
public SurfacePalette Surface { get; }
|
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 = new FastNoiseLite(seed + 101);
|
||||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
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);
|
this.Surface = new SurfacePalette(blocks.Sand, blocks.Sand, blocks.Stone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||||
=> WorldConstants.SeaLevel + 16f * noise.Continent(worldX, worldZ)
|
=> WorldConstants.SeaLevel + this._cfg.DesertContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||||
+ 6f * this._detail.GetNoise(worldX, worldZ);
|
+ this._cfg.DesertDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||||
|
|
||||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
|||||||
public sealed class ForestBiome : IBiome
|
public sealed class ForestBiome : IBiome
|
||||||
{
|
{
|
||||||
private readonly FastNoiseLite _detail;
|
private readonly FastNoiseLite _detail;
|
||||||
|
private readonly Configuration _cfg;
|
||||||
|
|
||||||
public string Name => "Forest";
|
public string Name => "Forest";
|
||||||
public SurfacePalette Surface { get; }
|
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 = new FastNoiseLite(seed + 103);
|
||||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
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);
|
this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||||
=> WorldConstants.SeaLevel + 24f * noise.Continent(worldX, worldZ)
|
=> WorldConstants.SeaLevel + this._cfg.ForestContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||||
+ 16f * this._detail.GetNoise(worldX, worldZ);
|
+ this._cfg.ForestDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||||
|
|
||||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
using VoxelEngine.World.Blocks;
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||||
|
|
||||||
public sealed class MountainsBiome : IBiome
|
public sealed class MountainsBiome : IBiome
|
||||||
{
|
{
|
||||||
private const int SnowLine = 384;
|
|
||||||
private readonly FastNoiseLite _ridged;
|
private readonly FastNoiseLite _ridged;
|
||||||
private readonly IBlock _stone;
|
private readonly IBlock _stone;
|
||||||
private readonly IBlock _snow;
|
private readonly IBlock _snow;
|
||||||
|
private readonly Configuration _cfg;
|
||||||
|
|
||||||
public string Name => "Mountains";
|
public string Name => "Mountains";
|
||||||
public SurfacePalette Surface { get; }
|
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 = new FastNoiseLite(seed + 102);
|
||||||
this._ridged.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
this._ridged.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||||
this._ridged.SetFractalType(FastNoiseLite.FractalType.Ridged);
|
this._ridged.SetFractalType(FastNoiseLite.FractalType.Ridged);
|
||||||
this._ridged.SetFractalOctaves(4);
|
this._ridged.SetFractalOctaves(cfg.MountainsRidgedOctaves);
|
||||||
this._ridged.SetFrequency(0.01f);
|
this._ridged.SetFrequency(cfg.MountainsRidgedFrequency);
|
||||||
this._stone = blocks.Stone;
|
this._stone = blocks.Stone;
|
||||||
this._snow = blocks.Snow;
|
this._snow = blocks.Snow;
|
||||||
this.Surface = new SurfacePalette(blocks.Stone, blocks.Stone, blocks.Stone);
|
this.Surface = new SurfacePalette(blocks.Stone, blocks.Stone, blocks.Stone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||||
=> WorldConstants.SeaLevel + 40f * noise.Continent(worldX, worldZ)
|
=> WorldConstants.SeaLevel + this._cfg.MountainsContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||||
+ 200f * this._ridged.GetNoise(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;
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen.Biomes;
|
namespace VoxelEngine.World.WorldGen.Biomes;
|
||||||
@@ -5,21 +6,23 @@ namespace VoxelEngine.World.WorldGen.Biomes;
|
|||||||
public sealed class PlainsBiome : IBiome
|
public sealed class PlainsBiome : IBiome
|
||||||
{
|
{
|
||||||
private readonly FastNoiseLite _detail;
|
private readonly FastNoiseLite _detail;
|
||||||
|
private readonly Configuration _cfg;
|
||||||
|
|
||||||
public string Name => "Plains";
|
public string Name => "Plains";
|
||||||
public SurfacePalette Surface { get; }
|
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 = new FastNoiseLite(seed + 100);
|
||||||
this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
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);
|
this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
public float SampleHeight(NoiseSource noise, float worldX, float worldZ)
|
||||||
=> WorldConstants.SeaLevel + 24f * noise.Continent(worldX, worldZ)
|
=> WorldConstants.SeaLevel + this._cfg.PlainsContinentAmplitude * noise.Continent(worldX, worldZ)
|
||||||
+ 8f * this._detail.GetNoise(worldX, worldZ);
|
+ this._cfg.PlainsDetailAmplitude * this._detail.GetNoise(worldX, worldZ);
|
||||||
|
|
||||||
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
public IBlock TopBlock(int worldY) => this.Surface.Top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen;
|
namespace VoxelEngine.World.WorldGen;
|
||||||
|
|
||||||
// Seeded wrapper over FastNoiseLite. All Get* methods return roughly [-1, 1].
|
// 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 _temperature;
|
||||||
private readonly FastNoiseLite _forest;
|
private readonly FastNoiseLite _forest;
|
||||||
|
|
||||||
public NoiseSource(int seed)
|
public NoiseSource(int seed, Configuration cfg)
|
||||||
{
|
{
|
||||||
this._continent = new FastNoiseLite(seed);
|
this._continent = new FastNoiseLite(seed);
|
||||||
this._continent.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
this._continent.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||||
this._continent.SetFractalType(FastNoiseLite.FractalType.FBm);
|
this._continent.SetFractalType(FastNoiseLite.FractalType.FBm);
|
||||||
this._continent.SetFractalOctaves(4);
|
this._continent.SetFractalOctaves(cfg.ContinentOctaves);
|
||||||
this._continent.SetFrequency(0.0035f);
|
this._continent.SetFrequency(cfg.ContinentFrequency);
|
||||||
|
|
||||||
this._temperature = new FastNoiseLite(seed + 1);
|
this._temperature = new FastNoiseLite(seed + 1);
|
||||||
this._temperature.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
this._temperature.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
||||||
this._temperature.SetFractalType(FastNoiseLite.FractalType.FBm);
|
this._temperature.SetFractalType(FastNoiseLite.FractalType.FBm);
|
||||||
this._temperature.SetFractalOctaves(3);
|
this._temperature.SetFractalOctaves(cfg.TemperatureOctaves);
|
||||||
this._temperature.SetFrequency(0.0015f);
|
this._temperature.SetFrequency(cfg.TemperatureFrequency);
|
||||||
|
|
||||||
this._forest = new FastNoiseLite(seed + 2);
|
this._forest = new FastNoiseLite(seed + 2);
|
||||||
this._forest.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
|
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);
|
public float Continent(float x, float z) => this._continent.GetNoise(x, z);
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
|
using VoxelEngine.Core;
|
||||||
|
|
||||||
namespace VoxelEngine.World.WorldGen;
|
namespace VoxelEngine.World.WorldGen;
|
||||||
|
|
||||||
public sealed class TerrainSampler
|
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 NoiseSource _noise;
|
||||||
private readonly BiomeMap _biomes;
|
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._noise = noise;
|
||||||
this._biomes = biomes;
|
this._biomes = biomes;
|
||||||
|
this._cfg = cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int HeightAt(float worldX, float worldZ)
|
public int HeightAt(float worldX, float worldZ)
|
||||||
{
|
{
|
||||||
// For each kernel point find the biome THERE, but evaluate its height AT the
|
// Blend the two nearest biomes' heights (each evaluated AT this column) by
|
||||||
// column. Averaging blurs the hard biome borders into smooth transitions.
|
// how deep inside the nearer cell we are: exactly on the border they mix
|
||||||
float sum = 0f;
|
// 50/50, easing to 100% of the nearer biome over BiomeBlendWidth blocks.
|
||||||
int count = 0;
|
BiomeMap.BiomeBlend blend = this._biomes.NearestTwo(worldX, worldZ);
|
||||||
for (int dz = -TerrainSampler.BlendRadius; dz <= TerrainSampler.BlendRadius; dz++)
|
float hNear = blend.Near.SampleHeight(this._noise, worldX, worldZ);
|
||||||
for (int dx = -TerrainSampler.BlendRadius; dx <= TerrainSampler.BlendRadius; dx++)
|
float hFar = blend.Far.SampleHeight(this._noise, worldX, worldZ);
|
||||||
{
|
|
||||||
IBiome biome = this._biomes.BiomeAt(worldX + dx, worldZ + dz);
|
float fracFar = 0.5f * (1f - SmoothStep01(blend.BorderDistance / this._cfg.BiomeBlendWidth));
|
||||||
sum += biome.SampleHeight(this._noise, worldX, worldZ);
|
float avg = hNear * (1f - fracFar) + hFar * fracFar;
|
||||||
count++;
|
|
||||||
|
int h = (int)MathF.Round(avg);
|
||||||
|
return Math.Clamp(h, 0, WorldConstants.WorldHeight - 1);
|
||||||
}
|
}
|
||||||
float avg = sum / count;
|
|
||||||
int h = (int)System.MathF.Round(avg);
|
// Hermite smoothstep on [0,1].
|
||||||
return System.Math.Clamp(h, 0, WorldConstants.WorldHeight - 1);
|
private static float SmoothStep01(float t)
|
||||||
|
{
|
||||||
|
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)
|
public Cell Nearest(float x, float z)
|
||||||
{
|
{
|
||||||
int gx = (int)System.MathF.Floor(x / this._cellSize);
|
int gx = (int)MathF.Floor(x / this._cellSize);
|
||||||
int gz = (int)System.MathF.Floor(z / this._cellSize);
|
int gz = (int)MathF.Floor(z / this._cellSize);
|
||||||
|
|
||||||
float bestDist = float.MaxValue;
|
float bestDist = float.MaxValue;
|
||||||
int bestId = 0;
|
int bestId = 0;
|
||||||
@@ -28,8 +28,8 @@ public sealed class VoronoiCells
|
|||||||
{
|
{
|
||||||
int cgx = gx + ox;
|
int cgx = gx + ox;
|
||||||
int cgz = gz + oz;
|
int cgz = gz + oz;
|
||||||
float jitterX = VoronoiCells.Jitter(cgx, cgz, this._seed);
|
float jitterX = Jitter(cgx, cgz, this._seed);
|
||||||
float jitterZ = VoronoiCells.Jitter(cgx, cgz, this._seed + 9871);
|
float jitterZ = Jitter(cgx, cgz, this._seed + 9871);
|
||||||
float cx = (cgx + jitterX) * this._cellSize;
|
float cx = (cgx + jitterX) * this._cellSize;
|
||||||
float cz = (cgz + jitterZ) * this._cellSize;
|
float cz = (cgz + jitterZ) * this._cellSize;
|
||||||
float dx = cx - x, dz = cz - z;
|
float dx = cx - x, dz = cz - z;
|
||||||
@@ -37,7 +37,7 @@ public sealed class VoronoiCells
|
|||||||
if (dist < bestDist)
|
if (dist < bestDist)
|
||||||
{
|
{
|
||||||
bestDist = dist;
|
bestDist = dist;
|
||||||
bestId = (int)VoronoiCells.Hash(cgx, cgz, this._seed);
|
bestId = (int)Hash(cgx, cgz, this._seed);
|
||||||
bestCx = cx;
|
bestCx = cx;
|
||||||
bestCz = cz;
|
bestCz = cz;
|
||||||
}
|
}
|
||||||
@@ -45,8 +45,48 @@ public sealed class VoronoiCells
|
|||||||
return new Cell(bestId, bestCx, bestCz);
|
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)
|
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)
|
private static uint Hash(int x, int z, int seed)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ namespace VoxelEngine.World.WorldGen;
|
|||||||
|
|
||||||
public sealed class WorldGenerator
|
public sealed class WorldGenerator
|
||||||
{
|
{
|
||||||
private const int FillerDepth = 3;
|
private readonly Configuration _cfg;
|
||||||
private const float CellSize = 96f;
|
|
||||||
|
|
||||||
private readonly BlockRegistry _blocks;
|
private readonly BlockRegistry _blocks;
|
||||||
private readonly BiomeMap _biomeMap;
|
private readonly BiomeMap _biomeMap;
|
||||||
private readonly TerrainSampler _terrain;
|
private readonly TerrainSampler _terrain;
|
||||||
@@ -17,17 +15,20 @@ public sealed class WorldGenerator
|
|||||||
|
|
||||||
public WorldGenerator(int seed, BlockRegistry blocks, Configuration cfg)
|
public WorldGenerator(int seed, BlockRegistry blocks, Configuration cfg)
|
||||||
{
|
{
|
||||||
|
this._cfg = cfg;
|
||||||
this._blocks = blocks;
|
this._blocks = blocks;
|
||||||
NoiseSource noise = new(seed);
|
NoiseSource noise = new(seed, cfg);
|
||||||
VoronoiCells cells = new(seed, WorldGenerator.CellSize);
|
VoronoiCells cells = new(seed, cfg.BiomeCellSize);
|
||||||
BiomeRegistry biomes = new(seed, blocks);
|
BiomeRegistry biomes = new(seed, blocks, cfg);
|
||||||
this._biomeMap = new BiomeMap(noise, cells, biomes);
|
this._biomeMap = new BiomeMap(noise, cells, biomes, cfg);
|
||||||
this._terrain = new TerrainSampler(noise, this._biomeMap);
|
this._terrain = new TerrainSampler(noise, this._biomeMap, cfg);
|
||||||
this._caves = new CaveCarver(seed, blocks, cfg);
|
this._caves = new CaveCarver(seed, blocks, cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int SurfaceHeightAt(int worldX, int worldZ) => this._terrain.HeightAt(worldX, worldZ);
|
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)
|
public VoxelChunk Generate(Vector3i chunkPos)
|
||||||
{
|
{
|
||||||
VoxelChunk chunk = new();
|
VoxelChunk chunk = new();
|
||||||
@@ -46,7 +47,7 @@ public sealed class WorldGenerator
|
|||||||
for (int y = 0; y < VoxelChunk.Size; y++)
|
for (int y = 0; y < VoxelChunk.Size; y++)
|
||||||
{
|
{
|
||||||
int worldY = baseY + 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;
|
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)
|
if (worldY > surfaceHeight)
|
||||||
return worldY <= WorldConstants.SeaLevel ? blocks.Water.Id : blocks.Air.Id;
|
return worldY <= WorldConstants.SeaLevel ? blocks.Water.Id : blocks.Air.Id;
|
||||||
if (worldY == surfaceHeight)
|
if (worldY == surfaceHeight)
|
||||||
return biome.TopBlock(worldY).Id;
|
return biome.TopBlock(worldY).Id;
|
||||||
if (worldY >= surfaceHeight - WorldGenerator.FillerDepth)
|
if (worldY >= surfaceHeight - fillerDepth)
|
||||||
return biome.Surface.Filler.Id;
|
return biome.Surface.Filler.Id;
|
||||||
return biome.Surface.Under.Id;
|
return biome.Surface.Under.Id;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user