324 lines
8.8 KiB
C#
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
|
||
|
|
) );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|