Editor Prism node definitions for signed distance field (SDF) primitives, combiners, modifiers and outputs. It defines helper HLSL functions, node classes that emit IR for shapes (circle, box, triangle, hexagon, arc, star, segment), boolean and smooth combines, modifiers (round, annular, onion, repeat) and conversion nodes (mask, outline) with filtering and stage constraints.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// Signed distance fields.
//
// Everything in this family speaks one currency: a signed distance, negative inside the shape, zero on
// its boundary and positive outside. That is what lets a circle, a box and a hexagon be combined,
// smoothed, inflated and hollowed by nodes that know nothing about any of them.
//
// The primitives are stage-agnostic on purpose — a distance needs no derivatives. Only the two nodes
// that turn a distance into pixels, Mask and Outline, are pixel-only, and even then only when they are
// left on analytic antialiasing.
// ---------------------------------------------------------------------------------------------------
/// <summary>Which curve a smooth combine uses to round off the join.</summary>
public enum PrismSmoothKind
{
/// <summary>Polynomial. Cheap, exact at the extremes, and the usual choice.</summary>
Polynomial,
/// <summary>Exponential. Smoother far from the join, at the cost of two exponentials.</summary>
Exponential,
/// <summary>Power. Very soft, and only meaningful for positive distances.</summary>
Power,
/// <summary>Root. A single square root, with a wider blend than the polynomial.</summary>
Root
}
/// <summary>How a signed distance is turned into coverage.</summary>
public enum PrismSdfFiltering
{
/// <summary>One pixel wide, measured with screen-space derivatives. Pixel stage only.</summary>
Analytic,
/// <summary>A fixed softness in distance units. Works in every stage.</summary>
Soft,
/// <summary>A hard cut with no antialiasing at all.</summary>
Hard
}
/// <summary>The signed-distance bodies.</summary>
internal static class PrismSdfHelpers
{
/// <summary>Distance to a circle of a given radius.</summary>
internal static readonly HelperFunction Circle = new( "Prism_SdfCircle", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "r", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfCircle( float2 p, float r )
{
return length( p ) - r;
}
"""
};
/// <summary>Distance to an axis-aligned box of given half-extents.</summary>
internal static readonly HelperFunction Box = new( "Prism_SdfBox", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "b", ShaderType.Float2 )
] )
{
Hlsl = """
float Prism_SdfBox( float2 p, float2 b )
{
float2 d = abs( p ) - b;
return length( max( d, 0.0f ) ) + min( max( d.x, d.y ), 0.0f );
}
"""
};
/// <summary>Distance to a box with rounded corners.</summary>
internal static readonly HelperFunction RoundedBox = new( "Prism_SdfRoundedBox", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "b", ShaderType.Float2 ),
new HelperParam( "r", ShaderType.Float )
] )
{
Requires = [Box],
Hlsl = """
float Prism_SdfRoundedBox( float2 p, float2 b, float r )
{
return Prism_SdfBox( p, b - r ) - r;
}
"""
};
/// <summary>Distance to a line segment between two points.</summary>
internal static readonly HelperFunction Segment = new( "Prism_SdfSegment", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "a", ShaderType.Float2 ),
new HelperParam( "b", ShaderType.Float2 )
] )
{
Hlsl = """
float Prism_SdfSegment( float2 p, float2 a, float2 b )
{
float2 pa = p - a;
float2 ba = b - a;
float h = saturate( dot( pa, ba ) / max( dot( ba, ba ), 1e-8f ) );
return length( pa - ba * h );
}
"""
};
/// <summary>Distance to an equilateral triangle of a given radius.</summary>
internal static readonly HelperFunction Triangle = new( "Prism_SdfTriangle", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "r", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfTriangle( float2 p, float r )
{
const float k = 1.73205080757f;
r = max( r, 1e-5f );
p.x = abs( p.x ) - r;
p.y = p.y + r / k;
if ( p.x + k * p.y > 0.0f )
p = float2( p.x - k * p.y, -k * p.x - p.y ) * 0.5f;
p.x -= clamp( p.x, -2.0f * r, 0.0f );
return -length( p ) * sign( p.y );
}
"""
};
/// <summary>Distance to a regular hexagon of a given radius.</summary>
internal static readonly HelperFunction Hexagon = new( "Prism_SdfHexagon", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "r", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfHexagon( float2 p, float r )
{
const float3 k = float3( -0.866025404f, 0.5f, 0.577350269f );
p = abs( p );
p -= 2.0f * min( dot( k.xy, p ), 0.0f ) * k.xy;
p -= float2( clamp( p.x, -k.z * r, k.z * r ), r );
return length( p ) * sign( p.y );
}
"""
};
/// <summary>Distance to an arc of a given aperture, radius and thickness.</summary>
internal static readonly HelperFunction Arc = new( "Prism_SdfArc", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "flAperture", ShaderType.Float ),
new HelperParam( "flRadius", ShaderType.Float ),
new HelperParam( "flThickness", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfArc( float2 p, float flAperture, float flRadius, float flThickness )
{
float2 sc = float2( sin( flAperture ), cos( flAperture ) );
p.x = abs( p.x );
float flOutside = length( p - sc * flRadius );
float flInside = abs( length( p ) - flRadius );
return ( sc.y * p.x > sc.x * p.y ? flOutside : flInside ) - flThickness;
}
"""
};
/// <summary>Distance to a five-pointed star.</summary>
internal static readonly HelperFunction Star = new( "Prism_SdfStar", ShaderType.Float,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "r", ShaderType.Float ),
new HelperParam( "rf", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfStar( float2 p, float r, float rf )
{
const float2 k1 = float2( 0.809016994f, -0.587785252f );
const float2 k2 = float2( -0.809016994f, -0.587785252f );
p.x = abs( p.x );
p -= 2.0f * max( dot( k1, p ), 0.0f ) * k1;
p -= 2.0f * max( dot( k2, p ), 0.0f ) * k2;
p.x = abs( p.x );
p.y -= r;
float2 ba = rf * float2( -k1.y, k1.x ) - float2( 0.0f, 1.0f );
float h = clamp( dot( p, ba ) / max( dot( ba, ba ), 1e-8f ), 0.0f, r );
return length( p - ba * h ) * sign( p.y * ba.x - p.x * ba.y );
}
"""
};
// ---- combination ------------------------------------------------------
/// <summary>Polynomial smooth minimum: the standard rounded union.</summary>
internal static readonly HelperFunction SmoothMinPoly = new( "Prism_SmoothMinPoly", ShaderType.Float,
[
new HelperParam( "a", ShaderType.Float ),
new HelperParam( "b", ShaderType.Float ),
new HelperParam( "k", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SmoothMinPoly( float a, float b, float k )
{
k = max( k, 1e-5f );
float h = saturate( 0.5f + 0.5f * ( b - a ) / k );
return lerp( b, a, h ) - k * h * ( 1.0f - h );
}
"""
};
/// <summary>Exponential smooth minimum: a wider, softer join.</summary>
internal static readonly HelperFunction SmoothMinExp = new( "Prism_SmoothMinExp", ShaderType.Float,
[
new HelperParam( "a", ShaderType.Float ),
new HelperParam( "b", ShaderType.Float ),
new HelperParam( "k", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SmoothMinExp( float a, float b, float k )
{
k = max( k, 1e-5f );
float res = exp2( -a / k ) + exp2( -b / k );
return -k * log2( max( res, 1e-8f ) );
}
"""
};
/// <summary>Power smooth minimum. Only meaningful where both distances are positive.</summary>
internal static readonly HelperFunction SmoothMinPower = new( "Prism_SmoothMinPower", ShaderType.Float,
[
new HelperParam( "a", ShaderType.Float ),
new HelperParam( "b", ShaderType.Float ),
new HelperParam( "k", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SmoothMinPower( float a, float b, float k )
{
k = max( k, 1e-5f );
float pa = pow( max( a, 0.0f ), k );
float pb = pow( max( b, 0.0f ), k );
return pow( ( pa * pb ) / max( pa + pb, 1e-8f ), 1.0f / k );
}
"""
};
/// <summary>Square-root smooth minimum: one root, and a wider blend than the polynomial.</summary>
internal static readonly HelperFunction SmoothMinRoot = new( "Prism_SmoothMinRoot", ShaderType.Float,
[
new HelperParam( "a", ShaderType.Float ),
new HelperParam( "b", ShaderType.Float ),
new HelperParam( "k", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SmoothMinRoot( float a, float b, float k )
{
k = max( k, 1e-5f );
float h = a - b;
return 0.5f * ( ( a + b ) - sqrt( h * h + k ) );
}
"""
};
// ---- domain -----------------------------------------------------------
/// <summary>Fold a coordinate into a repeating cell, so one primitive tiles forever.</summary>
internal static readonly HelperFunction Repeat = new( "Prism_SdfRepeat", ShaderType.Float4,
[
new HelperParam( "p", ShaderType.Float2 ),
new HelperParam( "c", ShaderType.Float2 )
] )
{
Hlsl = """
float4 Prism_SdfRepeat( float2 p, float2 c )
{
float2 vSize = max( abs( c ), 1e-5f );
float2 vCell = round( p / vSize );
return float4( p - vSize * vCell, vCell );
}
"""
};
/// <summary>The smooth-minimum body for a given curve.</summary>
internal static HelperFunction SmoothMin( PrismSmoothKind kind ) => kind switch
{
PrismSmoothKind.Exponential => SmoothMinExp,
PrismSmoothKind.Power => SmoothMinPower,
PrismSmoothKind.Root => SmoothMinRoot,
_ => SmoothMinPoly
};
}
/// <summary>Shared behaviour for a shape that measures a distance from a centred coordinate.</summary>
public abstract class SdfShapeNode : PrismNode
{
/// <summary>Where to evaluate.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The point the shape is centred on.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>The centre used when nothing is connected.</summary>
[InlineValue( nameof( Center ) )] public Vector2 DefaultCenter { get; set; } = new( 0.5f, 0.5f );
/// <summary>The signed distance: negative inside, zero on the boundary, positive outside.</summary>
[Out( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>The coordinate relative to the shape's centre.</summary>
protected IrValue Local( EmitContext ctx ) =>
ctx.Bin( BinaryOp.Sub, PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ) );
}
// ---------------------------------------------------------------------------------------------------
// Primitives
// ---------------------------------------------------------------------------------------------------
/// <summary>The signed distance to a circle.</summary>
[NodeInfo( Id = "prism.sdf.circle", Title = "SDF Circle", Category = "SDF/Shapes",
Icon = "circle", Keywords = new[] { "sdf", "circle", "disc", "distance" },
Description = "Signed distance to a circle." )]
[NodeVersion( 1 )]
public sealed class SdfCircleNode : SdfShapeNode
{
/// <summary>The circle's radius.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.3f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Circle, Local( ctx ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ) ) );
}
}
/// <summary>The signed distance to an axis-aligned box.</summary>
[NodeInfo( Id = "prism.sdf.box", Title = "SDF Box", Category = "SDF/Shapes",
Icon = "crop_square", Keywords = new[] { "sdf", "box", "rectangle", "distance" },
Description = "Signed distance to an axis-aligned box." )]
[NodeVersion( 1 )]
public sealed class SdfBoxNode : SdfShapeNode
{
/// <summary>Half the box's size on each axis.</summary>
[In( "float2", Name = "Size" )] public PortRef Size { get; set; }
/// <summary>The size used when nothing is connected.</summary>
[InlineValue( nameof( Size ) )] public Vector2 DefaultSize { get; set; } = new( 0.3f, 0.2f );
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Box, Local( ctx ),
ctx.In( nameof( Size ), ctx.Const( DefaultSize ) ) ) );
}
}
/// <summary>The signed distance to a box with rounded corners.</summary>
[NodeInfo( Id = "prism.sdf.roundedBox", Title = "SDF Rounded Box", Category = "SDF/Shapes",
Icon = "rounded_corner", Keywords = new[] { "sdf", "rounded", "box", "distance" },
Description = "Signed distance to a box whose corners are rounded by a radius." )]
[NodeVersion( 1 )]
public sealed class SdfRoundedBoxNode : SdfShapeNode
{
/// <summary>Half the box's size on each axis.</summary>
[In( "float2", Name = "Size" )] public PortRef Size { get; set; }
/// <summary>The corner radius.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>The size used when nothing is connected.</summary>
[InlineValue( nameof( Size ) )] public Vector2 DefaultSize { get; set; } = new( 0.3f, 0.2f );
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.05f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.RoundedBox, Local( ctx ),
ctx.In( nameof( Size ), ctx.Const( DefaultSize ) ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ) ) );
}
}
/// <summary>The distance to a line segment. Always positive, so round or hollow it to give it width.</summary>
[NodeInfo( Id = "prism.sdf.segment", Title = "SDF Line", Category = "SDF/Shapes",
Icon = "timeline", Keywords = new[] { "sdf", "line", "segment", "capsule", "distance" },
Description = "Distance to the line segment between two points. Feed it to Round for a capsule." )]
[NodeVersion( 1 )]
public sealed class SdfSegmentNode : PrismNode
{
/// <summary>Where to evaluate.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>One end of the segment.</summary>
[In( "float2", Name = "A" )] public PortRef A { get; set; }
/// <summary>The other end of the segment.</summary>
[In( "float2", Name = "B" )] public PortRef B { get; set; }
/// <summary>Half the line's width.</summary>
[In( "float", Name = "Thickness" )] public PortRef Thickness { get; set; }
/// <summary>The first endpoint used when nothing is connected.</summary>
[InlineValue( nameof( A ) )] public Vector2 DefaultA { get; set; } = new( 0.2f, 0.5f );
/// <summary>The second endpoint used when nothing is connected.</summary>
[InlineValue( nameof( B ) )] public Vector2 DefaultB { get; set; } = new( 0.8f, 0.5f );
/// <summary>The thickness used when nothing is connected.</summary>
[InlineValue( nameof( Thickness ) )] public float DefaultThickness { get; set; } = 0.02f;
/// <summary>The signed distance to the thickened segment.</summary>
[Out( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var distance = ctx.Helper( PrismSdfHelpers.Segment,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( A ), ctx.Const( DefaultA ) ),
ctx.In( nameof( B ), ctx.Const( DefaultB ) ) );
ctx.Out( nameof( Distance ), ctx.Bin( BinaryOp.Sub, distance,
ctx.In( nameof( Thickness ), ctx.Const( DefaultThickness ) ) ) );
}
}
/// <summary>The signed distance to an equilateral triangle.</summary>
[NodeInfo( Id = "prism.sdf.triangle", Title = "SDF Triangle", Category = "SDF/Shapes",
Icon = "change_history", Keywords = new[] { "sdf", "triangle", "distance" },
Description = "Signed distance to an equilateral triangle." )]
[NodeVersion( 1 )]
public sealed class SdfTriangleNode : SdfShapeNode
{
/// <summary>The triangle's radius.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.3f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Triangle, Local( ctx ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ) ) );
}
}
/// <summary>The signed distance to a regular hexagon.</summary>
[NodeInfo( Id = "prism.sdf.hexagon", Title = "SDF Hexagon", Category = "SDF/Shapes",
Icon = "hexagon", Keywords = new[] { "sdf", "hexagon", "hex", "distance" },
Description = "Signed distance to a regular hexagon." )]
[NodeVersion( 1 )]
public sealed class SdfHexagonNode : SdfShapeNode
{
/// <summary>The hexagon's radius, measured to the flat edge.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.3f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Hexagon, Local( ctx ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ) ) );
}
}
/// <summary>The signed distance to a circular arc.</summary>
[NodeInfo( Id = "prism.sdf.arc", Title = "SDF Arc", Category = "SDF/Shapes",
Icon = "donut_large", Keywords = new[] { "sdf", "arc", "ring", "gauge", "distance" },
Description = "Signed distance to a circular arc of a given aperture, radius and thickness." )]
[NodeVersion( 1 )]
public sealed class SdfArcNode : SdfShapeNode
{
/// <summary>Half the angle the arc spans, in radians. Pi is a full ring.</summary>
[In( "float", Name = "Aperture" )] public PortRef Aperture { get; set; }
/// <summary>The radius of the arc's centre line.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>Half the arc's width.</summary>
[In( "float", Name = "Thickness" )] public PortRef Thickness { get; set; }
/// <summary>The aperture used when nothing is connected.</summary>
[InlineValue( nameof( Aperture ) )] public float DefaultAperture { get; set; } = 1.2f;
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.3f;
/// <summary>The thickness used when nothing is connected.</summary>
[InlineValue( nameof( Thickness ) )] public float DefaultThickness { get; set; } = 0.03f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Arc, Local( ctx ),
ctx.In( nameof( Aperture ), ctx.Const( DefaultAperture ) ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ),
ctx.In( nameof( Thickness ), ctx.Const( DefaultThickness ) ) ) );
}
}
/// <summary>The signed distance to a five-pointed star.</summary>
[NodeInfo( Id = "prism.sdf.star", Title = "SDF Star", Category = "SDF/Shapes",
Icon = "star", Keywords = new[] { "sdf", "star", "distance" },
Description = "Signed distance to a five-pointed star." )]
[NodeVersion( 1 )]
public sealed class SdfStarNode : SdfShapeNode
{
/// <summary>The radius of the star's points.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>How deep the notches cut, as a fraction of the radius.</summary>
[In( "float", Name = "Inset" )] public PortRef Inset { get; set; }
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.35f;
/// <summary>The inset used when nothing is connected.</summary>
[InlineValue( nameof( Inset ) )] public float DefaultInset { get; set; } = 0.45f;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Distance ), ctx.Helper( PrismSdfHelpers.Star, Local( ctx ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ),
ctx.In( nameof( Inset ), ctx.Const( DefaultInset ) ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Boolean combination
// ---------------------------------------------------------------------------------------------------
/// <summary>Shared shape for the three hard boolean combines.</summary>
public abstract class SdfBooleanNode : PrismNode
{
/// <summary>The first distance.</summary>
[In( "float", Name = "A" )] public PortRef A { get; set; }
/// <summary>The second distance.</summary>
[In( "float", Name = "B" )] public PortRef B { get; set; }
/// <summary>The distance used for A when nothing is connected.</summary>
[InlineValue( nameof( A ) )] public float DefaultA { get; set; } = 1f;
/// <summary>The distance used for B when nothing is connected.</summary>
[InlineValue( nameof( B ) )] public float DefaultB { get; set; } = 1f;
/// <summary>The combined distance.</summary>
[Out( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>Combine two distances.</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 = ctx.In( nameof( A ), ctx.Const( DefaultA ) );
var b = ctx.In( nameof( B ), ctx.Const( DefaultB ) );
if ( !a.IsValid || !b.IsValid ) return;
ctx.Out( nameof( Distance ), Combine( ctx, a, b ) );
}
}
/// <summary>The union of two shapes: everything either one covers.</summary>
[NodeInfo( Id = "prism.sdf.union", Title = "SDF Union", Category = "SDF/Combine",
Icon = "join_full", Keywords = new[] { "sdf", "union", "or", "add", "combine" },
Description = "The union of two distance fields: the nearer of the two surfaces." )]
[NodeVersion( 1 )]
public sealed class SdfUnionNode : SdfBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) =>
ctx.Call( Intrinsic.Min, a, b );
}
/// <summary>The intersection of two shapes: only where both cover.</summary>
[NodeInfo( Id = "prism.sdf.intersect", Title = "SDF Intersect", Category = "SDF/Combine",
Icon = "join_inner", Keywords = new[] { "sdf", "intersect", "and", "combine" },
Description = "The intersection of two distance fields: the further of the two surfaces." )]
[NodeVersion( 1 )]
public sealed class SdfIntersectNode : SdfBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) =>
ctx.Call( Intrinsic.Max, a, b );
}
/// <summary>The first shape with the second cut out of it.</summary>
[NodeInfo( Id = "prism.sdf.subtract", Title = "SDF Subtract", Category = "SDF/Combine",
Icon = "join_left", Keywords = new[] { "sdf", "subtract", "difference", "cut", "combine" },
Description = "Cuts the second distance field out of the first." )]
[NodeVersion( 1 )]
public sealed class SdfSubtractNode : SdfBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) =>
ctx.Call( Intrinsic.Max, a, ctx.Un( UnaryOp.Negate, b ) );
}
/// <summary>Shared shape for the three smoothed boolean combines.</summary>
public abstract class SdfSmoothBooleanNode : PrismNode
{
/// <summary>The first distance.</summary>
[In( "float", Name = "A" )] public PortRef A { get; set; }
/// <summary>The second distance.</summary>
[In( "float", Name = "B" )] public PortRef B { get; set; }
/// <summary>How wide the join is rounded, in distance units.</summary>
[In( "float", Name = "Smoothing" )] public PortRef Smoothing { get; set; }
/// <summary>The distance used for A when nothing is connected.</summary>
[InlineValue( nameof( A ) )] public float DefaultA { get; set; } = 1f;
/// <summary>The distance used for B when nothing is connected.</summary>
[InlineValue( nameof( B ) )] public float DefaultB { get; set; } = 1f;
/// <summary>The smoothing used when nothing is connected.</summary>
[InlineValue( nameof( Smoothing ) )] public float DefaultSmoothing { get; set; } = 0.1f;
/// <summary>Which curve rounds the join.</summary>
public PrismSmoothKind Kind { get; set; } = PrismSmoothKind.Polynomial;
/// <summary>The combined distance.</summary>
[Out( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>Combine two distances with the chosen smoothing curve.</summary>
protected abstract IrValue Combine( EmitContext ctx, IrValue a, IrValue b, IrValue k );
/// <summary>The smooth minimum of two distances.</summary>
protected IrValue SmoothMin( EmitContext ctx, IrValue a, IrValue b, IrValue k ) =>
ctx.Helper( PrismSdfHelpers.SmoothMin( Kind ), a, b, k );
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var a = ctx.In( nameof( A ), ctx.Const( DefaultA ) );
var b = ctx.In( nameof( B ), ctx.Const( DefaultB ) );
var k = ctx.In( nameof( Smoothing ), ctx.Const( DefaultSmoothing ) );
if ( !a.IsValid || !b.IsValid || !k.IsValid ) return;
ctx.Out( nameof( Distance ), Combine( ctx, a, b, k ) );
}
}
/// <summary>The union of two shapes, with the join rounded off.</summary>
[NodeInfo( Id = "prism.sdf.smoothUnion", Title = "SDF Smooth Union", Category = "SDF/Combine",
Icon = "blur_on", Keywords = new[] { "sdf", "smooth", "union", "smin", "blend" },
Description = "Unions two distance fields and rounds the seam between them." )]
[NodeVersion( 1 )]
public sealed class SdfSmoothUnionNode : SdfSmoothBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b, IrValue k ) =>
SmoothMin( ctx, a, b, k );
}
/// <summary>The intersection of two shapes, with the join rounded off.</summary>
[NodeInfo( Id = "prism.sdf.smoothIntersect", Title = "SDF Smooth Intersect", Category = "SDF/Combine",
Icon = "blur_circular", Keywords = new[] { "sdf", "smooth", "intersect", "smax", "blend" },
Description = "Intersects two distance fields and rounds the seam between them." )]
[NodeVersion( 1 )]
public sealed class SdfSmoothIntersectNode : SdfSmoothBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b, IrValue k ) =>
ctx.Un( UnaryOp.Negate,
SmoothMin( ctx, ctx.Un( UnaryOp.Negate, a ), ctx.Un( UnaryOp.Negate, b ), k ) );
}
/// <summary>The first shape with the second cut out, with the join rounded off.</summary>
[NodeInfo( Id = "prism.sdf.smoothSubtract", Title = "SDF Smooth Subtract", Category = "SDF/Combine",
Icon = "blur_linear", Keywords = new[] { "sdf", "smooth", "subtract", "carve", "blend" },
Description = "Cuts the second distance field out of the first and rounds the seam." )]
[NodeVersion( 1 )]
public sealed class SdfSmoothSubtractNode : SdfSmoothBooleanNode
{
/// <inheritdoc/>
protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b, IrValue k ) =>
ctx.Un( UnaryOp.Negate, SmoothMin( ctx, ctx.Un( UnaryOp.Negate, a ), b, k ) );
}
// ---------------------------------------------------------------------------------------------------
// Modifiers
// ---------------------------------------------------------------------------------------------------
/// <summary>Inflates a shape by a radius, rounding every corner it has.</summary>
[NodeInfo( Id = "prism.sdf.round", Title = "SDF Round", Category = "SDF/Modify",
Icon = "rounded_corner", Keywords = new[] { "sdf", "round", "inflate", "grow", "offset" },
Description = "Grows a distance field outwards, which rounds off all of its corners at once." )]
[NodeVersion( 1 )]
public sealed class SdfRoundNode : PrismNode
{
/// <summary>The distance to inflate.</summary>
[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>How far to inflate. Negative shrinks the shape instead.</summary>
[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }
/// <summary>The distance used when nothing is connected.</summary>
[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; }
/// <summary>The radius used when nothing is connected.</summary>
[InlineValue( nameof( Radius ) )] public float DefaultRadius { get; set; } = 0.05f;
/// <summary>The inflated distance.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Sub,
ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) ),
ctx.In( nameof( Radius ), ctx.Const( DefaultRadius ) ) ) );
}
}
/// <summary>Hollows a shape into a shell of a given thickness.</summary>
[NodeInfo( Id = "prism.sdf.annular", Title = "SDF Annular", Category = "SDF/Modify",
Icon = "radio_button_unchecked", Keywords = new[] { "sdf", "annular", "shell", "hollow", "ring" },
Description = "Turns a solid distance field into a shell of a given thickness." )]
[NodeVersion( 1 )]
public sealed class SdfAnnularNode : PrismNode
{
/// <summary>The distance to hollow.</summary>
[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>Half the shell's thickness.</summary>
[In( "float", Name = "Thickness" )] public PortRef Thickness { get; set; }
/// <summary>The distance used when nothing is connected.</summary>
[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; }
/// <summary>The thickness used when nothing is connected.</summary>
[InlineValue( nameof( Thickness ) )] public float DefaultThickness { get; set; } = 0.02f;
/// <summary>The hollowed distance.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Sub,
ctx.Call( Intrinsic.Abs, ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) ) ),
ctx.In( nameof( Thickness ), ctx.Const( DefaultThickness ) ) ) );
}
}
/// <summary>Repeats a shell inwards, so one shape becomes a set of concentric rings.</summary>
[NodeInfo( Id = "prism.sdf.onion", Title = "SDF Onion", Category = "SDF/Modify",
Icon = "album", Keywords = new[] { "sdf", "onion", "rings", "layers", "contour" },
Description = "Repeating shells at a fixed spacing: contour lines of a distance field." )]
[NodeVersion( 1 )]
public sealed class SdfOnionNode : PrismNode
{
/// <summary>The distance to slice.</summary>
[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>How far apart the shells sit.</summary>
[In( "float", Name = "Spacing" )] public PortRef Spacing { get; set; }
/// <summary>Half the thickness of each shell.</summary>
[In( "float", Name = "Thickness" )] public PortRef Thickness { get; set; }
/// <summary>The distance used when nothing is connected.</summary>
[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; }
/// <summary>The spacing used when nothing is connected.</summary>
[InlineValue( nameof( Spacing ) )] public float DefaultSpacing { get; set; } = 0.1f;
/// <summary>The thickness used when nothing is connected.</summary>
[InlineValue( nameof( Thickness ) )] public float DefaultThickness { get; set; } = 0.01f;
/// <summary>The shelled distance.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var distance = ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) );
var spacing = ctx.In( nameof( Spacing ), ctx.Const( DefaultSpacing ) );
var thickness = ctx.In( nameof( Thickness ), ctx.Const( DefaultThickness ) );
// Fold the distance into one period, centred on zero, then hollow it.
var half = ctx.Bin( BinaryOp.Mul, spacing, ctx.Const( 0.5f ) );
var folded = ctx.Bin( BinaryOp.Sub, ctx.Call( Intrinsic.Fmod,
ctx.Bin( BinaryOp.Add, ctx.Call( Intrinsic.Abs, distance ), half ), spacing ), half );
ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Sub, ctx.Call( Intrinsic.Abs, folded ), thickness ) );
}
}
/// <summary>Folds a coordinate into a repeating cell, so one primitive tiles across the plane.</summary>
[NodeInfo( Id = "prism.sdf.repeat", Title = "SDF Repeat", Category = "SDF/Modify",
Icon = "grid_view", Keywords = new[] { "sdf", "repeat", "tile", "domain", "mod" },
Description = "Domain repetition: folds a coordinate into one cell and reports which cell it came from." )]
[NodeVersion( 1 )]
public sealed class SdfRepeatNode : PrismNode
{
/// <summary>The coordinate to fold.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The size of one cell.</summary>
[In( "float2", Name = "Spacing" )] public PortRef Spacing { get; set; }
/// <summary>The spacing used when nothing is connected.</summary>
[InlineValue( nameof( Spacing ) )] public Vector2 DefaultSpacing { get; set; } = new( 0.25f, 0.25f );
/// <summary>The coordinate, folded into one cell and centred on it.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>Which cell the coordinate came from.</summary>
[Out( "float2", Name = "Cell" )] public PortRef Cell { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var packed = ctx.Helper( PrismSdfHelpers.Repeat,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Spacing ), ctx.Const( DefaultSpacing ) ) );
ctx.Out( nameof( Out ), ctx.Swizzle( packed, "xy" ) );
ctx.Out( nameof( Cell ), ctx.Swizzle( packed, "zw" ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------------------------------------------
/// <summary>Turns a signed distance into a coverage mask.</summary>
[NodeInfo( Id = "prism.sdf.mask", Title = "SDF Mask", Category = "SDF/Output",
Icon = "opacity", Keywords = new[] { "sdf", "mask", "fill", "coverage", "antialias" },
Description = "Fills a distance field: one inside, zero outside, with an antialiased edge." )]
[NodeVersion( 1 )]
public sealed class SdfMaskNode : PrismNode, IStageConstrained
{
/// <summary>The distance to fill.</summary>
[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>How wide the soft edge is, when filtering is set to Soft.</summary>
[In( "float", Name = "Softness" )] public PortRef Softness { get; set; }
/// <summary>The distance used when nothing is connected.</summary>
[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; }
/// <summary>The softness used when nothing is connected.</summary>
[InlineValue( nameof( Softness ) )] public float DefaultSoftness { get; set; } = 0.01f;
/// <summary>How the edge is antialiased.</summary>
public PrismSdfFiltering Filtering { get; set; } = PrismSdfFiltering.Analytic;
/// <summary>The coverage mask.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages =>
Filtering == PrismSdfFiltering.Analytic ? StageMask.Pixel : StageMask.All;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var distance = ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) );
ctx.Out( nameof( Out ), PrismSdfSupport.Mask( ctx, distance, Filtering,
ctx.In( nameof( Softness ), ctx.Const( DefaultSoftness ) ) ) );
}
}
/// <summary>Turns a signed distance into an outline around its boundary.</summary>
[NodeInfo( Id = "prism.sdf.outline", Title = "SDF Outline", Category = "SDF/Output",
Icon = "border_style", Keywords = new[] { "sdf", "outline", "stroke", "border", "edge" },
Description = "Draws a stroke of a given width along the zero contour of a distance field." )]
[NodeVersion( 1 )]
public sealed class SdfOutlineNode : PrismNode, IStageConstrained
{
/// <summary>The distance to stroke.</summary>
[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }
/// <summary>How wide the stroke is.</summary>
[In( "float", Name = "Width" )] public PortRef Width { get; set; }
/// <summary>How wide the soft edge is, when filtering is set to Soft.</summary>
[In( "float", Name = "Softness" )] public PortRef Softness { get; set; }
/// <summary>The distance used when nothing is connected.</summary>
[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; }
/// <summary>The width used when nothing is connected.</summary>
[InlineValue( nameof( Width ) )] public float DefaultWidth { get; set; } = 0.02f;
/// <summary>The softness used when nothing is connected.</summary>
[InlineValue( nameof( Softness ) )] public float DefaultSoftness { get; set; } = 0.005f;
/// <summary>How the edge is antialiased.</summary>
public PrismSdfFiltering Filtering { get; set; } = PrismSdfFiltering.Analytic;
/// <summary>The stroke mask.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages =>
Filtering == PrismSdfFiltering.Analytic ? StageMask.Pixel : StageMask.All;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var distance = ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) );
var stroke = ctx.Bin( BinaryOp.Sub, ctx.Call( Intrinsic.Abs, distance ),
ctx.In( nameof( Width ), ctx.Const( DefaultWidth ) ) );
ctx.Out( nameof( Out ), PrismSdfSupport.Mask( ctx, stroke, Filtering,
ctx.In( nameof( Softness ), ctx.Const( DefaultSoftness ) ) ) );
}
}
/// <summary>Shared plumbing for turning distances into pixels.</summary>
internal static class PrismSdfSupport
{
/// <summary>Convert a signed distance into coverage with the chosen filtering.</summary>
internal static IrValue Mask( EmitContext ctx, IrValue distance, PrismSdfFiltering filtering, IrValue softness )
{
if ( !distance.IsValid ) return distance;
return filtering switch
{
PrismSdfFiltering.Hard => ctx.Call( Intrinsic.Step, distance, ctx.Const( 0f ) ),
PrismSdfFiltering.Soft => ctx.Helper( PrismCommon.SdfMaskSoft, distance, softness ),
_ => ctx.Helper( PrismCommon.SdfMask, distance )
};
}
}