namespace Editor.ShaderGraph.Nodes; public enum BlendNodeMode { Mix, Darken, Multiply, ColorBurn, LinearBurn, Lighten, Screen, ColorDodge, LinearDodge, Overlay, SoftLight, HardLight, VividLight, LinearLight, HardMix, Difference, Exclusion, Subtract, Divide, Add, } /// /// Normalize a vector to have a length of 1 unit /// [Title( "Normalize" ), Category( "Transform" ), Icon( "arrow_forward" )] public sealed class Normalize : Unary { [Hide] protected override string Op => "normalize"; } public enum NormalSpace { Tangent, Object, World, } public enum OutputNormalSpace { Tangent, World } /// /// Transforms a normal from tangent or object space into world space /// [Title( "Transform Normal" ), Category( "Transform" ), Icon( "shortcut" )] public sealed class TransformNormal : ShaderNode { /// /// Normal input. No input specified will output vertex normal in world space /// [Input] [Hide] public NodeInput Input { get; set; } /// /// Space of the input normal, tangent or object. /// public NormalSpace InputSpace { get; set; } = NormalSpace.Tangent; /// /// Should we output in world space or tangent space. /// public OutputNormalSpace OutputSpace { get; set; } = OutputNormalSpace.Tangent; /// /// Scale and shifts input value to ( -1, 1 ) range /// public bool DecodeNormal { get; set; } = true; [Output] [Hide] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var result = compiler.Result( Input ); if ( !result.IsValid ) { // No input, just return the vertex normal in worldspace or a default tangent space output. return OutputSpace == OutputNormalSpace.World ? new NodeResult( 3, "i.vNormalWs.xyz" ) : new NodeResult( 3, "float3( 0, 0, 1 )" ); } // Cast the result to a float3 var resultCast = result.Cast( 3 ); string inputNormal; if ( compiler.IsPreview ) { // Because this is in preview mode, we can afford to use a dynamic branch for the decode normal option inputNormal = $"{compiler.ResultValue( DecodeNormal )} ? DecodeNormal( {resultCast} ) : {resultCast}"; } else { // Decode normal if it's enabled, otherwise just use it as is inputNormal = DecodeNormal ? $"DecodeNormal( {resultCast} )" : resultCast; } if ( InputSpace == NormalSpace.Object ) { inputNormal = compiler.ResultFunction( "Vec3OsToTs", inputNormal, "i.vNormalOs.xyz", "i.vTangentUOs_flTangentVSign.xyz", "cross( i.vNormalOs.xyz, i.vTangentUOs_flTangentVSign.xyz ) * i.vTangentUOs_flTangentVSign.w" ); } else if ( InputSpace == NormalSpace.World ) { inputNormal = $"Vec3WsToTs( {inputNormal}, i.vNormalWs, i.vTangentUWs, i.vTangentVWs )"; } return OutputSpace == OutputNormalSpace.World ? new NodeResult( 3, $"TransformNormal( {inputNormal}, i.vNormalWs, i.vTangentUWs, i.vTangentVWs )" ) : new NodeResult( 3, $"{inputNormal}" ); }; } /// /// Translate, rotate and scale a . /// [Title( "Apply TRS" ), Category( "Transform" ), Icon( "3d_rotation" )] public sealed class ApplyTrs : ShaderNode { [Input( typeof( Vector3 ) )] [Hide] public NodeInput Vector { get; set; } [Input( typeof( Vector3 ) )] [Hide] public NodeInput Translation { get; set; } [Input( typeof( Vector3 ) )] [Hide] public NodeInput Rotation { get; set; } [Input( typeof( Vector3 ) )] [Hide] public NodeInput Scale { get; set; } [InputDefault( nameof( Vector ) )] public Vector3 DefaultVector { get; set; } = Vector3.Zero; [InputDefault( nameof( Translation ) )] public Vector3 DefaultTranslation { get; set; } = Vector3.Zero; [InputDefault( nameof( Rotation ) )] public Rotation DefaultRotation { get; set; } = global::Rotation.Identity; [InputDefault( nameof( Scale ) )] public Vector3 DefaultScale { get; set; } = Vector3.One; [Output] [Hide] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var vector = compiler.Result( Vector ); if ( !vector.IsValid() ) { vector = compiler.ResultValue( DefaultVector ); } // Only use DefaultXYZ if a non-default value is specified, so we can skip some matrix multiplications var translation = DefaultTranslation == Vector3.Zero ? compiler.Result( Translation ) : compiler.ResultOrDefault( Translation, DefaultTranslation ); var scale = DefaultScale == Vector3.One ? compiler.Result( Scale ) : compiler.ResultOrDefault( Scale, DefaultScale ); NodeResult rotation; if ( compiler.Result( Rotation ) is { IsValid: true } rotationResult ) { rotation = new NodeResult( 4, compiler.ResultFunction( "Quaternion_FromAngles", rotationResult.Code ) ); } else { rotation = compiler.ResultValue( new Vector4( DefaultRotation.x, DefaultRotation.y, DefaultRotation.z, DefaultRotation.w ) ); } string matrix = null; if ( scale.IsValid ) ApplyMatrix( ref matrix, compiler.ResultFunction( "Matrix_FromScale", scale.Code ) ); if ( rotation.IsValid ) ApplyMatrix( ref matrix, compiler.ResultFunction( "Matrix_FromQuaternion", rotation.Code ) ); if ( translation.IsValid ) ApplyMatrix( ref matrix, compiler.ResultFunction( "Matrix_FromTranslation", translation.Code ) ); return matrix is null ? vector : new NodeResult( 3, $"mul( {matrix}, float4( {vector.Code}, 1.0 ) ).xyz" ); }; private static void ApplyMatrix( ref string lhs, string rhs ) { lhs = lhs is null ? rhs : $"mul( {lhs}, {rhs} )"; } } /// /// Convert from Cartesian coordinates to polar coordinates. /// [Title( "Polar Coordinates" ), Category( "Transform" ), Icon( "explore" )] public sealed class PolarCoordinates : ShaderNode { [Input( typeof( Vector2 ) )] [Hide] public NodeInput Coords { get; set; } [Input( typeof( Vector2 ) )] [Hide] public NodeInput Center { get; set; } [Input( typeof( float ) )] [Hide] public NodeInput RadialScale { get; set; } [Input( typeof( float ) )] [Hide] public NodeInput LengthScale { get; set; } [InputDefault( nameof( Center ) )] public Vector2 DefaultCenter { get; set; } = 0.5f; [InputDefault( nameof( RadialScale ) )] public float DefaultRadialScale { get; set; } = 1.0f; [InputDefault( nameof( LengthScale ) )] public float DefaultLengthScale { get; set; } = 1.0f; [Output] [Hide] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var coords = compiler.Result( Coords ); var center = compiler.ResultOrDefault( Center, DefaultCenter ); var radialScale = compiler.ResultOrDefault( RadialScale, DefaultRadialScale ); var lengthScale = compiler.ResultOrDefault( LengthScale, DefaultLengthScale ); return new NodeResult( 2, $"PolarCoordinates( ( {(coords.IsValid ? coords : "i.vTextureCoords.xy")} ) - ( {(center.IsValid ? center : "0.0f")} ), {(radialScale.IsValid ? radialScale : "1.0f")}, {(lengthScale.IsValid ? lengthScale : "1.0f")} )" ); }; } /// /// Tile or shift your texture coordinates. Tile works by scaling the texture up /// and down. Offset works by adding or subtracting from the texture coordinates /// [Title( "Tile And Offset" ), Category( "Transform" ), Icon( "grid_view" )] public sealed class TileAndOffset : ShaderNode { [Input( typeof( Vector2 ) )] [Hide] public NodeInput Coords { get; set; } [Input( typeof( Vector2 ) )] [Hide] public NodeInput Tile { get; set; } [Input( typeof( Vector2 ) )] [Hide] public NodeInput Offset { get; set; } [InputDefault( nameof( Tile ) )] public Vector2 DefaultTile { get; set; } = 1.0f; [InputDefault( nameof( Offset ) )] public Vector2 DefaultOffset { get; set; } = 0.0f; public bool WrapTo01 { get; set; } = false; [Output( typeof( Vector2 ) ), Title( "Coords" )] [Hide] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var coords = compiler.Result( Coords ); var tile = compiler.ResultOrDefault( Tile, DefaultTile ); var offset = compiler.ResultOrDefault( Offset, DefaultOffset ); var resultCode = $"TileAndOffsetUv( {(coords.IsValid ? coords.Cast( 2 ) : "i.vTextureCoords.xy")}," + $" {(tile.IsValid ? tile.Cast( 2 ) : "1.0f")}," + $" {(offset.IsValid ? offset.Cast( 2 ) : "0.0f")} )"; if ( compiler.IsPreview ) { resultCode = $"{compiler.ResultValue( WrapTo01 )} ? frac( {resultCode} ) : {resultCode}"; } else if ( WrapTo01 ) { resultCode = $"frac( {resultCode} )"; } return new NodeResult( 2, resultCode ); }; } /// /// Blend two colors or textures together using various different blending modes /// [Title( "Blend" ), Category( "Transform" ), Icon( "blender" )] public sealed class Blend : ShaderNode { [Input( typeof( Color ) )] [Hide] public NodeInput A { get; set; } [Input( typeof( Color ) )] [Hide] public NodeInput B { get; set; } [Input( typeof( float ) ), Title( "Fraction" )] [Hide, Editor( nameof( Fraction ) )] public NodeInput C { get; set; } [InputDefault( nameof( A ) )] public Color DefaultA { get; set; } = Color.Black; [InputDefault( nameof( B ) )] public Color DefaultB { get; set; } = Color.White; [InputDefault( nameof( C ) ), MinMax( 0, 1 )] public float Fraction { get; set; } = 0.5f; public BlendNodeMode BlendMode { get; set; } = BlendNodeMode.Mix; /// /// Clamp result between 0 and 1 /// public bool Clamp { get; set; } = true; [Output] [Hide] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var resultA = compiler.Result( A ); var resultB = compiler.Result( B ); var results = compiler.Result( A, B ); var fraction = compiler.Result( C ); var fractionType = fraction.IsValid && fraction.Components > 1 ? Math.Max( results.Item1.Components, results.Item2.Components ) : 1; string fractionStr = $"{(fraction.IsValid ? fraction.Cast( fractionType ) : compiler.ResultValue( Fraction ))}"; string aStr = resultA.IsValid ? results.Item1.ToString() : compiler.ResultValue( DefaultA ).ToString(); string bStr = resultB.IsValid ? results.Item2.ToString() : compiler.ResultValue( DefaultB ).ToString(); string returnCall = string.Empty; switch ( BlendMode ) { case BlendNodeMode.Mix: returnCall = $"lerp( {aStr}, {bStr}, {fractionStr} )"; break; case BlendNodeMode.Darken: returnCall = $"min( {aStr}, {bStr} )"; break; case BlendNodeMode.Multiply: returnCall = $"{aStr}*{bStr}"; break; case BlendNodeMode.Lighten: returnCall = $"max( {aStr}, {bStr} )"; break; case BlendNodeMode.Screen: returnCall = $"({aStr}) + ({bStr}) - ({aStr}) * ({bStr})"; break; case BlendNodeMode.Difference: returnCall = $"abs( ({aStr}) - ({bStr}) )"; break; case BlendNodeMode.Exclusion: returnCall = $"({aStr}) + ({bStr}) - 2.0f * ({aStr}) * ({bStr})"; break; case BlendNodeMode.Subtract: returnCall = $"max( 0.0f, ({aStr}) - ({bStr}) )"; break; case BlendNodeMode.Add: returnCall = $"min( 1.0f, ({aStr}) + ({bStr}) )"; break; case BlendNodeMode.ColorBurn: returnCall = compiler.ResultFunction( "ColorBurn_blend", aStr, bStr ); break; case BlendNodeMode.LinearBurn: returnCall = compiler.ResultFunction( "LinearBurn_blend", aStr, bStr ); break; case BlendNodeMode.ColorDodge: returnCall = compiler.ResultFunction( "ColorDodge_blend", aStr, bStr ); break; case BlendNodeMode.LinearDodge: returnCall = compiler.ResultFunction( "LinearDodge_blend", aStr, bStr ); break; case BlendNodeMode.Overlay: returnCall = compiler.ResultFunction( "Overlay_blend", aStr, bStr ); break; case BlendNodeMode.SoftLight: returnCall = compiler.ResultFunction( "SoftLight_blend", aStr, bStr ); break; case BlendNodeMode.HardLight: returnCall = compiler.ResultFunction( "HardLight_blend", aStr, bStr ); break; case BlendNodeMode.VividLight: returnCall = compiler.ResultFunction( "VividLight_blend", aStr, bStr ); break; case BlendNodeMode.LinearLight: returnCall = compiler.ResultFunction( "LinearLight_blend", aStr, bStr ); break; case BlendNodeMode.HardMix: returnCall = compiler.ResultFunction( "HardMix_blend", aStr, bStr ); break; case BlendNodeMode.Divide: returnCall = compiler.ResultFunction( "Divide_blend", aStr, bStr ); break; } if ( BlendMode != BlendNodeMode.Mix ) returnCall = $"lerp( {aStr}, {returnCall}, {fractionStr} )"; if ( Clamp ) returnCall = $"saturate( {returnCall} )"; return new NodeResult( results.Item1.Components, returnCall ); }; } /// /// Blends two normal maps together, normalizing to return an appropriate normal. /// [Title( "Normal Blend" ), Category( "Transform" ), Icon( "gradient" )] public sealed class NormalBlend : ShaderNode { [Hide] public static string NormalBlendVector => @" float3 NormalBlendVector( float3 a, float3 b) { return normalize( float3( a.xy + b.xy, a.z * b.z ) ); } "; [Hide] public static string ReorientedNormalBlendVector => @" float3 ReorientedNormalBlendVector( float3 a, float3 b ) { float3 t = a.xyz + float3( 0.0, 0.0, 1.0 ); float3 u = b.xyz * float3( -1.0, -1.0, 1.0 ); return ( t / t.z ) * dot( t, u ) - u; } "; public enum BlendMode { Default, Reoriented } [Input( typeof( Vector3 ) )] [Hide] public NodeInput A { get; set; } [Input( typeof( Vector3 ) )] [Hide] public NodeInput B { get; set; } public BlendMode Mode { get; set; } = BlendMode.Default; [Hide] [Output( typeof( Vector3 ) )] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var a = compiler.Result( A ); var b = compiler.Result( B ); string func = compiler.RegisterFunction( NormalBlendVector ); if ( Mode == BlendMode.Reoriented ) { func = compiler.RegisterFunction( ReorientedNormalBlendVector ); } string funcResult = compiler.ResultFunction( func, $"{(a.IsValid ? a.Cast( 3 ) : "1.0")}", $"{(b.IsValid ? b.Cast( 3 ) : "1.0")}" ); return new NodeResult( NodeResultType.Vector3, $"{funcResult}" ); }; } /// /// Blends two normal maps together, normalizing to return an appropriate normal. /// [Title( "Reflection" ), Category( "Transform" ), Icon( "network_ping" )] public sealed class Reflection : ShaderNode { [Hide] public static string ReflectVector => @" float3 ReflectVector( float3 a, float3 b) { return reflect( a, b ); } "; [Input( typeof( Vector3 ) )] [Hide] public NodeInput A { get; set; } [Input( typeof( Vector3 ) )] [Hide] public NodeInput B { get; set; } [InputDefault( nameof( A ) )] public Vector3 DefaultA { get; set; } = Vector3.Zero; [InputDefault( nameof( B ) )] public Vector3 DefaultB { get; set; } = Vector3.One; [Hide] [Output( typeof( Vector3 ) )] public NodeResult.Func Result => ( GraphCompiler compiler ) => { var a = compiler.Result( A ); var b = compiler.Result( B ); string func = compiler.RegisterFunction( ReflectVector ); string funcResult = compiler.ResultFunction( func, $"{(a.IsValid ? a.Cast( 3 ) : "1.0")}", $"{(b.IsValid ? b.Cast( 3 ) : "1.0")}" ); return new NodeResult( NodeResultType.Vector3, $"{funcResult}" ); }; } [Title( "RGB to HSV" ), Category( "Transform" ), Icon( "invert_colors" )] public sealed class RGBtoHSV : ShaderNode { [Input( typeof( Vector3 ) )] [Hide] public NodeInput In { get; set; } [Output( typeof( Vector3 ) )] [Hide] public NodeResult.Func Out => ( GraphCompiler compiler ) => { return new NodeResult( 3, compiler.ResultFunction( "RGB2HSV", $"{compiler.ResultOrDefault( In, Vector3.One )}" ) ); }; } [Title( "HSV to RGB" ), Category( "Transform" ), Icon( "invert_colors" )] public sealed class HSVtoRGB : ShaderNode { [Input( typeof( Vector3 ) )] [Hide] public NodeInput In { get; set; } [Output( typeof( Vector3 ) )] [Hide] public NodeResult.Func Out => ( GraphCompiler compiler ) => { return new NodeResult( 3, compiler.ResultFunction( "HSV2RGB", $"{compiler.ResultOrDefault( In, Vector3.One )}" ) ); }; }