rokojori_action_library/Runtime/Procedural/Textures/TextureStack/TextureStackStamps.cs

324 lines
8.8 KiB
C#

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 <marked>output</marked>, 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<TextureStackPlacement> 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<TextureStackPlacement> 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
) );
}
}
}
}