Editor Prism node library for arithmetic and interpolation nodes. It defines helper IR builders and many Prism node types (binary/unary/fused/range/periodic/random) that emit IR calls for math intrinsics used by the Prism shader/node system.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// WP-6 · node library A — arithmetic.
//
// Every node here is polymorphic: the operands declare the type variable "T", so one Multiply serves
// floats, float2s, float3s and float4s and the solver picks the width. Nothing emits text; everything
// goes through EmitContext, so the same node lowers to both the s&box .shader and the .slang module.
// ---------------------------------------------------------------------------------------------------
/// <summary>Which base an exponential raises.</summary>
public enum PrismExponentBase
{
/// <summary>e to the power x.</summary>
Natural,
/// <summary>2 to the power x.</summary>
Two
}
/// <summary>Which base a logarithm is taken in.</summary>
public enum PrismLogBase
{
/// <summary>Natural logarithm.</summary>
Natural,
/// <summary>Base-2 logarithm.</summary>
Two,
/// <summary>Base-10 logarithm.</summary>
Ten
}
/// <summary>
/// Small IR-building utilities shared by every node in this package.
/// <para>
/// Nodes never write shader text, so anything that would have been an inline HLSL snippet in the
/// built-in editor is expressed here as a few <see cref="EmitContext"/> calls instead. The handful of
/// operations that genuinely need a function body live as <see cref="HelperFunction"/> fields on the
/// node that owns them, named with the mandatory <c>Prism_</c> prefix.
/// </para>
/// </summary>
internal static class PrismMathHelpers
{
/// <summary>The smallest magnitude a guarded division will divide by.</summary>
public const float Epsilon = 1e-6f;
/// <summary>Clamp to 0..1.</summary>
public static IrValue Saturate( EmitContext ctx, IrValue value ) =>
value.IsValid ? ctx.Call( Intrinsic.Saturate, value ) : value;
/// <summary>Reduce a per-component boolean to a single one, so a scalar socket gets a scalar answer.</summary>
public static IrValue Reduce( EmitContext ctx, IrValue value, bool all )
{
if ( !value.IsValid || value.Type.Components <= 1 ) return value;
return ctx.Call( all ? Intrinsic.All : Intrinsic.Any, value );
}
/// <summary>
/// <c>a / b</c>, with every component where <c>b</c> is exactly zero replaced by
/// <paramref name="fallback"/>. Lowered through component-wise <c>select()</c>, never a vector
/// ternary, so it stays correct when the operands are vectors.
/// </summary>
public static IrValue SafeDivide( EmitContext ctx, IrValue a, IrValue b, IrValue fallback )
{
if ( !a.IsValid || !b.IsValid ) return IrValue.Invalid;
var isZero = ctx.Bin( BinaryOp.Equal, b, ctx.Const( 0f ) );
if ( !isZero.IsValid ) return IrValue.Invalid;
var denominator = ctx.Select( isZero, ctx.Const( 1f ), b );
var quotient = ctx.Bin( BinaryOp.Div, a, denominator );
if ( !quotient.IsValid ) return IrValue.Invalid;
if ( !fallback.IsValid ) return quotient;
return ctx.Select( isZero, fallback, quotient );
}
/// <summary>The normalised position of <c>value</c> between <c>low</c> and <c>high</c>, guarded against an empty range.</summary>
public static IrValue InverseLerp( EmitContext ctx, IrValue value, IrValue low, IrValue high )
{
var numerator = ctx.Bin( BinaryOp.Sub, value, low );
var denominator = ctx.Bin( BinaryOp.Sub, high, low );
return SafeDivide( ctx, numerator, denominator, ctx.Const( 0f ) );
}
}
/// <summary>
/// The shared shape of a two-operand arithmetic node: two polymorphic operands, one result, and an
/// inline literal per operand so a freshly dropped node already computes something.
/// </summary>
public abstract class BinaryMathNode : PrismNode
{
/// <summary>Left operand.</summary>
[In( "T", Name = "A" )] public PortRef A { get; set; }
/// <summary>Right operand.</summary>
[In( "T", Name = "B" )] public PortRef B { get; set; }
/// <summary>The result.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>A</c> is unconnected.</summary>
[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }
/// <summary>The literal used when <c>B</c> is unconnected.</summary>
[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;
/// <summary>Combine the two promoted operands into the result.</summary>
protected abstract IrValue Combine( EmitContext ctx, IrValue a, IrValue b );
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );
if ( !a.IsValid || !b.IsValid ) return;
ctx.Out( nameof( Out ), Combine( ctx, a, b ) );
}
}
/// <summary>
/// The shared shape of a one-operand arithmetic node: one polymorphic input, one result of the same
/// type, and an inline literal so the node is useful before anything is wired to it.
/// </summary>
public abstract class UnaryMathNode : PrismNode
{
/// <summary>The operand.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The result.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>Apply the operation to the operand.</summary>
protected abstract IrValue Apply( EmitContext ctx, IrValue value );
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
if ( !value.IsValid ) return;
ctx.Out( nameof( Out ), Apply( ctx, value ) );
}
}
// ---- basic arithmetic -----------------------------------------------------------------------------
/// <summary>The sum of two values.</summary>
[NodeInfo( Id = "prism.math.add", Title = "Add", Category = "Math/Basic", Icon = "add",
Keywords = ["+", "plus", "sum", "addition"] )]
[NodeVersion( 1 )]
public sealed class AddNode : BinaryMathNode
{
/// <summary>Adding nothing should change nothing, so both literals start at zero.</summary>
public AddNode() => DefaultB = 0f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Add, a, b );
}
/// <summary>The difference of two values.</summary>
[NodeInfo( Id = "prism.math.subtract", Title = "Subtract", Category = "Math/Basic", Icon = "remove",
Keywords = ["-", "minus", "difference"] )]
[NodeVersion( 1 )]
public sealed class SubtractNode : BinaryMathNode
{
/// <summary>Subtracting nothing should change nothing, so both literals start at zero.</summary>
public SubtractNode() => DefaultB = 0f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Sub, a, b );
}
/// <summary>The component-wise product of two values.</summary>
[NodeInfo( Id = "prism.math.multiply", Title = "Multiply", Category = "Math/Basic", Icon = "close",
Keywords = ["*", "times", "product", "scale"] )]
[NodeVersion( 1 )]
public sealed class MultiplyNode : BinaryMathNode
{
/// <summary>One is the identity for multiplication, so a half-wired node passes its input through.</summary>
public MultiplyNode() => DefaultA = 1f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Mul, a, b );
}
/// <summary>The component-wise quotient of two values.</summary>
[NodeInfo( Id = "prism.math.divide", Title = "Divide", Category = "Math/Basic", Icon = "safety_divider",
Keywords = ["/", "quotient", "ratio", "over"] )]
[NodeVersion( 1 )]
public sealed class DivideNode : BinaryMathNode
{
/// <summary>Dividing by one is the identity, so a half-wired node passes its input through.</summary>
public DivideNode() => DefaultA = 1f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Div, a, b );
}
/// <summary>The remainder of a division, written with the <c>%</c> operator.</summary>
[NodeInfo( Id = "prism.math.modulo", Title = "Modulo", Category = "Math/Basic", Icon = "percent",
Keywords = ["%", "mod", "remainder"] )]
[NodeVersion( 1 )]
public sealed class ModuloNode : BinaryMathNode
{
/// <summary>A modulus of one gives the fractional part, which is a useful starting point.</summary>
public ModuloNode() => DefaultA = 1f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Mod, a, b );
}
/// <summary>The floating-point remainder, written as an explicit <c>fmod</c> call.</summary>
[NodeInfo( Id = "prism.math.fmod", Title = "FMod", Category = "Math/Basic", Icon = "percent",
Tier = NodeTier.Advanced, Keywords = ["fmod", "remainder", "modulo"],
Description = "The same value as Modulo, spelled as an intrinsic call rather than the % operator." )]
[NodeVersion( 1 )]
public sealed class FmodNode : BinaryMathNode
{
/// <summary>A modulus of one gives the fractional part, which is a useful starting point.</summary>
public FmodNode() => DefaultA = 1f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Fmod, a, b );
}
/// <summary>One value raised to the power of another.</summary>
[NodeInfo( Id = "prism.math.power", Title = "Power", Category = "Math/Basic", Icon = "superscript",
Keywords = ["pow", "^", "exponent", "raise"] )]
[NodeVersion( 1 )]
public sealed class PowerNode : BinaryMathNode
{
/// <summary>A base of one and an exponent of one both leave the other operand alone.</summary>
public PowerNode() => DefaultA = 1f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Pow, a, b );
}
/// <summary>The smaller of two values, component by component.</summary>
[NodeInfo( Id = "prism.math.min", Title = "Minimum", Category = "Math/Range", Icon = "arrow_downward",
Keywords = ["min", "smaller", "lower", "floor"] )]
[NodeVersion( 1 )]
public sealed class MinimumNode : BinaryMathNode
{
/// <summary>Both literals start at zero so the node reads as an ordinary comparison.</summary>
public MinimumNode() => DefaultB = 0f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Min, a, b );
}
/// <summary>The larger of two values, component by component.</summary>
[NodeInfo( Id = "prism.math.max", Title = "Maximum", Category = "Math/Range", Icon = "arrow_upward",
Keywords = ["max", "larger", "upper", "ceiling"] )]
[NodeVersion( 1 )]
public sealed class MaximumNode : BinaryMathNode
{
/// <summary>Both literals start at zero so the node reads as an ordinary comparison.</summary>
public MaximumNode() => DefaultB = 0f;
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Max, a, b );
}
// ---- single-operand arithmetic --------------------------------------------------------------------
/// <summary>The magnitude of a value, discarding its sign.</summary>
[NodeInfo( Id = "prism.math.abs", Title = "Absolute", Category = "Math/Basic", Icon = "exposure",
Keywords = ["abs", "magnitude", "unsigned"] )]
[NodeVersion( 1 )]
public sealed class AbsoluteNode : UnaryMathNode
{
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Abs, value );
}
/// <summary>Minus one, zero or plus one, per component.</summary>
[NodeInfo( Id = "prism.math.sign", Title = "Sign", Category = "Math/Basic", Icon = "trending_flat",
Keywords = ["sign", "signum", "direction"] )]
[NodeVersion( 1 )]
public sealed class SignNode : UnaryMathNode
{
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Sign, value );
}
/// <summary>Negate a value.</summary>
[NodeInfo( Id = "prism.math.negate", Title = "Negate", Category = "Math/Basic", Icon = "exposure_neg_1",
Keywords = ["negate", "minus", "invert", "flip sign"] )]
[NodeVersion( 1 )]
public sealed class NegateNode : UnaryMathNode
{
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Un( UnaryOp.Negate, value );
}
/// <summary>The square root of a value.</summary>
[NodeInfo( Id = "prism.math.sqrt", Title = "Square Root", Category = "Math/Advanced", Icon = "square_foot",
Keywords = ["sqrt", "root", "radical"] )]
[NodeVersion( 1 )]
public sealed class SquareRootNode : UnaryMathNode
{
/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
public SquareRootNode() => DefaultIn = 1f;
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Sqrt, value );
}
/// <summary>One over the square root of a value — the fast normalisation primitive.</summary>
[NodeInfo( Id = "prism.math.rsqrt", Title = "Reciprocal Square Root", Category = "Math/Advanced",
Icon = "square_foot", Tier = NodeTier.Advanced, Keywords = ["rsqrt", "inverse sqrt", "fast"] )]
[NodeVersion( 1 )]
public sealed class ReciprocalSquareRootNode : UnaryMathNode
{
/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
public ReciprocalSquareRootNode() => DefaultIn = 1f;
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Rsqrt, value );
}
/// <summary>One over a value.</summary>
[NodeInfo( Id = "prism.math.reciprocal", Title = "Reciprocal", Category = "Math/Advanced", Icon = "swap_vert",
Keywords = ["rcp", "inverse", "one over", "1/x"] )]
[NodeVersion( 1 )]
public sealed class ReciprocalNode : UnaryMathNode
{
/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
public ReciprocalNode() => DefaultIn = 1f;
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Rcp, value );
}
/// <summary>Clamp a value into the zero-to-one range.</summary>
[NodeInfo( Id = "prism.math.saturate", Title = "Saturate", Category = "Math/Range", Icon = "opacity",
Keywords = ["saturate", "clamp01", "clamp", "0..1"] )]
[NodeVersion( 1 )]
public sealed class SaturateNode : UnaryMathNode
{
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Saturate, value );
}
/// <summary>Raise a fixed base to a value, in base e or base 2.</summary>
[NodeInfo( Id = "prism.math.exp", Title = "Exponential", Category = "Math/Advanced", Icon = "trending_up",
Keywords = ["exp", "exp2", "e", "power of two"] )]
[NodeVersion( 1 )]
public sealed class ExponentialNode : UnaryMathNode
{
/// <summary>Which base to raise.</summary>
public PrismExponentBase Base { get; set; } = PrismExponentBase.Natural;
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) =>
ctx.Call( Base == PrismExponentBase.Two ? Intrinsic.Exp2 : Intrinsic.Exp, value );
}
/// <summary>The logarithm of a value, in base e, 2 or 10.</summary>
[NodeInfo( Id = "prism.math.log", Title = "Logarithm", Category = "Math/Advanced", Icon = "show_chart",
Keywords = ["log", "log2", "log10", "ln"] )]
[NodeVersion( 1 )]
public sealed class LogarithmNode : UnaryMathNode
{
/// <summary>Which base the logarithm is taken in.</summary>
public PrismLogBase Base { get; set; } = PrismLogBase.Natural;
/// <summary>A default of one makes the unconnected result exactly zero in every base.</summary>
public LogarithmNode() => DefaultIn = 1f;
/// <inheritdoc/>
protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Base switch
{
PrismLogBase.Two => Intrinsic.Log2,
PrismLogBase.Ten => Intrinsic.Log10,
_ => Intrinsic.Log
}, value );
}
// ---- range and interpolation ----------------------------------------------------------------------
/// <summary>Constrain a value to a range.</summary>
[NodeInfo( Id = "prism.math.clamp", Title = "Clamp", Category = "Math/Range", Icon = "compress",
Keywords = ["clamp", "limit", "bound", "range"] )]
[NodeVersion( 1 )]
public sealed class ClampNode : PrismNode
{
/// <summary>The value to constrain.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The lower bound.</summary>
[In( "T", Name = "Min" )] public PortRef Min { get; set; }
/// <summary>The upper bound.</summary>
[In( "T", Name = "Max" )] public PortRef Max { get; set; }
/// <summary>The constrained value.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }
/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( ctx.IsConnected( nameof( Min ) ) || ctx.IsConnected( nameof( Max ) ) ) return;
if ( DefaultMin <= DefaultMax ) return;
ctx.Warn( $"The lower bound ({DefaultMin}) is above the upper bound ({DefaultMax}); every result will be the upper bound",
nameof( Min ) );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var low = ctx.In( nameof( Min ) );
var high = ctx.In( nameof( Max ) );
if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Clamp, value, low, high ) );
}
}
/// <summary>Blend linearly between two values.</summary>
[NodeInfo( Id = "prism.math.lerp", Title = "Lerp", Category = "Math/Interpolation", Icon = "linear_scale",
Keywords = ["mix", "blend", "interpolate", "lerp"] )]
[NodeVersion( 1 )]
public sealed class LerpNode : PrismNode
{
/// <summary>The value returned when the fraction is zero.</summary>
[In( "T", Name = "A" )] public PortRef A { get; set; }
/// <summary>The value returned when the fraction is one.</summary>
[In( "T", Name = "B" )] public PortRef B { get; set; }
/// <summary>How far between <c>A</c> and <c>B</c> the result sits.</summary>
[In( "T", Name = "T" )] public PortRef T { get; set; }
/// <summary>The blended value.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>A</c> is unconnected.</summary>
[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }
/// <summary>The literal used when <c>B</c> is unconnected.</summary>
[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;
/// <summary>The literal used when <c>T</c> is unconnected.</summary>
[InlineValue( nameof( T ) ), Title( "T" ), Range( 0f, 1f )] public float DefaultT { get; set; } = 0.5f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );
var t = ctx.In( nameof( T ) );
if ( !a.IsValid || !b.IsValid || !t.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, a, b, t ) );
}
}
/// <summary>Where a value sits between two others — the inverse of a Lerp.</summary>
[NodeInfo( Id = "prism.math.inverseLerp", Title = "Inverse Lerp", Category = "Math/Interpolation",
Icon = "swap_horiz", Keywords = ["unlerp", "inverse", "normalize", "fraction"] )]
[NodeVersion( 1 )]
public sealed class InverseLerpNode : PrismNode
{
/// <summary>The value that maps to zero.</summary>
[In( "T", Name = "A" )] public PortRef A { get; set; }
/// <summary>The value that maps to one.</summary>
[In( "T", Name = "B" )] public PortRef B { get; set; }
/// <summary>The value to locate.</summary>
[In( "T", Name = "T" )] public PortRef T { get; set; }
/// <summary>The normalised position.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>A</c> is unconnected.</summary>
[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }
/// <summary>The literal used when <c>B</c> is unconnected.</summary>
[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;
/// <summary>The literal used when <c>T</c> is unconnected.</summary>
[InlineValue( nameof( T ) ), Title( "T" )] public float DefaultT { get; set; } = 0.5f;
/// <summary>Clamp the result to 0..1 instead of letting it run past the ends of the range.</summary>
public bool Clamp { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var a = ctx.In( nameof( A ) );
var b = ctx.In( nameof( B ) );
var t = ctx.In( nameof( T ) );
if ( !a.IsValid || !b.IsValid || !t.IsValid ) return;
var result = PrismMathHelpers.InverseLerp( ctx, t, a, b );
if ( Clamp ) result = PrismMathHelpers.Saturate( ctx, result );
ctx.Out( nameof( Out ), result );
}
}
/// <summary>Move a value from one range into another.</summary>
[NodeInfo( Id = "prism.math.remap", Title = "Remap", Category = "Math/Range", Icon = "tune",
Keywords = ["remap", "range", "rescale", "fit"] )]
[NodeVersion( 1 )]
public sealed class RemapNode : PrismNode
{
/// <summary>The value to remap.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The bottom of the input range.</summary>
[In( "T", Name = "In Min" )] public PortRef InMin { get; set; }
/// <summary>The top of the input range.</summary>
[In( "T", Name = "In Max" )] public PortRef InMax { get; set; }
/// <summary>The bottom of the output range.</summary>
[In( "T", Name = "Out Min" )] public PortRef OutMin { get; set; }
/// <summary>The top of the output range.</summary>
[In( "T", Name = "Out Max" )] public PortRef OutMax { get; set; }
/// <summary>The remapped value.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;
/// <summary>The literal used when <c>In Min</c> is unconnected.</summary>
[InlineValue( nameof( InMin ) ), Title( "In Min" )] public float DefaultInMin { get; set; }
/// <summary>The literal used when <c>In Max</c> is unconnected.</summary>
[InlineValue( nameof( InMax ) ), Title( "In Max" )] public float DefaultInMax { get; set; } = 1f;
/// <summary>The literal used when <c>Out Min</c> is unconnected.</summary>
[InlineValue( nameof( OutMin ) ), Title( "Out Min" )] public float DefaultOutMin { get; set; }
/// <summary>The literal used when <c>Out Max</c> is unconnected.</summary>
[InlineValue( nameof( OutMax ) ), Title( "Out Max" )] public float DefaultOutMax { get; set; } = 1f;
/// <summary>Clamp to the output range instead of extrapolating past its ends.</summary>
public bool Clamp { get; set; } = true;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( ctx.IsConnected( nameof( InMin ) ) || ctx.IsConnected( nameof( InMax ) ) ) return;
if ( DefaultInMin != DefaultInMax ) return;
ctx.Warn( "The input range is empty, so every result will be the bottom of the output range",
nameof( InMin ) );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var inMin = ctx.In( nameof( InMin ) );
var inMax = ctx.In( nameof( InMax ) );
var outMin = ctx.In( nameof( OutMin ) );
var outMax = ctx.In( nameof( OutMax ) );
if ( !value.IsValid || !inMin.IsValid || !inMax.IsValid || !outMin.IsValid || !outMax.IsValid ) return;
var normalized = PrismMathHelpers.InverseLerp( ctx, value, inMin, inMax );
if ( Clamp ) normalized = PrismMathHelpers.Saturate( ctx, normalized );
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, outMin, outMax, normalized ) );
}
}
/// <summary>Zero below an edge and one above it — a hard threshold.</summary>
[NodeInfo( Id = "prism.logic.step", Title = "Step", Category = "Math/Interpolation", Icon = "stairs",
Keywords = ["step", "threshold", "cutoff", "edge"] )]
[NodeVersion( 1 )]
public sealed class StepNode : PrismNode
{
/// <summary>The value to test.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The threshold the value is compared against.</summary>
[In( "T", Name = "Edge" )] public PortRef Edge { get; set; }
/// <summary>Zero or one.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>The literal used when <c>Edge</c> is unconnected.</summary>
[InlineValue( nameof( Edge ) ), Title( "Edge" )] public float DefaultEdge { get; set; } = 0.5f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (value, edge) = ctx.InPair( nameof( In ), nameof( Edge ) );
if ( !value.IsValid || !edge.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Step, edge, value ) );
}
}
/// <summary>A smooth Hermite ramp between two edges.</summary>
[NodeInfo( Id = "prism.logic.smoothstep", Title = "Smoothstep", Category = "Math/Interpolation",
Icon = "gradient", Keywords = ["smoothstep", "ramp", "ease", "hermite"] )]
[NodeVersion( 1 )]
public sealed class SmoothStepNode : PrismNode
{
/// <summary>The value to ramp.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The value that maps to zero.</summary>
[In( "T", Name = "Low" )] public PortRef Low { get; set; }
/// <summary>The value that maps to one.</summary>
[In( "T", Name = "High" )] public PortRef High { get; set; }
/// <summary>The eased result.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;
/// <summary>The literal used when <c>Low</c> is unconnected.</summary>
[InlineValue( nameof( Low ) ), Title( "Low" )] public float DefaultLow { get; set; }
/// <summary>The literal used when <c>High</c> is unconnected.</summary>
[InlineValue( nameof( High ) ), Title( "High" )] public float DefaultHigh { get; set; } = 1f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var low = ctx.In( nameof( Low ) );
var high = ctx.In( nameof( High ) );
if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.SmoothStep, low, high, value ) );
}
}
/// <summary>Ken Perlin's second-order smoothstep: zero first <em>and</em> second derivatives at both ends.</summary>
[NodeInfo( Id = "prism.math.smootherStep", Title = "Smoother Step", Category = "Math/Interpolation",
Icon = "gradient", Tier = NodeTier.Advanced,
Keywords = ["smootherstep", "quintic", "ease", "perlin"] )]
[NodeVersion( 1 )]
public sealed class SmootherStepNode : PrismNode
{
/// <summary>The value to ramp.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The value that maps to zero.</summary>
[In( "T", Name = "Low" )] public PortRef Low { get; set; }
/// <summary>The value that maps to one.</summary>
[In( "T", Name = "High" )] public PortRef High { get; set; }
/// <summary>The eased result.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;
/// <summary>The literal used when <c>Low</c> is unconnected.</summary>
[InlineValue( nameof( Low ) ), Title( "Low" )] public float DefaultLow { get; set; }
/// <summary>The literal used when <c>High</c> is unconnected.</summary>
[InlineValue( nameof( High ) ), Title( "High" )] public float DefaultHigh { get; set; } = 1f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var low = ctx.In( nameof( Low ) );
var high = ctx.In( nameof( High ) );
if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;
var t = ctx.Let( "t", PrismMathHelpers.Saturate( ctx, PrismMathHelpers.InverseLerp( ctx, value, low, high ) ) );
if ( !t.IsValid ) return;
// t * t * t * ( t * ( t * 6 - 15 ) + 10 )
var inner = ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Mul, t, ctx.Const( 6f ) ), ctx.Const( 15f ) );
var poly = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, t, inner ), ctx.Const( 10f ) );
var cube = ctx.Bin( BinaryOp.Mul, ctx.Bin( BinaryOp.Mul, t, t ), t );
ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Mul, cube, poly ) );
}
}
// ---- periodic and guarded arithmetic --------------------------------------------------------------
/// <summary>Fold a value back into a range, repeating it forever in both directions.</summary>
[NodeInfo( Id = "prism.math.wrap", Title = "Wrap", Category = "Math/Range", Icon = "wrap_text",
Keywords = ["wrap", "repeat", "tile", "cycle", "modulo"] )]
[NodeVersion( 1 )]
public sealed class WrapNode : PrismNode
{
/// <summary>The value to fold.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The bottom of the range.</summary>
[In( "T", Name = "Min" )] public PortRef Min { get; set; }
/// <summary>The top of the range.</summary>
[In( "T", Name = "Max" )] public PortRef Max { get; set; }
/// <summary>The folded value, always inside the range.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }
/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var low = ctx.In( nameof( Min ) );
var high = ctx.In( nameof( Max ) );
if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;
var range = ctx.Let( "range", ctx.Bin( BinaryOp.Sub, high, low ) );
if ( !range.IsValid ) return;
var empty = ctx.Bin( BinaryOp.Equal, range, ctx.Const( 0f ) );
var safeRange = ctx.Select( empty, ctx.Const( 1f ), range );
// in - range * floor( ( in - min ) / range )
var offset = ctx.Bin( BinaryOp.Sub, value, low );
var turns = ctx.Call( Intrinsic.Floor, ctx.Bin( BinaryOp.Div, offset, safeRange ) );
var wrapped = ctx.Bin( BinaryOp.Sub, value, ctx.Bin( BinaryOp.Mul, safeRange, turns ) );
ctx.Out( nameof( Out ), ctx.Select( empty, low, wrapped ) );
}
}
/// <summary>Bounce a value back and forth between zero and a length.</summary>
[NodeInfo( Id = "prism.math.pingPong", Title = "Ping Pong", Category = "Math/Range", Icon = "sync_alt",
Keywords = ["pingpong", "bounce", "triangle", "yoyo"] )]
[NodeVersion( 1 )]
public sealed class PingPongNode : PrismNode
{
/// <summary>The value to bounce.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The distance travelled before the value turns around.</summary>
[In( "T", Name = "Length" )] public PortRef Length { get; set; }
/// <summary>The bounced value, always between zero and <c>Length</c>.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>The literal used when <c>Length</c> is unconnected.</summary>
[InlineValue( nameof( Length ) ), Title( "Length" )] public float DefaultLength { get; set; } = 1f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (value, length) = ctx.InPair( nameof( In ), nameof( Length ) );
if ( !value.IsValid || !length.IsValid ) return;
var span = ctx.Let( "span", length );
var period = ctx.Bin( BinaryOp.Mul, span, ctx.Const( 2f ) );
var empty = ctx.Bin( BinaryOp.Equal, period, ctx.Const( 0f ) );
var safePeriod = ctx.Select( empty, ctx.Const( 1f ), period );
// length - abs( fmod( in, 2 * length ) - length )
var folded = ctx.Call( Intrinsic.Fmod, value, safePeriod );
var distance = ctx.Call( Intrinsic.Abs, ctx.Bin( BinaryOp.Sub, folded, span ) );
var result = ctx.Bin( BinaryOp.Sub, span, distance );
ctx.Out( nameof( Out ), ctx.Select( empty, ctx.Const( 0f ), result ) );
}
}
/// <summary>Divide, substituting a chosen value wherever the divisor is exactly zero.</summary>
[NodeInfo( Id = "prism.math.safeDivide", Title = "Safe Divide", Category = "Math/Basic", Icon = "shield",
Keywords = ["divide", "guard", "zero", "nan", "safe"] )]
[NodeVersion( 1 )]
public sealed class SafeDivideNode : PrismNode
{
/// <summary>The numerator.</summary>
[In( "T", Name = "A" )] public PortRef A { get; set; }
/// <summary>The denominator.</summary>
[In( "T", Name = "B" )] public PortRef B { get; set; }
/// <summary>The value used wherever the denominator is zero.</summary>
[In( "T", Name = "Fallback" )] public PortRef Fallback { get; set; }
/// <summary>The quotient.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>A</c> is unconnected.</summary>
[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; } = 1f;
/// <summary>The literal used when <c>B</c> is unconnected.</summary>
[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;
/// <summary>The literal used when <c>Fallback</c> is unconnected.</summary>
[InlineValue( nameof( Fallback ) ), Title( "Fallback" )] public float DefaultFallback { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );
var fallback = ctx.In( nameof( Fallback ) );
if ( !a.IsValid || !b.IsValid ) return;
ctx.Out( nameof( Out ), PrismMathHelpers.SafeDivide( ctx, a, b, fallback ) );
}
}
// ---- fused forms ----------------------------------------------------------------------------------
/// <summary>Multiply by a scale and add an offset in one operation.</summary>
[NodeInfo( Id = "prism.math.scaleAndOffset", Title = "Scale And Offset", Category = "Math/Basic",
Icon = "open_in_full", Keywords = ["scale", "bias", "offset", "mad", "affine"] )]
[NodeVersion( 1 )]
public sealed class ScaleAndOffsetNode : PrismNode
{
/// <summary>The value to transform.</summary>
[In( "T", Name = "In" )] public PortRef In { get; set; }
/// <summary>The multiplier.</summary>
[In( "T", Name = "Scale" )] public PortRef Scale { get; set; }
/// <summary>The addend.</summary>
[In( "T", Name = "Offset" )] public PortRef Offset { get; set; }
/// <summary>The transformed value.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>In</c> is unconnected.</summary>
[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }
/// <summary>The literal used when <c>Scale</c> is unconnected.</summary>
[InlineValue( nameof( Scale ) ), Title( "Scale" )] public float DefaultScale { get; set; } = 1f;
/// <summary>The literal used when <c>Offset</c> is unconnected.</summary>
[InlineValue( nameof( Offset ) ), Title( "Offset" )] public float DefaultOffset { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.In( nameof( In ) );
var scale = ctx.In( nameof( Scale ) );
var offset = ctx.In( nameof( Offset ) );
if ( !value.IsValid || !scale.IsValid || !offset.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Mad, value, scale, offset ) );
}
}
/// <summary><c>A * B + C</c>, emitted as a single multiply-add.</summary>
[NodeInfo( Id = "prism.math.multiplyAdd", Title = "Multiply Add", Category = "Math/Basic",
Icon = "calculate", Keywords = ["mad", "fma", "multiply add", "a*b+c"] )]
[NodeVersion( 1 )]
public sealed class MultiplyAddNode : PrismNode
{
/// <summary>First factor.</summary>
[In( "T", Name = "A" )] public PortRef A { get; set; }
/// <summary>Second factor.</summary>
[In( "T", Name = "B" )] public PortRef B { get; set; }
/// <summary>The addend.</summary>
[In( "T", Name = "C" )] public PortRef C { get; set; }
/// <summary>The result.</summary>
[Out( "T", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>A</c> is unconnected.</summary>
[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; } = 1f;
/// <summary>The literal used when <c>B</c> is unconnected.</summary>
[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;
/// <summary>The literal used when <c>C</c> is unconnected.</summary>
[InlineValue( nameof( C ) ), Title( "C" )] public float DefaultC { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var a = ctx.In( nameof( A ) );
var b = ctx.In( nameof( B ) );
var c = ctx.In( nameof( C ) );
if ( !a.IsValid || !b.IsValid || !c.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Mad, a, b, c ) );
}
}
/// <summary>A repeatable pseudo-random value in a range, derived from a two-dimensional seed.</summary>
[NodeInfo( Id = "prism.math.randomRange", Title = "Random Range", Category = "Math/Advanced",
Icon = "casino", Keywords = ["random", "hash", "noise", "rand", "seed"] )]
[NodeVersion( 1 )]
public sealed class RandomRangeNode : PrismNode
{
/// <summary>The seed the value is derived from. The same seed always gives the same result.</summary>
[In( "float2", Name = "Seed" )] public PortRef Seed { get; set; }
/// <summary>The lowest value that can be produced.</summary>
[In( "float", Name = "Min" )] public PortRef Min { get; set; }
/// <summary>The highest value that can be produced.</summary>
[In( "float", Name = "Max" )] public PortRef Max { get; set; }
/// <summary>The pseudo-random value.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The literal used when <c>Seed</c> is unconnected.</summary>
[InlineValue( nameof( Seed ) ), Title( "Seed" )] public Vector2 DefaultSeed { get; set; } = new( 0f, 0f );
/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }
/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var seed = ctx.InAs( nameof( Seed ), ShaderType.Float2 );
var low = ctx.In( nameof( Min ) );
var high = ctx.In( nameof( Max ) );
if ( !seed.IsValid || !low.IsValid || !high.IsValid ) return;
// frac( sin( dot( seed, float2( 12.9898, 78.233 ) ) ) * 43758.5453 )
var dot = ctx.Call( Intrinsic.Dot, seed, ctx.Const( new Vector2( 12.9898f, 78.233f ) ) );
var wave = ctx.Bin( BinaryOp.Mul, ctx.Call( Intrinsic.Sin, dot ), ctx.Const( 43758.5453f ) );
var hash = ctx.Call( Intrinsic.Frac, wave );
if ( !hash.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, low, high, hash ) );
}
}