Editor/Prism/Nodes/NoiseNodes.cs

Shader noise node implementations for the Prism editor. Defines enums and many helper HLSL snippets (value, Perlin, simplex, gradient, Voronoi, curl, fractals, dither sources) and node classes that emit IR to call those helpers, plus a fractal factory that generates per-basis fractal helper functions on demand.

Native Interop
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;

namespace Editor.Prism.Nodes;

// ---------------------------------------------------------------------------------------------------
// Procedural noise.
//
// Four bases (value, classic Perlin, simplex, gradient), each in two and three dimensions, plus the
// three fractal stacks built on top of them and the two cellular functions. The bodies are the
// reference implementations — Gustavson and Ashima for Perlin and simplex, Unity's for value and
// gradient noise — transcribed rather than reinvented, because a subtly wrong hash constant produces
// noise that looks fine until it tiles.
//
// A fractal helper is generated per basis and dimension, so a graph that only uses simplex fBm never
// carries the Perlin implementation into its shader.
// ---------------------------------------------------------------------------------------------------

/// <summary>Which gradient field a noise node evaluates.</summary>
public enum PrismNoiseBasis
{
	/// <summary>Value noise: smoothed random per lattice point. Cheapest, blockiest.</summary>
	Value,
	/// <summary>Classic Perlin gradient noise.</summary>
	Perlin,
	/// <summary>Simplex noise. Fewer directional artefacts than Perlin, and cheaper in three dimensions.</summary>
	Simplex,
	/// <summary>Unity-compatible gradient noise. In three dimensions this falls back to Perlin.</summary>
	Gradient
}

/// <summary>Whether a procedural function is evaluated over a plane or a volume.</summary>
public enum PrismNoiseDimension
{
	/// <summary>Two dimensions: a texture coordinate.</summary>
	Plane,
	/// <summary>Three dimensions: a position.</summary>
	Volume
}

/// <summary>How the octaves of a fractal noise are combined.</summary>
public enum PrismFractalKind
{
	/// <summary>Fractional Brownian motion: the plain sum of octaves. Clouds and terrain.</summary>
	Fbm,
	/// <summary>Turbulence: the sum of absolute octaves. Fire, smoke and marble.</summary>
	Turbulence,
	/// <summary>Ridged multifractal: inverted, squared and weighted by the previous octave. Mountains.</summary>
	Ridged
}

/// <summary>The noise bodies, and the factory that generates a fractal stack for a basis.</summary>
internal static class PrismNoiseHelpers
{
	// ---- shared plumbing --------------------------------------------------

	/// <summary>The 289 modulus every Gustavson-style permutation needs, on a float2.</summary>
	internal static readonly HelperFunction Mod289_2 = new( "Prism_Mod289_2", ShaderType.Float2,
		[new HelperParam( "x", ShaderType.Float2 )] )
	{
		Hlsl = """
			float2 Prism_Mod289_2( float2 x )
			{
				return x - floor( x * ( 1.0f / 289.0f ) ) * 289.0f;
			}
			"""
	};

	/// <summary>The 289 modulus on a float3.</summary>
	internal static readonly HelperFunction Mod289_3 = new( "Prism_Mod289_3", ShaderType.Float3,
		[new HelperParam( "x", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_Mod289_3( float3 x )
			{
				return x - floor( x * ( 1.0f / 289.0f ) ) * 289.0f;
			}
			"""
	};

	/// <summary>The 289 modulus on a float4.</summary>
	internal static readonly HelperFunction Mod289_4 = new( "Prism_Mod289_4", ShaderType.Float4,
		[new HelperParam( "x", ShaderType.Float4 )] )
	{
		Hlsl = """
			float4 Prism_Mod289_4( float4 x )
			{
				return x - floor( x * ( 1.0f / 289.0f ) ) * 289.0f;
			}
			"""
	};

	/// <summary>The permutation polynomial on a float4.</summary>
	internal static readonly HelperFunction Permute4 = new( "Prism_Permute4", ShaderType.Float4,
		[new HelperParam( "x", ShaderType.Float4 )] )
	{
		Requires = [Mod289_4],
		Hlsl = """
			float4 Prism_Permute4( float4 x )
			{
				return Prism_Mod289_4( ( ( x * 34.0f ) + 1.0f ) * x );
			}
			"""
	};

	/// <summary>The permutation polynomial on a float3.</summary>
	internal static readonly HelperFunction Permute3 = new( "Prism_Permute3", ShaderType.Float3,
		[new HelperParam( "x", ShaderType.Float3 )] )
	{
		Requires = [Mod289_3],
		Hlsl = """
			float3 Prism_Permute3( float3 x )
			{
				return Prism_Mod289_3( ( ( x * 34.0f ) + 1.0f ) * x );
			}
			"""
	};

	/// <summary>The rational approximation to the inverse square root the gradient normalisation uses.</summary>
	internal static readonly HelperFunction TaylorInvSqrt4 = new( "Prism_TaylorInvSqrt4", ShaderType.Float4,
		[new HelperParam( "r", ShaderType.Float4 )] )
	{
		Hlsl = """
			float4 Prism_TaylorInvSqrt4( float4 r )
			{
				return 1.79284291400159f - 0.85373472095314f * r;
			}
			"""
	};

	/// <summary>The quintic interpolant on a float2.</summary>
	internal static readonly HelperFunction Fade2 = new( "Prism_Fade2", ShaderType.Float2,
		[new HelperParam( "t", ShaderType.Float2 )] )
	{
		Hlsl = """
			float2 Prism_Fade2( float2 t )
			{
				return t * t * t * ( t * ( t * 6.0f - 15.0f ) + 10.0f );
			}
			"""
	};

	/// <summary>The quintic interpolant on a float3.</summary>
	internal static readonly HelperFunction Fade3 = new( "Prism_Fade3", ShaderType.Float3,
		[new HelperParam( "t", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_Fade3( float3 t )
			{
				return t * t * t * ( t * ( t * 6.0f - 15.0f ) + 10.0f );
			}
			"""
	};

	// ---- value ------------------------------------------------------------

	/// <summary>Smoothed random per lattice point, in 0..1.</summary>
	internal static readonly HelperFunction ValueNoise2D = new( "Prism_ValueNoise2D", ShaderType.Float,
		[new HelperParam( "vUv", ShaderType.Float2 )] )
	{
		Requires = [PrismCommon.Hash12],
		Hlsl = """
			float Prism_ValueNoise2D( float2 vUv )
			{
				float2 i = floor( vUv );
				float2 f = frac( vUv );
				f = f * f * ( 3.0f - 2.0f * f );

				float r0 = Prism_Hash12( i + float2( 0.0f, 0.0f ) );
				float r1 = Prism_Hash12( i + float2( 1.0f, 0.0f ) );
				float r2 = Prism_Hash12( i + float2( 0.0f, 1.0f ) );
				float r3 = Prism_Hash12( i + float2( 1.0f, 1.0f ) );

				return lerp( lerp( r0, r1, f.x ), lerp( r2, r3, f.x ), f.y );
			}
			"""
	};

	/// <summary>Value noise over a volume, in 0..1.</summary>
	internal static readonly HelperFunction ValueNoise3D = new( "Prism_ValueNoise3D", ShaderType.Float,
		[new HelperParam( "vPos", ShaderType.Float3 )] )
	{
		Requires = [PrismCommon.Hash13],
		Hlsl = """
			float Prism_ValueNoise3D( float3 vPos )
			{
				float3 i = floor( vPos );
				float3 f = frac( vPos );
				f = f * f * ( 3.0f - 2.0f * f );

				float n000 = Prism_Hash13( i + float3( 0.0f, 0.0f, 0.0f ) );
				float n100 = Prism_Hash13( i + float3( 1.0f, 0.0f, 0.0f ) );
				float n010 = Prism_Hash13( i + float3( 0.0f, 1.0f, 0.0f ) );
				float n110 = Prism_Hash13( i + float3( 1.0f, 1.0f, 0.0f ) );
				float n001 = Prism_Hash13( i + float3( 0.0f, 0.0f, 1.0f ) );
				float n101 = Prism_Hash13( i + float3( 1.0f, 0.0f, 1.0f ) );
				float n011 = Prism_Hash13( i + float3( 0.0f, 1.0f, 1.0f ) );
				float n111 = Prism_Hash13( i + float3( 1.0f, 1.0f, 1.0f ) );

				float4 nz0 = float4( lerp( n000, n100, f.x ), lerp( n010, n110, f.x ),
									 lerp( n001, n101, f.x ), lerp( n011, n111, f.x ) );

				return lerp( lerp( nz0.x, nz0.y, f.y ), lerp( nz0.z, nz0.w, f.y ), f.z );
			}
			"""
	};

	/// <summary>Three octaves of value noise, matching the shape of Unity's Simple Noise.</summary>
	internal static readonly HelperFunction SimpleNoise2D = new( "Prism_SimpleNoise2D", ShaderType.Float,
		[
			new HelperParam( "vUv", ShaderType.Float2 ),
			new HelperParam( "flScale", ShaderType.Float )
		] )
	{
		Requires = [ValueNoise2D],
		Hlsl = """
			float Prism_SimpleNoise2D( float2 vUv, float flScale )
			{
				float t = 0.0f;

				t += Prism_ValueNoise2D( vUv * flScale / 1.0f ) * 0.125f;
				t += Prism_ValueNoise2D( vUv * flScale / 2.0f ) * 0.25f;
				t += Prism_ValueNoise2D( vUv * flScale / 4.0f ) * 0.5f;

				return t;
			}
			"""
	};

	// ---- gradient ---------------------------------------------------------

	/// <summary>The hashed unit gradient a gradient-noise lattice point carries.</summary>
	internal static readonly HelperFunction GradientDir = new( "Prism_GradientNoiseDir", ShaderType.Float2,
		[new HelperParam( "p", ShaderType.Float2 )] )
	{
		Hlsl = """
			float2 Prism_GradientNoiseDir( float2 p )
			{
				p = fmod( p, 289.0f );

				float x = fmod( ( 34.0f * p.x + 1.0f ) * p.x, 289.0f ) + p.y;
				x = fmod( ( 34.0f * x + 1.0f ) * x, 289.0f );
				x = frac( x / 41.0f ) * 2.0f - 1.0f;

				return normalize( float2( x - floor( x + 0.5f ), abs( x ) - 0.5f ) );
			}
			"""
	};

	/// <summary>Unity-compatible gradient noise, roughly -0.5..0.5.</summary>
	internal static readonly HelperFunction GradientNoise2D = new( "Prism_GradientNoise2D", ShaderType.Float,
		[new HelperParam( "p", ShaderType.Float2 )] )
	{
		Requires = [GradientDir],
		Hlsl = """
			float Prism_GradientNoise2D( float2 p )
			{
				float2 ip = floor( p );
				float2 fp = frac( p );

				float d00 = dot( Prism_GradientNoiseDir( ip ), fp );
				float d01 = dot( Prism_GradientNoiseDir( ip + float2( 0.0f, 1.0f ) ), fp - float2( 0.0f, 1.0f ) );
				float d10 = dot( Prism_GradientNoiseDir( ip + float2( 1.0f, 0.0f ) ), fp - float2( 1.0f, 0.0f ) );
				float d11 = dot( Prism_GradientNoiseDir( ip + float2( 1.0f, 1.0f ) ), fp - float2( 1.0f, 1.0f ) );

				fp = fp * fp * fp * ( fp * ( fp * 6.0f - 15.0f ) + 10.0f );

				return lerp( lerp( d00, d01, fp.y ), lerp( d10, d11, fp.y ), fp.x );
			}
			"""
	};

	// ---- classic Perlin ---------------------------------------------------

	/// <summary>Classic Perlin noise over a plane, roughly -1..1.</summary>
	internal static readonly HelperFunction Perlin2D = new( "Prism_Perlin2D", ShaderType.Float,
		[new HelperParam( "P", ShaderType.Float2 )] )
	{
		Requires = [Mod289_4, Permute4, TaylorInvSqrt4, Fade2],
		Hlsl = """
			float Prism_Perlin2D( float2 P )
			{
				float4 Pi = floor( P.xyxy ) + float4( 0.0f, 0.0f, 1.0f, 1.0f );
				float4 Pf = frac( P.xyxy ) - float4( 0.0f, 0.0f, 1.0f, 1.0f );
				Pi = Prism_Mod289_4( Pi );

				float4 ix = Pi.xzxz;
				float4 iy = Pi.yyww;
				float4 fx = Pf.xzxz;
				float4 fy = Pf.yyww;

				float4 i = Prism_Permute4( Prism_Permute4( ix ) + iy );

				float4 gx = frac( i * ( 1.0f / 41.0f ) ) * 2.0f - 1.0f;
				float4 gy = abs( gx ) - 0.5f;
				float4 tx = floor( gx + 0.5f );
				gx = gx - tx;

				float2 g00 = float2( gx.x, gy.x );
				float2 g10 = float2( gx.y, gy.y );
				float2 g01 = float2( gx.z, gy.z );
				float2 g11 = float2( gx.w, gy.w );

				float4 norm = Prism_TaylorInvSqrt4( float4( dot( g00, g00 ), dot( g01, g01 ), dot( g10, g10 ), dot( g11, g11 ) ) );
				g00 *= norm.x;
				g01 *= norm.y;
				g10 *= norm.z;
				g11 *= norm.w;

				float n00 = dot( g00, float2( fx.x, fy.x ) );
				float n10 = dot( g10, float2( fx.y, fy.y ) );
				float n01 = dot( g01, float2( fx.z, fy.z ) );
				float n11 = dot( g11, float2( fx.w, fy.w ) );

				float2 vFade = Prism_Fade2( Pf.xy );
				float2 n_x = lerp( float2( n00, n01 ), float2( n10, n11 ), vFade.x );

				return 2.3f * lerp( n_x.x, n_x.y, vFade.y );
			}
			"""
	};

	/// <summary>Classic Perlin noise over a volume, roughly -1..1.</summary>
	internal static readonly HelperFunction Perlin3D = new( "Prism_Perlin3D", ShaderType.Float,
		[new HelperParam( "P", ShaderType.Float3 )] )
	{
		Requires = [Mod289_3, Mod289_4, Permute4, TaylorInvSqrt4, Fade3],
		Hlsl = """
			float Prism_Perlin3D( float3 P )
			{
				float3 Pi0 = floor( P );
				float3 Pi1 = Pi0 + 1.0f;
				Pi0 = Prism_Mod289_3( Pi0 );
				Pi1 = Prism_Mod289_3( Pi1 );

				float3 Pf0 = frac( P );
				float3 Pf1 = Pf0 - 1.0f;

				float4 ix = float4( Pi0.x, Pi1.x, Pi0.x, Pi1.x );
				float4 iy = float4( Pi0.yy, Pi1.yy );
				float4 iz0 = Pi0.zzzz;
				float4 iz1 = Pi1.zzzz;

				float4 ixy = Prism_Permute4( Prism_Permute4( ix ) + iy );
				float4 ixy0 = Prism_Permute4( ixy + iz0 );
				float4 ixy1 = Prism_Permute4( ixy + iz1 );

				float4 gx0 = ixy0 * ( 1.0f / 7.0f );
				float4 gy0 = frac( floor( gx0 ) * ( 1.0f / 7.0f ) ) - 0.5f;
				gx0 = frac( gx0 );
				float4 gz0 = 0.5f - abs( gx0 ) - abs( gy0 );
				float4 sz0 = step( gz0, 0.0f );
				gx0 -= sz0 * ( step( 0.0f, gx0 ) - 0.5f );
				gy0 -= sz0 * ( step( 0.0f, gy0 ) - 0.5f );

				float4 gx1 = ixy1 * ( 1.0f / 7.0f );
				float4 gy1 = frac( floor( gx1 ) * ( 1.0f / 7.0f ) ) - 0.5f;
				gx1 = frac( gx1 );
				float4 gz1 = 0.5f - abs( gx1 ) - abs( gy1 );
				float4 sz1 = step( gz1, 0.0f );
				gx1 -= sz1 * ( step( 0.0f, gx1 ) - 0.5f );
				gy1 -= sz1 * ( step( 0.0f, gy1 ) - 0.5f );

				float3 g000 = float3( gx0.x, gy0.x, gz0.x );
				float3 g100 = float3( gx0.y, gy0.y, gz0.y );
				float3 g010 = float3( gx0.z, gy0.z, gz0.z );
				float3 g110 = float3( gx0.w, gy0.w, gz0.w );
				float3 g001 = float3( gx1.x, gy1.x, gz1.x );
				float3 g101 = float3( gx1.y, gy1.y, gz1.y );
				float3 g011 = float3( gx1.z, gy1.z, gz1.z );
				float3 g111 = float3( gx1.w, gy1.w, gz1.w );

				float4 norm0 = Prism_TaylorInvSqrt4( float4( dot( g000, g000 ), dot( g010, g010 ), dot( g100, g100 ), dot( g110, g110 ) ) );
				g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;

				float4 norm1 = Prism_TaylorInvSqrt4( float4( dot( g001, g001 ), dot( g011, g011 ), dot( g101, g101 ), dot( g111, g111 ) ) );
				g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;

				float n000 = dot( g000, Pf0 );
				float n100 = dot( g100, float3( Pf1.x, Pf0.yz ) );
				float n010 = dot( g010, float3( Pf0.x, Pf1.y, Pf0.z ) );
				float n110 = dot( g110, float3( Pf1.xy, Pf0.z ) );
				float n001 = dot( g001, float3( Pf0.xy, Pf1.z ) );
				float n101 = dot( g101, float3( Pf1.x, Pf0.y, Pf1.z ) );
				float n011 = dot( g011, float3( Pf0.x, Pf1.yz ) );
				float n111 = dot( g111, Pf1 );

				float3 vFade = Prism_Fade3( Pf0 );
				float4 n_z = lerp( float4( n000, n100, n010, n110 ), float4( n001, n101, n011, n111 ), vFade.z );
				float2 n_yz = lerp( n_z.xy, n_z.zw, vFade.y );

				return 2.2f * lerp( n_yz.x, n_yz.y, vFade.x );
			}
			"""
	};

	// ---- simplex ----------------------------------------------------------

	/// <summary>Simplex noise over a plane, roughly -1..1.</summary>
	internal static readonly HelperFunction Simplex2D = new( "Prism_Simplex2D", ShaderType.Float,
		[new HelperParam( "vUv", ShaderType.Float2 )] )
	{
		Requires = [Mod289_2, Permute3],
		Hlsl = """
			float Prism_Simplex2D( float2 vUv )
			{
				const float4 C = float4( 0.2113248654f, 0.36602540378f, -0.5773502692f, 0.0243902439f );

				float2 vCorner = floor( vUv + dot( vUv, C.yy ) );
				float2 x0 = vUv - vCorner + dot( vCorner, C.xx );

				float2 i1 = ( x0.x > x0.y ) ? float2( 1.0f, 0.0f ) : float2( 0.0f, 1.0f );
				float4 x12 = x0.xyxy + C.xxzz;
				x12.xy -= i1;

				vCorner = Prism_Mod289_2( vCorner );

				float3 vPermuted = Prism_Permute3( Prism_Permute3( vCorner.y + float3( 0.0f, i1.y, 1.0f ) ) + vCorner.x + float3( 0.0f, i1.x, 1.0f ) );

				float3 vContribution = max( 0.5f - float3( dot( x0, x0 ), dot( x12.xy, x12.xy ), dot( x12.zw, x12.zw ) ), 0.0f );
				vContribution = vContribution * vContribution;
				vContribution = vContribution * vContribution;

				float3 x = 2.0f * frac( vPermuted * C.www ) - 1.0f;
				float3 h = abs( x ) - 0.5f;
				float3 ox = floor( x + 0.5f );
				float3 a0 = x - ox;

				vContribution *= rsqrt( a0 * a0 + h * h );

				float3 vGradient = float3( a0.x * x0.x + h.x * x0.y, a0.yz * x12.xz + h.yz * x12.yw );

				return 130.0f * dot( vContribution, vGradient );
			}
			"""
	};

	/// <summary>Simplex noise over a volume, roughly -1..1.</summary>
	internal static readonly HelperFunction Simplex3D = new( "Prism_Simplex3D", ShaderType.Float,
		[new HelperParam( "v", ShaderType.Float3 )] )
	{
		Requires = [Mod289_3, Permute4, TaylorInvSqrt4],
		Hlsl = """
			float Prism_Simplex3D( float3 v )
			{
				const float2 C = float2( 1.0f / 6.0f, 1.0f / 3.0f );
				const float4 D = float4( 0.0f, 0.5f, 1.0f, 2.0f );

				float3 i = floor( v + dot( v, C.yyy ) );
				float3 x0 = v - i + dot( i, C.xxx );

				float3 g = step( x0.yzx, x0.xyz );
				float3 l = 1.0f - g;
				float3 i1 = min( g.xyz, l.zxy );
				float3 i2 = max( g.xyz, l.zxy );

				float3 x1 = x0 - i1 + C.xxx;
				float3 x2 = x0 - i2 + C.yyy;
				float3 x3 = x0 - D.yyy;

				i = Prism_Mod289_3( i );

				float4 p = Prism_Permute4( Prism_Permute4( Prism_Permute4(
							i.z + float4( 0.0f, i1.z, i2.z, 1.0f ) )
						  + i.y + float4( 0.0f, i1.y, i2.y, 1.0f ) )
						  + i.x + float4( 0.0f, i1.x, i2.x, 1.0f ) );

				float n_ = 0.142857142857f;
				float3 ns = n_ * D.wyz - D.xzx;

				float4 j = p - 49.0f * floor( p * ns.z * ns.z );

				float4 x_ = floor( j * ns.z );
				float4 y_ = floor( j - 7.0f * x_ );

				float4 x = x_ * ns.x + ns.yyyy;
				float4 y = y_ * ns.x + ns.yyyy;
				float4 h = 1.0f - abs( x ) - abs( y );

				float4 b0 = float4( x.xy, y.xy );
				float4 b1 = float4( x.zw, y.zw );

				float4 s0 = floor( b0 ) * 2.0f + 1.0f;
				float4 s1 = floor( b1 ) * 2.0f + 1.0f;
				float4 sh = -step( h, 0.0f );

				float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
				float4 a1 = b1.xzyw + s1.xzyw * sh.zzww;

				float3 p0 = float3( a0.xy, h.x );
				float3 p1 = float3( a0.zw, h.y );
				float3 p2 = float3( a1.xy, h.z );
				float3 p3 = float3( a1.zw, h.w );

				float4 norm = Prism_TaylorInvSqrt4( float4( dot( p0, p0 ), dot( p1, p1 ), dot( p2, p2 ), dot( p3, p3 ) ) );
				p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;

				float4 m = max( 0.6f - float4( dot( x0, x0 ), dot( x1, x1 ), dot( x2, x2 ), dot( x3, x3 ) ), 0.0f );
				m = m * m;

				return 42.0f * dot( m * m, float4( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 ), dot( p3, x3 ) ) );
			}
			"""
	};

	// ---- cellular ---------------------------------------------------------

	/// <summary>The jittered cell centre a Voronoi lattice point carries.</summary>
	internal static readonly HelperFunction VoronoiOffset = new( "Prism_VoronoiOffset", ShaderType.Float2,
		[
			new HelperParam( "vUv", ShaderType.Float2 ),
			new HelperParam( "flAngleOffset", ShaderType.Float )
		] )
	{
		Hlsl = """
			float2 Prism_VoronoiOffset( float2 vUv, float flAngleOffset )
			{
				const float2x2 m = float2x2( 15.27f, 47.63f, 99.41f, 89.98f );

				vUv = frac( sin( mul( vUv, m ) ) * 46839.32f );

				return float2( sin( vUv.y * flAngleOffset ) * 0.5f + 0.5f,
							   cos( vUv.x * flAngleOffset ) * 0.5f + 0.5f );
			}
			"""
	};

	/// <summary>
	/// Voronoi over a plane. Returns <c>x</c> = the distance to the nearest cell centre, <c>y</c> = the
	/// distance to the second nearest, <c>z</c> = a stable random value for the nearest cell, and
	/// <c>w</c> = the difference between the two distances, which is the cell border.
	/// </summary>
	internal static readonly HelperFunction Voronoi2D = new( "Prism_Voronoi2D", ShaderType.Float4,
		[
			new HelperParam( "vUv", ShaderType.Float2 ),
			new HelperParam( "flAngleOffset", ShaderType.Float ),
			new HelperParam( "flDensity", ShaderType.Float )
		] )
	{
		Requires = [VoronoiOffset],
		Capabilities = [Capability.Loops],
		Hlsl = """
			float4 Prism_Voronoi2D( float2 vUv, float flAngleOffset, float flDensity )
			{
				float2 g = floor( vUv * flDensity );
				float2 f = frac( vUv * flDensity );

				float flF1 = 8.0f;
				float flF2 = 8.0f;
				float flCell = 0.0f;

				[unroll]
				for ( int y = -1; y <= 1; y++ )
				{
					[unroll]
					for ( int x = -1; x <= 1; x++ )
					{
						float2 vLattice = float2( x, y );
						float2 vOffset = Prism_VoronoiOffset( vLattice + g, flAngleOffset );
						float flDistance = distance( vLattice + vOffset, f );

						if ( flDistance < flF1 )
						{
							flF2 = flF1;
							flF1 = flDistance;
							flCell = vOffset.x;
						}
						else if ( flDistance < flF2 )
						{
							flF2 = flDistance;
						}
					}
				}

				return float4( flF1, flF2, flCell, flF2 - flF1 );
			}
			"""
	};

	/// <summary>Voronoi over a volume, packed the same way as the planar form.</summary>
	internal static readonly HelperFunction Voronoi3D = new( "Prism_Voronoi3D", ShaderType.Float4,
		[
			new HelperParam( "vPos", ShaderType.Float3 ),
			new HelperParam( "flDensity", ShaderType.Float )
		] )
	{
		Requires = [PrismCommon.Hash33],
		Capabilities = [Capability.Loops],
		Hlsl = """
			float4 Prism_Voronoi3D( float3 vPos, float flDensity )
			{
				float3 g = floor( vPos * flDensity );
				float3 f = frac( vPos * flDensity );

				float flF1 = 8.0f;
				float flF2 = 8.0f;
				float flCell = 0.0f;

				[unroll]
				for ( int z = -1; z <= 1; z++ )
				{
					[unroll]
					for ( int y = -1; y <= 1; y++ )
					{
						[unroll]
						for ( int x = -1; x <= 1; x++ )
						{
							float3 vLattice = float3( x, y, z );
							float3 vOffset = Prism_Hash33( vLattice + g );
							float flDistance = distance( vLattice + vOffset, f );

							if ( flDistance < flF1 )
							{
								flF2 = flF1;
								flF1 = flDistance;
								flCell = vOffset.x;
							}
							else if ( flDistance < flF2 )
							{
								flF2 = flDistance;
							}
						}
					}
				}

				return float4( flF1, flF2, flCell, flF2 - flF1 );
			}
			"""
	};

	// ---- curl -------------------------------------------------------------

	/// <summary>The divergence-free gradient of a scalar simplex field, over a plane.</summary>
	internal static readonly HelperFunction Curl2D = new( "Prism_CurlNoise2D", ShaderType.Float2,
		[
			new HelperParam( "p", ShaderType.Float2 ),
			new HelperParam( "flEpsilon", ShaderType.Float )
		] )
	{
		Requires = [Simplex2D],
		Hlsl = """
			float2 Prism_CurlNoise2D( float2 p, float flEpsilon )
			{
				float flE = max( flEpsilon, 1e-4f );

				float n1 = Prism_Simplex2D( p + float2( 0.0f, flE ) );
				float n2 = Prism_Simplex2D( p - float2( 0.0f, flE ) );
				float n3 = Prism_Simplex2D( p + float2( flE, 0.0f ) );
				float n4 = Prism_Simplex2D( p - float2( flE, 0.0f ) );

				float dx = ( n1 - n2 ) / ( 2.0f * flE );
				float dy = ( n3 - n4 ) / ( 2.0f * flE );

				return float2( dx, -dy );
			}
			"""
	};

	/// <summary>The curl of a three-component simplex potential, over a volume.</summary>
	internal static readonly HelperFunction Curl3D = new( "Prism_CurlNoise3D", ShaderType.Float3,
		[
			new HelperParam( "p", ShaderType.Float3 ),
			new HelperParam( "flEpsilon", ShaderType.Float )
		] )
	{
		Requires = [Simplex3D],
		Hlsl = """
			float3 Prism_CurlPotential3D( float3 p )
			{
				return float3( Prism_Simplex3D( p ),
							   Prism_Simplex3D( p + 19.19f ),
							   Prism_Simplex3D( p - 43.83f ) );
			}

			float3 Prism_CurlNoise3D( float3 p, float flEpsilon )
			{
				float flE = max( flEpsilon, 1e-4f );

				float3 p0 = Prism_CurlPotential3D( p );
				float3 px = Prism_CurlPotential3D( p + float3( flE, 0.0f, 0.0f ) );
				float3 py = Prism_CurlPotential3D( p + float3( 0.0f, flE, 0.0f ) );
				float3 pz = Prism_CurlPotential3D( p + float3( 0.0f, 0.0f, flE ) );

				float x = ( py.z - p0.z ) - ( pz.y - p0.y );
				float y = ( pz.x - p0.x ) - ( px.z - p0.z );
				float z = ( px.y - p0.y ) - ( py.x - p0.x );

				return float3( x, y, z ) / flE;
			}
			"""
	};

	// ---- dither sources ---------------------------------------------------

	/// <summary>
	/// Interleaved gradient noise: the cheapest source of a well-distributed, temporally stable dither
	/// pattern, and the reason a modern dissolve looks like grain rather than a grid.
	/// </summary>
	internal static readonly HelperFunction InterleavedGradient = new( "Prism_InterleavedGradientNoise", ShaderType.Float,
		[
			new HelperParam( "vPositionSs", ShaderType.Float2 ),
			new HelperParam( "flFrame", ShaderType.Float )
		] )
	{
		Hlsl = """
			float Prism_InterleavedGradientNoise( float2 vPositionSs, float flFrame )
			{
				vPositionSs += 5.588238f * flFrame;

				const float3 vMagic = float3( 0.06711056f, 0.00583715f, 52.9829189f );

				return frac( vMagic.z * frac( dot( vPositionSs, vMagic.xy ) ) );
			}
			"""
	};

	/// <summary>The TV-static hash the engine's own procedural header calls Fuzzy Noise.</summary>
	internal static readonly HelperFunction FuzzyNoise = new( "Prism_FuzzyNoise", ShaderType.Float,
		[
			new HelperParam( "vUv", ShaderType.Float2 ),
			new HelperParam( "vDot", ShaderType.Float2 )
		] )
	{
		Hlsl = """
			float Prism_FuzzyNoise( float2 vUv, float2 vDot )
			{
				return frac( sin( dot( vUv, vDot ) ) * 43758.5453f );
			}
			"""
	};

	// ---- fractal factory --------------------------------------------------

	static readonly Dictionary<string, HelperFunction> s_fractals = new( StringComparer.Ordinal );
	static readonly object s_lock = new();

	/// <summary>The basis function for a given kind of noise and dimensionality.</summary>
	internal static HelperFunction Basis( PrismNoiseBasis basis, bool volume ) => basis switch
	{
		PrismNoiseBasis.Value => volume ? ValueNoise3D : ValueNoise2D,
		PrismNoiseBasis.Perlin => volume ? Perlin3D : Perlin2D,
		PrismNoiseBasis.Gradient => volume ? Perlin3D : GradientNoise2D,
		_ => volume ? Simplex3D : Simplex2D
	};

	/// <summary>The expression that evaluates a basis over roughly -1..1, correcting value noise's range.</summary>
	static string BasisExpression( PrismNoiseBasis basis, bool volume )
	{
		var call = $"{Basis( basis, volume ).Name}( p * flFreq )";

		return basis == PrismNoiseBasis.Value ? $"( {call} * 2.0f - 1.0f )" : call;
	}

	/// <summary>
	/// The fractal stack for one basis, generated on demand so a graph only carries the octave loop it
	/// actually uses. Cached by name, because two nodes asking for the same stack must get one body.
	/// </summary>
	internal static HelperFunction Fractal( PrismFractalKind kind, PrismNoiseBasis basis, bool volume )
	{
		var name = $"Prism_{kind}{( volume ? "3D" : "2D" )}_{basis}";

		lock ( s_lock )
		{
			if ( s_fractals.TryGetValue( name, out var cached ) ) return cached;

			var vector = volume ? ShaderType.Float3 : ShaderType.Float2;
			var spelling = volume ? "float3" : "float2";
			var sample = BasisExpression( basis, volume );

			var body = kind switch
			{
				PrismFractalKind.Turbulence => $"\t\tflSum += abs( {sample} ) * flAmp;",
				PrismFractalKind.Ridged =>
					$"\t\tfloat flSignal = flOffset - abs( {sample} );\r\n" +
					"\t\tflSignal *= flSignal;\r\n" +
					"\t\tflSignal *= flPrev;\r\n" +
					"\t\tflPrev = saturate( flSignal );\r\n" +
					"\t\tflSum += flSignal * flAmp;",
				_ => $"\t\tflSum += {sample} * flAmp;"
			};

			var hlsl =
				$"float {name}( {spelling} p, int nOctaves, float flLacunarity, float flGain, float flOffset )\r\n" +
				"{\r\n" +
				"\tfloat flSum = 0.0f;\r\n" +
				"\tfloat flAmp = 0.5f;\r\n" +
				"\tfloat flFreq = 1.0f;\r\n" +
				"\tfloat flNorm = 0.0f;\r\n" +
				"\tfloat flPrev = 1.0f;\r\n" +
				"\r\n" +
				"\t[loop]\r\n" +
				"\tfor ( int n = 0; n < nOctaves; n++ )\r\n" +
				"\t{\r\n" +
				body + "\r\n" +
				"\t\tflNorm += flAmp;\r\n" +
				"\t\tflFreq *= flLacunarity;\r\n" +
				"\t\tflAmp *= flGain;\r\n" +
				"\t}\r\n" +
				"\r\n" +
				"\treturn flSum / max( flNorm, 1e-5f );\r\n" +
				"}\r\n";

			var helper = new HelperFunction( name, ShaderType.Float,
				[
					new HelperParam( "p", vector ),
					new HelperParam( "nOctaves", ShaderType.Int ),
					new HelperParam( "flLacunarity", ShaderType.Float ),
					new HelperParam( "flGain", ShaderType.Float ),
					new HelperParam( "flOffset", ShaderType.Float )
				] )
			{
				Requires = [Basis( basis, volume )],
				Capabilities = [Capability.Loops],
				Hlsl = hlsl
			};

			s_fractals[name] = helper;
			return helper;
		}
	}
}

/// <summary>
/// Shared behaviour for a node evaluated over either a plane or a volume: the coordinate port changes
/// type with the dimension, and the fallback changes with it.
/// </summary>
public abstract class ProceduralFieldNode : PrismNode
{
	/// <summary>Whether this node is evaluated over a plane or a volume.</summary>
	public PrismNoiseDimension Dimension { get; set; } = PrismNoiseDimension.Plane;

	/// <summary>True when the node is running over a volume.</summary>
	protected bool IsVolume => Dimension == PrismNoiseDimension.Volume;

	/// <summary>The declared type of the coordinate port for the current dimension.</summary>
	protected string CoordinateType => IsVolume ? "float3" : "float2";

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		base.OnDefinePorts( b );

		if ( b is null || !b.Has( "UV" ) ) return;

		b.Retype( "UV", CoordinateType );
		b.Rename( "UV", IsVolume ? "Position" : "UV" );
	}

	/// <summary>
	/// The coordinate to evaluate at: the port when wired, the first UV set over a plane, and the world
	/// position in metres over a volume.
	/// </summary>
	protected IrValue Coordinate( EmitContext ctx, string port = "UV" )
	{
		if ( ctx.TryIn( port, out var wired ) ) return wired;

		if ( !IsVolume ) return ctx.Builtin( Builtin.TexCoord0 );

		return ctx.Bin( BinaryOp.Div, ctx.Builtin( Builtin.WorldPosition ),
			ctx.Const( PrismTriplanarSupport.InchesPerMetre ) );
	}
}

// ---------------------------------------------------------------------------------------------------
// The single-octave bases
// ---------------------------------------------------------------------------------------------------

/// <summary>Smoothed random values on a lattice: the cheapest usable noise.</summary>
[NodeInfo( Id = "prism.noise.value", Title = "Value Noise", Category = "Noise",
	Icon = "dashboard", Keywords = new[] { "value", "noise", "random", "lattice" },
	Description = "Smoothed random values interpolated across a lattice." )]
[NodeVersion( 1 )]
public sealed class ValueNoiseNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 8f;

	/// <summary>The noise, in 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var p = ctx.Bin( BinaryOp.Mul, Coordinate( ctx ), ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );

		ctx.Out( nameof( Out ), ctx.Helper( PrismNoiseHelpers.Basis( PrismNoiseBasis.Value, IsVolume ), p ) );
	}
}

/// <summary>Three octaves of value noise, matching the shape most shader graphs call Simple Noise.</summary>
[NodeInfo( Id = "prism.noise.simple", Title = "Simple Noise", Category = "Noise",
	Icon = "grain", Keywords = new[] { "simple", "noise", "fbm", "clouds" },
	Description = "Three octaves of value noise. The quick, familiar cloud pattern." )]
[NodeVersion( 1 )]
public sealed class SimpleNoiseNode : PrismNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 32f;

	/// <summary>The noise, in 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var uv = ctx.TryIn( nameof( UV ), out var wired ) ? wired : ctx.Builtin( Builtin.TexCoord0 );

		ctx.Out( nameof( Out ), ctx.Helper( PrismNoiseHelpers.SimpleNoise2D, uv,
			ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) ) );
	}
}

/// <summary>Classic Perlin gradient noise.</summary>
[NodeInfo( Id = "prism.noise.perlin", Title = "Perlin Noise", Category = "Noise",
	Icon = "waves", Keywords = new[] { "perlin", "gradient", "noise", "classic" },
	Description = "Classic Perlin noise over a plane or a volume." )]
[NodeVersion( 1 )]
public sealed class PerlinNoiseNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 8f;

	/// <summary>The noise, remapped to 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The raw noise, roughly -1..1.</summary>
	[Out( "float", Name = "Signed" )] public PortRef Signed { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var p = ctx.Bin( BinaryOp.Mul, Coordinate( ctx ), ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );
		var signed = ctx.Helper( PrismNoiseHelpers.Basis( PrismNoiseBasis.Perlin, IsVolume ), p );

		ctx.Out( nameof( Signed ), signed );
		ctx.Out( nameof( Out ), PrismNoiseSupport.ToUnit( ctx, signed ) );
	}
}

/// <summary>Simplex noise: fewer directional artefacts than Perlin, and cheaper in three dimensions.</summary>
[NodeInfo( Id = "prism.noise.simplex", Title = "Simplex Noise", Category = "Noise",
	Icon = "blur_on", Keywords = new[] { "simplex", "noise", "gradient", "perlin" },
	Description = "Simplex noise over a plane or a volume." )]
[NodeVersion( 1 )]
public sealed class SimplexNoiseNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 8f;

	/// <summary>The noise, remapped to 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The raw noise, roughly -1..1.</summary>
	[Out( "float", Name = "Signed" )] public PortRef Signed { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var p = ctx.Bin( BinaryOp.Mul, Coordinate( ctx ), ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );
		var signed = ctx.Helper( PrismNoiseHelpers.Basis( PrismNoiseBasis.Simplex, IsVolume ), p );

		ctx.Out( nameof( Signed ), signed );
		ctx.Out( nameof( Out ), PrismNoiseSupport.ToUnit( ctx, signed ) );
	}
}

/// <summary>Gradient noise with the exact lattice hash other shader graphs use, for matching a reference.</summary>
[NodeInfo( Id = "prism.noise.gradient", Title = "Gradient Noise", Category = "Noise",
	Icon = "gradient", Keywords = new[] { "gradient", "noise", "perlin" },
	Description = "Gradient noise on a unit lattice." )]
[NodeVersion( 1 )]
public sealed class GradientNoiseNode : PrismNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 8f;

	/// <summary>The noise, remapped to 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var uv = ctx.TryIn( nameof( UV ), out var wired ) ? wired : ctx.Builtin( Builtin.TexCoord0 );
		var p = ctx.Bin( BinaryOp.Mul, uv, ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );

		var noise = ctx.Helper( PrismNoiseHelpers.GradientNoise2D, p );

		ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Add, noise, ctx.Const( 0.5f ) ) );
	}
}

// ---------------------------------------------------------------------------------------------------
// Cellular
// ---------------------------------------------------------------------------------------------------

/// <summary>Cellular noise: distance to scattered feature points, and everything derived from it.</summary>
[NodeInfo( Id = "prism.noise.voronoi", Title = "Voronoi", Category = "Noise",
	Icon = "ssid_chart", Keywords = new[] { "voronoi", "worley", "cellular", "cells", "cracks" },
	Description = "Voronoi cells. F1 is the distance to the nearest feature point, F2 to the second, and Borders is their difference." )]
[NodeVersion( 1 )]
public sealed class VoronoiNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many cells the coordinate covers.</summary>
	[In( "float", Name = "Density" )] public PortRef Density { get; set; }

	/// <summary>How far the feature points wander inside their cell. Planar only.</summary>
	[In( "float", Name = "Angle Offset" )] public PortRef AngleOffset { get; set; }

	/// <summary>The density used when nothing is connected.</summary>
	[InlineValue( nameof( Density ) )] public float DefaultDensity { get; set; } = 8f;

	/// <summary>The angle offset used when nothing is connected.</summary>
	[InlineValue( nameof( AngleOffset ) )] public float DefaultAngleOffset { get; set; } = 3.1415926f;

	/// <summary>
	/// The distance to the nearest feature point. Named <c>Out</c> as a port so a graph imported from
	/// the built-in editor, whose Voronoi node had a single result, still finds it.
	/// </summary>
	[Out( "float", Name = "F1" )] public PortRef Out { get; set; }

	/// <summary>The distance to the second nearest feature point.</summary>
	[Out( "float", Name = "F2" )] public PortRef F2 { get; set; }

	/// <summary>One minus F1: bright blobs rather than dark ones.</summary>
	[Out( "float", Name = "Worley" )] public PortRef Worley { get; set; }

	/// <summary>A stable random value per cell, for tinting or offsetting each one.</summary>
	[Out( "float", Name = "Cells" )] public PortRef Cells { get; set; }

	/// <summary>F2 minus F1: zero on the cell walls, so it draws cracks.</summary>
	[Out( "float", Name = "Borders" )] public PortRef Borders { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var coordinate = Coordinate( ctx );
		var density = ctx.In( nameof( Density ), ctx.Const( DefaultDensity ) );

		var packed = IsVolume
			? ctx.Helper( PrismNoiseHelpers.Voronoi3D, coordinate, density )
			: ctx.Helper( PrismNoiseHelpers.Voronoi2D, coordinate,
				ctx.In( nameof( AngleOffset ), ctx.Const( DefaultAngleOffset ) ), density );

		var f1 = ctx.Swizzle( packed, "x" );

		ctx.Out( nameof( Out ), f1 );
		ctx.Out( nameof( F2 ), ctx.Swizzle( packed, "y" ) );
		ctx.Out( nameof( Cells ), ctx.Swizzle( packed, "z" ) );
		ctx.Out( nameof( Borders ), ctx.Swizzle( packed, "w" ) );
		ctx.Out( nameof( Worley ), ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), f1 ) );
	}
}

// ---------------------------------------------------------------------------------------------------
// Fractals
// ---------------------------------------------------------------------------------------------------

/// <summary>Stacked octaves of a noise basis. The workhorse of every natural-looking surface.</summary>
public abstract class FractalNoiseNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers before the octaves start.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>How much the frequency grows each octave.</summary>
	[In( "float", Name = "Lacunarity" )] public PortRef Lacunarity { get; set; }

	/// <summary>How much the amplitude falls each octave.</summary>
	[In( "float", Name = "Gain" )] public PortRef Gain { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 4f;

	/// <summary>The lacunarity used when nothing is connected.</summary>
	[InlineValue( nameof( Lacunarity ) )] public float DefaultLacunarity { get; set; } = 2f;

	/// <summary>The gain used when nothing is connected.</summary>
	[InlineValue( nameof( Gain ) )] public float DefaultGain { get; set; } = 0.5f;

	/// <summary>How many octaves to stack. Each one doubles the cost.</summary>
	public int Octaves { get; set; } = 5;

	/// <summary>Which noise the octaves are built from.</summary>
	public PrismNoiseBasis Basis { get; set; } = PrismNoiseBasis.Simplex;

	/// <summary>Which way the octaves are combined.</summary>
	protected abstract PrismFractalKind Kind { get; }

	/// <summary>The ridge offset. Only the ridged stack reads it.</summary>
	protected virtual float Offset => 1f;

	/// <summary>The stacked noise, in 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The raw stacked noise, before it is remapped into 0..1.</summary>
	[Out( "float", Name = "Signed" )] public PortRef Signed { get; set; }

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;

		if ( Octaves < 1 ) ctx.Warn( "Octaves is below one, so the stack produces nothing.", null );
		if ( Octaves > 12 ) ctx.Warn( $"{Octaves} octaves is a lot of texture-free noise for one pixel.", null );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var p = ctx.Bin( BinaryOp.Mul, Coordinate( ctx ), ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );

		var value = ctx.Helper( PrismNoiseHelpers.Fractal( Kind, Basis, IsVolume ), p,
			ctx.Const( Math.Clamp( Octaves, 1, 16 ) ),
			ctx.In( nameof( Lacunarity ), ctx.Const( DefaultLacunarity ) ),
			ctx.In( nameof( Gain ), ctx.Const( DefaultGain ) ),
			ctx.Const( Offset ) );

		ctx.Out( nameof( Signed ), value );

		ctx.Out( nameof( Out ), Kind == PrismFractalKind.Fbm
			? PrismNoiseSupport.ToUnit( ctx, value )
			: ctx.Call( Intrinsic.Saturate, value ) );
	}
}

/// <summary>Fractional Brownian motion: the plain sum of octaves.</summary>
[NodeInfo( Id = "prism.noise.fbm", Title = "fBm", Category = "Noise",
	Icon = "layers", Keywords = new[] { "fbm", "fractal", "octaves", "brownian", "clouds" },
	Description = "Stacks octaves of a noise basis with falling amplitude. Clouds, terrain, weathering." )]
[NodeVersion( 1 )]
public sealed class FbmNoiseNode : FractalNoiseNode
{
	/// <inheritdoc/>
	protected override PrismFractalKind Kind => PrismFractalKind.Fbm;
}

/// <summary>Turbulence: the sum of absolute octaves, which creases the field along its zero crossings.</summary>
[NodeInfo( Id = "prism.noise.turbulence", Title = "Turbulence", Category = "Noise",
	Icon = "local_fire_department", Keywords = new[] { "turbulence", "fractal", "fire", "smoke", "marble" },
	Description = "Stacks the absolute value of each octave, creasing the field where the noise crosses zero." )]
[NodeVersion( 1 )]
public sealed class TurbulenceNoiseNode : FractalNoiseNode
{
	/// <inheritdoc/>
	protected override PrismFractalKind Kind => PrismFractalKind.Turbulence;
}

/// <summary>Ridged multifractal: inverted, squared and weighted by the previous octave. Mountains.</summary>
[NodeInfo( Id = "prism.noise.ridged", Title = "Ridged Multifractal", Category = "Noise",
	Icon = "terrain", Keywords = new[] { "ridged", "multifractal", "mountains", "erosion" },
	Description = "Ridged noise: each octave is inverted, squared and scaled by the one before it." )]
[NodeVersion( 1 )]
public sealed class RidgedNoiseNode : FractalNoiseNode
{
	/// <summary>Where the ridge crest sits. One puts it exactly on the zero crossing.</summary>
	public float RidgeOffset { get; set; } = 1f;

	/// <inheritdoc/>
	protected override PrismFractalKind Kind => PrismFractalKind.Ridged;

	/// <inheritdoc/>
	protected override float Offset => RidgeOffset;
}

// ---------------------------------------------------------------------------------------------------
// Flow and hashes
// ---------------------------------------------------------------------------------------------------

/// <summary>The curl of a noise field: a divergence-free flow, which is what makes smoke swirl.</summary>
[NodeInfo( Id = "prism.noise.curl", Title = "Curl Noise", Category = "Noise",
	Icon = "cyclone", Keywords = new[] { "curl", "flow", "divergence", "smoke", "vector field" },
	Description = "A divergence-free vector field built from the curl of simplex noise." )]
[NodeVersion( 1 )]
public sealed class CurlNoiseNode : ProceduralFieldNode
{
	/// <summary>Where to evaluate.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the coordinate covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 4f;

	/// <summary>How far apart the finite differences are taken.</summary>
	public float Epsilon { get; set; } = 0.01f;

	/// <summary>The flow field.</summary>
	[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		base.OnDefinePorts( b );

		b?.Retype( nameof( Out ), CoordinateType );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var p = ctx.Bin( BinaryOp.Mul, Coordinate( ctx ), ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) ) );
		var epsilon = ctx.Const( Epsilon );

		ctx.Out( nameof( Out ), IsVolume
			? ctx.Helper( PrismNoiseHelpers.Curl3D, p, epsilon )
			: ctx.Helper( PrismNoiseHelpers.Curl2D, p, epsilon ) );
	}
}

/// <summary>An uncorrelated random value per input: white noise, with no smoothing at all.</summary>
[NodeInfo( Id = "prism.noise.white", Title = "White Noise", Category = "Noise",
	Icon = "grain", Keywords = new[] { "white", "hash", "random", "static", "noise" },
	Description = "A stable hash of the input. The same input always gives the same value, with no correlation between neighbours." )]
[NodeVersion( 1 )]
public sealed class WhiteNoiseNode : ProceduralFieldNode
{
	/// <summary>What to hash.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>A scalar random value.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>A vector random value, for offsetting or rotating per cell.</summary>
	[Out( "float3", Name = "Vector" )] public PortRef Vector { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var coordinate = Coordinate( ctx );

		if ( IsVolume )
		{
			ctx.Out( nameof( Out ), ctx.Helper( PrismCommon.Hash13, coordinate ) );
			ctx.Out( nameof( Vector ), ctx.Helper( PrismCommon.Hash33, coordinate ) );
			return;
		}

		ctx.Out( nameof( Out ), ctx.Helper( PrismCommon.Hash12, coordinate ) );

		var pair = ctx.Helper( PrismCommon.Hash22, coordinate );

		ctx.Out( nameof( Vector ), ctx.Construct( ShaderType.Float3, pair,
			ctx.Helper( PrismCommon.Hash12, ctx.Bin( BinaryOp.Add, coordinate, ctx.Const( 17.13f ) ) ) ) );
	}
}

/// <summary>The engine's own TV-static hash, kept so imported graphs keep looking the same.</summary>
[NodeInfo( Id = "prism.noise.fuzzy", Title = "Fuzzy Noise", Category = "Noise",
	Icon = "blur_on", Keywords = new[] { "fuzzy", "static", "tv", "hash", "noise" },
	Description = "The sine-hash TV static the engine's procedural header ships." )]
[NodeVersion( 1 )]
public sealed class FuzzyNoiseNode : PrismNode
{
	/// <summary>What to hash.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>The two constants the hash dots the coordinate against.</summary>
	public Vector2 Seed { get; set; } = new( 12.9898f, 78.233f );

	/// <summary>The static, in 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var uv = ctx.TryIn( nameof( UV ), out var wired ) ? wired : ctx.Builtin( Builtin.TexCoord0 );

		ctx.Out( nameof( Out ), ctx.Helper( PrismNoiseHelpers.FuzzyNoise, uv, ctx.Const( Seed ) ) );
	}
}

/// <summary>
/// A screen-space dither source. Interleaved gradient noise looks like film grain rather than a grid,
/// and stays stable between frames unless you animate it.
/// </summary>
[NodeInfo( Id = "prism.noise.blue", Title = "Blue Noise", Category = "Noise",
	Icon = "blur_linear", Keywords = new[] { "blue", "dither", "ign", "grain", "dissolve" },
	Description = "Interleaved gradient noise over screen position: the cheapest well-distributed dither source." )]
[NodeVersion( 1 )]
public sealed class BlueNoiseNode : PrismNode, IStageConstrained
{
	/// <summary>Where to sample. Defaults to the pixel's screen position.</summary>
	[In( "float2", Name = "Position" )] public PortRef ScreenPosition { get; set; }

	/// <summary>True to advance the pattern every frame, which trades grain for temporal shimmer.</summary>
	public bool Animate { get; set; }

	/// <summary>The dither value, in 0..1.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	StageMask IStageConstrained.RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var position = ctx.TryIn( nameof( ScreenPosition ), out var wired )
			? wired
			: ctx.Builtin( Builtin.PixelPosition );

		var frame = Animate
			? ctx.Cast( ctx.Builtin( Builtin.FrameCount ), ShaderType.Float )
			: ctx.Const( 0f );

		ctx.Out( nameof( Out ), ctx.Helper( PrismNoiseHelpers.InterleavedGradient, position, frame ) );
	}
}

/// <summary>Distorts a coordinate with noise before anything else samples it.</summary>
[NodeInfo( Id = "prism.noise.warp", Title = "Domain Warp", Category = "Noise",
	Icon = "waves", Keywords = new[] { "warp", "domain", "distort", "flow", "turbulence" },
	Description = "Offsets a coordinate by two decorrelated noise fields. The single cheapest way to make noise stop looking like noise." )]
[NodeVersion( 1 )]
public sealed class DomainWarpNode : ProceduralFieldNode
{
	/// <summary>The coordinate to warp.</summary>
	[In( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <summary>How many lattice cells the warp field covers.</summary>
	[In( "float", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>How far the coordinate is pushed.</summary>
	[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }

	/// <summary>The scale used when nothing is connected.</summary>
	[InlineValue( nameof( Scale ) )] public float DefaultScale { get; set; } = 3f;

	/// <summary>The strength used when nothing is connected.</summary>
	[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 0.2f;

	/// <summary>How many octaves the warp field uses.</summary>
	public int Octaves { get; set; } = 3;

	/// <summary>Which noise the warp field is built from.</summary>
	public PrismNoiseBasis Basis { get; set; } = PrismNoiseBasis.Simplex;

	/// <summary>The warped coordinate.</summary>
	[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		base.OnDefinePorts( b );

		b?.Retype( nameof( Out ), CoordinateType );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var coordinate = Coordinate( ctx );
		var scale = ctx.In( nameof( Scale ), ctx.Const( DefaultScale ) );
		var strength = ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) );

		var fractal = PrismNoiseHelpers.Fractal( PrismFractalKind.Fbm, Basis, IsVolume );
		var octaves = ctx.Const( Math.Clamp( Octaves, 1, 16 ) );
		var lacunarity = ctx.Const( 2f );
		var gain = ctx.Const( 0.5f );
		var offset = ctx.Const( 1f );

		var p = ctx.Bin( BinaryOp.Mul, coordinate, scale );

		if ( IsVolume )
		{
			var x = ctx.Helper( fractal, p, octaves, lacunarity, gain, offset );
			var y = ctx.Helper( fractal, ctx.Bin( BinaryOp.Add, p, ctx.Const( new Vector3( 5.2f, 1.3f, 7.1f ) ) ),
				octaves, lacunarity, gain, offset );
			var z = ctx.Helper( fractal, ctx.Bin( BinaryOp.Add, p, ctx.Const( new Vector3( 11.7f, 9.2f, 3.4f ) ) ),
				octaves, lacunarity, gain, offset );

			ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Add, coordinate,
				ctx.Bin( BinaryOp.Mul, ctx.Construct( ShaderType.Float3, x, y, z ), strength ) ) );

			return;
		}

		var u = ctx.Helper( fractal, p, octaves, lacunarity, gain, offset );
		var v = ctx.Helper( fractal, ctx.Bin( BinaryOp.Add, p, ctx.Const( new Vector2( 5.2f, 1.3f ) ) ),
			octaves, lacunarity, gain, offset );

		ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Add, coordinate,
			ctx.Bin( BinaryOp.Mul, ctx.Construct( ShaderType.Float2, u, v ), strength ) ) );
	}
}

/// <summary>Shared plumbing for the noise family.</summary>
internal static class PrismNoiseSupport
{
	/// <summary>Map a roughly -1..1 field into 0..1 without clipping the tails harder than it has to.</summary>
	internal static IrValue ToUnit( EmitContext ctx, IrValue signed ) =>
		ctx.Call( Intrinsic.Saturate,
			ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, signed, ctx.Const( 0.5f ) ), ctx.Const( 0.5f ) ) );
}