Sorry the code is a mess. But it works.
It uses FastNoiseLite.
This code is used in "Medieval Life". The game is not released yet.
Learn more about the game: https://www.youtube.com/@ciberman
Sorry the code is a mess. But it works.
It uses FastNoiseLite.
This code is used in "Medieval Life". The game is not released yet.
Learn more about the game: https://www.youtube.com/@ciberman
using System; | |
using System.Numerics; | |
using LifeSim.Support.Numerics; | |
namespace LifeSim.Procedural.Terrain; | |
public class CliffsNoiseValueGenerator : IValueGenerator | |
{ | |
public class Settings | |
{ | |
public float Scale { get; set; } | |
public float Quality { get; set; } | |
public float Gain { get; set; } | |
public float Lacunarity { get; set; } | |
} | |
private readonly FastNoiseLite _samplerBase; | |
private readonly FastNoiseLite _samplerNoiseX; | |
private readonly FastNoiseLite _samplerNoiseY; | |
private readonly FastNoiseLite _samplerCliffIntensity; | |
//private readonly FastNoiseLite _samplerRiver; | |
public float MaxCliffIntensity { get; } = 0.7f; | |
private readonly FalloffValueGenerator _falloffValueGenerator; | |
public CliffsNoiseValueGenerator(int seed, Vector2Int mapSize, Settings settings) | |
{ | |
float noiseToOctavesRatio = 0.5f; | |
int octaves = System.Math.Max(1, (int) MathF.Ceiling(MathF.Sqrt(settings.Scale) * noiseToOctavesRatio * settings.Quality)); | |
this._samplerBase = MakeNoise(seed, octaves, 1f / settings.Scale, settings.Lacunarity, settings.Gain); | |
this._samplerNoiseX = MakeNoise(seed + 1, octaves, 3f / settings.Scale, settings.Lacunarity, settings.Gain); | |
this._samplerNoiseY = MakeNoise(seed + 2, octaves, 3f / settings.Scale, settings.Lacunarity, settings.Gain); | |
this._samplerCliffIntensity = MakeNoise(seed + 3, octaves, 3f / settings.Scale, settings.Lacunarity, settings.Gain); | |
//this._samplerRiver = MakeNoise(seed + 4, octaves, 0.5f / settings.Scale, settings.Lacunarity, settings.Gain); | |
this._falloffValueGenerator = new FalloffValueGenerator | |
{ | |
MinHeight = 0f, | |
MaxHeight = 1f, | |
Center = new Vector2(0.5f, 0.5f), | |
Size = new Vector2(0.9f, 0.9f), | |
MapSize = mapSize, | |
FalloffModel = FalloffModel.Circular, | |
SquareCoords = true, | |
SquareHeight = true, | |
}; | |
} | |
private static FastNoiseLite MakeNoise(int seed, int octaves, float frequency, float lacunarity, float gain) | |
{ | |
FastNoiseLite noise = new FastNoiseLite(seed); | |
noise.SetFractalType(FastNoiseLite.FractalType.FBm); | |
noise.SetFrequency(frequency); | |
noise.SetFractalOctaves(octaves); | |
noise.SetFractalLacunarity(lacunarity); | |
noise.SetFractalGain(gain); // Persistence | |
return noise; | |
} | |
public float CalculateHeight(int x, int y) | |
{ | |
//return (this._sampler.GetNoise(x, y) + 1f) * 0.5f; | |
var noiseX = this._samplerNoiseX.GetNoise(x, y); | |
var noiseY = this._samplerNoiseY.GetNoise(x, y); | |
var noiseBase = this._samplerBase.GetNoise(x, y); | |
var noiseCliffIntensity = this._samplerCliffIntensity.GetNoise(x, y); | |
var angle = MathF.Atan2(noiseY, noiseX); | |
// rescale from -pi to pi to 0 to 1 | |
var cliffHeight = (angle + MathF.PI) / (2f * MathF.PI); | |
var baseHeight = (noiseBase + 1f) * 0.5f; | |
var cliffIntensity = (noiseCliffIntensity + 1f) * 0.5f; | |
// River generation (Perlin worms) | |
//float riverValue = 1f - MathF.Abs(this._samplerRiver.GetNoise(x, y)); | |
//riverValue = MathF.Pow(riverValue, 8f); | |
var noiseValue = baseHeight + cliffHeight * this.MaxCliffIntensity * cliffIntensity; | |
var falloffValue = 1f - this._falloffValueGenerator.CalculateHeight(x, y); | |
float riverValue = 0f; // commented out for now | |
return Math.Max(0f, noiseValue - riverValue - falloffValue); | |
} | |
} |
using System; | |
using System.Numerics; | |
using LifeSim.Support.Numerics; | |
namespace LifeSim.Procedural.Terrain; | |
public class FalloffValueGenerator : IValueGenerator | |
{ | |
public Vector2 Center { get; set; } = new Vector2(0.5f, 0.5f); | |
public Vector2 Size { get; set; } = Vector2.One; | |
public Vector2Int MapSize { get; set; } = new Vector2Int(100, 100); | |
public bool SquareCoords { get; set; } = true; | |
public bool SquareHeight { get; set; } = true; | |
public bool Invert { get; set; } = false; | |
public float MinHeight { get; set; } = 0f; | |
public float MaxHeight { get; set; } = 0f; | |
public FalloffModel FalloffModel { get; set; } = FalloffModel.Circular; | |
public FalloffValueGenerator() | |
{ | |
} | |
public float CalculateHeight(int x, int y) | |
{ | |
Vector2 normalized = new Vector2((float) x / (float) this.MapSize.X, (float) y / (float) this.MapSize.Y); | |
normalized = (normalized - this.Center) * new Vector2(2f / this.Size.X, 2f / this.Size.Y); | |
if (this.SquareCoords) normalized *= normalized; | |
float value = this.FalloffModel == FalloffModel.Circular | |
? normalized.Length() | |
: MathF.Max(MathF.Abs(normalized.X), MathF.Abs(normalized.Y)); | |
if (this.SquareHeight) value *= value; | |
value = Math.Clamp(this.MinHeight + (this.MaxHeight - this.MinHeight) * value, 0f, 1f); | |
return 1f - value; | |
} | |
} |
using System; | |
using LifeSim.Core.Content; | |
using LifeSim.Core.Terrain; | |
using LifeSim.Support.Numerics; | |
namespace LifeSim.Procedural.Terrain; | |
public class TerrainGenerator : IChunkProvider | |
{ | |
private readonly float _maxHeight; | |
private readonly IValueGenerator _valueGenerator; | |
private readonly FastNoiseLite _deltaHeightSampler; | |
private readonly float _deltaHeightNoiseScale = 0.10f; | |
private readonly Ground _ground; | |
private readonly WallCover _defaultRoofGables; | |
public TerrainGenerator(float maxHeight, IValueGenerator valueGenerator) | |
{ | |
this._maxHeight = maxHeight; | |
this._valueGenerator = valueGenerator; | |
this._deltaHeightSampler = new FastNoiseLite(1234); // Fixed seed, I don't care, It's just for the tiny variation in the real height | |
this._deltaHeightSampler.SetFractalType(FastNoiseLite.FractalType.FBm); | |
this._deltaHeightSampler.SetFrequency(1f / 2f); | |
this._deltaHeightSampler.SetFractalOctaves(2); | |
//this._deltaHeightSampler.SetFractalLacunarity(settings.Lacunarity); | |
//this._deltaHeightSampler.SetFractalGain(settings.Gain); // Persistence | |
this._ground = GameContent.Grounds.Get("medieval_life:ground.grass"); | |
this._defaultRoofGables = GameContent.Walls.Get("medieval_life:wall.mediewall"); // TODO: Remove hardcoded value | |
} | |
public Chunk CreateChunk(World world, Vector2Int coords) | |
{ | |
Chunk chunk = new Chunk(world, coords, this._ground, this._defaultRoofGables); | |
Vector2Int offset = chunk.WorldOffset; | |
for (int y = 0; y < Chunk.SIZE; y++) | |
{ | |
for (int x = 0; x < Chunk.SIZE; x++) | |
{ | |
int tileIndex = Tile.GetIndex(x, y); | |
float regularHeight = this._maxHeight * this._valueGenerator.CalculateHeight(offset.X + x, offset.Y + y); | |
short level = (short)MathF.Round(regularHeight / Tile.LEVEL_HEIGHT); | |
chunk.SetLevel(tileIndex, level); | |
float deltaHeight = this._deltaHeightSampler.GetNoise(offset.X + x, offset.Y + y); | |
deltaHeight *= this._deltaHeightNoiseScale; | |
chunk.SetVisualHeightNoiseDelta(tileIndex, deltaHeight); | |
} | |
} | |
return chunk; | |
} | |
} |
using System; | |
using LifeSim.Procedural.Terrain; | |
using LifeSim.Support.Numerics; | |
namespace LifeSim.Procedural; | |
public class WorldSettings | |
{ | |
public Vector2Int Size { get; set; } | |
public int Seed { get; set; } | |
public float FloraDensity { get; set; } = 0.3f; | |
public bool GenerateHouses { get; set; } = true; | |
public float MaximumHeight { get; set; } = 5f; | |
public float MaximumWaterPercentage { get; set; } = 0.6f; | |
public WorldSettings() : this(new Vector2Int(300, 300), 0) { } | |
public WorldSettings(Vector2Int size, int seed = 0) | |
{ | |
this.Seed = (seed == 0) ? Random.Shared.Next() : seed; | |
this.Size = size; | |
} | |
public WorldGenerator BuildGenerator() | |
{ | |
var valueGenerator = this.MakeValueGenerator(); | |
var terrainGenerator = new TerrainGenerator(this.MaximumHeight, valueGenerator); | |
var pipeline = new WorldGenerator(this.Size, terrainGenerator); | |
pipeline.Add(new WaterLevelCalculator(this.MaximumWaterPercentage)); | |
pipeline.Add(new GroundGenerator(this.Seed)); | |
pipeline.Add(new FertilityGenerator(this.Seed)); | |
pipeline.Add(new FloraGenerator(this.Seed, this.FloraDensity)); | |
if (this.GenerateHouses) pipeline.Add(new VillageGenerator(this.Seed)); | |
pipeline.Add(new PathsGenerator(this.Seed)); | |
pipeline.Add(new PlayerSpawner(this.Seed)); | |
return pipeline; | |
} | |
private CliffsNoiseValueGenerator MakeValueGenerator() | |
{ | |
var settings = new CliffsNoiseValueGenerator.Settings | |
{ | |
Scale = 600f, | |
Quality = 0.7f, | |
Lacunarity = 2.0f, | |
Gain = 0.5f, | |
}; | |
return new CliffsNoiseValueGenerator(this.Seed, this.Size, settings); | |
} | |
} |