Editor/TerrainGenerationTool.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Drawing;
using System.IO;
using System.Linq;
using Editor;
using Editor.ShaderGraph.Nodes;
using Editor.Widgets;
using Sandbox;
using SkiaSharp;
using static Sandbox.Gradient;
using Sturnus.TerrainGenerationTool;
using Sandbox.Utility;
using System.Threading;
using Sturnus.TerrainGenerationTool.RiverStream;
using Sandbox.Services;
using System.Reflection;
using static TerrainGenerationTool;
[Dock( "Editor", "Terrain Generation Tool", "terrain" )]
public class TerrainGenerationTool : Widget
{
public string GenerationPath { get; set; } = Editor.FileSystem.Content.GetFullPath( "" ) + "\\TerrainGenerationTool\\";
public string GenerationLocalPath { get; set; } = "\\TerrainGenerationTool\\";
public string ExportPath { get; set; } = Project.Current.RootDirectory + "\\Assets\\";
HashSet<string> TerrainCategoryArray { get; set; } = new HashSet<string>();
HashSet<string> TerrainShapeArray { get; set; } = new HashSet<string>();
List<Type> terrainCategoryClassesTypes = new List<Type> { typeof( Islands ), typeof( Mountainous ), typeof( Planetary ), typeof( Realistic ), typeof( Sea ), typeof( Volcanic ) };
List<Type> terrainShapeMethodTypes { get; set; }
enum TerrainDimensions : int
{
x512 = 512,
x1024 = 1024,
x2048 = 2048,
x4096 = 4096,
X8192 = 8192
}
//enum TerrainCategoryEnum;
DynamicEnum TerrainCategoryEnum = new DynamicEnum();
DynamicEnum TerrainShapeEnum = new DynamicEnum();
TerrainDimensions TerrainDimensionsEnum { get; set; } = TerrainDimensions.x512;
//TerrainCategoryEnum TerrainShapeEnumSelect { get; set; }
[Range( 0.1f, 1f, 0.01f, true, true )] float TerrainMinHeight { get; set; } = 0.2f;
[Range( 0.1f, 1f, 0.01f, true, true )] float TerrainMaxHeight { get; set; } = 0.5f;
[Range( 0.1f, 1f, 0.01f, true, true )] float TerrainPlaneScale { get; set; } = 0.5f;
long TerrainSeed { get; set; } = 1234567890;
[Range( 0, 20, 1, true, true )] int SmoothingPasses { get; set; } = 10;
[Group( "Domain Warping" )] bool DomainWarping { get; set; } = true;
[Group( "Domain Warping" )][Range( 0.1f, 1f, 0.01f, true, true )] float DomainWarpingSize { get; set; } = 0.25f;
[Group( "Domain Warping" )][Range( 0.1f, 1f, 0.01f, true, true )] float DomainWarpingStrength { get; set; } = 0.15f;
bool ErosionSimulation { get; set; } = false;
[Range( 1f, 25f, 1f, true, true )] int NoiseLayerStacks { get; set; } = 1;
///
/// River Carving Variables
///
[Group( "River & Stream Carving" )] bool RiverCarvingBool { get; set; } = true;
[Group( "River & Stream Carving" )][Range( 0.5f, 10f, 0.1f, true, true )] float RiverCarvingFrequency { get; set; } = 1.5f;
[Group( "River & Stream Carving" )][Range( 0.01f, 5f, 0.01f, true, true )] float RiverCarvingStrength { get; set; } = 0.3f;
[Group( "River & Stream Carving" )][Range( 0.01f, 0.25f, 0.001f, true, true )] float RiverCarvingDepth { get; set; } = 0.01f;
[Group( "River & Stream Carving" )][Range( 0.001f, 2f, 0.01f, true, true )] float RiverCarvingWidth { get; set; } = 0.25f;
[Group( "River & Stream Carving" )][Range( 0.05f, 1f, 0.01f, true, true )] float RiverCarvingSpacing { get; set; } = 0.05f;
[Group( "River & Stream Carving" )][Range( 0.01f, 1f, 0.01f, true, true )] float RiverCarvingTurbulenceStrength { get; set; } = 0.01f;
[Group( "River & Stream Carving" )][Range( 0.01f, 10f, 0.01f, true, true )] float RiverCarvingTurbulenceFrequency { get; set; } = 0.01f;
///
/// Tool Placement Square
///
[Group( "Tool Placement" )] bool StagingArea { get; set; } = true;
[Group( "Tool Placement" )][Range( 1, 100, 1, true, true )] int StagingAreaSize { get; set; } = 10; // Size of the square (in grid units)
[Group( "Tool Placement" )][Range( 0, 1, 0.01f, true, true )] float StagingAreaHeight { get; set; } = 0.1f; // Height of the flat square
[Group( "Tool Placement" )][Range( 0, 1, 0.01f, true, true )] float StagingAreaX { get; set; } = 0.1f; // X-center of the square as a ratio
[Group( "Tool Placement" )][Range( 0, 1, 0.01f, true, true )] float StagingAreaY { get; set; } = 0.1f; // Y-center of the square as a ratio
Gradient SplatMapGradient = new Gradient( new Gradient.ColorFrame( 0.0f, Color.Cyan ), new Gradient.ColorFrame( 0.25f, Color.Red ), new Gradient.ColorFrame( 0.5f, Color.Yellow ), new Gradient.ColorFrame( 0.75f, Color.Green ) );
SKColor[] _splatcolors { get; set; }
float[] _splatthresholds = { 0f, 0.25f, 0.50f, 0.75f };
float[,] _heightmap;
float[,] _splatmap;
Texture _preview_image_texture;
Editor.TextureEditor.Preview PreviewImage;
Texture _preview_splatmap_texture;
Editor.TextureEditor.Preview PreviewSplatmap;
SegmentedControl ShapeArray;
SegmentedControl CategoryArray;
public class DynamicEnum
{
private readonly Dictionary<string, int> _values = new Dictionary<string, int>();
private int _nextValue = 0;
public void Add( string name )
{
if ( !_values.ContainsKey( name ) )
{
_values[name] = _nextValue++;
}
}
public int GetValue( string name )
{
return _values.TryGetValue( name, out var value ) ? value : -1; // Return -1 if not found
}
public string GetName( int key )
{
return _values.FirstOrDefault( pair => pair.Value == key ).Key ?? "Unknown"; // Return "Unknown" if not found
}
public string[] GetNames()
{
return _values.Keys.ToArray();
}
}
public static string[] GetMethodsFromClass( string className )
{
// Attempt to get the Type from the class name (fully qualified)
Type classType = Type.GetType( className );
if ( classType == null )
{
throw new ArgumentException( $"Class '{className}' could not be found. Ensure the namespace is included." );
}
List<string> methodNames = new List<string>();
// Get all public methods (static and instance) from the class
MethodInfo[] methods = classType.GetMethods( BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static );
foreach ( var method in methods )
{
// Exclude methods not declared in this class
if ( method.DeclaringType == classType )
{
methodNames.Add( method.Name );
}
}
return methodNames.ToArray();
}
public string[] GetTerrainCategoryClasses( params Type[] CategoryClasses )
{
HashSet<string> methodNames = new HashSet<string>();
foreach ( Type CategoryClass in CategoryClasses )
{
// Get all public static methods from the class
MethodInfo[] methods = CategoryClass.GetMethods( BindingFlags.Public | BindingFlags.Static );
foreach ( MethodInfo method in methods )
{
// Exclude inherited methods or non-relevant ones
if ( method.DeclaringType == CategoryClass )
{
methodNames.Add( $"{CategoryClass.Name}.{method.Name}" );
TerrainCategoryEnum.Add( CategoryClass.Name);
TerrainCategoryArray.Add( CategoryClass.Name );
}
}
}
return methodNames.ToArray();
}
public string[] GetTerrainShapeMethods( Type shapeClass )
{
HashSet<string> methodNames = new HashSet<string>();
MethodInfo[] methods = shapeClass.GetMethods( BindingFlags.Public | BindingFlags.Static );
foreach ( MethodInfo method in methods )
{
// Exclude inherited methods or non-relevant ones
if ( method.DeclaringType == shapeClass )
{
//TerrainShapeArray.Add( shapeClass.Name );
//TerrainShapeEnum.Add( shapeClass.Name );
}
}
return methodNames.ToArray();
}
public static object CallMethod( string className, string methodName, object[] parameters = null )
{
// Get the class type
Type classType = Type.GetType( className );
if ( classType == null )
{
throw new ArgumentException( $"Class '{className}' could not be found." );
}
// Get the method info
MethodInfo method = classType.GetMethod( methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance );
if ( method == null )
{
throw new ArgumentException( $"Method '{methodName}' could not be found in class '{className}'." );
}
// Check if the method is static or instance
object instance = null;
if ( !method.IsStatic )
{
instance = Activator.CreateInstance( classType );
}
// Invoke the method
object result = method.Invoke( instance, parameters );
// Ensure the return type is compatible
if ( result is not float )
{
throw new InvalidOperationException( $"Method '{methodName}' does not return a float." );
}
return result;
}
public void InitialShapes()
{
ShapeArray.DestroyChildren();
TerrainShapeArray.Clear();
string className = $"Sturnus.TerrainGenerationTool.Islands"; // Fully qualified name
string[] methods = GetMethodsFromClass( className );
// Print the methods
foreach ( string method in methods )
{
TerrainShapeArray.Add( method );
TerrainShapeEnum.Add( method );
}
foreach ( var shape in TerrainShapeArray )
{
ShapeArray.AddOption( shape );
}
}
public TerrainGenerationTool( Widget parent ) : base( parent, false )
{
string[] terrainCategoryClasses = GetTerrainCategoryClasses( terrainCategoryClassesTypes.ToArray() );
string[] terrainShapeMethods = GetTerrainShapeMethods( typeof(Islands) );
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( GenerationPath );
SplatMapGradient.Blending = Gradient.BlendMode.Stepped;
MinimumSize = 500;
var scroll = new ScrollArea( null );
scroll.Canvas = new Widget( scroll );
scroll.Canvas.Layout = Layout.Column();
scroll.Canvas.Layout.Margin = 10;
Layout = Layout.Column();
Layout.Add( scroll );
Layout.Margin = 0;
Layout.Spacing = 5;
var body = scroll.Canvas.Layout;
var DimensionsLabel = body.Add( new Label( "Terrain Dimensions" ) );
var DimensionsEnum = body.Add( new EnumControlWidget( this.GetSerialized().GetProperty( nameof( TerrainDimensionsEnum ) ) ) );
var CategoryLabel = body.Add( new Label( "Terrain Category" ) );
CategoryArray = body.Add( new SegmentedControl( ) );
for ( int i = 0; i < TerrainCategoryArray.ToArray().GetLength( 0 ); i++ )
{
// Initialize an empty list to store the values of the current row
List<string> rowValues = new List<string>();
rowValues.Add( TerrainCategoryArray.ToArray()[i] );
// Join the row's values with a comma and print
CategoryArray.AddOption( rowValues[0] );
}
var ShapeLabel = body.Add( new Label( "Terrain Shape" ) );
ShapeArray = body.Add( new SegmentedControl() );
InitialShapes();
CategoryArray.MouseClick += () =>
{
RebuildShapes();
};
body.AddSpacingCell( 5 );
var MinHeightLabel = body.Add( new Label( "Min Height (relative)" ) );
var MinHeightFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( TerrainMinHeight ) ) ) );
body.AddSpacingCell( 5 );
var MaxHeightLabel = body.Add( new Label( "Max Height (relative)" ) );
var MaxHeightFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( TerrainMaxHeight ) ) ) );
body.AddSpacingCell( 5 );
var TerrainPlaneScaleLabel = body.Add( new Label( "Terrain Plane Scale" ) );
var TerrainPlaneScaleNumber = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( TerrainPlaneScale ) ) ) );
body.AddSpacingCell( 5 );
var TerrainSeedLabel = body.Add( new Label( "Terrain Seed" ) );
var TerrainSeedNumber = body.Add( new IntegerControlWidget( this.GetSerialized().GetProperty( nameof( TerrainSeed ) ) ) );
body.AddSpacingCell( 5 );
var SmoothingPassesLabel = body.Add( new Label( "Smoothing Passes" ) );
var SmoothingPassesNumber = body.Add( new IntegerControlWidget( this.GetSerialized().GetProperty( nameof( SmoothingPasses ) ) ) );
body.AddSpacingCell( 5 );
var NoiseLayerStacksLabel = body.Add( new Label( "Noise Layer Stacks" ) );
var NoiseLayerStacksNumber = body.Add( new IntegerControlWidget( this.GetSerialized().GetProperty( nameof( NoiseLayerStacks ) ) ) );
body.AddSpacingCell( 5 );
var SplatMapColorsLabel = body.Add( new Label( "Splatmap Colors/Threshold" ) );
var SplatMapColorsNumber = body.Add( new GradientControlWidget( this.GetSerialized().GetProperty( nameof( SplatMapGradient ) ) ) );
body.AddSpacingCell( 5 );
var DomainWarpingLabel = body.Add( new Label( "Domain Warping" ) );
var DomainWarpingBool = body.Add( new BoolControlWidget( this.GetSerialized().GetProperty( nameof( DomainWarping ) ) ) );
body.AddSpacingCell( 5 );
if ( DomainWarping )
{
var DomainWarpingSizeLabel = body.Add( new Label( "Domain Warping (Size)" ) );
var DomainWarpingSizeFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( DomainWarpingSize ) ) ) );
var DomainWarpingStrengthLabel = body.Add( new Label( "Domain Warping (Strength)" ) );
var DomainWarpingStrengthFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( DomainWarpingStrength ) ) ) );
DomainWarpingBool.MouseClick += () =>
{
if ( DomainWarping )
{
DomainWarpingSizeLabel.Enabled = true;
DomainWarpingSizeFloat.Enabled = true;
DomainWarpingSizeLabel.Visible = true;
DomainWarpingSizeFloat.Visible = true;
DomainWarpingStrengthLabel.Enabled = true;
DomainWarpingStrengthFloat.Enabled = true;
DomainWarpingStrengthLabel.Visible = true;
DomainWarpingStrengthFloat.Visible = true;
}
else
{
DomainWarpingSizeLabel.Enabled = false;
DomainWarpingSizeFloat.Enabled = false;
DomainWarpingSizeLabel.Visible = false;
DomainWarpingSizeFloat.Visible = false;
DomainWarpingStrengthLabel.Enabled = false;
DomainWarpingStrengthFloat.Enabled = false;
DomainWarpingStrengthLabel.Visible = false;
DomainWarpingStrengthFloat.Visible = false;
}
};
}
/*body.AddSpacingCell( 5 );
var ErosionSimulationLabel = body.Add( new Label( "Erosion Simulation" ) );
var ErosionSimulationBool = body.Add( new BoolControlWidget( this.GetSerialized().GetProperty( nameof( ErosionSimulation ) ) ) );
if ( ErosionSimulation )
{
}*/
body.AddSpacingCell( 5 );
var RiverCarvingLabel = body.Add( new Label( "River Carving" ) );
var RiverCarvingBoolControl = body.Add( new BoolControlWidget( this.GetSerialized().GetProperty( nameof( RiverCarvingBool ) ) ) );
if ( RiverCarvingBool )
{
var RiverCarvingFrequency = body.Add( new Label( "RiverCarvingFrequency" ));
var RiverCarvingFrequencyFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( RiverCarvingFrequency ) ) ) );
/*var RiverCarvingStrength = body.Add( new Label("RiverCarvingStrength"));
var RiverCarvingStrengthFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingStrength))));*/
var RiverCarvingDepth = body.Add( new Label("RiverCarvingDepth"));
var RiverCarvingDepthFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingDepth))));
var RiverCarvingWidth = body.Add( new Label("RiverCarvingWidth"));
var RiverCarvingWidthFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingWidth))));
var RiverCarvingSpacing = body.Add( new Label("RiverCarvingSpacing"));
var RiverCarvingSpacingFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingSpacing))));
var RiverCarvingTurbulenceStrength = body.Add( new Label("RiverCarvingTurbulenceStrength"));
var RiverCarvingTurbulenceStrengthFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingTurbulenceStrength))));
var RiverCarvingTurbulenceFrequency = body.Add( new Label("RiverCarvingTurbulenceFrequency"));
var RiverCarvingTurbulenceFrequencyFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty(nameof(RiverCarvingTurbulenceFrequency))));
RiverCarvingBoolControl.MouseClick += () =>
{
if ( RiverCarvingBool )
{
RiverCarvingFrequency.Enabled = true;
RiverCarvingFrequencyFloat.Enabled = true;
/*RiverCarvingStrength.Enabled = true;
RiverCarvingStrengthFloat.Enabled = true;*/
RiverCarvingDepth.Enabled = true;
RiverCarvingDepthFloat.Enabled = true;
RiverCarvingWidth.Enabled = true;
RiverCarvingWidthFloat.Enabled = true;
RiverCarvingSpacing.Enabled = true;
RiverCarvingSpacingFloat.Enabled = true;
RiverCarvingTurbulenceStrength.Enabled = true;
RiverCarvingTurbulenceStrengthFloat.Enabled = true;
RiverCarvingTurbulenceFrequency.Enabled = true;
RiverCarvingTurbulenceFrequencyFloat.Enabled = true;
RiverCarvingFrequency.Visible = true;
RiverCarvingFrequencyFloat.Visible = true;
/*RiverCarvingStrength.Visible = true;
RiverCarvingStrengthFloat.Visible = true;*/
RiverCarvingDepth.Visible = true;
RiverCarvingDepthFloat.Visible = true;
RiverCarvingWidth.Visible = true;
RiverCarvingWidthFloat.Visible = true;
RiverCarvingSpacing.Visible = true;
RiverCarvingSpacingFloat.Visible = true;
RiverCarvingTurbulenceStrength.Visible = true;
RiverCarvingTurbulenceStrengthFloat.Visible = true;
RiverCarvingTurbulenceFrequency.Visible = true;
RiverCarvingTurbulenceFrequencyFloat.Visible = true;
}
else
{
RiverCarvingFrequency.Enabled = false;
RiverCarvingFrequencyFloat.Enabled = false;
/*RiverCarvingStrength.Enabled = false;
RiverCarvingStrengthFloat.Enabled = false;*/
RiverCarvingDepth.Enabled = false;
RiverCarvingDepthFloat.Enabled = false;
RiverCarvingWidth.Enabled = false;
RiverCarvingWidthFloat.Enabled = false;
RiverCarvingSpacing.Enabled = false;
RiverCarvingSpacingFloat.Enabled = false;
RiverCarvingTurbulenceStrength.Enabled = false;
RiverCarvingTurbulenceStrengthFloat.Enabled = false;
RiverCarvingTurbulenceFrequency.Enabled = false;
RiverCarvingTurbulenceFrequencyFloat.Enabled = false;
RiverCarvingFrequency.Visible = false;
RiverCarvingFrequencyFloat.Visible = false;
/*RiverCarvingStrength.Visible = false;
RiverCarvingStrengthFloat.Visible = false;*/
RiverCarvingDepth.Visible = false;
RiverCarvingDepthFloat.Visible = false;
RiverCarvingWidth.Visible = false;
RiverCarvingWidthFloat.Visible = false;
RiverCarvingSpacing.Visible = false;
RiverCarvingSpacingFloat.Visible = false;
RiverCarvingTurbulenceStrength.Visible = false;
RiverCarvingTurbulenceStrengthFloat.Visible = false;
RiverCarvingTurbulenceFrequency.Visible = false;
RiverCarvingTurbulenceFrequencyFloat.Visible = false;
}
};
}
else
{
//RiverStreamCarving = !RiverStreamCarving;
}
body.AddSpacingCell( 5 );
var ToolPlacementLabel = body.Add( new Label( "Staging Area" ) );
var ToolPlacementBool = body.Add( new BoolControlWidget( this.GetSerialized().GetProperty( nameof( StagingArea ) ) ) );
if ( StagingArea )
{
var StagingAreaSizeLabel = body.Add( new Label( "Staging Area (Size)" ) );
var StagingAreaSizeFloat = body.Add( new IntegerControlWidget( this.GetSerialized().GetProperty( nameof( StagingAreaSize ) ) ) );
var StagingAreaHeightLabel = body.Add( new Label( "Staging Area (Height)" ) );
var StagingAreaHeightFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( StagingAreaHeight ) ) ) );
var StagingAreaXLabel = body.Add( new Label( "Staging Area (X)" ) );
var StagingAreaXFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( StagingAreaX ) ) ) );
var StagingAreaYLabel = body.Add( new Label( "Staging Area (Y)" ) );
var StagingAreaYFloat = body.Add( new FloatControlWidget( this.GetSerialized().GetProperty( nameof( StagingAreaY ) ) ) );
ToolPlacementBool.MouseClick += () =>
{
if ( StagingArea )
{
StagingAreaSizeLabel.Enabled = true;
StagingAreaSizeFloat.Enabled = true;
StagingAreaHeightLabel.Enabled = true;
StagingAreaHeightFloat.Enabled = true;
StagingAreaXLabel.Enabled = true;
StagingAreaXFloat.Enabled = true;
StagingAreaYLabel.Enabled = true;
StagingAreaYFloat.Enabled = true;
StagingAreaSizeLabel.Visible = true;
StagingAreaSizeFloat.Visible = true;
StagingAreaHeightLabel.Visible = true;
StagingAreaHeightFloat.Visible = true;
StagingAreaXLabel.Visible = true;
StagingAreaXFloat.Visible = true;
StagingAreaYLabel.Visible = true;
StagingAreaYFloat.Visible = true;
}
else
{
StagingAreaSizeLabel.Enabled = false;
StagingAreaSizeFloat.Enabled = false;
StagingAreaHeightLabel.Enabled = false;
StagingAreaHeightFloat.Enabled = false;
StagingAreaXLabel.Enabled = false;
StagingAreaXFloat.Enabled = false;
StagingAreaYLabel.Enabled = false;
StagingAreaYFloat.Enabled = false;
StagingAreaSizeLabel.Visible = false;
StagingAreaSizeFloat.Visible = false;
StagingAreaHeightLabel.Visible = false;
StagingAreaHeightFloat.Visible = false;
StagingAreaXLabel.Visible = false;
StagingAreaXFloat.Visible = false;
StagingAreaYLabel.Visible = false;
StagingAreaYFloat.Visible = false;
}
};
}
var GenerateButton = Layout.Add( new Button.Primary( "Generate", "auto_awesome", this ) );
var LayoutRow = Layout.AddRow( 1 );
//var PreviewLabel = Layout.Add( new Label( "Preview" ) ); //Will attempt to get this working in a future update.
var _image_preview = new Editor.TextureEditor.Preview( this );
_image_preview.Texture = _preview_image_texture;
_image_preview.Size = new Vector2( 512, 512 );
PreviewImage = LayoutRow.Add( _image_preview, 50 );
var _splatmap_preview = new Editor.TextureEditor.Preview( this );
_splatmap_preview.Texture = _preview_splatmap_texture;
_splatmap_preview.Size = new Vector2( 512, 512 );
PreviewSplatmap = LayoutRow.Add( _splatmap_preview, 50 );
/*RenderCanvas = new SceneRenderingWidget( this );
RenderCanvas.OnPreFrame += OnPreFrame;
RenderCanvas.FocusMode = FocusMode.Click;
RenderCanvas.Scene = Scene.CreateEditorScene();
RenderCanvas.Scene.SceneWorld.AmbientLightColor = Theme.Blue * 0.4f;
_terrain = new GameObject( RenderCanvas.Scene,true, "terrain" ).GetOrAddComponent<Terrain>( true );
//_terrain.Storage.SetResolution( 512 );
_terrain.TerrainSize = 5007;
_terrain.TerrainHeight = 1000;
using ( RenderCanvas.Scene.Push() )
{
Camera = new GameObject( true, "camera" ).GetOrAddComponent<CameraComponent>( false );
Camera.BackgroundColor = Theme.Grey;
Camera.ZFar = 100000;
Camera.Enabled = true;
Camera.WorldPosition = new Vector3( -2000, 0, 2000 );
Camera.LocalRotation = new Angles( 30, 0, 0 );
RenderCanvas.Camera = Camera;
}
GizmoInstance = RenderCanvas.GizmoInstance;
Layout.Add( RenderCanvas,1 ); //Will attempt to get this working in a future update.*/
var ExportButton = Layout.Add( new Button( "Export", "file_download", this ) );
ExportButton.Tint = "#41AF20";
var ApplyButton = Layout.Add( new Button( "Apply To Terrain", "file_upload", this ) );
ApplyButton.Tint = "#AF2020";
if ( _heightmap == null )
{
ExportButton.Enabled = false;
ApplyButton.Enabled = false;
}
GenerateButton.Clicked += () =>
{
List<float> _splatthresholdtime = new List<float>();
List<SKColor> _splatmapgradients = new List<SKColor>();
foreach ( var color in SplatMapGradient.Colors )
{
_splatthresholdtime.Add( color.Time );
_splatmapgradients.Add( new SKColor( color.Value.ToColor32().r, color.Value.ToColor32().g, color.Value.ToColor32().b, color.Value.ToColor32().a ) );
}
_splatthresholds = _splatthresholdtime.ToArray();
_splatcolors = _splatmapgradients.ToArray();
var fullclass = Type.GetType($"Sturnus.TerrainGenerationTool.{CategoryArray.Selected}");
Log.Info( fullclass.ToString() );
var fullclassmethod = fullclass.GetMethod(ShapeArray.Selected);
Log.Info( fullclassmethod.ToString() );
_heightmap = GenerateStackedNoise(
(int)TerrainDimensionsEnum,
(int)TerrainDimensionsEnum,
TerrainSeed,
NoiseLayerStacks,
1.0f,
2.0f,
1.0f,
0.5f,
( x, y ) => (float)CallMethod( $"Sturnus.TerrainGenerationTool.{CategoryArray.Selected}", ShapeArray.Selected, new object[] {
x, y,
(int)TerrainDimensionsEnum,
(int)TerrainDimensionsEnum,
TerrainSeed,
TerrainMinHeight,
DomainWarping,
DomainWarpingSize,
DomainWarpingStrength
} ),
TerrainMaxHeight,
SmoothingPasses,
TerrainPlaneScale
);
if ( ErosionSimulation )
{
}
if ( RiverCarvingBool )
{
_heightmap = AddTurbulenceForRivers(
_heightmap,
seed: TerrainSeed,
riverFrequency: RiverCarvingFrequency, // Frequency of rivers
riverWidth: RiverCarvingWidth, // Width of rivers
riverDepth: RiverCarvingDepth, // Depth of rivers
turbulenceFrequency: RiverCarvingTurbulenceFrequency, // Turbulence frequency
turbulenceStrength: RiverCarvingTurbulenceStrength, // Turbulence strength
minRiverSpacing: RiverCarvingSpacing, // Minimum spacing between rivers
slopeSteepness:10f,
terrainNoiseFrequency: 2.0f, // Matches terrain noise frequency
terrainNoiseAmplitude: 0.5f // Matches terrain noise amplitude
);
}
if ( _heightmap == null )
{
Log.Error( "Heightmap is not generated. Aborting." );
return;
}
if ( StagingArea )
{
_heightmap = AddStagingSquare(
_heightmap,
StagingAreaSize,
StagingAreaHeight,
StagingAreaX,
StagingAreaY );
}
_splatmap = GenerateSplatmap( _heightmap, _splatthresholds, TerrainMaxHeight );
GeneratePreviewFile( GenerationPath );
//_terrain.HeightMap.Update( _heightmap_byte ,0,0, (int)TerrainDimensionsEnum , (int)TerrainDimensionsEnum );
string preview_image_path = Path.Combine( GenerationLocalPath, $"TerrainGenerationUtility_preview.png" );
_preview_image_texture = Texture.Load( Editor.FileSystem.Mounted, preview_image_path );
PreviewImage.Texture = _preview_image_texture;
string preview_splatmap_path = Path.Combine( GenerationLocalPath, $"TerrainGenerationUtility_splat_preview.png" );
_preview_splatmap_texture = Texture.Load( Editor.FileSystem.Mounted, preview_splatmap_path );
PreviewSplatmap.Texture = _preview_splatmap_texture;
ExportButton.Enabled = true;
ApplyButton.Enabled = true;
};
ExportButton.Clicked += () =>
{
GenerateImageFiles( ExportPath );
var PopUp = new PopupWindow( "Export Complete", $"Files exported to {ExportPath}", "Okay" );
PopUp.Show();
};
ApplyButton.Clicked += () =>
{
IDictionary<string, Action> WarnDiaglog = new Dictionary<string, Action>(); ;
WarnDiaglog.Add( "Apply", UpdateTerrain );
var PopUpWarn = new PopupWindow( "Warning: Terrain Override", "This will override your current scene's terrain data.","Cancel", WarnDiaglog );
PopUpWarn.Show();
};
Layout.AddStretchCell();
Show();
}
public void RebuildShapes()
{
ShapeArray.DestroyChildren();
TerrainShapeArray.Clear();
string className = $"Sturnus.TerrainGenerationTool.{TerrainCategoryEnum.GetName( TerrainCategoryEnum.GetValue( CategoryArray.Selected ) )}"; // Fully qualified name
string[] methods = GetMethodsFromClass( className );
// Print the methods
foreach ( string method in methods )
{
TerrainShapeArray.Add( method );
//Log.Info( method );
}
foreach ( var shape in TerrainShapeArray )
{
ShapeArray.AddOption( shape );
}
ShapeArray.SelectedIndex = 0;
ShapeArray.Selected = ShapeArray.Children.FirstOrDefault().Name;
foreach(var test in ShapeArray.Children )
{
//Log.Info( test.Name );
}
}
private void UpdateTerrain()
{
var ActiveScene = Editor.SceneEditorSession.Active.Scene;
var FirstTerrain = ActiveScene.GetAllComponents<Terrain>().FirstOrDefault();
FirstTerrain.UpdateMaterialsBuffer();
FirstTerrain.SyncGPUTexture();
FirstTerrain.UpdateMaterialsBuffer();
FirstTerrain.HeightMap.Update( ConvertRawFloatArrayToByteArray( _heightmap ), 0, 0, (int)TerrainDimensionsEnum, (int)TerrainDimensionsEnum );
FirstTerrain.Storage.HeightMap = ConvertFloatArrayToUShortArray( _heightmap );
FirstTerrain.UpdateMaterialsBuffer();
FirstTerrain.SyncGPUTexture();
FirstTerrain.UpdateMaterialsBuffer();
}
private float[,] AddStagingSquare( float[,] heightmap, int squareSize, float squareHeight, float centerX, float centerY )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
// Calculate the center and bounds of the square
int centerXPixel = (int)(centerX * width);
int centerYPixel = (int)(centerY * height);
int halfSize = squareSize / 2;
int startX = Math.Max( centerXPixel - halfSize, 0 );
int startY = Math.Max( centerYPixel - halfSize, 0 );
int endX = Math.Min( centerXPixel + halfSize, width - 1 );
int endY = Math.Min( centerYPixel + halfSize, height - 1 );
// Set the height values inside the square to be completely flat
for ( int y = startY; y <= endY; y++ )
{
for ( int x = startX; x <= endX; x++ )
{
heightmap[x, y] = squareHeight;
}
}
// Add the slope around the square
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Skip the flat square area
if ( x >= startX && x <= endX && y >= startY && y <= endY )
continue;
// Calculate the distance to the nearest edge of the square
int dx = Math.Max( Math.Abs( x - centerXPixel ) - halfSize, 0 );
int dy = Math.Max( Math.Abs( y - centerYPixel ) - halfSize, 0 );
float distanceToSquare = MathF.Sqrt( dx * dx + dy * dy );
// Calculate the target height for the slope
float slopeHeight = squareHeight - (distanceToSquare * 0.0038f); // 0.0038f is perfect for players
// Ensure the slope transitions smoothly into the existing terrain
heightmap[x, y] = Math.Max( heightmap[x, y], slopeHeight );
}
}
return heightmap;
}
private void GeneratePreviewFile( string path )
{
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( path );
string previewfile = Path.Combine( path, $"TerrainGenerationUtility_preview.png" );
string splatfile = Path.Combine( path, $"TerrainGenerationUtility_splat_preview.png" );
SKBitmap image = HeightmapToBitMap( _heightmap );
SaveImage( image, previewfile );
//float[,] splatmap = GenerateSplatmap( _heightmap, _splatthresholds, TerrainMaxHeight );
SKBitmap splat = SplatmapToBitMap( _splatmap, _splatcolors );
SaveSplatmapAsPng( splat, splatfile );
}
private void GenerateImageFiles( string output_path )
{
string UsingDomainWarping = "";
string UsingErosionEmulation = "";
string UsingWaterCarving = "";
if ( DomainWarping )
{
UsingDomainWarping = "_warp";
}
if ( ErosionSimulation )
{
UsingErosionEmulation = "_erosion";
}
if ( RiverCarvingBool )
{
UsingWaterCarving = "_watercarving";
}
//Create TerrainGenerationTool folder if it doesn't exist.
Directory.CreateDirectory( output_path );
string rawfile = Path.Combine( output_path, $"TerrainGenerationUtility_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.raw" );
string previewfile = Path.Combine( output_path, $"TerrainGenerationUtility_preview_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png" );
string splatfile = Path.Combine( output_path, $"TerrainGenerationUtility_splat_export_{/*TerrainShapeEnumSelect*/null}{UsingDomainWarping}{UsingErosionEmulation}{UsingWaterCarving}.png" );
//Export RAW HeightMap file
SaveRaw( _heightmap, rawfile );
Log.Info( $"Raw file generated! - {rawfile}" );
//Generate & Export Preview image for widget
SKBitmap image = HeightmapToBitMap( _heightmap );
SaveImage( image, previewfile );
Log.Info( $"HeightMap preview file generated! - {previewfile}" );
//Generate & Export SplatMap image
float[,] splatmap = GenerateSplatmap( _heightmap, _splatthresholds, TerrainMaxHeight );
SKBitmap splat = SplatmapToBitMap( splatmap, _splatcolors );
SaveSplatmapAsPng( splat, splatfile );
Log.Info( $"Splatmap file generated! - {splatfile}" );
Log.Info( $"All export files saved! {output_path}" );
}
public float[,] GenerateHeightmap( int width, int height, Func<int, int, float> generator, float maxHeight, int smoothpasses )
{
float[,] heightmap = new float[width, height];
float actualMaxHeight = float.MinValue;
// Use parallel processing to generate heightmap
object maxLock = new object(); // Lock object for thread safety
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float value = generator( x, y );
heightmap[x, y] = value;
// Update actual max height (thread-safe)
lock ( maxLock )
{
if ( value > actualMaxHeight )
{
actualMaxHeight = value;
}
}
}
} );
// Scale all values by the actual max height and up to the specified max height
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = (heightmap[x, y] / actualMaxHeight) * maxHeight;
}
} );
// Apply smoothing if needed
if ( smoothpasses > 0 )
{
return SmoothHeightmap( heightmap, smoothpasses );
}
else
{
return heightmap;
}
}
public static float[,] GenerateStackedNoise(
int width,
int height,
long seed,
int layers,
float initialFrequency,
float frequencyMultiplier,
float initialAmplitude,
float amplitudeMultiplier,
Func<int, int, float> shapeFunction, // Shape function applied after stacking noise
float maxHeight,
int smoothingPasses,
float terrainPlaneScale // New variable to scale the noise
)
{
// Initialize the heightmap with zeros
float[,] heightmap = new float[width, height];
// Random offset generator for noise layers
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float[] xOffsets = new float[layers];
float[] yOffsets = new float[layers];
for ( int i = 0; i < layers; i++ )
{
xOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;
yOffsets[i] = random.Next( -100000, 100000 ) / 1000.0f;
}
// Adjust frequency based on TerrainPlaneScale
float scaleFactor = Math.Clamp( terrainPlaneScale, 0.01f, 1.0f );
// Multithreaded noise generation
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
float value = 0.0f;
for ( int layer = 0; layer < layers; layer++ )
{
float frequency = initialFrequency * MathF.Pow( frequencyMultiplier, layer ) / scaleFactor;
float amplitude = initialAmplitude * MathF.Pow( amplitudeMultiplier, layer );
// Normalized coordinates adjusted by scale factor
float nx = (x / (float)width) * frequency;
float ny = (y / (float)height) * frequency;
// Apply random offsets
nx += xOffsets[layer];
ny += yOffsets[layer];
// Generate noise
float noiseValue = OpenSimplex2S.Noise2( (seed + layer) & 0xFFFFFFFF, nx, ny );
value += Math.Clamp( noiseValue, -1.0f, 1.0f ) * amplitude;
}
// Save the computed value to the heightmap
lock ( heightmap )
{
heightmap[x, y] += value;
}
}
} );
// Normalize the heightmap to the range [0, 1]
heightmap = NormalizeHeightmap( heightmap );
// Apply the shape function and amplify its contribution if needed
float shapeAmplification = 1.2f; // Adjust for stronger shape effects
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] *= MathF.Pow( shapeFunction( x, y ), shapeAmplification );
}
} );
// Rescale the heightmap to the desired maxHeight
float currentMax = FindMaxHeight( heightmap );
if ( currentMax > 0 )
{
Parallel.For( 0, height, y =>
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = (heightmap[x, y] / currentMax) * maxHeight;
}
} );
}
// Apply smoothing
if ( smoothingPasses > 0 )
{
heightmap = SmoothHeightmap( heightmap, smoothingPasses );
}
return heightmap;
}
// Helper method to find the maximum height in a heightmap
private static float FindMaxHeight( float[,] heightmap )
{
float max = float.MinValue;
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
if ( heightmap[x, y] > max )
{
max = heightmap[x, y];
}
}
}
return max;
}
private static float[,] NormalizeHeightmap( float[,] heightmap )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float min = float.MaxValue;
float max = float.MinValue;
// Find the min and max values
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
float value = heightmap[x, y];
if ( value < min ) min = value;
if ( value > max ) max = value;
}
}
// Normalize the values
float[,] normalized = new float[width, height];
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
normalized[x, y] = (heightmap[x, y] - min) / (max - min);
}
}
return normalized;
}
public static float[,] AddTurbulenceForRivers(
float[,] heightmap,
long seed,
float riverFrequency, // Frequency for river placement
float riverWidth, // Width of the rivers
float riverDepth, // Depth of the rivers
float turbulenceFrequency, // Turbulence frequency
float turbulenceStrength, // Turbulence strength
float minRiverSpacing, // Minimum spacing between rivers
float slopeSteepness, // Controls the gradual slope of the riverbanks
float terrainNoiseFrequency, // Matches terrain surface noise frequency
float terrainNoiseAmplitude // Matches terrain surface noise amplitude
)
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] newHeightmap = (float[,])heightmap.Clone();
Random random = new Random( (int)(seed & 0xFFFFFFFF) );
float[,] riverPlacementNoise = new float[width, height];
// Generate river placement noise
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
// Noise for river placement
riverPlacementNoise[x, y] = OpenSimplex2S.Noise2( seed, nx * riverFrequency, ny * riverFrequency );
}
}
// Process heightmap with river carving
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
float riverNoise = MathF.Abs( riverPlacementNoise[x, y] ); // Use absolute noise for placement
// Determine if the point is within the river carving zone
if ( riverNoise < riverWidth )
{
// Calculate the smooth curve effect based on distance from the center
float distanceFactor = 1.0f - (riverNoise / riverWidth); // 1 at center, 0 at edge
float smoothDepthReduction = MathF.Pow( distanceFactor, slopeSteepness ) * riverDepth;
// Add turbulence for a more organic flow
float turbulence = OpenSimplex2S.Noise2( seed + 1, nx * turbulenceFrequency, ny * turbulenceFrequency )
* turbulenceStrength;
// Apply smooth depth reduction and turbulence
float reducedHeight = newHeightmap[x, y] - smoothDepthReduction + turbulence;
// Clamp height to ensure it doesn't rise above the original
newHeightmap[x, y] = MathF.Max( 0, MathF.Min( newHeightmap[x, y], reducedHeight ) );
}
// Enforce minimum spacing between rivers
if ( riverNoise < minRiverSpacing )
{
// Slightly raise the terrain to enforce separation
newHeightmap[x, y] += (minRiverSpacing - riverNoise) * 0.05f;
}
}
}
// Add base noise to the entire heightmap after carving
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
float nx = x / (float)width;
float ny = y / (float)height;
// Generate base noise
float baseNoise = OpenSimplex2S.Noise2( seed + 2, nx * 6f, ny * 6f )
* 0.02f;
// Add noise to the heightmap
newHeightmap[x, y] = MathF.Max( 0, newHeightmap[x, y] + baseNoise );
}
}
return newHeightmap;
}
// Smooths the heightmap using a simple box blur with adjustable strength
private static float[,] SmoothHeightmap( float[,] heightmap, int smoothingPasses )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] smoothed = new float[width, height];
for ( int pass = 0; pass < smoothingPasses; pass++ )
{
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
float sum = 0;
int count = 0;
// Iterate through neighbors
for ( int dy = -1; dy <= 1; dy++ )
{
for ( int dx = -1; dx <= 1; dx++ )
{
int nx = x + dx;
int ny = y + dy;
if ( nx >= 0 && nx < width && ny >= 0 && ny < height )
{
sum += heightmap[nx, ny];
count++;
}
}
}
smoothed[x, y] = sum / count;
}
}
// Copy smoothed values back to the original heightmap for the next pass
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
heightmap[x, y] = smoothed[x, y];
}
}
}
return smoothed;
}
public static ushort[] ConvertFloatArrayToUShortArray( float[,] input, float scale = 65535.0f )
{
// Get the dimensions of the 2D array
int rows = input.GetLength( 0 );
int cols = input.GetLength( 1 );
// Initialize the 1D ushort array
ushort[] output = new ushort[rows * cols];
// Iterate over the 2D array row by row
int index = 0;
for ( int row = 0; row < rows; row++ )
{
for ( int col = 0; col < cols; col++ )
{
// Convert the float to ushort, scaling if necessary
float value = input[row, col];
value = Math.Clamp( value, 0.0f, 1.0f ); // Ensure the float is in the 0 to 1 range
output[index++] = (ushort)(value * scale);
}
}
return output;
}
public static byte[] ConvertRawFloatArrayToByteArray( float[,] rawData, float scale = 65535.0f )
{
if ( rawData == null )
{
throw new ArgumentNullException( nameof( rawData ), "Input rawData cannot be null." );
}
int rows = rawData.GetLength( 0 );
int cols = rawData.GetLength( 1 );
// Create a byte array with 2 bytes per value
byte[] byteArray = new byte[rows * cols * 2]; // 2 bytes per ushort
int index = 0;
for ( int row = 0; row < rows; row++ )
{
for ( int col = 0; col < cols; col++ )
{
float value = rawData[row, col];
value = Math.Clamp( value, 0.0f, 1.0f ); // Ensure value is in the range [0, 1]
// Convert to 16-bit unsigned integer
ushort ushortValue = (ushort)(value * scale);
// Store in byte array (little-endian order)
byteArray[index++] = (byte)(ushortValue & 0xFF); // Lower byte
byteArray[index++] = (byte)((ushortValue >> 8) & 0xFF); // Upper byte
}
}
return byteArray;
}
// Converts a heightmap to a grayscale image using SkiaSharp
public static SKBitmap HeightmapToBitMap( float[,] heightmap )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
SKBitmap bitmap = new SKBitmap( width, height );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
int intensity = (int)(heightmap[x, y] * 255);
intensity = Math.Clamp( intensity, 0, 255 );
bitmap.SetPixel( x, y, new SKColor( (byte)intensity, (byte)intensity, (byte)intensity ) );
}
}
return bitmap;
}
public byte[] ConvertSKBitmapToBytes( SKBitmap bitmap, SKEncodedImageFormat format, int quality = 100 )
{
// Create an SKImage from the SKBitmap
using ( var image = SKImage.FromBitmap( bitmap ) )
{
// Encode the image to the desired format (e.g., PNG, JPEG)
using ( var data = image.Encode( format, quality ) )
{
// Convert SKData to a byte array
return data.ToArray();
}
}
}
public static void SaveImage(
SKBitmap bitmap,
string filename,
float rotationDegrees = 270f,
bool reverseHorizontal = false,
bool reverseVertical = true
)
{
int width = bitmap.Width;
int height = bitmap.Height;
// Create a new bitmap to hold the transformed image
using var transformedBitmap = new SKBitmap( width, height );
// Create a canvas to draw the transformed image
using var canvas = new SKCanvas( transformedBitmap );
// Clear the canvas with transparency
canvas.Clear( SKColors.Transparent );
// Apply transformations
canvas.Save();
// Translate to the center of the canvas for rotation and flipping
canvas.Translate( width / 2f, height / 2f );
// Apply flipping first
float scaleX = reverseHorizontal ? -1f : 1f;
float scaleY = reverseVertical ? -1f : 1f;
canvas.Scale( scaleX, scaleY );
// Apply rotation
if ( rotationDegrees != 0 )
{
canvas.RotateDegrees( rotationDegrees );
}
// Translate back to ensure the image is drawn correctly
canvas.Translate( -width / 2f, -height / 2f );
// Draw the original bitmap onto the transformed canvas
canvas.DrawBitmap( bitmap, 0, 0 );
// Restore the canvas to finalize the transformations
canvas.Restore();
// Flush the canvas
canvas.Flush();
// Save the transformed bitmap as a PNG file
using var pixmap = transformedBitmap.PeekPixels();
using var image = SKImage.FromPixels( pixmap );
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
using var stream = File.OpenWrite( filename );
data.SaveTo( stream );
}
public static void SaveRaw( float[,] heightmap, string filename, int rotationDegrees = 270, bool reverseHorizontal = true, bool reverseVertical = false )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
// Rotate the heightmap if requested
if ( rotationDegrees != 0 )
{
heightmap = RotateHeightmap( heightmap, rotationDegrees );
if ( rotationDegrees == 90 || rotationDegrees == 270 )
{
// Swap width and height for 90° or 270° rotations
(width, height) = (height, width);
}
}
// Reverse the heightmap if requested
if ( reverseHorizontal || reverseVertical )
{
heightmap = ReverseHeightmap( heightmap, reverseHorizontal, reverseVertical );
}
using var fileStream = new FileStream( filename, FileMode.Create, FileAccess.Write );
using var binaryWriter = new BinaryWriter( fileStream );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Scale image data to 16-bit
ushort value = (ushort)(Math.Clamp( heightmap[x, y], 0, 1 ) * 65535);
binaryWriter.Write( value );
}
}
}
// Helper method to rotate the heightmap by 90°, 180°, or 270°
private static float[,] RotateHeightmap( float[,] original, int rotationDegrees )
{
int originalWidth = original.GetLength( 0 );
int originalHeight = original.GetLength( 1 );
float[,] rotated;
switch ( rotationDegrees )
{
case 90:
rotated = new float[originalHeight, originalWidth];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[y, originalWidth - 1 - x] = original[x, y];
}
}
break;
case 180:
rotated = new float[originalWidth, originalHeight];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[originalWidth - 1 - x, originalHeight - 1 - y] = original[x, y];
}
}
break;
case 270:
rotated = new float[originalHeight, originalWidth];
for ( int y = 0; y < originalHeight; y++ )
{
for ( int x = 0; x < originalWidth; x++ )
{
rotated[originalHeight - 1 - y, x] = original[x, y];
}
}
break;
default:
throw new ArgumentException( "Rotation must be 0, 90, 180, or 270 degrees." );
}
return rotated;
}
// Helper method to reverse the heightmap horizontally and/or vertically
private static float[,] ReverseHeightmap( float[,] original, bool reverseHorizontal, bool reverseVertical )
{
int width = original.GetLength( 0 );
int height = original.GetLength( 1 );
float[,] reversed = new float[width, height];
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
int targetX = reverseHorizontal ? width - 1 - x : x;
int targetY = reverseVertical ? height - 1 - y : y;
reversed[targetX, targetY] = original[x, y];
}
}
return reversed;
}
public static float[,] GenerateSplatmap( float[,] heightmap, float[] thresholds, float maxHeight )
{
int width = heightmap.GetLength( 0 );
int height = heightmap.GetLength( 1 );
float[,] splatmap = new float[width, height];
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Normalize height value relative to the maximum height
float normalizedHeight = heightmap[x, y] / maxHeight;
// Interpolate between thresholds for smoother transitions
for ( int i = 0; i < thresholds.Length - 1; i++ )
{
if ( normalizedHeight >= thresholds[i] && normalizedHeight <= thresholds[i + 1] )
{
// Linear interpolation between the two layers
float t = (normalizedHeight - thresholds[i]) / (thresholds[i + 1] - thresholds[i]);
splatmap[x, y] = i + t; // Interpolated layer index
break;
}
}
// Assign the last layer if above the highest threshold
if ( normalizedHeight > thresholds[thresholds.Length - 1] )
{
splatmap[x, y] = thresholds.Length - 1;
}
}
}
return splatmap;
}
public static SKBitmap SplatmapToBitMap( float[,] splatmap, SKColor[] colors )
{
int width = splatmap.GetLength( 0 );
int height = splatmap.GetLength( 1 );
SKBitmap bitmap = new SKBitmap( width, height );
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
// Map the splatmap value to a valid layer index
int layer = (int)Math.Clamp( splatmap[x, y], 0, colors.Length - 1 );
// Assign the corresponding color
SKColor color = colors[layer];
bitmap.SetPixel( x, y, color );
}
}
return bitmap;
}
public static void SaveSplatmapAsPng(
SKBitmap bitmap,
string filename,
float rotationDegrees = 270f,
bool reverseHorizontal = false,
bool reverseVertical = true
)
{
int width = bitmap.Width;
int height = bitmap.Height;
using var transformedBitmap = new SKBitmap( width, height );
using var canvas = new SKCanvas( transformedBitmap );
// Clear the canvas with transparency
canvas.Clear( SKColors.Transparent );
// Apply transformations
canvas.Save();
// Translate to the center of the canvas for rotation and flipping
canvas.Translate( width / 2f, height / 2f );
// Apply flipping first
float scaleX = reverseHorizontal ? -1f : 1f;
float scaleY = reverseVertical ? -1f : 1f;
canvas.Scale( scaleX, scaleY );
// Apply rotation
if ( rotationDegrees != 0 )
{
canvas.RotateDegrees( rotationDegrees );
}
// Translate back to ensure the image is drawn correctly
canvas.Translate( -width / 2f, -height / 2f );
// Draw the original bitmap onto the transformed canvas
canvas.DrawBitmap( bitmap, 0, 0 );
// Restore the canvas to finalize the transformations
canvas.Restore();
// Flush the canvas
canvas.Flush();
// Save the transformed bitmap as a PNG file
using var pixmap = transformedBitmap.PeekPixels();
using var image = SKImage.FromPixels( pixmap );
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
using var stream = File.OpenWrite( filename );
data.SaveTo( stream );
}
}