Shader helper and node implementations for blending and normal-map operations in the Prism editor. Defines enums for blend and normal-blend modes, many HLSL helper functions (as HelperFunction objects) for W3C blend modes and normal blending techniques, and several Prism nodes that emit IR for color blending, alpha compositing, normal combination, strength scaling, packing/unpacking normals and deriving normal Z.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// Blending.
//
// The complete W3C compositing set, including the four non-separable modes that need the whole pixel
// rather than one channel at a time, and the six normal-map blending techniques from "Blending in
// Detail". Two mistakes in the shipped implementations are deliberately not reproduced: soft light's
// `1.0 * 2.0 * b` term, which should be `1.0 - 2.0 * b`, and the vector ternaries, which HLSL 2021 and
// Slang both reject — every per-channel branch here is written with step() and lerp().
// ---------------------------------------------------------------------------------------------------
/// <summary>Every blend mode the Blend node can apply. Cb is the base layer, Cs is the blend layer.</summary>
public enum PrismBlendMode
{
/// <summary>The blend layer, unchanged.</summary>
Normal,
/// <summary>The darker of the two, per channel.</summary>
Darken,
/// <summary>Multiply. Always darkens.</summary>
Multiply,
/// <summary>Colour burn. Darkens by increasing contrast.</summary>
ColorBurn,
/// <summary>Linear burn. Adds and subtracts one.</summary>
LinearBurn,
/// <summary>The darker of the two pixels as a whole, by luminance.</summary>
DarkerColor,
/// <summary>The lighter of the two, per channel.</summary>
Lighten,
/// <summary>Screen. Always lightens.</summary>
Screen,
/// <summary>Colour dodge. Lightens by decreasing contrast.</summary>
ColorDodge,
/// <summary>Linear dodge, which is a plain add.</summary>
LinearDodge,
/// <summary>The lighter of the two pixels as a whole, by luminance.</summary>
LighterColor,
/// <summary>Overlay: multiply the dark half, screen the light half, keyed on the base.</summary>
Overlay,
/// <summary>Soft light. A gentle dodge and burn.</summary>
SoftLight,
/// <summary>Hard light: overlay keyed on the blend layer instead of the base.</summary>
HardLight,
/// <summary>Vivid light: burn below the mid-point, dodge above it.</summary>
VividLight,
/// <summary>Linear light: linear burn below the mid-point, linear dodge above it.</summary>
LinearLight,
/// <summary>Pin light: darken below the mid-point, lighten above it.</summary>
PinLight,
/// <summary>Hard mix. Snaps every channel to zero or one.</summary>
HardMix,
/// <summary>The absolute difference.</summary>
Difference,
/// <summary>Exclusion. Like difference, with a softer mid-tone.</summary>
Exclusion,
/// <summary>Subtract the blend layer from the base.</summary>
Subtract,
/// <summary>Divide the base by the blend layer.</summary>
Divide,
/// <summary>Negation. Difference reflected about white.</summary>
Negation,
/// <summary>The hue of the blend layer, with the saturation and luminosity of the base.</summary>
Hue,
/// <summary>The saturation of the blend layer, with the hue and luminosity of the base.</summary>
Saturation,
/// <summary>The hue and saturation of the blend layer, with the luminosity of the base.</summary>
Color,
/// <summary>The luminosity of the blend layer, with the hue and saturation of the base.</summary>
Luminosity
}
/// <summary>Which technique combines two tangent-space normals.</summary>
public enum PrismNormalBlendMode
{
/// <summary>Add and normalise. Cheapest, and flattens detail the most.</summary>
Linear,
/// <summary>Whiteout: add the tangents, multiply the up axes. The usual default.</summary>
Whiteout,
/// <summary>UDN: add the tangents, keep the base up axis. Cheapest of the good ones.</summary>
Udn,
/// <summary>Partial derivative blending. Correct slopes, cheap.</summary>
PartialDerivative,
/// <summary>Reoriented normal mapping: rotate the detail into the base's frame. The correct one.</summary>
Reoriented,
/// <summary>Photoshop-style overlay on the encoded normals.</summary>
Overlay,
/// <summary>Build a tangent basis from the base normal and rotate the detail through it.</summary>
TangentBasis
}
/// <summary>The blend-mode bodies.</summary>
internal static class PrismBlendHelpers
{
/// <summary>Colour dodge, with the two limit cases the spec calls out.</summary>
internal static readonly HelperFunction ColorDodge = new( "Prism_BlendColorDodge", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_BlendColorDodge( float3 Cb, float3 Cs )
{
float3 vResult = saturate( Cb / max( 1.0f - Cs, 1e-5f ) );
vResult = lerp( vResult, 1.0f, step( 1.0f, Cs ) );
vResult = lerp( vResult, 0.0f, step( Cb, 0.0f ) );
return vResult;
}
"""
};
/// <summary>Colour burn, with the two limit cases the spec calls out.</summary>
internal static readonly HelperFunction ColorBurn = new( "Prism_BlendColorBurn", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_BlendColorBurn( float3 Cb, float3 Cs )
{
float3 vResult = 1.0f - saturate( ( 1.0f - Cb ) / max( Cs, 1e-5f ) );
vResult = lerp( vResult, 0.0f, step( Cs, 0.0f ) );
vResult = lerp( vResult, 1.0f, step( 1.0f, Cb ) );
return vResult;
}
"""
};
/// <summary>Hard light: multiply below the mid-point, screen above it, keyed on the blend layer.</summary>
internal static readonly HelperFunction HardLight = new( "Prism_BlendHardLight", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_BlendHardLight( float3 Cb, float3 Cs )
{
float3 vMultiply = Cb * ( 2.0f * Cs );
float3 vScreenSource = 2.0f * Cs - 1.0f;
float3 vScreen = Cb + vScreenSource - Cb * vScreenSource;
return lerp( vMultiply, vScreen, step( 0.5f, Cs ) );
}
"""
};
/// <summary>Overlay is hard light with the layers swapped.</summary>
internal static readonly HelperFunction Overlay = new( "Prism_BlendOverlay", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [HardLight],
Hlsl = """
float3 Prism_BlendOverlay( float3 Cb, float3 Cs )
{
return Prism_BlendHardLight( Cs, Cb );
}
"""
};
/// <summary>Soft light, with the W3C transfer function rather than the approximation.</summary>
internal static readonly HelperFunction SoftLight = new( "Prism_BlendSoftLight", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_BlendSoftLight( float3 Cb, float3 Cs )
{
float3 vSteep = ( ( 16.0f * Cb - 12.0f ) * Cb + 4.0f ) * Cb;
float3 vD = lerp( sqrt( max( Cb, 0.0f ) ), vSteep, step( Cb, 0.25f ) );
float3 vLow = Cb - ( 1.0f - 2.0f * Cs ) * Cb * ( 1.0f - Cb );
float3 vHigh = Cb + ( 2.0f * Cs - 1.0f ) * ( vD - Cb );
return lerp( vLow, vHigh, step( 0.5f, Cs ) );
}
"""
};
/// <summary>Vivid light: colour burn below the mid-point, colour dodge above it.</summary>
internal static readonly HelperFunction VividLight = new( "Prism_BlendVividLight", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [ColorBurn, ColorDodge],
Hlsl = """
float3 Prism_BlendVividLight( float3 Cb, float3 Cs )
{
float3 vLow = Prism_BlendColorBurn( Cb, 2.0f * Cs );
float3 vHigh = Prism_BlendColorDodge( Cb, 2.0f * Cs - 1.0f );
return lerp( vLow, vHigh, step( 0.5f, Cs ) );
}
"""
};
/// <summary>Pin light: darken below the mid-point, lighten above it.</summary>
internal static readonly HelperFunction PinLight = new( "Prism_BlendPinLight", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_BlendPinLight( float3 Cb, float3 Cs )
{
float3 vLow = min( Cb, 2.0f * Cs );
float3 vHigh = max( Cb, 2.0f * Cs - 1.0f );
return lerp( vLow, vHigh, step( 0.5f, Cs ) );
}
"""
};
// ---- non-separable ----------------------------------------------------
/// <summary>The luminosity the compositing spec uses, which is not the Rec.709 one.</summary>
internal static readonly HelperFunction Lum = new( "Prism_BlendLum", ShaderType.Float,
[new HelperParam( "c", ShaderType.Float3 )] )
{
Hlsl = """
float Prism_BlendLum( float3 c )
{
return dot( c, float3( 0.3f, 0.59f, 0.11f ) );
}
"""
};
/// <summary>Pull a colour back inside the unit cube without changing its luminosity.</summary>
internal static readonly HelperFunction ClipColor = new( "Prism_BlendClipColor", ShaderType.Float3,
[new HelperParam( "c", ShaderType.Float3 )] )
{
Requires = [Lum],
Hlsl = """
float3 Prism_BlendClipColor( float3 c )
{
float l = Prism_BlendLum( c );
float n = min( c.r, min( c.g, c.b ) );
float x = max( c.r, max( c.g, c.b ) );
if ( n < 0.0f ) c = l + ( ( c - l ) * l ) / max( l - n, 1e-6f );
if ( x > 1.0f ) c = l + ( ( c - l ) * ( 1.0f - l ) ) / max( x - l, 1e-6f );
return c;
}
"""
};
/// <summary>Set a colour's luminosity, keeping its hue and saturation.</summary>
internal static readonly HelperFunction SetLum = new( "Prism_BlendSetLum", ShaderType.Float3,
[
new HelperParam( "c", ShaderType.Float3 ),
new HelperParam( "l", ShaderType.Float )
] )
{
Requires = [Lum, ClipColor],
Hlsl = """
float3 Prism_BlendSetLum( float3 c, float l )
{
return Prism_BlendClipColor( c + ( l - Prism_BlendLum( c ) ) );
}
"""
};
/// <summary>The saturation the compositing spec uses: the channel range.</summary>
internal static readonly HelperFunction Sat = new( "Prism_BlendSat", ShaderType.Float,
[new HelperParam( "c", ShaderType.Float3 )] )
{
Hlsl = """
float Prism_BlendSat( float3 c )
{
return max( c.r, max( c.g, c.b ) ) - min( c.r, min( c.g, c.b ) );
}
"""
};
/// <summary>Rescale a colour so its channel range becomes the given saturation.</summary>
internal static readonly HelperFunction SetSat = new( "Prism_BlendSetSat", ShaderType.Float3,
[
new HelperParam( "c", ShaderType.Float3 ),
new HelperParam( "s", ShaderType.Float )
] )
{
Hlsl = """
float3 Prism_BlendSetSat( float3 c, float s )
{
float mn = min( c.r, min( c.g, c.b ) );
float mx = max( c.r, max( c.g, c.b ) );
if ( mx <= mn )
return float3( 0.0f, 0.0f, 0.0f );
return ( c - mn ) * s / ( mx - mn );
}
"""
};
/// <summary>The hue of the blend layer over the base.</summary>
internal static readonly HelperFunction Hue = new( "Prism_BlendHue", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum, Sat, SetSat, SetLum],
Hlsl = """
float3 Prism_BlendHue( float3 Cb, float3 Cs )
{
return Prism_BlendSetLum( Prism_BlendSetSat( Cs, Prism_BlendSat( Cb ) ), Prism_BlendLum( Cb ) );
}
"""
};
/// <summary>The saturation of the blend layer over the base.</summary>
internal static readonly HelperFunction Saturation = new( "Prism_BlendSaturation", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum, Sat, SetSat, SetLum],
Hlsl = """
float3 Prism_BlendSaturation( float3 Cb, float3 Cs )
{
return Prism_BlendSetLum( Prism_BlendSetSat( Cb, Prism_BlendSat( Cs ) ), Prism_BlendLum( Cb ) );
}
"""
};
/// <summary>The hue and saturation of the blend layer over the base.</summary>
internal static readonly HelperFunction Color = new( "Prism_BlendColor", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum, SetLum],
Hlsl = """
float3 Prism_BlendColor( float3 Cb, float3 Cs )
{
return Prism_BlendSetLum( Cs, Prism_BlendLum( Cb ) );
}
"""
};
/// <summary>The luminosity of the blend layer over the base.</summary>
internal static readonly HelperFunction Luminosity = new( "Prism_BlendLuminosity", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum, SetLum],
Hlsl = """
float3 Prism_BlendLuminosity( float3 Cb, float3 Cs )
{
return Prism_BlendSetLum( Cb, Prism_BlendLum( Cs ) );
}
"""
};
/// <summary>Whichever of the two pixels is darker overall.</summary>
internal static readonly HelperFunction DarkerColor = new( "Prism_BlendDarkerColor", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum],
Hlsl = """
float3 Prism_BlendDarkerColor( float3 Cb, float3 Cs )
{
return Prism_BlendLum( Cs ) < Prism_BlendLum( Cb ) ? Cs : Cb;
}
"""
};
/// <summary>Whichever of the two pixels is lighter overall.</summary>
internal static readonly HelperFunction LighterColor = new( "Prism_BlendLighterColor", ShaderType.Float3,
[
new HelperParam( "Cb", ShaderType.Float3 ),
new HelperParam( "Cs", ShaderType.Float3 )
] )
{
Requires = [Lum],
Hlsl = """
float3 Prism_BlendLighterColor( float3 Cb, float3 Cs )
{
return Prism_BlendLum( Cs ) > Prism_BlendLum( Cb ) ? Cs : Cb;
}
"""
};
/// <summary>
/// Apply one blend mode. The cheap modes are built straight out of IR so they fold and common
/// subexpression eliminate with everything around them; only the ones with real branching or a
/// whole-pixel dependency become helper calls.
/// </summary>
internal static IrValue Apply( EmitContext ctx, PrismBlendMode mode, IrValue cb, IrValue cs )
{
if ( ctx is null || !cb.IsValid || !cs.IsValid ) return IrValue.Invalid;
switch ( mode )
{
case PrismBlendMode.Normal:
return cs;
case PrismBlendMode.Darken:
return ctx.Call( Intrinsic.Min, cb, cs );
case PrismBlendMode.Lighten:
return ctx.Call( Intrinsic.Max, cb, cs );
case PrismBlendMode.Multiply:
return ctx.Bin( BinaryOp.Mul, cb, cs );
case PrismBlendMode.Screen:
// Cb + Cs - Cb * Cs
return ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Add, cb, cs ), ctx.Bin( BinaryOp.Mul, cb, cs ) );
case PrismBlendMode.LinearBurn:
return ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Add, cb, cs ), ctx.Const( 1f ) );
case PrismBlendMode.LinearDodge:
return ctx.Bin( BinaryOp.Add, cb, cs );
case PrismBlendMode.LinearLight:
// LinearBurn( Cb, 2Cs ) and LinearDodge( Cb, 2Cs - 1 ) are the same expression.
return ctx.Bin( BinaryOp.Sub,
ctx.Bin( BinaryOp.Add, cb, ctx.Bin( BinaryOp.Mul, ctx.Const( 2f ), cs ) ), ctx.Const( 1f ) );
case PrismBlendMode.Difference:
return ctx.Call( Intrinsic.Abs, ctx.Bin( BinaryOp.Sub, cb, cs ) );
case PrismBlendMode.Exclusion:
// Cb + Cs - 2 * Cb * Cs
return ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Add, cb, cs ),
ctx.Bin( BinaryOp.Mul, ctx.Const( 2f ), ctx.Bin( BinaryOp.Mul, cb, cs ) ) );
case PrismBlendMode.Subtract:
return ctx.Bin( BinaryOp.Sub, cb, cs );
case PrismBlendMode.Divide:
return ctx.Bin( BinaryOp.Div, cb, ctx.Call( Intrinsic.Max, cs, ctx.Const( 1e-5f ) ) );
case PrismBlendMode.Negation:
// 1 - |1 - Cb - Cs|
return ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), ctx.Call( Intrinsic.Abs,
ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), cb ), cs ) ) );
case PrismBlendMode.HardMix:
return ctx.Call( Intrinsic.Step, ctx.Const( 1f ), ctx.Bin( BinaryOp.Add, cb, cs ) );
case PrismBlendMode.ColorBurn:
return ctx.Helper( ColorBurn, cb, cs );
case PrismBlendMode.ColorDodge:
return ctx.Helper( ColorDodge, cb, cs );
case PrismBlendMode.DarkerColor:
return ctx.Helper( DarkerColor, cb, cs );
case PrismBlendMode.LighterColor:
return ctx.Helper( LighterColor, cb, cs );
case PrismBlendMode.Overlay:
return ctx.Helper( Overlay, cb, cs );
case PrismBlendMode.HardLight:
return ctx.Helper( HardLight, cb, cs );
case PrismBlendMode.SoftLight:
return ctx.Helper( SoftLight, cb, cs );
case PrismBlendMode.VividLight:
return ctx.Helper( VividLight, cb, cs );
case PrismBlendMode.PinLight:
return ctx.Helper( PinLight, cb, cs );
case PrismBlendMode.Hue:
return ctx.Helper( Hue, cb, cs );
case PrismBlendMode.Saturation:
return ctx.Helper( Saturation, cb, cs );
case PrismBlendMode.Color:
return ctx.Helper( Color, cb, cs );
case PrismBlendMode.Luminosity:
return ctx.Helper( Luminosity, cb, cs );
default:
return cs;
}
}
}
/// <summary>The normal-blending bodies.</summary>
internal static class PrismNormalHelpers
{
/// <summary>Scale the tangent of a normal, keeping it a unit vector.</summary>
internal static readonly HelperFunction NormalStrength = new( "Prism_NormalStrength", ShaderType.Float3,
[
new HelperParam( "vNormal", ShaderType.Float3 ),
new HelperParam( "flStrength", ShaderType.Float )
] )
{
Hlsl = """
float3 Prism_NormalStrength( float3 vNormal, float flStrength )
{
return float3( vNormal.xy * flStrength, lerp( 1.0f, vNormal.z, saturate( flStrength ) ) );
}
"""
};
/// <summary>Reconstruct the up axis of a normal from its tangent components.</summary>
internal static readonly HelperFunction DeriveZ = new( "Prism_DeriveNormalZ", ShaderType.Float3,
[new HelperParam( "vXY", ShaderType.Float2 )] )
{
Hlsl = """
float3 Prism_DeriveNormalZ( float2 vXY )
{
return float3( vXY, sqrt( saturate( 1.0f - dot( vXY, vXY ) ) ) );
}
"""
};
/// <summary>Whiteout blending: add the tangents, multiply the up axes.</summary>
internal static readonly HelperFunction Whiteout = new( "Prism_NormalBlendWhiteout", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendWhiteout( float3 n1, float3 n2 )
{
return normalize( float3( n1.xy + n2.xy, n1.z * n2.z ) );
}
"""
};
/// <summary>UDN blending: add the tangents, keep the base up axis.</summary>
internal static readonly HelperFunction Udn = new( "Prism_NormalBlendUdn", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendUdn( float3 n1, float3 n2 )
{
return normalize( float3( n1.xy + n2.xy, n1.z ) );
}
"""
};
/// <summary>Partial-derivative blending: add the slopes rather than the vectors.</summary>
internal static readonly HelperFunction PartialDerivative = new( "Prism_NormalBlendPartialDerivative", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendPartialDerivative( float3 n1, float3 n2 )
{
return normalize( float3( n1.xy * n2.z + n2.xy * n1.z, n1.z * n2.z ) );
}
"""
};
/// <summary>Reoriented normal mapping: rotate the detail normal into the base normal's frame.</summary>
internal static readonly HelperFunction Reoriented = new( "Prism_NormalBlendReoriented", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendReoriented( float3 n1, float3 n2 )
{
float3 t = n1.xyz + float3( 0.0f, 0.0f, 1.0f );
float3 u = n2.xyz * float3( -1.0f, -1.0f, 1.0f );
return normalize( t * dot( t, u ) / max( t.z, 1e-5f ) - u );
}
"""
};
/// <summary>Photoshop overlay on the encoded normals, then decode.</summary>
internal static readonly HelperFunction Overlay = new( "Prism_NormalBlendOverlay", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendOverlay( float3 n1, float3 n2 )
{
float3 e1 = n1 * 0.5f + 0.5f;
float3 e2 = n2 * 0.5f + 0.5f;
float3 vLow = 2.0f * e1 * e2;
float3 vHigh = 1.0f - 2.0f * ( 1.0f - e1 ) * ( 1.0f - e2 );
float3 r = lerp( vLow, vHigh, step( 0.5f, e1 ) );
return normalize( r * 2.0f - 1.0f );
}
"""
};
/// <summary>Build a tangent basis from the base normal and rotate the detail through it.</summary>
internal static readonly HelperFunction TangentBasis = new( "Prism_NormalBlendTangentBasis", ShaderType.Float3,
[
new HelperParam( "n1", ShaderType.Float3 ),
new HelperParam( "n2", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_NormalBlendTangentBasis( float3 n1, float3 n2 )
{
float3 vRow0 = float3( n1.z, n1.y, -n1.x );
float3 vRow1 = float3( n1.x, n1.z, -n1.y );
float3 vRow2 = float3( n1.x, n1.y, n1.z );
return normalize( n2.x * vRow0 + n2.y * vRow1 + n2.z * vRow2 );
}
"""
};
/// <summary>The body for a normal-blending technique. Linear has none: it is one add and a normalise.</summary>
internal static HelperFunction For( PrismNormalBlendMode mode ) => mode switch
{
PrismNormalBlendMode.Udn => Udn,
PrismNormalBlendMode.PartialDerivative => PartialDerivative,
PrismNormalBlendMode.Reoriented => Reoriented,
PrismNormalBlendMode.Overlay => Overlay,
PrismNormalBlendMode.TangentBasis => TangentBasis,
_ => Whiteout
};
}
// ---------------------------------------------------------------------------------------------------
// Colour blending
// ---------------------------------------------------------------------------------------------------
/// <summary>Composites one colour over another with a blend mode and an opacity.</summary>
[NodeInfo( Id = "prism.blend.mix", Title = "Blend", Category = "Blend",
Icon = "layers", Keywords = new[] { "blend", "mix", "overlay", "multiply", "screen", "photoshop" },
Description = "The full W3C blend-mode set, including the four non-separable modes, with an opacity and an optional clamp." )]
[NodeVersion( 1 )]
public sealed class BlendNode : PrismNode
{
/// <summary>The base layer, underneath.</summary>
[In( "float3", Name = "Base" )] public PortRef A { get; set; }
/// <summary>The blend layer, on top.</summary>
[In( "float3", Name = "Blend" )] public PortRef B { get; set; }
/// <summary>How much of the blended result to keep.</summary>
[In( "float", Name = "Opacity" )] public PortRef T { get; set; }
/// <summary>The base used when nothing is connected.</summary>
[InlineValue( nameof( A ) )] public Color DefaultA { get; set; } = Color.Black;
/// <summary>The blend layer used when nothing is connected.</summary>
[InlineValue( nameof( B ) )] public Color DefaultB { get; set; } = Color.White;
/// <summary>The opacity used when nothing is connected.</summary>
[InlineValue( nameof( T ) )] public float DefaultT { get; set; } = 1f;
/// <summary>Which blend mode to apply.</summary>
public PrismBlendMode Mode { get; set; } = PrismBlendMode.Normal;
/// <summary>True to clamp the result into 0..1, which most of the modes assume.</summary>
public bool Clamp { get; set; } = true;
/// <summary>The composited colour.</summary>
[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null || Clamp ) return;
if ( Mode is PrismBlendMode.Hue or PrismBlendMode.Saturation or PrismBlendMode.Color or PrismBlendMode.Luminosity )
{
return;
}
ctx.Info( "This mode is defined on 0..1 values. With Clamp off, out-of-range input will not behave.", null );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var cb = ctx.InAs( nameof( A ), ShaderType.Float3 );
var cs = ctx.InAs( nameof( B ), ShaderType.Float3 );
if ( !cb.IsValid || !cs.IsValid ) return;
var blended = PrismBlendHelpers.Apply( ctx, Mode, cb, cs );
if ( !blended.IsValid ) return;
var opacity = ctx.In( nameof( T ), ctx.Const( DefaultT ) );
var result = ctx.Call( Intrinsic.Lerp, cb, blended, opacity );
ctx.Out( nameof( Out ), Clamp ? ctx.Call( Intrinsic.Saturate, result ) : result );
}
}
/// <summary>Composites a colour with straight alpha over another, the way a painting program does.</summary>
[NodeInfo( Id = "prism.blend.alpha", Title = "Alpha Composite", Category = "Blend",
Icon = "opacity", Keywords = new[] { "alpha", "composite", "over", "premultiplied", "porter duff" },
Description = "Source-over compositing of two colours with alpha, producing the correct combined alpha." )]
[NodeVersion( 1 )]
public sealed class AlphaCompositeNode : PrismNode
{
/// <summary>The layer underneath.</summary>
[In( "float4", Name = "Base" )] public PortRef Base { get; set; }
/// <summary>The layer on top.</summary>
[In( "float4", Name = "Source" )] public PortRef Source { get; set; }
/// <summary>An extra opacity applied to the source before compositing.</summary>
[In( "float", Name = "Opacity" )] public PortRef Opacity { get; set; }
/// <summary>The base used when nothing is connected.</summary>
[InlineValue( nameof( Base ) )] public Color DefaultBase { get; set; } = Color.Black;
/// <summary>The source used when nothing is connected.</summary>
[InlineValue( nameof( Source ) )] public Color DefaultSource { get; set; } = Color.White;
/// <summary>The opacity used when nothing is connected.</summary>
[InlineValue( nameof( Opacity ) )] public float DefaultOpacity { get; set; } = 1f;
/// <summary>True when the inputs already have their colour multiplied by their alpha.</summary>
public bool Premultiplied { get; set; }
/// <summary>The composited colour and alpha.</summary>
[Out( "float4", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var baseColor = ctx.InAs( nameof( Base ), ShaderType.Float4 );
var source = ctx.InAs( nameof( Source ), ShaderType.Float4 );
if ( !baseColor.IsValid || !source.IsValid ) return;
var opacity = ctx.In( nameof( Opacity ), ctx.Const( DefaultOpacity ) );
var srcAlpha = ctx.Bin( BinaryOp.Mul, ctx.Swizzle( source, "w" ), opacity );
var dstAlpha = ctx.Swizzle( baseColor, "w" );
// out.a = as + ab * ( 1 - as )
var outAlpha = ctx.Bin( BinaryOp.Add, srcAlpha,
ctx.Bin( BinaryOp.Mul, dstAlpha, ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), srcAlpha ) ) );
var srcRgb = ctx.Swizzle( source, "xyz" );
var dstRgb = ctx.Swizzle( baseColor, "xyz" );
IrValue rgb;
if ( Premultiplied )
{
// Colour is already scaled by alpha, so the source term only needs the extra opacity.
rgb = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, srcRgb, opacity ),
ctx.Bin( BinaryOp.Mul, dstRgb, ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), srcAlpha ) ) );
}
else
{
var weighted = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, srcRgb, srcAlpha ),
ctx.Bin( BinaryOp.Mul, ctx.Bin( BinaryOp.Mul, dstRgb, dstAlpha ),
ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), srcAlpha ) ) );
rgb = ctx.Bin( BinaryOp.Div, weighted, ctx.Call( Intrinsic.Max, outAlpha, ctx.Const( 1e-5f ) ) );
}
ctx.Out( nameof( Out ), ctx.Construct( ShaderType.Float4, rgb, outAlpha ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Normal blending
// ---------------------------------------------------------------------------------------------------
/// <summary>Combines two tangent-space normals.</summary>
[NodeInfo( Id = "prism.blend.normal", Title = "Normal Blend", Category = "Blend",
Icon = "gradient", Keywords = new[] { "normal", "blend", "detail", "whiteout", "rnm", "reoriented" },
Description = "Combines a base normal with a detail normal, using any of six published techniques." )]
[NodeVersion( 1 )]
public sealed class NormalBlendNode : PrismNode
{
/// <summary>The base normal.</summary>
[In( "float3", Name = "A" )] public PortRef A { get; set; }
/// <summary>The detail normal to layer on top.</summary>
[In( "float3", Name = "B" )] public PortRef B { get; set; }
/// <summary>The base used when nothing is connected.</summary>
[InlineValue( nameof( A ) )] public Vector3 DefaultA { get; set; } = new( 0f, 0f, 1f );
/// <summary>The detail used when nothing is connected.</summary>
[InlineValue( nameof( B ) )] public Vector3 DefaultB { get; set; } = new( 0f, 0f, 1f );
/// <summary>Which technique to combine them with.</summary>
public PrismNormalBlendMode Mode { get; set; } = PrismNormalBlendMode.Whiteout;
/// <summary>The combined normal.</summary>
[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var a = ctx.InAs( nameof( A ), ShaderType.Float3 );
var b = ctx.InAs( nameof( B ), ShaderType.Float3 );
if ( !a.IsValid || !b.IsValid ) return;
if ( Mode == PrismNormalBlendMode.Linear )
{
ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Normalize, ctx.Bin( BinaryOp.Add, a, b ) ) );
return;
}
ctx.Out( nameof( Out ), ctx.Helper( PrismNormalHelpers.For( Mode ), a, b ) );
}
}
/// <summary>Scales how far a tangent-space normal leans away from straight up.</summary>
[NodeInfo( Id = "prism.blend.normalStrength", Title = "Normal Strength", Category = "Blend",
Icon = "tune", Keywords = new[] { "normal", "strength", "intensity", "flatten" },
Description = "Scales the tangent components of a normal. Zero flattens it, one leaves it alone." )]
[NodeVersion( 1 )]
public sealed class NormalStrengthNode : PrismNode
{
/// <summary>The normal to scale.</summary>
[In( "float3", Name = "In" )] public PortRef In { get; set; }
/// <summary>How far it leans.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>The normal used when nothing is connected.</summary>
[InlineValue( nameof( In ) )] public Vector3 DefaultIn { get; set; } = new( 0f, 0f, 1f );
/// <summary>The strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 1f;
/// <summary>The scaled normal.</summary>
[Out( "float3", 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( PrismNormalHelpers.NormalStrength,
ctx.InAs( nameof( In ), ShaderType.Float3 ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ) ) );
}
}
/// <summary>Unpacks a 0..1 encoded normal into a signed unit vector.</summary>
[NodeInfo( Id = "prism.blend.normalUnpack", Title = "Normal Unpack", Category = "Blend",
Icon = "unfold_more", Keywords = new[] { "normal", "unpack", "decode", "expand" },
Description = "Maps 0..1 to -1..1 and normalises. What a raw normal-map read needs before anything else touches it." )]
[NodeVersion( 1 )]
public sealed class NormalUnpackNode : PrismNode
{
/// <summary>The encoded normal.</summary>
[In( "float3", Name = "In" )] public PortRef In { get; set; }
/// <summary>The value used when nothing is connected.</summary>
[InlineValue( nameof( In ) )] public Vector3 DefaultIn { get; set; } = new( 0.5f, 0.5f, 1f );
/// <summary>The unit normal.</summary>
[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx ) =>
ctx?.Out( nameof( Out ), ctx.Helper( PrismCommon.DecodeNormal, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}
/// <summary>Packs a signed unit normal back into the 0..1 range a texture stores.</summary>
[NodeInfo( Id = "prism.blend.normalPack", Title = "Normal Pack", Category = "Blend",
Icon = "unfold_less", Keywords = new[] { "normal", "pack", "encode", "compress" },
Description = "Maps -1..1 to 0..1, for writing a normal into a colour buffer." )]
[NodeVersion( 1 )]
public sealed class NormalPackNode : PrismNode
{
/// <summary>The unit normal.</summary>
[In( "float3", Name = "In" )] public PortRef In { get; set; }
/// <summary>The value used when nothing is connected.</summary>
[InlineValue( nameof( In ) )] public Vector3 DefaultIn { get; set; } = new( 0f, 0f, 1f );
/// <summary>The encoded normal.</summary>
[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var value = ctx.InAs( nameof( In ), ShaderType.Float3 );
if ( !value.IsValid ) return;
ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Add,
ctx.Bin( BinaryOp.Mul, value, ctx.Const( 0.5f ) ), ctx.Const( 0.5f ) ) );
}
}
/// <summary>Rebuilds the up axis of a normal from its two tangent components.</summary>
[NodeInfo( Id = "prism.blend.deriveNormalZ", Title = "Derive Normal Z", Category = "Blend",
Icon = "height", Keywords = new[] { "normal", "z", "reconstruct", "derive", "bc5" },
Description = "Reconstructs the third component of a unit normal. What a two-channel normal map needs." )]
[NodeVersion( 1 )]
public sealed class DeriveNormalZNode : PrismNode
{
/// <summary>The two tangent components.</summary>
[In( "float2", Name = "XY" )] public PortRef XY { get; set; }
/// <summary>The value used when nothing is connected.</summary>
[InlineValue( nameof( XY ) )] public Vector2 DefaultXY { get; set; }
/// <summary>The complete unit normal.</summary>
[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx ) =>
ctx?.Out( nameof( Out ), ctx.Helper( PrismNormalHelpers.DeriveZ, ctx.InAs( nameof( XY ), ShaderType.Float2 ) ) );
}