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); | |
| } | |
| } |