using System.Collections.Generic; using Godot; using System; using Rokojori.Extensions; namespace Rokojori { public enum TextureStackStampMode { /** columns x rows, one stamp per cell centre. Regular by default; add positionJitter to break it up. */ Grid = 0, /** count stamps at seeded random positions. */ Scatter = 1 } /** Turns one layer into many blits - a grid or a seeded scatter of the same content. This is the anti-repetition tool the blended-ground-texture example needs: a handful of 1k source textures stamped a few dozen times across a 4k output, each stamp at its own position, size and rotation, beats tiling the same square 16 times. Everything is in normalized units relative to the layer's own placement rect, so the same stamp setup composites identically at 512 and at 4096. Stamps are cheap because a blit only touches its own rect (spike gate G6): 32 stamps of 1/8th the output each cost half an output's worth of pixels, not 32 outputs' worth. */ [Tool] [GlobalClass] public partial class TextureStackStamps:Resource { TextureStackStampMode _mode = TextureStackStampMode.Grid; /** Properties rather than fields throughout, because Godot does not emit a scripted Resource's changed signal for a plain exported field, and the consuming texture recomposites off that signal. See TextureStackChangeSource. */ [Export] public TextureStackStampMode mode { get => _mode; set { _mode = value; TextureStackNotify.Changed( this ); } } Vector2 _stampSize = new Vector2( 0.25f, 0.25f ); /** Size of one stamp, as a fraction of the layer's placement rect. */ [Export] public Vector2 stampSize { get => _stampSize; set { _stampSize = value; TextureStackNotify.Changed( this ); } } bool _edgeWrapping = true; /** A stamp that crosses the output's border is blitted again on the opposite side, so the output stays seamlessly tileable. Costs up to three extra blits for the stamps that actually cross - corner stamps cross in both axes. Wrapping is against the output, not the layer's placement rect: tileability is a property of the finished texture. */ [Export] public bool edgeWrapping { get => _edgeWrapping; set { _edgeWrapping = value; TextureStackNotify.Changed( this ); } } [ExportGroup( "Grid" )] int _columns = 4; [Export] public int columns { get => _columns; set { _columns = value; TextureStackNotify.Changed( this ); } } int _rows = 4; [Export] public int rows { get => _rows; set { _rows = value; TextureStackNotify.Changed( this ); } } [ExportGroup( "Scatter" )] int _count = 16; [Export] public int count { get => _count; set { _count = value; TextureStackNotify.Changed( this ); } } int _seed = 0; /** Same seed means the same arrangement every composite - stamps must not move when a texture is recomposited after an unrelated edit. */ [Export] public int seed { get => _seed; set { _seed = value; TextureStackNotify.Changed( this ); } } [ExportGroup( "Variation" )] Vector2 _positionJitter = Vector2.Zero; /** Random offset per stamp, as a fraction of the placement rect. Mostly for Grid, where it breaks up the regularity without losing the coverage. */ [Export] public Vector2 positionJitter { get => _positionJitter; set { _positionJitter = value; TextureStackNotify.Changed( this ); } } float _sizeVariation = 0; /** 0 = every stamp at stampSize. 0.5 = anywhere between half and one and a half times it. Scales the rect, not the content's UV, so the texture inside is not zoomed. */ [Export( PropertyHint.Range, "0,1" )] public float sizeVariation { get => _sizeVariation; set { _sizeVariation = value; TextureStackNotify.Changed( this ); } } float _rotationVariationDegrees = 0; /** Random content rotation per stamp, symmetric around 0, so 360 is fully random. Rotates inside the axis-aligned rect - see the note on TextureStackPlacement.rotation. */ [Export( PropertyHint.Range, "0,360" )] public float rotationVariationDegrees { get => _rotationVariationDegrees; set { _rotationVariationDegrees = value; TextureStackNotify.Changed( this ); } } /** How many stamps before edge wrapping multiplies some of them. */ public int stampCount { get { if ( mode == TextureStackStampMode.Scatter ) { return Mathf.Max( 0, count ); } return Mathf.Max( 0, columns ) * Mathf.Max( 0, rows ); } } /** Appends every blit this stamp set wants, rects unclipped. baseRect is the layer's own placement rect, wrapBounds the whole output. */ public void CollectPlacements( Rect2I baseRect, Rect2I wrapBounds, List into ) { var stamps = stampCount; if ( stamps <= 0 ) { return; } // Seeded, so the arrangement is stable across composites. Jitter, size and // rotation all draw from this one stream, which is why changing any of // them reshuffles the others - deliberate, and cheaper to explain than // three separate streams. var random = LCG.WithSeed( seed ); var baseSize = new Vector2( baseRect.Size.X, baseRect.Size.Y ); var basePosition = new Vector2( baseRect.Position.X, baseRect.Position.Y ); var gridColumns = Mathf.Max( 1, columns ); var gridRows = Mathf.Max( 1, rows ); for ( var i = 0; i < stamps; i++ ) { var center = Vector2.Zero; if ( mode == TextureStackStampMode.Scatter ) { center = new Vector2( random.Next(), random.Next() ); } else { var column = i % gridColumns; var row = i / gridColumns; center = new Vector2( ( column + 0.5f ) / gridColumns, ( row + 0.5f ) / gridRows ); } if ( positionJitter != Vector2.Zero ) { center += new Vector2( random.Polar( positionJitter.X ), random.Polar( positionJitter.Y ) ); } var sizeFactor = 1f; if ( sizeVariation > 0 ) { sizeFactor = 1f + random.Polar( sizeVariation ); } var rotation = 0f; if ( rotationVariationDegrees > 0 ) { rotation = Mathf.DegToRad( random.Polar( rotationVariationDegrees * 0.5f ) ); } var size = stampSize * baseSize * sizeFactor; if ( size.X < 1 || size.Y < 1 ) { continue; } var centerPixels = basePosition + center * baseSize; var topLeft = centerPixels - size * 0.5f; var rect = new Rect2I( Mathf.RoundToInt( topLeft.X ), Mathf.RoundToInt( topLeft.Y ), Mathf.Max( 1, Mathf.RoundToInt( size.X ) ), Mathf.Max( 1, Mathf.RoundToInt( size.Y ) ) ); AddWithWrapping( rect, i, rotation, wrapBounds, into ); } } /** Adds the stamp, plus a copy on the far side for each axis it hangs over. A corner stamp therefore produces four, which together read as one continuous stamp on a tiled surface. */ void AddWithWrapping( Rect2I rect, int index, float rotation, Rect2I wrapBounds, List into ) { into.Add( TextureStackPlacement.At( rect, index, rotation ) ); if ( ! edgeWrapping ) { return; } var shiftX = 0; var shiftY = 0; if ( rect.Position.X < wrapBounds.Position.X ) { shiftX = wrapBounds.Size.X; } else if ( rect.Position.X + rect.Size.X > wrapBounds.Position.X + wrapBounds.Size.X ) { shiftX = -wrapBounds.Size.X; } if ( rect.Position.Y < wrapBounds.Position.Y ) { shiftY = wrapBounds.Size.Y; } else if ( rect.Position.Y + rect.Size.Y > wrapBounds.Position.Y + wrapBounds.Size.Y ) { shiftY = -wrapBounds.Size.Y; } if ( shiftX != 0 ) { into.Add( TextureStackPlacement.At( new Rect2I( rect.Position + new Vector2I( shiftX, 0 ), rect.Size ), index, rotation ) ); } if ( shiftY != 0 ) { into.Add( TextureStackPlacement.At( new Rect2I( rect.Position + new Vector2I( 0, shiftY ), rect.Size ), index, rotation ) ); } if ( shiftX != 0 && shiftY != 0 ) { into.Add( TextureStackPlacement.At( new Rect2I( rect.Position + new Vector2I( shiftX, shiftY ), rect.Size ), index, rotation ) ); } } } }