Editor tool that flattens a circular pad into the heightmap of a Terrain under the cursor. Draws a radius ring gizmo and applies one undoable height blend per click, with radius, falloff and strength settings.
namespace AdvancedMappingTools;
/// <summary>
/// Flattens a round pad into the terrain under the cursor. One click per pad, undo per pad.
/// </summary>
[ToolCategory( "Terrain" )]
[Title( "Terrain Pad" ), Icon( "crop_square" )]
public sealed class TerrainPadEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
protected override int MinPoints => int.MaxValue;
protected override IEnumerable<string> Hints => ["Click the terrain: flatten a pad"];
[Property, Group( "Pad" ), Title( "Radius" ), Range( 8, 4096, slider: false ), WideMode]
public float Radius { get; set; } = 128.0f;
[Property, Group( "Pad" ), Title( "Falloff" ), Range( 0, 1 ), WideMode]
public float Falloff { get; set; } = 0.5f;
[Property, Group( "Pad" ), Range( 0.05f, 1 ), WideMode]
public float Strength { get; set; } = 1.0f;
protected override void BuildMesh( MeshKit kit ) { }
public override void OnUpdate( SceneTrace trace )
{
var tr = trace.Run();
if ( !tr.Hit || tr.GameObject is null ) return;
var terrain = tr.GameObject.GetComponent<Terrain>();
if ( terrain is null || terrain.Storage?.HeightMap is null ) return;
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.Color = Color.Yellow;
for ( var i = 0; i < 32; i++ )
{
var a = i * MathF.PI * 2.0f / 32.0f;
var b = (i + 1) * MathF.PI * 2.0f / 32.0f;
var p = tr.EndPosition;
Gizmo.Draw.Line( p + new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0.0f ) * Radius, p + new Vector3( MathF.Cos( b ), MathF.Sin( b ), 0.0f ) * Radius );
}
if ( !Gizmo.WasLeftMousePressed || Gizmo.HasHovered ) return;
Apply( terrain, tr.EndPosition );
}
void Apply( Terrain terrain, Vector3 point )
{
var session = SceneEditorSession.Active;
if ( session is null ) return;
var storage = terrain.Storage;
var size = storage.Resolution;
var cell = storage.TerrainSize / (size - 1.0f);
var scale = storage.TerrainHeight / (float)ushort.MaxValue;
var world = terrain.WorldTransform;
var center = world.PointToLocal( point );
var before = storage.HeightMap.ToArray();
for ( var y = 0; y < size; y++ )
{
for ( var x = 0; x < size; x++ )
{
var flat = new Vector3( x * cell, y * cell, 0.0f );
var distance = flat.WithZ( 0.0f ).Distance( center.WithZ( 0.0f ) );
if ( distance > Radius ) continue;
var weight = Weight( distance / MathF.Max( Radius, 0.01f ) ) * Strength;
if ( weight <= 0.0001f ) continue;
var index = x + y * size;
var height = storage.HeightMap[index] * scale;
var next = height + (center.z - height) * weight;
storage.HeightMap[index] = (ushort)Math.Clamp( MathF.Round( next / MathF.Max( scale, 0.0001f ) ), 0.0f, ushort.MaxValue );
}
}
var after = storage.HeightMap.ToArray();
var region = new RectInt( new Vector2Int( 0, 0 ), new Vector2Int( size, size ) );
Refresh( terrain, region );
session.AddUndo( "Terrain Pad", () => Restore( terrain, before, region ), () => Restore( terrain, after, region ) );
}
static void Restore( Terrain terrain, ushort[] heights, RectInt region )
{
heights.CopyTo( terrain.Storage.HeightMap, 0 );
Refresh( terrain, region );
}
static void Refresh( Terrain terrain, RectInt region )
{
terrain.Storage.StateHasChanged();
terrain.ApplyStorageChanges( (Terrain.SyncFlags)(-1), region );
}
float Weight( float t )
{
t = Math.Clamp( t, 0.0f, 1.0f );
if ( Falloff <= 0.001f ) return 1.0f;
var edge = Math.Clamp( (1.0f - t) / Falloff, 0.0f, 1.0f );
return edge * edge * (3.0f - 2.0f * edge);
}
}