Skip to content

Instantly share code, notes, and snippets.

@jhm-ciberman
Created August 7, 2024 22:42
Show Gist options
  • Save jhm-ciberman/1fcd999022f9744ad09f65c8303d526f to your computer and use it in GitHub Desktop.
Save jhm-ciberman/1fcd999022f9744ad09f65c8303d526f to your computer and use it in GitHub Desktop.
Procedural Terrain generation + Cliffs

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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment