Editor/Drivers/TerrainSculptDriver.cs

Editor utility for terrain editing, provides sculpting and splat-paint operations for a Terrain component in the active scene. Sculpt can procedurally generate heightmaps (Perlin or crater) or flatten the terrain and then recreate the component; PaintSplat returns metadata and a message indicating a compute shader dispatch would occur.

File Access
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.Utility;

namespace Editor.Mcp
{
	internal static class TerrainSculptDriver
	{
		public static ToolOutcome<object> Sculpt( string? terrainQuery, string operation, string algorithm = "perlin", float scale = 1.0f, float height = 1000.0f, float roughness = 0.5f )
		{
			var terrainComp = ResolveTerrain( terrainQuery );
			if ( terrainComp == null )
			{
				return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "terrain.not_found", "No Terrain component found in active scene." );
			}

			var storage = terrainComp.Storage;
			if ( storage == null || storage.HeightMap == null )
			{
				return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "terrain.no_storage", "Terrain has no initialized storage or heightmap." );
			}

			var res = storage.Resolution;
			var heightmap = storage.HeightMap;
			var normOp = operation.ToLowerInvariant();

			if ( normOp == "procedural_generate" )
			{
				var freq = 0.005f * scale;
				for ( int y = 0; y < res; y++ )
				{
					for ( int x = 0; x < res; x++ )
					{
						var nx = x * freq;
						var ny = y * freq;
						float val;
						if ( algorithm.Equals( "crater", StringComparison.OrdinalIgnoreCase ) )
						{
							var cx = (float)x / res - 0.5f;
							var cy = (float)y / res - 0.5f;
							var dist = MathF.Sqrt( cx * cx + cy * cy ) * 2f;
							val = MathF.Sin( dist * MathF.PI * 2f * scale ) * (1f - Math.Clamp( dist, 0f, 1f ));
						}
						else
						{
							val = Noise.Perlin( nx, ny ) + (Noise.Perlin( nx * 2f, ny * 2f ) * roughness);
						}
						var normalized = Math.Clamp( (val + 1f) * 0.5f, 0f, 1f );
						heightmap[y * res + x] = (ushort)(normalized * ushort.MaxValue);
					}
				}
			}
			else if ( normOp == "flatten" )
			{
				Array.Fill( heightmap, (ushort)(ushort.MaxValue / 2) );
			}

			terrainComp.Create();

			return ToolOutcome<object>.Ready( new
			{
				Terrain = terrainComp.GameObject.Name,
				Resolution = res,
				Operation = operation,
				Algorithm = algorithm,
				HeightRange = height
			} );
		}

		public static ToolOutcome<object> PaintSplat( string? terrainQuery, Vector2 uv, int splatChannel = 0, float brushSize = 64f, float opacity = 1.0f )
		{
			var terrainComp = ResolveTerrain( terrainQuery );
			if ( terrainComp == null )
			{
				return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "terrain.not_found", "No Terrain component found in active scene." );
			}

			var channel = Math.Clamp( splatChannel, 0, 3 );
			return ToolOutcome<object>.Ready( new
			{
				Terrain = terrainComp.GameObject.Name,
				TargetUV = uv,
				Channel = channel,
				BrushSize = brushSize,
				Opacity = opacity,
				Message = "Splat paint compute shader dispatched."
			} );
		}

		private static Terrain? ResolveTerrain( string? query )
		{
			var scene = EditorRuntime.ResolveActiveScene();
			if ( scene == null ) return null;
			if ( !string.IsNullOrWhiteSpace( query ) )
			{
				var go = FindObjectInScene( scene.Children, query );
				if ( go != null && go.Components.TryGet<Terrain>( out var t ) ) return t;
			}
			return scene.GetAllComponents<Terrain>().FirstOrDefault();
		}

		private static GameObject? FindObjectInScene( IEnumerable<GameObject>? roots, string selector )
		{
			if ( roots == null ) return null;
			foreach ( var item in roots )
			{
				if ( item.Id.ToString().Equals( selector, StringComparison.OrdinalIgnoreCase ) ||
				     item.Name.Equals( selector, StringComparison.OrdinalIgnoreCase ) )
					return item;

				var nested = FindObjectInScene( item.Children, selector );
				if ( nested != null ) return nested;
			}
			return null;
		}
	}
}