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

330 lines
9.8 KiB
C#

using System.Collections.Generic;
using Godot;
using System;
using Rokojori.Extensions;
namespace Rokojori
{
/** How a layer's placement rect is expressed. */
public enum TextureStackPlacementUnits
{
/** 0-1 of the output size, so the same layer composites correctly at 256
for a preview and at 4096 for the final. This is the resolution-
independent default.
*/
Normalized = 0,
/** Absolute pixels of the output. Only correct for one output size. */
Pixels = 1
}
/** Base class for everything a TextureStack can draw.
A layer does two separate things, and the split is deliberate (see the
"two-pass via scratch buffer" decision in the design doc):
1. Draw() renders the layer's own *content* into context.scratch,
inside context.layerRect, with blending disabled. It knows nothing
about blend modes, opacity or masks.
2. The compositor then runs one generic composite pass that applies
blendMode, opacity, the masks and clipToBelow.
That is why subclasses only ever implement Draw() plus, if they place
themselves, ComputeBaseRect() or CollectPlacements().
Mask count is capped because a blit takes exactly 4 sources (spike gate
G4) and the composite pass already spends two of them on the layer
content and the accumulation.
<marked>Every exported value here is a property that emits changed</marked>,
not a plain field. Godot does not emit the signal for a scripted
Resource's exported field, and the consuming TextureStackTexture2D needs
it to know when to recomposite - see TextureStackChangeSource.
*/
[Tool]
[GlobalClass]
public abstract partial class TextureStackLayer:Resource
{
/** Content + accumulation + 2 masks == the 4-source ceiling from G4.
More masks need a second composite pass, which is not built yet.
*/
public const int MaxMasks = 2;
string _layerName = "";
/** Authoring label only - shown by the custom inspector, never used by the
compositor.
*/
[Export]
public string layerName
{
get => _layerName;
set { _layerName = value; TextureStackNotify.Changed( this ); }
}
bool _visible = true;
[Export]
public bool visible
{
get => _visible;
set { _visible = value; TextureStackNotify.Changed( this ); }
}
TextureStackBlendMode _blendMode = TextureStackBlendMode.Mix;
[Export]
public TextureStackBlendMode blendMode
{
get => _blendMode;
set { _blendMode = value; TextureStackNotify.Changed( this ); }
}
float _opacity = 1;
[Export( PropertyHint.Range, "0,1" )]
public float opacity
{
get => _opacity;
set { _opacity = value; TextureStackNotify.Changed( this ); }
}
TextureStackMask[] _masks = new TextureStackMask[ 0 ];
/** Multiplied together, capped at MaxMasks. Entries without a texture are
skipped rather than treated as black - a missing mask means "no mask",
not "mask everything out".
*/
[Export]
public TextureStackMask[] masks
{
get => _masks;
set { _masks = value; TextureStackNotify.Changed( this ); }
}
bool _clipToBelow = false;
/** Photoshop-style "use the alpha beneath as this layer's mask". Reads the
accumulation's alpha, so it forces the composite-shader path.
*/
[Export]
public bool clipToBelow
{
get => _clipToBelow;
set { _clipToBelow = value; TextureStackNotify.Changed( this ); }
}
[ExportGroup( "Placement" )]
bool _fullTexture = true;
/** On: the layer covers the whole output and the three fields below are
ignored. Off: it is blitted into placementPosition/placementSize only,
which is what makes a decal cheap - the blit touches just those pixels.
*/
[Export]
public bool fullTexture
{
get => _fullTexture;
set { _fullTexture = value; TextureStackNotify.Changed( this ); }
}
TextureStackPlacementUnits _placementUnits = TextureStackPlacementUnits.Normalized;
[Export]
public TextureStackPlacementUnits placementUnits
{
get => _placementUnits;
set { _placementUnits = value; TextureStackNotify.Changed( this ); }
}
Vector2 _placementPosition = Vector2.Zero;
[Export]
public Vector2 placementPosition
{
get => _placementPosition;
set { _placementPosition = value; TextureStackNotify.Changed( this ); }
}
Vector2 _placementSize = Vector2.One;
[Export]
public Vector2 placementSize
{
get => _placementSize;
set { _placementSize = value; TextureStackNotify.Changed( this ); }
}
TextureStackStamps _stamps;
/** Repeats this layer across its placement rect as a grid or a seeded
scatter instead of drawing it once. Empty means one blit.
*/
[Export]
public TextureStackStamps stamps
{
get => _stamps;
set { _stamps = value; TextureStackNotify.Changed( this ); }
}
/** The layer's own rect, in image space and <marked>unclipped</marked> -
the compositor clips it and works out the resulting UV slice, so a
placement hanging over the output's edge shows the part that fits rather
than squeezing the whole thing into it.
Rect2I's origin is the image top-left and UV.y runs with image rows
(spike gate G6), so there is no Y flip anywhere in this system.
*/
public virtual Rect2I ComputeBaseRect( TextureStackCompositorContext context )
{
if ( fullTexture )
{
return context.fullRect;
}
var position = placementPosition;
var size = placementSize;
if ( placementUnits == TextureStackPlacementUnits.Normalized )
{
position = position * context.sizeF;
size = size * context.sizeF;
}
return new Rect2I(
Mathf.RoundToInt( position.X ), Mathf.RoundToInt( position.Y ),
Mathf.RoundToInt( size.X ), Mathf.RoundToInt( size.Y )
);
}
/** Every blit this layer wants, rects unclipped. One entry without stamps,
one per stamp plus edge-wrap copies with them.
Override to place a layer by some rule of its own; the compositor only
ever asks through here.
*/
public virtual void CollectPlacements( TextureStackCompositorContext context,
List<TextureStackPlacement> into
)
{
var baseRect = ComputeBaseRect( context );
if ( stamps == null )
{
into.Add( TextureStackPlacement.At( baseRect ) );
return;
}
stamps.CollectPlacements( baseRect, context.fullRect, into );
}
/** Renders this layer's content into context.scratch over
context.scratchRect, with blending disabled.
The scratch buffer is pooled but always arrives cleared to transparent,
so a layer that writes nothing simply contributes nothing - no need to
cover the whole rect defensively.
*/
public abstract void Draw( TextureStackCompositorContext context );
/** How many buffers beyond the scratch one this layer rents while it draws,
for the memory estimate. An effect layer chains passes through
intermediates; most layers need none.
*/
public virtual int extraBuffers => 0;
/** Every nested Resource an edit to which has to recomposite this layer.
The base collects the masks, their TextureSettings and the stamp set.
Subclasses add their own content - a texture and its settings, or a
whole nested stack. Plain Texture2Ds are collected too: they are
built-in resources and do emit changed properly, so editing a
GradientTexture2D mask in place recomposites.
Overrides must call the base. Nothing here recurses into a
TextureStackTexture2D's own stack - that one notifies its dependents
itself after it recomposites, see TextureStackTexture2D.
*/
public virtual void CollectChangeSources( List<TextureStackChangeSource> into, int drawIndex )
{
TextureStackChangeSource.Add( into, _stamps, drawIndex );
if ( _masks == null )
{
return;
}
for ( var i = 0; i < _masks.Length; i++ )
{
var mask = _masks[ i ];
if ( mask == null )
{
continue;
}
TextureStackChangeSource.Add( into, mask, drawIndex );
TextureStackChangeSource.Add( into, mask.textureSettings, drawIndex );
AddTextureChangeSource( into, mask.texture, drawIndex );
}
}
/** A plain Texture2D as a change source. */
protected static void AddTextureChangeSource( List<TextureStackChangeSource> into,
Texture2D texture, int drawIndex
)
{
if ( texture == null )
{
return;
}
TextureStackChangeSource.Add( into, texture, drawIndex );
}
public List<TextureStackMask> GetUsableMasks()
{
var usable = new List<TextureStackMask>();
if ( masks == null )
{
return usable;
}
for ( var i = 0; i < masks.Length; i++ )
{
var mask = masks[ i ];
if ( mask == null || ! mask.isUsable )
{
continue;
}
usable.Add( mask );
if ( usable.Count == MaxMasks )
{
if ( i < masks.Length - 1 )
{
RJLog.Log( "TextureStackLayer:", layerName,
"has more than", MaxMasks, "usable masks - the rest are ignored.",
"A blit takes 4 sources and the composite pass needs 2 of them."
);
}
break;
}
}
return usable;
}
}
}