Editor Prism UV node definitions and HLSL helper functions. Declares enums for channels, wrap modes and angle units, provides HLSL helper bodies used to generate shader code, and many Prism nodes that emit IR for UV manipulation (tile/offset, rotate, panner, twirl, polar, flipbook, grid, wrap, parallax, etc.).
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// UV manipulation.
//
// Everything here takes a coordinate and returns a coordinate, so the nodes chain end to end. Each one
// falls back to the mesh's first UV set when nothing is wired to it, which means dropping a Twirl on an
// empty canvas already does something visible.
// ---------------------------------------------------------------------------------------------------
/// <summary>Which UV set a coordinate node reads.</summary>
public enum PrismUvChannel
{
/// <summary>The mesh's primary texture coordinates.</summary>
UV0,
/// <summary>The mesh's secondary texture coordinates, usually the lightmap set.</summary>
UV1
}
/// <summary>How a coordinate outside the 0..1 range is folded back into it.</summary>
public enum PrismUvWrapMode
{
/// <summary>Repeat: the fractional part.</summary>
Wrap,
/// <summary>Clamp to the edge.</summary>
Clamp,
/// <summary>Mirror on every repeat.</summary>
Mirror,
/// <summary>Mirror once, then clamp.</summary>
MirrorOnce,
/// <summary>Leave the coordinate alone and report which texels fell outside.</summary>
Border
}
/// <summary>Whether an angle is authored in radians or degrees.</summary>
public enum PrismAngleUnit
{
/// <summary>Radians.</summary>
Radians,
/// <summary>Degrees.</summary>
Degrees
}
/// <summary>The HLSL bodies the UV family needs.</summary>
internal static class PrismUvHelpers
{
/// <summary>Rotate a coordinate about a centre, in radians.</summary>
internal static readonly HelperFunction Rotate = new( "Prism_RotateUv", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flRotation", ShaderType.Float )
] )
{
Hlsl = """
float2 Prism_RotateUv( float2 vUv, float2 vCenter, float flRotation )
{
vUv -= vCenter;
float s = sin( flRotation );
float c = cos( flRotation );
float2x2 m = float2x2( c, -s, s, c );
vUv = mul( vUv, m );
return vUv + vCenter;
}
"""
};
/// <summary>Cartesian to polar: x is the radius, y is the angle normalised to 0..1.</summary>
internal static readonly HelperFunction Polar = new( "Prism_PolarCoordinates", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flRadialScale", ShaderType.Float ),
new HelperParam( "flLengthScale", ShaderType.Float )
] )
{
Hlsl = """
float2 Prism_PolarCoordinates( float2 vUv, float2 vCenter, float flRadialScale, float flLengthScale )
{
float2 vDelta = vUv - vCenter;
float flRadius = length( vDelta ) * 2.0f * flRadialScale;
float flAngle = atan2( vDelta.x, vDelta.y ) * 0.15915494309f * flLengthScale;
return float2( flRadius, flAngle );
}
"""
};
/// <summary>Polar back to cartesian, for unwrapping an effect authored in polar space.</summary>
internal static readonly HelperFunction InversePolar = new( "Prism_InversePolarCoordinates", ShaderType.Float2,
[
new HelperParam( "vPolar", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 )
] )
{
Hlsl = """
float2 Prism_InversePolarCoordinates( float2 vPolar, float2 vCenter )
{
float flAngle = vPolar.y * 6.28318530718f;
return vCenter + float2( sin( flAngle ), cos( flAngle ) ) * ( vPolar.x * 0.5f );
}
"""
};
/// <summary>Rotate by an amount that grows with the distance from the centre.</summary>
internal static readonly HelperFunction Twirl = new( "Prism_Twirl", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flStrength", ShaderType.Float ),
new HelperParam( "vOffset", ShaderType.Float2 )
] )
{
Hlsl = """
float2 Prism_Twirl( float2 vUv, float2 vCenter, float flStrength, float2 vOffset )
{
float2 vDelta = vUv - vCenter;
float flAngle = flStrength * length( vDelta );
float x = cos( flAngle ) * vDelta.x - sin( flAngle ) * vDelta.y;
float y = sin( flAngle ) * vDelta.x + cos( flAngle ) * vDelta.y;
return float2( x + vCenter.x + vOffset.x, y + vCenter.y + vOffset.y );
}
"""
};
/// <summary>Shear tangentially, proportional to the squared distance from the centre.</summary>
internal static readonly HelperFunction RadialShear = new( "Prism_RadialShear", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flStrength", ShaderType.Float ),
new HelperParam( "vOffset", ShaderType.Float2 )
] )
{
Hlsl = """
float2 Prism_RadialShear( float2 vUv, float2 vCenter, float flStrength, float2 vOffset )
{
float2 vDelta = vUv - vCenter;
float flDelta2 = dot( vDelta.xy, vDelta.xy );
float2 vDeltaOffset = flDelta2 * flStrength;
return vUv + float2( vDelta.y, -vDelta.x ) * vDeltaOffset + vOffset;
}
"""
};
/// <summary>Push the coordinate outwards with the fourth power of the radius: a lens bulge.</summary>
internal static readonly HelperFunction Spherize = new( "Prism_Spherize", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flStrength", ShaderType.Float ),
new HelperParam( "vOffset", ShaderType.Float2 )
] )
{
Hlsl = """
float2 Prism_Spherize( float2 vUv, float2 vCenter, float flStrength, float2 vOffset )
{
float2 vDelta = vUv - vCenter;
float flDelta2 = dot( vDelta.xy, vDelta.xy );
float flDelta4 = flDelta2 * flDelta2;
float2 vDeltaOffset = flDelta4 * flStrength;
return vUv + vDelta * vDeltaOffset + vOffset;
}
"""
};
/// <summary>Barrel and pincushion distortion, quadratic in the radius.</summary>
internal static readonly HelperFunction Fisheye = new( "Prism_Fisheye", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCenter", ShaderType.Float2 ),
new HelperParam( "flStrength", ShaderType.Float )
] )
{
Hlsl = """
float2 Prism_Fisheye( float2 vUv, float2 vCenter, float flStrength )
{
float2 vDelta = vUv - vCenter;
float flRadius2 = dot( vDelta, vDelta );
return vCenter + vDelta * ( 1.0f + flStrength * flRadius2 );
}
"""
};
/// <summary>Remap a coordinate into one cell of a sprite sheet.</summary>
internal static readonly HelperFunction Flipbook = new( "Prism_Flipbook", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "flWidth", ShaderType.Float ),
new HelperParam( "flHeight", ShaderType.Float ),
new HelperParam( "flTile", ShaderType.Float ),
new HelperParam( "vInvert", ShaderType.Float2 )
] )
{
Hlsl = """
float2 Prism_Flipbook( float2 vUv, float flWidth, float flHeight, float flTile, float2 vInvert )
{
flTile = floor( fmod( flTile + 0.00001f, flWidth * flHeight ) );
float2 vTileCount = float2( 1.0f, 1.0f ) / float2( flWidth, flHeight );
float flBase = floor( ( flTile + 0.5f ) * vTileCount.x );
float flTileX = flTile - flWidth * flBase;
float flTileY = vInvert.y * flHeight - ( flBase + vInvert.y * 1.0f );
vUv.x = lerp( vUv.x, 1.0f - vUv.x, vInvert.x );
return ( vUv + float2( flTileX, flTileY ) ) * vTileCount;
}
"""
};
/// <summary>
/// Offset a coordinate along the tangent-space view direction by a height, the classic bump offset.
/// The reference plane decides which height leaves the coordinate untouched.
/// </summary>
internal static readonly HelperFunction BumpOffset = new( "Prism_BumpOffset", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "flHeight", ShaderType.Float ),
new HelperParam( "vViewDirTs", ShaderType.Float3 ),
new HelperParam( "flAmplitude", ShaderType.Float ),
new HelperParam( "flRefPlane", ShaderType.Float )
] )
{
Hlsl = """
float2 Prism_BumpOffset( float2 vUv, float flHeight, float3 vViewDirTs, float flAmplitude, float flRefPlane )
{
float3 V = normalize( vViewDirTs );
return vUv + ( flHeight - flRefPlane ) * flAmplitude * ( V.xy / max( abs( V.z ), 1e-4f ) );
}
"""
};
/// <summary>
/// Fold a coordinate back into 0..1. The mode is passed per axis as a number, matching the way the
/// engine's own scene-colour node expresses addressing it cannot hand to a sampler.
/// </summary>
internal static readonly HelperFunction Wrap = new( "Prism_WrapUv", ShaderType.Float2,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vModes", ShaderType.Float2 )
] )
{
Hlsl = """
float Prism_WrapUvAxis( float flUv, float flMode )
{
// 0 wrap, 1 clamp, 2 mirror, 3 mirror once, 4 border.
if ( flMode < 0.5f )
return frac( flUv );
if ( flMode < 1.5f )
return saturate( flUv );
if ( flMode < 2.5f )
{
float flT = frac( flUv * 0.5f ) * 2.0f;
return 1.0f - abs( flT - 1.0f );
}
if ( flMode < 3.5f )
return saturate( abs( flUv ) );
return flUv;
}
float2 Prism_WrapUv( float2 vUv, float2 vModes )
{
return float2( Prism_WrapUvAxis( vUv.x, vModes.x ), Prism_WrapUvAxis( vUv.y, vModes.y ) );
}
"""
};
/// <summary>One where the coordinate lies inside 0..1 on both axes, zero outside.</summary>
internal static readonly HelperFunction InsideMask = new( "Prism_UvInside", ShaderType.Float,
[new HelperParam( "vUv", ShaderType.Float2 )] )
{
Hlsl = """
float Prism_UvInside( float2 vUv )
{
float2 vInside = step( 0.0f, vUv ) * step( vUv, 1.0f );
return vInside.x * vInside.y;
}
"""
};
/// <summary>
/// Split a coordinate into a cell index and a coordinate local to that cell. Returns
/// <c>xy</c> = the local coordinate, <c>zw</c> = the cell index.
/// </summary>
internal static readonly HelperFunction Grid = new( "Prism_UvGrid", ShaderType.Float4,
[
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vCount", ShaderType.Float2 )
] )
{
Hlsl = """
float4 Prism_UvGrid( float2 vUv, float2 vCount )
{
float2 vScaled = vUv * vCount;
float2 vCell = floor( vScaled );
return float4( vScaled - vCell, vCell );
}
"""
};
}
// ---------------------------------------------------------------------------------------------------
// Sources
// ---------------------------------------------------------------------------------------------------
/// <summary>The mesh's texture coordinates, with an optional tile and offset folded in.</summary>
[NodeInfo( Id = "prism.uv.texcoord", Title = "UV", Category = "UV",
Icon = "grid_on", Keywords = new[] { "uv", "texcoord", "coordinate", "uv0", "uv1" },
Description = "The mesh's texture coordinates, optionally scaled and shifted." )]
[NodeVersion( 1 )]
public sealed class UvChannelNode : PrismNode
{
/// <summary>Which UV set to read.</summary>
public PrismUvChannel Channel { get; set; } = PrismUvChannel.UV0;
/// <summary>How many times the coordinate repeats.</summary>
[In( "float2", Name = "Tile" )] public PortRef Tile { get; set; }
/// <summary>How far the coordinate is shifted.</summary>
[In( "float2", Name = "Offset" )] public PortRef Offset { get; set; }
/// <summary>The tiling used when nothing is connected.</summary>
[InlineValue( nameof( Tile ) )] public Vector2 DefaultTile { get; set; } = new( 1f, 1f );
/// <summary>The offset used when nothing is connected.</summary>
[InlineValue( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; }
/// <summary>The coordinate.</summary>
[Out( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Horizontal component.</summary>
[Out( "float", Name = "U" )] public PortRef U { get; set; }
/// <summary>Vertical component.</summary>
[Out( "float", Name = "V" )] public PortRef V { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var source = ctx.Builtin( Channel == PrismUvChannel.UV1 ? Builtin.TexCoord1 : Builtin.TexCoord0 );
var tile = ctx.In( nameof( Tile ), ctx.Const( DefaultTile ) );
var offset = ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) );
var uv = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, source, tile ), offset );
ctx.Out( nameof( UV ), uv );
ctx.Out( nameof( U ), ctx.Swizzle( uv, "x" ) );
ctx.Out( nameof( V ), ctx.Swizzle( uv, "y" ) );
}
}
/// <summary>The pixel's position on screen, normalised to 0..1 across the viewport.</summary>
[NodeInfo( Id = "prism.uv.screen", Title = "Screen UV", Category = "UV",
Icon = "aspect_ratio", Keywords = new[] { "screen", "viewport", "uv", "fullscreen" },
Description = "Viewport-relative screen coordinates, 0..1 across the render target." )]
[NodeVersion( 1 )]
public sealed class ScreenUvNode : PrismNode
{
/// <summary>True to scale the coordinate so a square stays square on a non-square viewport.</summary>
public bool CorrectAspect { get; set; }
/// <summary>The screen coordinate.</summary>
[Out( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var uv = ctx.Builtin( Builtin.ScreenUv );
if ( !CorrectAspect )
{
ctx.Out( nameof( UV ), uv );
return;
}
var size = ctx.Builtin( Builtin.ViewportSize );
var aspect = ctx.Bin( BinaryOp.Div, ctx.Swizzle( size, "x" ), ctx.Swizzle( size, "y" ) );
ctx.Out( nameof( UV ), ctx.Bin( BinaryOp.Mul, uv, ctx.Construct( ShaderType.Float2, aspect, ctx.Const( 1f ) ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Affine
// ---------------------------------------------------------------------------------------------------
/// <summary>Scales and shifts a coordinate. The one every graph starts with.</summary>
[NodeInfo( Id = "prism.uv.tileAndOffset", Title = "Tile And Offset", Category = "UV",
Icon = "grid_view", Keywords = new[] { "tile", "offset", "scale", "repeat", "uv" },
Description = "Multiplies a coordinate by a tile factor and adds an offset." )]
[NodeVersion( 1 )]
public sealed class TileAndOffsetNode : PrismNode
{
/// <summary>The coordinate to transform. Defaults to the mesh's first UV set.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How many times it repeats.</summary>
[In( "float2", Name = "Tile" )] public PortRef Tile { get; set; }
/// <summary>How far it shifts.</summary>
[In( "float2", Name = "Offset" )] public PortRef Offset { get; set; }
/// <summary>The tiling used when nothing is connected.</summary>
[InlineValue( nameof( Tile ) )] public Vector2 DefaultTile { get; set; } = new( 1f, 1f );
/// <summary>The offset used when nothing is connected.</summary>
[InlineValue( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; }
/// <summary>True to fold the result back into 0..1.</summary>
public bool WrapTo01 { get; set; }
/// <summary>The transformed coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var uv = PrismUvSupport.Coordinate( ctx, nameof( UV ) );
var tile = ctx.In( nameof( Tile ), ctx.Const( DefaultTile ) );
var offset = ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) );
var result = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, uv, tile ), offset );
if ( WrapTo01 ) result = ctx.Call( Intrinsic.Frac, result );
ctx.Out( nameof( Out ), result );
}
}
/// <summary>Rotates a coordinate about a centre.</summary>
[NodeInfo( Id = "prism.uv.rotate", Title = "Rotate UV", Category = "UV",
Icon = "rotate_right", Keywords = new[] { "rotate", "spin", "angle", "uv" },
Description = "Rotates a coordinate about a pivot." )]
[NodeVersion( 1 )]
public sealed class RotateUvNode : PrismNode
{
/// <summary>The coordinate to rotate.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The pivot to rotate about.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>How far to rotate.</summary>
[In( "float", Name = "Rotation" )] public PortRef Rotation { get; set; }
/// <summary>The pivot used when nothing is connected.</summary>
[InlineValue( nameof( Center ) )] public Vector2 DefaultCenter { get; set; } = new( 0.5f, 0.5f );
/// <summary>The rotation used when nothing is connected.</summary>
[InlineValue( nameof( Rotation ) )] public float DefaultRotation { get; set; }
/// <summary>Whether the rotation is authored in radians or degrees.</summary>
public PrismAngleUnit Unit { get; set; } = PrismAngleUnit.Radians;
/// <summary>The rotated coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var rotation = ctx.In( nameof( Rotation ), ctx.Const( DefaultRotation ) );
if ( Unit == PrismAngleUnit.Degrees ) rotation = ctx.Call( Intrinsic.Radians, rotation );
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Rotate,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
rotation ) );
}
}
/// <summary>Scrolls a coordinate over time.</summary>
[NodeInfo( Id = "prism.uv.panner", Title = "Panner", Category = "UV",
Icon = "swipe", Keywords = new[] { "pan", "scroll", "flow", "time", "animate" },
Description = "Scrolls a coordinate at a constant speed. Wire Time or leave it for the engine clock." )]
[NodeVersion( 1 )]
public sealed class PannerNode : PrismNode
{
/// <summary>The coordinate to scroll.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How fast it scrolls, per second, on each axis.</summary>
[In( "float2", Name = "Speed" )] public PortRef Speed { get; set; }
/// <summary>The clock to scroll against. Defaults to the engine's time.</summary>
[In( "float", Name = "Time" )] public PortRef Time { get; set; }
/// <summary>The speed used when nothing is connected.</summary>
[InlineValue( nameof( Speed ) )] public Vector2 DefaultSpeed { get; set; } = new( 0.1f, 0f );
/// <summary>True to fold the result back into 0..1, so a non-repeating sampler still tiles.</summary>
public bool WrapTo01 { get; set; }
/// <summary>The scrolled coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var time = ctx.TryIn( nameof( Time ), out var wired ) ? wired : ctx.Builtin( Builtin.Time );
var speed = ctx.In( nameof( Speed ), ctx.Const( DefaultSpeed ) );
var result = ctx.Bin( BinaryOp.Add, PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.Bin( BinaryOp.Mul, speed, time ) );
if ( WrapTo01 ) result = ctx.Call( Intrinsic.Frac, result );
ctx.Out( nameof( Out ), result );
}
}
/// <summary>Spins a coordinate over time.</summary>
[NodeInfo( Id = "prism.uv.rotator", Title = "Rotator", Category = "UV",
Icon = "autorenew", Keywords = new[] { "rotate", "spin", "time", "animate", "rotator" },
Description = "Rotates a coordinate about a pivot at a constant angular speed." )]
[NodeVersion( 1 )]
public sealed class RotatorNode : PrismNode
{
/// <summary>The coordinate to spin.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The pivot to spin about.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>Turns per second.</summary>
[In( "float", Name = "Speed" )] public PortRef Speed { get; set; }
/// <summary>The clock to spin against. Defaults to the engine's time.</summary>
[In( "float", Name = "Time" )] public PortRef Time { get; set; }
/// <summary>The pivot used when nothing is connected.</summary>
[InlineValue( nameof( Center ) )] public Vector2 DefaultCenter { get; set; } = new( 0.5f, 0.5f );
/// <summary>The speed used when nothing is connected.</summary>
[InlineValue( nameof( Speed ) )] public float DefaultSpeed { get; set; } = 0.25f;
/// <summary>The spinning coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var time = ctx.TryIn( nameof( Time ), out var wired ) ? wired : ctx.Builtin( Builtin.Time );
var speed = ctx.In( nameof( Speed ), ctx.Const( DefaultSpeed ) );
// One unit of speed is one full turn per second, which is what an artist expects from "speed".
var rotation = ctx.Bin( BinaryOp.Mul, ctx.Bin( BinaryOp.Mul, speed, time ), ctx.Const( 6.28318530718f ) );
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Rotate,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
rotation ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Distortion
// ---------------------------------------------------------------------------------------------------
/// <summary>Converts a coordinate to polar space: radius on x, angle on y.</summary>
[NodeInfo( Id = "prism.uv.polar", Title = "Polar Coordinates", Category = "UV",
Icon = "donut_large", Keywords = new[] { "polar", "radial", "angle", "radius" },
Description = "Cartesian to polar. X becomes the distance from the centre, Y the angle in 0..1." )]
[NodeVersion( 1 )]
public sealed class PolarCoordinatesNode : PrismNode
{
/// <summary>The coordinate to convert.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The origin of the polar space.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>Scales the radius.</summary>
[In( "float", Name = "Radial Scale" )] public PortRef RadialScale { get; set; }
/// <summary>Scales the angle, so a value of four gives four repeats around the circle.</summary>
[In( "float", Name = "Length Scale" )] public PortRef LengthScale { 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 radial scale used when nothing is connected.</summary>
[InlineValue( nameof( RadialScale ) )] public float DefaultRadialScale { get; set; } = 1f;
/// <summary>The length scale used when nothing is connected.</summary>
[InlineValue( nameof( LengthScale ) )] public float DefaultLengthScale { get; set; } = 1f;
/// <summary>The polar coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Polar,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
ctx.In( nameof( RadialScale ), ctx.Const( DefaultRadialScale ) ),
ctx.In( nameof( LengthScale ), ctx.Const( DefaultLengthScale ) ) ) );
}
}
/// <summary>Converts a polar coordinate back to cartesian.</summary>
[NodeInfo( Id = "prism.uv.polarInverse", Title = "Inverse Polar Coordinates", Category = "UV",
Icon = "donut_small", Keywords = new[] { "polar", "inverse", "unwrap", "cartesian" },
Description = "Polar to cartesian. Undoes Polar Coordinates, for effects authored in radius and angle." )]
[NodeVersion( 1 )]
public sealed class InversePolarCoordinatesNode : PrismNode
{
/// <summary>The polar coordinate: radius on x, angle in 0..1 on y.</summary>
[In( "float2", Name = "Polar" )] public PortRef Polar { get; set; }
/// <summary>The origin to unwrap around.</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 cartesian coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.InversePolar,
PrismUvSupport.Coordinate( ctx, nameof( Polar ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ) ) );
}
}
/// <summary>Twists a coordinate around a centre, harder the further out you go.</summary>
[NodeInfo( Id = "prism.uv.twirl", Title = "Twirl", Category = "UV",
Icon = "cyclone", Keywords = new[] { "twirl", "swirl", "vortex", "whirl" },
Description = "Rotates a coordinate by an amount proportional to its distance from the centre." )]
[NodeVersion( 1 )]
public sealed class TwirlNode : PrismNode
{
/// <summary>The coordinate to twist.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The centre of the twist.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>How hard it twists.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>A constant shift applied afterwards.</summary>
[In( "float2", Name = "Offset" )] public PortRef Offset { 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 strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 10f;
/// <summary>The offset used when nothing is connected.</summary>
[InlineValue( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; }
/// <summary>The twisted coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Twirl,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ),
ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) ) ) );
}
}
/// <summary>Shears a coordinate tangentially about a centre.</summary>
[NodeInfo( Id = "prism.uv.radialShear", Title = "Radial Shear", Category = "UV",
Icon = "blur_circular", Keywords = new[] { "shear", "radial", "wave", "ripple" },
Description = "Displaces a coordinate perpendicular to the radius, growing with the squared distance." )]
[NodeVersion( 1 )]
public sealed class RadialShearNode : PrismNode
{
/// <summary>The coordinate to shear.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The centre of the shear.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>How hard it shears.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>A constant shift applied afterwards.</summary>
[In( "float2", Name = "Offset" )] public PortRef Offset { 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 strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 10f;
/// <summary>The offset used when nothing is connected.</summary>
[InlineValue( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; }
/// <summary>The sheared coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.RadialShear,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ),
ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) ) ) );
}
}
/// <summary>Bulges a coordinate outwards, like a fisheye lens.</summary>
[NodeInfo( Id = "prism.uv.spherize", Title = "Spherize", Category = "UV",
Icon = "lens", Keywords = new[] { "spherize", "fisheye", "bulge", "lens", "distort" },
Description = "Pushes a coordinate outwards with the fourth power of its radius." )]
[NodeVersion( 1 )]
public sealed class SpherizeNode : PrismNode
{
/// <summary>The coordinate to bulge.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The centre of the bulge.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>How hard it bulges.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>A constant shift applied afterwards.</summary>
[In( "float2", Name = "Offset" )] public PortRef Offset { 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 strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 10f;
/// <summary>The offset used when nothing is connected.</summary>
[InlineValue( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; }
/// <summary>The bulged coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Spherize,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ),
ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) ) ) );
}
}
/// <summary>Barrel and pincushion lens distortion.</summary>
[NodeInfo( Id = "prism.uv.fisheye", Title = "Lens Distortion", Category = "UV",
Icon = "camera", Keywords = new[] { "barrel", "pincushion", "fisheye", "lens", "distort" },
Description = "Quadratic radial distortion. Positive strength barrels outwards, negative pinches in." )]
[NodeVersion( 1 )]
public sealed class LensDistortionNode : PrismNode
{
/// <summary>The coordinate to distort.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The centre of the lens.</summary>
[In( "float2", Name = "Center" )] public PortRef Center { get; set; }
/// <summary>How hard it distorts. Positive bulges, negative pinches.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { 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 strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 0.5f;
/// <summary>The distorted coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Fisheye,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Center ), ctx.Const( DefaultCenter ) ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Sheets and grids
// ---------------------------------------------------------------------------------------------------
/// <summary>Picks one cell out of a sprite sheet.</summary>
[NodeInfo( Id = "prism.uv.flipbook", Title = "Flipbook", Category = "UV",
Icon = "burst_mode", Keywords = new[] { "flipbook", "sprite", "sheet", "atlas", "animation" },
Description = "Remaps a coordinate into one cell of a sprite sheet, optionally advancing with time." )]
[NodeVersion( 1 )]
public sealed class FlipbookNode : PrismNode
{
/// <summary>The coordinate to remap.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Which cell to show. Ignored when <see cref="Animate"/> is on.</summary>
[In( "float", Name = "Tile" )] public PortRef Tile { get; set; }
/// <summary>The clock to animate against. Defaults to the engine's time.</summary>
[In( "float", Name = "Time" )] public PortRef Time { get; set; }
/// <summary>The cell used when nothing is connected.</summary>
[InlineValue( nameof( Tile ) )] public float DefaultTile { get; set; }
/// <summary>How many cells across the sheet is.</summary>
public int Columns { get; set; } = 4;
/// <summary>How many cells down the sheet is.</summary>
public int Rows { get; set; } = 4;
/// <summary>True to advance the cell with time instead of reading the Tile input.</summary>
public bool Animate { get; set; } = true;
/// <summary>Cells per second when animating.</summary>
public float FramesPerSecond { get; set; } = 12f;
/// <summary>True to mirror the sheet horizontally.</summary>
public bool InvertX { get; set; }
/// <summary>True to count rows from the bottom instead of the top.</summary>
public bool InvertY { get; set; } = true;
/// <summary>The remapped coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>The cell index actually used, so a second sampler can cross-fade against it.</summary>
[Out( "float", Name = "Tile" )] public PortRef TileOut { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var columns = ctx.Const( Math.Max( 1, Columns ) );
var rows = ctx.Const( Math.Max( 1, Rows ) );
var frames = ctx.Const( Math.Max( 1, Columns ) * Math.Max( 1, Rows ) );
IrValue tile;
if ( Animate )
{
var time = ctx.TryIn( nameof( Time ), out var wired ) ? wired : ctx.Builtin( Builtin.Time );
var position = ctx.Bin( BinaryOp.Div,
ctx.Bin( BinaryOp.Mul, time, ctx.Const( FramesPerSecond ) ), frames );
tile = ctx.Call( Intrinsic.Floor, ctx.Bin( BinaryOp.Mul, ctx.Call( Intrinsic.Frac, position ), frames ) );
}
else
{
tile = ctx.In( nameof( Tile ), ctx.Const( DefaultTile ) );
}
ctx.Out( nameof( TileOut ), tile );
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Flipbook,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ), columns, rows, tile,
ctx.Const( new Vector2( InvertX ? 1f : 0f, InvertY ? 1f : 0f ) ) ) );
}
}
/// <summary>Splits a coordinate into a cell index and a coordinate local to that cell.</summary>
[NodeInfo( Id = "prism.uv.grid", Title = "UV Grid", Category = "UV",
Icon = "grid_4x4", Keywords = new[] { "grid", "cell", "tile", "index", "checker" },
Description = "Divides a coordinate into a grid, returning the coordinate inside a cell and the cell's index." )]
[NodeVersion( 1 )]
public sealed class UvGridNode : PrismNode
{
/// <summary>The coordinate to divide.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How many cells across and down.</summary>
[In( "float2", Name = "Count" )] public PortRef Count { get; set; }
/// <summary>The cell count used when nothing is connected.</summary>
[InlineValue( nameof( Count ) )] public Vector2 DefaultCount { get; set; } = new( 4f, 4f );
/// <summary>The coordinate inside the cell, 0..1.</summary>
[Out( "float2", Name = "Cell UV" )] public PortRef CellUv { get; set; }
/// <summary>The integer index of the cell.</summary>
[Out( "float2", Name = "Cell ID" )] public PortRef CellId { get; set; }
/// <summary>A stable random value per cell, for breaking up repetition.</summary>
[Out( "float", Name = "Random" )] public PortRef Random { get; set; }
/// <summary>A checkerboard of the cells, alternating zero and one.</summary>
[Out( "float", Name = "Checker" )] public PortRef Checker { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var packed = ctx.Helper( PrismUvHelpers.Grid,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Count ), ctx.Const( DefaultCount ) ) );
var cell = ctx.Swizzle( packed, "zw" );
ctx.Out( nameof( CellUv ), ctx.Swizzle( packed, "xy" ) );
ctx.Out( nameof( CellId ), cell );
ctx.Out( nameof( Random ), ctx.Helper( PrismCommon.Hash12, cell ) );
var sum = ctx.Bin( BinaryOp.Add, ctx.Swizzle( cell, "x" ), ctx.Swizzle( cell, "y" ) );
ctx.Out( nameof( Checker ), ctx.Call( Intrinsic.Frac, ctx.Bin( BinaryOp.Mul, sum, ctx.Const( 0.5f ) ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Addressing and parallax
// ---------------------------------------------------------------------------------------------------
/// <summary>Folds a coordinate back into the 0..1 range.</summary>
[NodeInfo( Id = "prism.uv.wrap", Title = "Wrap UV", Category = "UV",
Icon = "repeat", Keywords = new[] { "wrap", "clamp", "mirror", "repeat", "address" },
Description = "Applies wrap, clamp or mirror addressing in the shader, for coordinates a sampler will never see." )]
[NodeVersion( 1 )]
public sealed class WrapUvNode : PrismNode
{
/// <summary>The coordinate to fold.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Horizontal addressing.</summary>
public PrismUvWrapMode ModeU { get; set; } = PrismUvWrapMode.Wrap;
/// <summary>Vertical addressing.</summary>
public PrismUvWrapMode ModeV { get; set; } = PrismUvWrapMode.Wrap;
/// <summary>The folded coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <summary>One inside the original 0..1 range, zero outside. Multiply it in for a border.</summary>
[Out( "float", Name = "Inside" )] public PortRef Inside { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var uv = PrismUvSupport.Coordinate( ctx, nameof( UV ) );
ctx.Out( nameof( Inside ), ctx.Helper( PrismUvHelpers.InsideMask, uv ) );
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.Wrap, uv,
ctx.Const( new Vector2( ModeIndex( ModeU ), ModeIndex( ModeV ) ) ) ) );
}
static float ModeIndex( PrismUvWrapMode mode ) => mode switch
{
PrismUvWrapMode.Clamp => 1f,
PrismUvWrapMode.Mirror => 2f,
PrismUvWrapMode.MirrorOnce => 3f,
PrismUvWrapMode.Border => 4f,
_ => 0f
};
}
/// <summary>Shifts a coordinate along the tangent-space view direction by a height. The cheap fake depth.</summary>
[NodeInfo( Id = "prism.uv.parallax", Title = "Parallax UV", Category = "UV",
Icon = "layers", Keywords = new[] { "parallax", "bumpoffset", "height", "depth", "offset" },
Description = "Bump offset: displaces a coordinate by a height along the tangent-space view direction." )]
[NodeVersion( 1 )]
public sealed class ParallaxUvNode : PrismNode
{
/// <summary>The coordinate to shift.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The height at that coordinate, 0..1.</summary>
[In( "float", Name = "Height" )] public PortRef Height { get; set; }
/// <summary>The direction from the surface towards the eye, in tangent space.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>How far the shift reaches, in UV units.</summary>
[In( "float", Name = "Amplitude" )] public PortRef Amplitude { get; set; }
/// <summary>The height used when nothing is connected.</summary>
[InlineValue( nameof( Height ) )] public float DefaultHeight { get; set; } = 0.5f;
/// <summary>The amplitude used when nothing is connected.</summary>
[InlineValue( nameof( Amplitude ) )] public float DefaultAmplitude { get; set; } = 0.05f;
/// <summary>Which height leaves the coordinate untouched. Half centres the displacement.</summary>
public float ReferencePlane { get; set; } = 0.5f;
/// <summary>The shifted coordinate.</summary>
[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismUvHelpers.BumpOffset,
PrismUvSupport.Coordinate( ctx, nameof( UV ) ),
ctx.In( nameof( Height ), ctx.Const( DefaultHeight ) ),
ctx.In( nameof( ViewDirection ), ctx.Const( new Vector3( 0f, 0f, 1f ) ) ),
ctx.In( nameof( Amplitude ), ctx.Const( DefaultAmplitude ) ),
ctx.Const( ReferencePlane ) ) );
}
}
/// <summary>Shared plumbing for the UV family.</summary>
internal static class PrismUvSupport
{
/// <summary>Read a coordinate port, falling back to the mesh's first UV set.</summary>
internal static IrValue Coordinate( EmitContext ctx, string port ) =>
ctx.TryIn( port, out var value ) ? value : ctx.Builtin( Builtin.TexCoord0 );
}