Terrain/TerrainGenerator.cs
using Sandbox.Utility;
using System;
using HC3.Persistence;
namespace HC3.Terrain;
#nullable enable
public class TerrainGeneratorOptions
{
public int MinHeight { get; set; } = 8;
public int MaxHeight { get; set; } = 96;
public int Seed { get; set; } = 5633;
public int CenterHeight { get; set; } = 12;
public Curve CentralityCurve { get; set; } = Json.Deserialize<Curve>(
"""
[
{
"x": 0,
"y": 0.97606385,
"in": -0.071428776,
"out": 0.071428776,
"mode": "Mirrored"
},
{
"x": 0.38803685,
"y": 0.9286111,
"in": -0.008528285,
"out": 0.008528285,
"mode": "Mirrored"
},
{
"x": 0.7009202,
"y": 0,
"in": 7.400002,
"out": -7.400002,
"mode": "Mirrored"
}
]
""" );
}
public sealed class TerrainGenerator : Component,
Component.ExecuteInEditor,
ISaveDataProperty<TerrainGeneratorOptions>
{
[RequireComponent]
public ParkTerrain Terrain { get; private set; } = null!;
[Property] public TerrainGeneratorOptions Options { get; set; } = new TerrainGeneratorOptions();
[Button( icon: "casino" )]
public void RandomizeSeed()
{
Options.Seed = Random.Shared.Next();
Generate();
}
protected override void OnEnabled()
{
if ( Scene.IsEditor )
{
Generate();
}
}
[Button( icon: "terrain" )]
public void Generate()
{
ClearScenery();
var data = new TileArraySlice( Terrain.Size );
var random = new Random( Options.Seed );
var noise = Noise.SimplexField( new Noise.FractalParameters( random.Next() ) );
var lakeNoise = Noise.SimplexField( new Noise.FractalParameters( random.Next() ) );
var paintNoise = Noise.SimplexField( new Noise.FractalParameters( random.Next(), 0.04f, 2 ) );
var heightMapStride = data.Size.x + 1;
var heightMapSize = heightMapStride * (data.Size.y + 1);
var heightMap = new int[heightMapSize];
var gradientMap = new int[heightMapSize];
var paintMap = new int[heightMapSize];
var center = data.Size * 0.5f;
var start = new Vector2( 32f, data.Size.y * 0.5f );
var centerRadius = data.Size.x * 0.5f;
var minHeight = Options.MinHeight;
var maxHeight = MathX.Lerp( Options.MinHeight, Options.MaxHeight, 0.5f + random.Float() * 0.5f );
var centerHeight = Options.CenterHeight;
var curve = Options.CentralityCurve;
var lakeBias = random.Float( 1.75f, 2.25f );
for ( var y = 0; y <= data.Size.y; ++y )
{
for ( var x = 0; x <= data.Size.x; ++x )
{
var noiseHeight = noise.Sample( x, y );
var centerDist = center.Distance( new Vector2( Math.Max( x, center.x ), y ) );
var startDist = start.Distance( new Vector2( Math.Max( x, start.x ), y ) );
var centrality = curve.Evaluate( Math.Clamp( centerDist / centerRadius, 0f, 1f ) );
var lake = lakeNoise.Sample( x, y ) * Math.Clamp( startDist * 4f / centerRadius, 0f, 1f );
lake = Math.Clamp( lake * lakeBias - 1f, 0f, 1f );
var height = MathX.Lerp( minHeight + noiseHeight * (maxHeight - minHeight), centerHeight, centrality );
height = MathX.Lerp( height, 0f, lake );
heightMap[x + y * heightMapStride] = (int)height;
var paintNoiseScale = height.Remap( 10f, 32f, 1f, 16f );
paintMap[x + y * heightMapStride] = (int)height + (int)((paintNoise.Sample( x, y ) - 0.5f) * paintNoiseScale);
}
}
// Calculate gradients
for ( var y = 0; y <= data.Size.y; ++y )
{
for ( var x = 0; x <= data.Size.x; ++x )
{
var height = heightMap[x + y * heightMapStride];
var left = heightMap[Math.Max( x - 1, 0 ) + y * heightMapStride];
var right = heightMap[Math.Min( x + 1, data.Size.x ) + y * heightMapStride];
var top = heightMap[x + Math.Max( y - 1, 0 ) * heightMapStride];
var bottom = heightMap[x + Math.Min( y + 1, data.Size.y ) * heightMapStride];
var horz = Math.Abs( right - left );
var vert = Math.Abs( top - bottom );
gradientMap[x + y * heightMapStride] = horz + vert;
}
}
var tileset = Terrain.Tileset;
Span<int> cornerHeights = stackalloc int[4];
Span<int> paintHeights = stackalloc int[4];
Span<int> gradients = stackalloc int[4];
Span<int> cornerMaterials = stackalloc int[4];
for ( var y = 0; y < data.Size.y; ++y )
{
for ( var x = 0; x < data.Size.x; ++x )
{
cornerHeights[0] = heightMap[x + y * heightMapStride];
cornerHeights[1] = heightMap[x + 1 + y * heightMapStride];
cornerHeights[2] = heightMap[x + (y + 1) * heightMapStride];
cornerHeights[3] = heightMap[x + 1 + (y + 1) * heightMapStride];
paintHeights[0] = paintMap[x + y * heightMapStride];
paintHeights[1] = paintMap[x + 1 + y * heightMapStride];
paintHeights[2] = paintMap[x + (y + 1) * heightMapStride];
paintHeights[3] = paintMap[x + 1 + (y + 1) * heightMapStride];
gradients[0] = gradientMap[x + y * heightMapStride];
gradients[1] = gradientMap[x + 1 + y * heightMapStride];
gradients[2] = gradientMap[x + (y + 1) * heightMapStride];
gradients[3] = gradientMap[x + 1 + (y + 1) * heightMapStride];
const int sand = 1;
const int snow = 2;
const int rock = 3;
var tile = tileset.FromCornerHeights( cornerHeights, default );
for ( var i = 0; i < 4; ++i )
{
if ( gradients[i] > 3 )
{
cornerMaterials[i] = rock;
}
else
{
cornerMaterials[i] = paintHeights[i] switch
{
< 12 => sand,
> 32 => snow,
_ => 0
};
}
}
data[new Vector2Int( x, y )] = tile with { Paint = TilePaint.FromMaterialIndices( cornerMaterials ) };
}
}
Terrain.SetTiles( Terrain.Bounds, data );
PlaceScenery( data, Options.Seed );
ITerrainEvent.Post( x => x.Generated( Terrain, Options.Seed ) );
}
private void ClearScenery()
{
// Only the host should do this.
if ( !Networking.IsHost )
return;
foreach ( var scenery in Scene.GetAllComponents<TerrainScenery>() )
{
scenery.DestroyGameObject();
}
}
private void PlaceScenery( TileArraySlice data, int seed )
{
// Only the host should do this.
if ( !Networking.IsHost )
return;
var random = new Random( seed );
(GameObject Prefab, SceneryGenerationOptions Options)[] prefabs = TerrainScenery.AllPrefabs.ToArray();
if ( prefabs is null )
{
Log.Warning( "Couldn't find prefabs!" );
return;
}
var gridManager = GridManager.Instance;
foreach ( var (prefab, options) in prefabs )
{
if ( options.ClusterDensity <= 0f ) continue;
if ( options.AltitudeRange.Max <= options.AltitudeRange.Min ) continue;
var noise = Noise.SimplexField( new Noise.FractalParameters( random.Next(), Frequency: 0.04f * (1f - options.ClusterScale), Octaves: 2 ) );
var heightRangeDenom = 1f / (options.AltitudeRange.Max - options.AltitudeRange.Min);
foreach ( var (index, tile) in data )
{
var noiseFactor = noise.Sample( index.x, index.y );
if ( tile.MinHeight < options.AltitudeRange.Min || tile.MaxHeight > options.AltitudeRange.Max ) continue;
if ( tile.Slope.MaxHeightOffset > options.MaxGradient ) continue;
var height = (tile.MinHeight + tile.MaxHeight) * 0.5f;
var relHeight = (height - options.AltitudeRange.Min) * heightRangeDenom;
if ( random.NextSingle() > options.AltitudeCurve.Evaluate( relHeight ) ) continue;
if ( random.NextSingle() < 1.2f - noiseFactor * (0.3f + options.ClusterDensity * 0.3f) ) continue;
// if ( gridManager?.IsOccupied( index ) ?? false ) continue;
var gridPos = new Vector3( Terrain.Bounds.Position + index + 0.5f, tile.MinHeight );
var worldPos = Terrain.GridToWorld( gridPos );
var clone = prefab.Clone( transform: new Transform( worldPos ) );
if ( !Scene.IsEditor )
{
clone.NetworkSpawn();
}
else
{
clone.Flags |= GameObjectFlags.NotSaved;
}
}
}
}
string ISaveDataProperty.PropertyName => "TerrainOptions";
int ISaveDataProperty.PropertyOrder => -2_000;
TerrainGeneratorOptions ISaveDataProperty<TerrainGeneratorOptions>.WriteValue( Scene scene ) => Options;
void ISaveDataProperty<TerrainGeneratorOptions>.ReadValue( Scene scene, TerrainGeneratorOptions model )
{
Options = model;
}
}