330 lines
9.8 KiB
C#
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|