Editor/SeamlessImageProcessor.cs
using System;
using Sandbox;
public static class SeamlessImageProcessor
{
public static Bitmap Process( Bitmap source, SeamlessToolSettings settings )
{
if ( source == null || !source.IsValid )
return null;
settings ??= new SeamlessToolSettings();
var width = source.Width;
var height = source.Height;
if ( width <= 0 || height <= 0 )
return null;
var sourcePixels = source.GetPixels();
var offsetBlendPixels = CreateOffsetBlendPixels( sourcePixels, width, height, settings );
var outputPixels = settings.ProcessingMode switch
{
SeamlessProcessingMode.IrregularPatch => ProcessIrregularPatch( sourcePixels, offsetBlendPixels, width, height, settings ),
SeamlessProcessingMode.TextureBombing => ProcessTextureBombing( sourcePixels, offsetBlendPixels, width, height, settings ),
SeamlessProcessingMode.BrickAndTile => ProcessBrickAndTile( sourcePixels, width, height, settings ),
_ => offsetBlendPixels
};
var result = new Bitmap( width, height, source.IsFloatingPoint );
result.SetPixels( outputPixels );
if ( settings.BlurRadius > 0.01f )
{
result.Blur( settings.BlurRadius, true );
}
if ( settings.SharpenAmount > 0.01f )
{
result.Sharpen( settings.SharpenAmount, true );
}
return result;
}
private static Color[] CreateOffsetBlendPixels( Color[] sourcePixels, int width, int height, SeamlessToolSettings settings )
{
var halfWidth = width / 2;
var halfHeight = height / 2;
var pixels = CreateOffsetPixels( sourcePixels, width, height, halfWidth, halfHeight );
BlendVerticalSeam( pixels, width, height, settings );
BlendHorizontalSeam( pixels, width, height, settings );
return pixels;
}
private static Color[] CreateOffsetPixels( Color[] sourcePixels, int width, int height, int offsetX, int offsetY )
{
var pixels = new Color[sourcePixels.Length];
for ( var y = 0; y < height; y++ )
{
var sourceY = WrapIndex( y + offsetY, height );
for ( var x = 0; x < width; x++ )
{
var sourceX = WrapIndex( x + offsetX, width );
pixels[y * width + x] = sourcePixels[sourceY * width + sourceX];
}
}
return pixels;
}
private static Color[] ProcessBrickAndTile( Color[] sourcePixels, int width, int height, SeamlessToolSettings settings )
{
var offsetX = GetCellAlignedOffset( width, settings.BrickTileCellWidth );
var offsetY = GetCellAlignedOffset( height, settings.BrickTileCellHeight );
var pixels = CreateOffsetPixels( sourcePixels, width, height, offsetX, offsetY );
var seamX = GetSeamCenterFromOffset( width, offsetX );
var seamY = GetSeamCenterFromOffset( height, offsetY );
BlendBrickTileVerticalSeam( pixels, width, height, seamX, settings );
BlendBrickTileHorizontalSeam( pixels, width, height, seamY, settings );
return pixels;
}
private static Color[] ProcessIrregularPatch( Color[] sourcePixels, Color[] basePixels, int width, int height, SeamlessToolSettings settings )
{
var result = (Color[])basePixels.Clone();
var gridX = GetPatchGridX( width, height, settings.PatchCount );
var gridY = GetPatchGridY( gridX, settings.PatchCount );
var strength = Clamp01( settings.VariationStrength * GetVariationMultiplier( settings.VariationPreset ) );
for ( var y = 0; y < height; y++ )
{
var gridYPosition = (y + 0.5f) / Math.Max( 1, height ) * gridY;
var cellY = WrapIndex( (int)MathF.Floor( gridYPosition ), gridY );
var localY = gridYPosition - MathF.Floor( gridYPosition );
var blendY = SmoothFade( localY );
for ( var x = 0; x < width; x++ )
{
var gridXPosition = (x + 0.5f) / Math.Max( 1, width ) * gridX;
var cellX = WrapIndex( (int)MathF.Floor( gridXPosition ), gridX );
var localX = gridXPosition - MathF.Floor( gridXPosition );
var blendX = SmoothFade( localX );
var c00 = SampleWangCell( sourcePixels, width, height, x, y, cellX, cellY, gridX, gridY, settings );
var c10 = SampleWangCell( sourcePixels, width, height, x, y, cellX + 1, cellY, gridX, gridY, settings );
var c01 = SampleWangCell( sourcePixels, width, height, x, y, cellX, cellY + 1, gridX, gridY, settings );
var c11 = SampleWangCell( sourcePixels, width, height, x, y, cellX + 1, cellY + 1, gridX, gridY, settings );
var top = Color.Lerp( c00, c10, blendX, true );
var bottom = Color.Lerp( c01, c11, blendX, true );
var wangSample = Color.Lerp( top, bottom, blendY, true );
var mask = GetIrregularPatchMask( localX, localY, x, y, width, height, settings );
var amount = Clamp01( strength * mask );
var index = y * width + x;
if ( amount <= 0.001f )
continue;
result[index] = Color.Lerp( basePixels[index], wangSample, amount, true );
}
}
return result;
}
private static Color[] ProcessTextureBombing( Color[] sourcePixels, Color[] basePixels, int width, int height, SeamlessToolSettings settings )
{
var result = (Color[])basePixels.Clone();
var pixelCount = result.Length;
var red = new float[pixelCount];
var green = new float[pixelCount];
var blue = new float[pixelCount];
var alpha = new float[pixelCount];
var weights = new float[pixelCount];
var random = new Random( settings.RandomSeed );
var patchRadius = Math.Clamp( settings.PatchSize * 0.5f, 4f, Math.Max( width, height ) );
var density = Math.Clamp( settings.TextureBombDensity, 0.1f, 4f );
var bombCount = Math.Clamp( (int)MathF.Ceiling( settings.PatchCount * density ), 1, 2048 );
var strength = Clamp01( settings.VariationStrength * GetVariationMultiplier( settings.VariationPreset ) );
var allowTransforms = settings.VariationPreset != SeamlessVariationPreset.Gentle;
for ( var i = 0; i < bombCount; i++ )
{
GetTextureBombCenter( random, width, height, settings.SeamGravity, out var centerX, out var centerY );
var sampleCenterX = random.Next( width );
var sampleCenterY = random.Next( height );
var radius = patchRadius * Lerp( 0.65f, 1.35f, (float)random.NextDouble() );
var radiusInt = Math.Max( 2, (int)MathF.Ceiling( radius ) );
var rotationSteps = allowTransforms ? random.Next( 4 ) : 0;
var flipX = allowTransforms && random.NextDouble() > 0.5;
var flipY = settings.VariationPreset == SeamlessVariationPreset.Strong && random.NextDouble() > 0.5;
var noiseSeed = random.Next();
for ( var localY = -radiusInt; localY <= radiusInt; localY++ )
{
for ( var localX = -radiusInt; localX <= radiusInt; localX++ )
{
var distance = GetPatchShapeDistance( localX, localY, radius, settings.PatchShape );
if ( distance >= 1f )
continue;
var destX = WrapIndex( centerX + localX, width );
var destY = WrapIndex( centerY + localY, height );
var mask = GetSoftPatchMask( 1f - distance, settings.MaskSoftness );
var noise = Hash01( destX, destY, noiseSeed );
mask *= Lerp( 0.7f, 1f, noise );
mask = ApplySeamOverlayPressure( mask, destX, destY, width, height, settings );
if ( mask <= 0.001f )
continue;
GetTransformedRelativePosition( localX, localY, rotationSteps, flipX, flipY, out var sampleXOffset, out var sampleYOffset );
var sourceX = WrapIndex( sampleCenterX + sampleXOffset, width );
var sourceY = WrapIndex( sampleCenterY + sampleYOffset, height );
var color = sourcePixels[sourceY * width + sourceX];
var index = destY * width + destX;
red[index] += color.r * mask;
green[index] += color.g * mask;
blue[index] += color.b * mask;
alpha[index] += color.a * mask;
weights[index] += mask;
}
}
}
for ( var i = 0; i < result.Length; i++ )
{
var weight = weights[i];
if ( weight <= 0.001f )
continue;
var bombed = new Color( red[i] / weight, green[i] / weight, blue[i] / weight, alpha[i] / weight );
var amount = Clamp01( strength * weight / (weight + 1f) );
result[i] = Color.Lerp( result[i], bombed, amount, true );
}
return result;
}
private static void BlendVerticalSeam( Color[] pixels, int width, int height, SeamlessToolSettings settings )
{
if ( width < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
return;
var source = (Color[])pixels.Clone();
var seamCenter = width / 2;
var maxDistance = MathF.Min( settings.SeamWidth, width * 0.5f );
var startX = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
var endX = Math.Min( width - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );
for ( var y = 0; y < height; y++ )
{
for ( var x = startX; x <= endX; x++ )
{
var distance = MathF.Abs( (x + 0.5f) - seamCenter );
var baseWeight = 1f - Clamp01( distance / maxDistance );
var weight = GetSeamWeight( baseWeight, x, y, width, height, settings );
var amount = Clamp01( weight * settings.BlendStrength ) * 0.5f;
if ( amount <= 0f )
continue;
var mirrorX = MirrorAcrossCenterSeam( x, seamCenter, width );
var index = y * width + x;
var mirrorIndex = y * width + mirrorX;
pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
}
}
}
private static void BlendHorizontalSeam( Color[] pixels, int width, int height, SeamlessToolSettings settings )
{
if ( height < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
return;
var source = (Color[])pixels.Clone();
var seamCenter = height / 2;
var maxDistance = MathF.Min( settings.SeamWidth, height * 0.5f );
var startY = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
var endY = Math.Min( height - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );
for ( var y = startY; y <= endY; y++ )
{
var distance = MathF.Abs( (y + 0.5f) - seamCenter );
var baseWeight = 1f - Clamp01( distance / maxDistance );
for ( var x = 0; x < width; x++ )
{
var weight = GetSeamWeight( baseWeight, x, y, width, height, settings );
var amount = Clamp01( weight * settings.BlendStrength ) * 0.5f;
if ( amount <= 0f )
continue;
var mirrorY = MirrorAcrossCenterSeam( y, seamCenter, height );
var index = y * width + x;
var mirrorIndex = mirrorY * width + x;
pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
}
}
}
private static void BlendBrickTileVerticalSeam( Color[] pixels, int width, int height, int seamCenter, SeamlessToolSettings settings )
{
if ( width < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
return;
var source = (Color[])pixels.Clone();
var maxDistance = MathF.Min( settings.SeamWidth, width * 0.5f );
var startX = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
var endX = Math.Min( width - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );
for ( var y = 0; y < height; y++ )
{
for ( var x = startX; x <= endX; x++ )
{
var distance = MathF.Abs( (x + 0.5f) - seamCenter );
var baseWeight = 1f - Clamp01( distance / maxDistance );
var amount = GetBrickTileSeamAmount( baseWeight, x, y, width, height, settings );
if ( amount <= 0f )
continue;
var mirrorX = MirrorAcrossCenterSeam( x, seamCenter, width );
var index = y * width + x;
var mirrorIndex = y * width + mirrorX;
pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
}
}
}
private static void BlendBrickTileHorizontalSeam( Color[] pixels, int width, int height, int seamCenter, SeamlessToolSettings settings )
{
if ( height < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
return;
var source = (Color[])pixels.Clone();
var maxDistance = MathF.Min( settings.SeamWidth, height * 0.5f );
var startY = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
var endY = Math.Min( height - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );
for ( var y = startY; y <= endY; y++ )
{
var distance = MathF.Abs( (y + 0.5f) - seamCenter );
var baseWeight = 1f - Clamp01( distance / maxDistance );
for ( var x = 0; x < width; x++ )
{
var amount = GetBrickTileSeamAmount( baseWeight, x, y, width, height, settings );
if ( amount <= 0f )
continue;
var mirrorY = MirrorAcrossCenterSeam( y, seamCenter, height );
var index = y * width + x;
var mirrorIndex = mirrorY * width + x;
pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
}
}
}
private static Color SampleWangCell( Color[] sourcePixels, int width, int height, int x, int y, int cellX, int cellY, int gridX, int gridY, SeamlessToolSettings settings )
{
var hash = GetWangCellHash( cellX, cellY, gridX, gridY, settings.RandomSeed );
var offsetX = (int)MathF.Round( Hash01( hash, 17, settings.RandomSeed ) * width );
var offsetY = (int)MathF.Round( Hash01( hash, 31, settings.RandomSeed ) * height );
var sourceX = WrapIndex( x + offsetX, width );
var sourceY = WrapIndex( y + offsetY, height );
return sourcePixels[sourceY * width + sourceX];
}
private static int GetWangCellHash( int cellX, int cellY, int gridX, int gridY, int seed )
{
cellX = WrapIndex( cellX, gridX );
cellY = WrapIndex( cellY, gridY );
var left = GetVerticalEdgeColor( cellX, cellY, gridX, gridY, seed );
var right = GetVerticalEdgeColor( cellX + 1, cellY, gridX, gridY, seed );
var top = GetHorizontalEdgeColor( cellX, cellY, gridX, gridY, seed );
var bottom = GetHorizontalEdgeColor( cellX, cellY + 1, gridX, gridY, seed );
return HashInt( left, right, top, bottom, seed );
}
private static int GetVerticalEdgeColor( int edgeX, int cellY, int gridX, int gridY, int seed )
{
return Math.Min( 3, (int)MathF.Floor( Hash01( WrapIndex( edgeX, gridX ), WrapIndex( cellY, gridY ), seed + 101 ) * 4f ) );
}
private static int GetHorizontalEdgeColor( int cellX, int edgeY, int gridX, int gridY, int seed )
{
return Math.Min( 3, (int)MathF.Floor( Hash01( WrapIndex( cellX, gridX ), WrapIndex( edgeY, gridY ), seed + 211 ) * 4f ) );
}
private static float GetIrregularPatchMask( float localX, float localY, int x, int y, int width, int height, SeamlessToolSettings settings )
{
var dx = MathF.Abs( localX - 0.5f ) * 2f;
var dy = MathF.Abs( localY - 0.5f ) * 2f;
var centerWeight = 1f - Clamp01( GetPatchShapeDistance( dx, dy, 1f, settings.PatchShape ) );
var mask = GetSoftPatchMask( centerWeight, settings.MaskSoftness );
var noiseScale = Math.Max( 4f, settings.PatchSize * 0.25f );
var noiseX = (int)MathF.Floor( x / noiseScale );
var noiseY = (int)MathF.Floor( y / noiseScale );
var noise = Hash01( noiseX, noiseY, settings.RandomSeed + 773 );
var baseMask = Clamp01( mask * Lerp( 0.65f, 1f, noise ) + 0.25f );
var gravity = Clamp01( settings.SeamGravity );
if ( gravity <= 0f )
return baseMask;
return ApplySeamOverlayPressure( baseMask, x, y, width, height, settings );
}
private static float ApplySeamOverlayPressure( float value, int x, int y, int width, int height, SeamlessToolSettings settings )
{
var gravity = Clamp01( settings.SeamGravity );
if ( gravity <= 0f )
return value;
var seamProximity = GetCenterSeamProximity( x, y, width, height, settings );
var offSeamFalloff = Lerp( 1f, seamProximity, gravity );
var seamBoost = Lerp( 1f, 2.5f, gravity * seamProximity );
return value * offSeamFalloff * seamBoost;
}
private static void GetTextureBombCenter( Random random, int width, int height, float seamGravity, out int centerX, out int centerY )
{
var gravity = Clamp01( seamGravity );
if ( gravity <= 0f || (float)random.NextDouble() > gravity )
{
centerX = random.Next( width );
centerY = random.Next( height );
return;
}
var useVerticalSeam = random.NextDouble() < 0.5;
if ( useVerticalSeam )
{
centerX = GetSeamBiasedAxisPosition( random, width, gravity );
centerY = random.Next( height );
return;
}
centerX = random.Next( width );
centerY = GetSeamBiasedAxisPosition( random, height, gravity );
}
private static int GetSeamBiasedAxisPosition( Random random, int size, float gravity )
{
if ( size <= 1 )
return 0;
var seamCenter = size * 0.5f;
var maxDistance = size * 0.5f;
var falloffPower = Lerp( 1f, 4f, gravity );
var distance = maxDistance * MathF.Pow( (float)random.NextDouble(), falloffPower );
var direction = random.NextDouble() < 0.5 ? -1f : 1f;
var position = (int)MathF.Floor( seamCenter + distance * direction );
return WrapIndex( position, size );
}
private static float GetPatchShapeDistance( float x, float y, float radius, SeamlessPatchShape shape )
{
var safeRadius = Math.Max( 0.001f, radius );
var normalizedX = MathF.Abs( x ) / safeRadius;
var normalizedY = MathF.Abs( y ) / safeRadius;
return shape switch
{
SeamlessPatchShape.Square => MathF.Max( normalizedX, normalizedY ),
SeamlessPatchShape.Diamond => normalizedX + normalizedY,
SeamlessPatchShape.Hexagon => GetHexagonDistance( normalizedX, normalizedY ),
_ => MathF.Sqrt( normalizedX * normalizedX + normalizedY * normalizedY )
};
}
private static float GetHexagonDistance( float normalizedX, float normalizedY )
{
var flatSideDistance = normalizedX;
var angledSideDistance = normalizedX * 0.5f + normalizedY * 0.8660254f;
return MathF.Max( flatSideDistance, angledSideDistance );
}
private static float GetCenterSeamProximity( int x, int y, int width, int height, SeamlessToolSettings settings )
{
var verticalDistance = MathF.Abs( (x + 0.5f) - width * 0.5f );
var horizontalDistance = MathF.Abs( (y + 0.5f) - height * 0.5f );
var distanceToNearestSeam = MathF.Min( verticalDistance, horizontalDistance );
var spread = MathF.Max( settings.SeamWidth, settings.PatchSize ) * 1.5f;
spread = Math.Clamp( spread, 1f, MathF.Max( 1f, MathF.Min( width, height ) * 0.5f ) );
return SmoothFade( 1f - Clamp01( distanceToNearestSeam / spread ) );
}
private static float GetSoftPatchMask( float value, float softness )
{
value = Clamp01( value );
softness = Math.Clamp( softness, 0.05f, 1f );
var shaped = Clamp01( value / softness );
return SmoothFade( shaped );
}
private static int GetCellAlignedOffset( int size, float cellSize )
{
if ( size <= 1 )
return 0;
if ( cellSize <= 1f )
return size / 2;
var aligned = (int)MathF.Round( (size * 0.5f) / cellSize ) * (int)MathF.Round( cellSize );
if ( aligned <= 0 )
aligned = size / 2;
var wrapped = WrapIndex( aligned, size );
return wrapped == 0 ? size / 2 : wrapped;
}
private static int GetSeamCenterFromOffset( int size, int offset )
{
var wrappedOffset = WrapIndex( offset, size );
if ( wrappedOffset <= 0 )
return size / 2;
var seamCenter = size - wrappedOffset;
if ( seamCenter <= 0 || seamCenter >= size )
return size / 2;
return seamCenter;
}
private static float GetBrickTileSeamAmount( float baseWeight, int x, int y, int width, int height, SeamlessToolSettings settings )
{
var normalAmount = Clamp01( GetSeamWeight( baseWeight, x, y, width, height, settings ) * settings.BlendStrength ) * 0.5f;
var structureMask = GetBrickTileStructureMask( x, y, settings );
var structureStrength = Clamp01( settings.BrickTileStructureStrength );
var structuredAmount = normalAmount * Lerp( 0.35f, 1.2f, structureMask );
return Clamp01( Lerp( normalAmount, structuredAmount, structureStrength ) );
}
private static float GetBrickTileStructureMask( int x, int y, SeamlessToolSettings settings )
{
var cellWidth = Math.Max( 4f, settings.BrickTileCellWidth );
var cellHeight = Math.Max( 4f, settings.BrickTileCellHeight );
var groutWidth = Math.Clamp( settings.BrickTileGroutWidth, 0f, MathF.Min( cellWidth, cellHeight ) * 0.5f );
var row = (int)MathF.Floor( y / cellHeight );
var rowOffset = GetBrickTileRowOffset( row, cellWidth, settings );
var xDistance = GetDistanceToRepeatLine( x + rowOffset, cellWidth );
var yDistance = GetDistanceToRepeatLine( y, cellHeight );
var xMask = GetBrickTileLineMask( xDistance, groutWidth );
var yMask = GetBrickTileLineMask( yDistance, groutWidth );
return settings.BrickTilePattern switch
{
SeamlessBrickTilePattern.Planks => Math.Max( yMask, xMask * 0.65f ),
SeamlessBrickTilePattern.Panels => Math.Max( xMask, yMask ),
_ => Math.Max( xMask, yMask )
};
}
private static float GetBrickTileRowOffset( int row, float cellWidth, SeamlessToolSettings settings )
{
var offset = Math.Clamp( settings.BrickTileRowOffset, 0f, 1f ) * cellWidth;
return settings.BrickTilePattern switch
{
SeamlessBrickTilePattern.RunningBrick => row % 2 == 0 ? 0f : offset,
SeamlessBrickTilePattern.Planks => row % 3 * offset,
SeamlessBrickTilePattern.Panels => row % 2 == 0 ? 0f : offset * 0.5f,
_ => 0f
};
}
private static float GetDistanceToRepeatLine( float value, float period )
{
if ( period <= 0.001f )
return 0f;
var wrapped = value - MathF.Floor( value / period ) * period;
return MathF.Min( wrapped, period - wrapped );
}
private static float GetBrickTileLineMask( float distance, float groutWidth )
{
var halfWidth = Math.Max( 0.5f, groutWidth * 0.5f );
var softness = Math.Max( 1f, halfWidth * 0.5f );
return SmoothFade( 1f - Clamp01( (distance - halfWidth) / softness ) );
}
private static float GetSeamWeight( float baseWeight, int x, int y, int width, int height, SeamlessToolSettings settings )
{
baseWeight = Clamp01( baseWeight );
return settings.SeamShape switch
{
SeamlessSeamShape.Linear => baseWeight,
SeamlessSeamShape.Wavy => Clamp01( SmoothFade( baseWeight ) * (0.75f + 0.25f * MathF.Sin( y * 0.08f + settings.RandomSeed * 0.001f )) ),
SeamlessSeamShape.Noise => Clamp01( SmoothFade( baseWeight ) * Lerp( 0.55f, 1.15f, Hash01( x / 8, y / 8, settings.RandomSeed ) ) ),
SeamlessSeamShape.Irregular => GetIrregularSeamWeight( baseWeight, x, y, width, height, settings.RandomSeed ),
_ => SmoothFade( baseWeight )
};
}
private static float GetIrregularSeamWeight( float baseWeight, int x, int y, int width, int height, int seed )
{
var wave = 0.5f + 0.5f * MathF.Sin( (x + y) * 0.045f + seed * 0.002f );
var noise = Hash01( x / 12, y / 12, seed + 19 );
var shifted = baseWeight + (noise - 0.5f) * 0.28f + (wave - 0.5f) * 0.18f;
return SmoothFade( Clamp01( shifted ) );
}
private static void GetTransformedRelativePosition( int x, int y, int rotationSteps, bool flipX, bool flipY, out int resultX, out int resultY )
{
if ( flipX )
x = -x;
if ( flipY )
y = -y;
rotationSteps = WrapIndex( rotationSteps, 4 );
switch ( rotationSteps )
{
case 1:
resultX = -y;
resultY = x;
return;
case 2:
resultX = -x;
resultY = -y;
return;
case 3:
resultX = y;
resultY = -x;
return;
default:
resultX = x;
resultY = y;
return;
}
}
private static int GetPatchGridX( int width, int height, int patchCount )
{
patchCount = Math.Clamp( patchCount, 2, 64 );
var aspect = Math.Max( 0.25f, width / (float)Math.Max( 1, height ) );
return Math.Clamp( (int)MathF.Round( MathF.Sqrt( patchCount * aspect ) ), 2, 16 );
}
private static int GetPatchGridY( int gridX, int patchCount )
{
patchCount = Math.Clamp( patchCount, 2, 64 );
return Math.Clamp( (int)MathF.Ceiling( patchCount / (float)Math.Max( 1, gridX ) ), 2, 16 );
}
private static float GetVariationMultiplier( SeamlessVariationPreset preset )
{
return preset switch
{
SeamlessVariationPreset.Gentle => 0.55f,
SeamlessVariationPreset.Strong => 1.15f,
_ => 0.85f
};
}
private static int MirrorAcrossCenterSeam( int value, int seamCenter, int size )
{
if ( value < seamCenter )
return Math.Clamp( seamCenter + (seamCenter - 1 - value), 0, size - 1 );
return Math.Clamp( seamCenter - 1 - (value - seamCenter), 0, size - 1 );
}
private static int WrapIndex( int value, int size )
{
if ( size <= 0 )
return 0;
var wrapped = value % size;
return wrapped < 0 ? wrapped + size : wrapped;
}
private static float SmoothFade( float value )
{
value = Clamp01( value );
return value * value * (3f - 2f * value);
}
private static float Lerp( float a, float b, float amount )
{
return a + (b - a) * Clamp01( amount );
}
private static float Clamp01( float value )
{
if ( value < 0f )
return 0f;
if ( value > 1f )
return 1f;
return value;
}
private static float Hash01( int a, int b, int c )
{
var hash = HashInt( a, b, c, 374761393, 668265263 );
return (hash & 0x00FFFFFF) / 16777215f;
}
private static int HashInt( int a, int b, int c, int d, int e )
{
unchecked
{
var hash = 2166136261u;
hash = (hash ^ (uint)a) * 16777619u;
hash = (hash ^ (uint)b) * 16777619u;
hash = (hash ^ (uint)c) * 16777619u;
hash = (hash ^ (uint)d) * 16777619u;
hash = (hash ^ (uint)e) * 16777619u;
hash ^= hash >> 13;
hash *= 1274126177u;
hash ^= hash >> 16;
return (int)(hash & 0x7FFFFFFF);
}
}
}