TerrainBrushes.cs
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.
using System;
using Sandbox;
using Sandbox.Utility;
namespace SubZero.SubTerrain;
static class TerrainBrushes
{
public struct Context
{
public ushort[] Heights;
public ushort[] Base;
public uint[] Control;
public int Res;
public int X0;
public int Y0;
public int X1;
public int Y1;
public float OriginX;
public float OriginY;
public float RadiusTexels;
public float Ca;
public float Sa;
public float StretchX;
public float StretchY;
public float Ox;
public float Oy;
public float Oz;
public float Freq;
public float Irreg;
public int DepthU;
public int MaxDepthU;
public int RaiseU;
public float BowlFrac;
public float SmoothK;
public int DugU;
public bool Paint;
public int PaintIndex;
public float PaintStrength;
public int FlattenTargetU;
public float FlattenStrength;
public int FlattenMaxDeltaU;
public float SpoilAngle;
public bool Painted;
public int Written;
public int VolumeU;
}
public static void ApplyCrater( ref Context ctx )
{
ApplyBowl( ref ctx, flatFloor: false, spoil: false );
}
public static void ApplyDig( ref Context ctx )
{
ApplyBowl( ref ctx, flatFloor: true, spoil: true );
}
public static void ApplyFlatten( ref Context ctx )
{
for ( var y = ctx.Y0; y < ctx.Y1; y++ )
{
for ( var x = ctx.X0; x < ctx.X1; x++ )
{
var dx = (x + 0.5f) - ctx.OriginX;
var dy = (y + 0.5f) - ctx.OriginY;
var t = MathF.Sqrt( dx * dx + dy * dy ) / ctx.RadiusTexels;
if ( t >= 1f )
continue;
ctx.Written++;
var i = x + y * ctx.Res;
var h = (int)ctx.Heights[i];
var falloff = 1f - t;
falloff *= falloff;
var target = Math.Clamp( ctx.FlattenTargetU, h - ctx.FlattenMaxDeltaU, h + ctx.FlattenMaxDeltaU );
var next = (int)MathF.Round( h + (target - h) * ctx.FlattenStrength * falloff );
ctx.VolumeU += h - next;
ctx.Heights[i] = (ushort)Math.Clamp( next, 0, ushort.MaxValue );
if ( !ctx.Paint || falloff < 0.08f || ctx.Control == null )
continue;
Paint( ref ctx, i, falloff );
}
}
}
static void ApplyBowl( ref Context ctx, bool flatFloor, bool spoil )
{
for ( var y = ctx.Y0; y < ctx.Y1; y++ )
{
for ( var x = ctx.X0; x < ctx.X1; x++ )
{
var dx = (x + 0.5f) - ctx.OriginX;
var dy = (y + 0.5f) - ctx.OriginY;
var rx = dx * ctx.Ca + dy * ctx.Sa;
var ry = -dx * ctx.Sa + dy * ctx.Ca;
var dist = MathF.Sqrt( (rx / ctx.StretchX) * (rx / ctx.StretchX) + (ry / ctx.StretchY) * (ry / ctx.StretchY) );
var theta = MathF.Atan2( ry, rx );
var n1 = SignedFbm( 3, MathF.Cos( theta ) * ctx.Freq + ctx.Ox, MathF.Sin( theta ) * ctx.Freq + ctx.Oy );
var n2 = SignedFbm( 2, MathF.Cos( theta ) * ctx.Freq * 2.6f + ctx.Oy, MathF.Sin( theta ) * ctx.Freq * 2.6f + ctx.Oz );
var radiusScale = Math.Clamp( 1f + (n1 * 0.72f + n2 * 0.28f) * ctx.Irreg, 0.5f, 1.65f );
var t = dist / (ctx.RadiusTexels * radiusScale);
if ( t >= 1f )
continue;
ctx.Written++;
var i = x + y * ctx.Res;
var b = (int)ctx.Base[i];
var h = (int)ctx.Heights[i];
var bowlWeight = BowlWeight( t, ctx.BowlFrac, flatFloor );
var slopeN = SignedFbm( 2, x * 0.09f + ctx.Ox, y * 0.09f + ctx.Oy );
var depthMul = 1f + slopeN * ctx.Irreg * 0.12f * Math.Clamp( t / Math.Max( ctx.BowlFrac, 0.01f ), 0f, 1f );
var oldDig = Math.Max( 0, b - h );
var newDig = Math.Max( 0, (int)(bowlWeight * ctx.DepthU * depthMul) );
var mergedDig = SmoothMax( oldDig, newDig, ctx.SmoothK );
h = b - (int)MathF.Round( mergedDig );
h = Math.Max( h, b - ctx.MaxDepthU );
if ( spoil )
{
var spoilBias = MathF.Cos( theta - ctx.SpoilAngle );
if ( t > 0.62f && spoilBias > 0.12f && oldDig < ctx.DugU )
{
var spoilW = (1f - t) * spoilBias;
var rimTarget = b + (int)(spoilW * ctx.RaiseU);
h = Math.Max( h, rimTarget );
}
}
else
{
var rimWeight = RimWeight( t, ctx.BowlFrac );
if ( rimWeight > 0.01f && bowlWeight < 0.25f && oldDig < ctx.DugU )
{
var rimN = SignedFbm( 2, MathF.Cos( theta ) * 1.7f + ctx.Oz, MathF.Sin( theta ) * 1.7f + ctx.Ox );
var rimTarget = b + (int)(rimWeight * ctx.RaiseU * (0.55f + 0.6f * (rimN * 0.5f + 0.5f)));
h = Math.Max( h, rimTarget );
}
}
var next = (ushort)Math.Clamp( h, 0, ushort.MaxValue );
ctx.VolumeU += ctx.Heights[i] - next;
ctx.Heights[i] = next;
if ( !ctx.Paint || bowlWeight < 0.05f || ctx.Control == null )
continue;
Paint( ref ctx, i, bowlWeight );
}
}
}
static void Paint( ref Context ctx, int i, float weight )
{
var control = ctx.Control;
if ( control == null )
return;
var mat = new CompactTerrainMaterial( control[i] );
if ( mat.IsHole )
return;
mat.OverlayTextureId = (byte)ctx.PaintIndex;
var add = (int)(weight * ctx.PaintStrength * 255f);
mat.BlendFactor = (byte)Math.Clamp( mat.BlendFactor + add, 0, 255 );
control[i] = mat.Packed;
ctx.Painted = true;
}
static float BowlWeight( float t, float bowlFrac, bool flatFloor )
{
if ( flatFloor )
{
if ( t <= 0.5f )
return 1f;
var u = Math.Clamp( (t - 0.5f) / 0.5f, 0f, 1f );
var s = 1f - u;
return s * s;
}
var bowlT = Math.Clamp( t / bowlFrac, 0f, 1f );
var s2 = 1f - bowlT * bowlT;
return s2 * s2;
}
public static float SmoothMax( float a, float b, float k )
{
if ( k <= 0.001f )
return MathF.Max( a, b );
var h = Math.Clamp( 0.5f + 0.5f * (a - b) / k, 0f, 1f );
return b + (a - b) * h + k * h * (1f - h);
}
public static float SignedFbm( int octaves, float x, float y )
{
return Math.Clamp( Noise.Fbm( octaves, x, y ) * 2f - 1f, -1f, 1f );
}
public static float RimWeight( float t, float bowlFrac )
{
if ( t < bowlFrac * 0.85f || t >= 1f )
return 0f;
var u = (t - bowlFrac * 0.85f) / (1f - bowlFrac * 0.85f);
return MathF.Sin( u * MathF.PI );
}
}