using Editor.NodeEditor; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; namespace Editor.ShaderGraph; public sealed partial class GraphCompiler { public struct Error { public BaseNode Node; public string Message; } public static Dictionary ValueTypes => new() { {typeof(Color), "float4" }, {typeof(Vector4), "float4" }, {typeof(Vector3), "float3" }, {typeof(Vector2), "float2" }, {typeof(float), "float" }, {typeof(bool), "bool" } }; /// /// Current graph we're compiling /// public ShaderGraph Graph { get; private set; } private ShaderGraph Subgraph = null; private SubgraphNode SubgraphNode = null; private List<(SubgraphNode, ShaderGraph)> SubgraphStack = new(); /// /// The loaded sub-graphs /// public List Subgraphs { get; private set; } public HashSet PixelIncludes { get; private set; } = new(); public HashSet VertexIncludes { get; private set; } = new(); /// /// Is this compile for just the preview or not, preview uses attributes for constant values /// public bool IsPreview { get; private set; } public bool IsNotPreview => !IsPreview; private class CompileResult { public List<(NodeResult, NodeResult)> Results = new(); public Dictionary InputResults = new(); public List SamplerStates = new(); public Dictionary TextureInputs = new(); public Dictionary Parameters = new(); public Dictionary Globals { get; private set; } = new(); public Dictionary Attributes { get; private set; } = new(); public HashSet Functions { get; private set; } = new(); public string RepresentativeTexture { get; set; } } public enum ShaderStage { Vertex, Pixel, } public ShaderStage Stage { get; private set; } public bool IsVs => Stage == ShaderStage.Vertex; public bool IsPs => Stage == ShaderStage.Pixel; private string StageName => Stage == ShaderStage.Vertex ? "vs" : "ps"; private string CurrentResultInput; private readonly CompileResult VertexResult = new(); private readonly CompileResult PixelResult = new(); private CompileResult ShaderResult => Stage == ShaderStage.Vertex ? VertexResult : PixelResult; public Action OnAttribute { get; set; } public List Nodes { get; private set; } = new(); private List InputStack = new(); private readonly Dictionary> NodeErrors = new(); /// /// Error list, doesn't give you much information currently /// public IEnumerable Errors => NodeErrors .Select( x => new Error { Node = x.Key, Message = x.Value.FirstOrDefault() } ); public GraphCompiler( ShaderGraph graph, bool preview ) { Graph = graph; IsPreview = preview; Stage = ShaderStage.Pixel; Subgraphs = new(); AddSubgraphs( Graph ); } private void AddSubgraphs( ShaderGraph graph ) { if ( graph != Graph ) { if ( Subgraphs.Contains( graph ) ) return; Subgraphs.Add( graph ); } foreach ( var node in graph.Nodes ) { if ( node is SubgraphNode subgraphNode ) { AddSubgraphs( subgraphNode.Subgraph ); } } } private static string CleanName( string name ) { if ( string.IsNullOrWhiteSpace( name ) ) return ""; name = name.Trim(); name = new string( name.Where( x => char.IsLetter( x ) || char.IsNumber( x ) || x == '_' ).ToArray() ); return name; } public void RegisterInclude( string path ) { var list = IsVs ? VertexIncludes : PixelIncludes; if ( list.Contains( path ) ) return; list.Add( path ); } public string ResultFunction( string name, params string[] args ) { if ( !ShaderTemplate.HasFunction( name ) ) return null; var result = ShaderResult; if ( !result.Functions.Contains( name ) ) result.Functions.Add( name ); return $"{name}( {string.Join( ", ", args )} )"; } public string RegisterFunction( string code, [CallerArgumentExpression( "code" )] string propertyName = "" ) { if ( !ShaderTemplate.HasFunction( propertyName ) ) { ShaderTemplate.RegisterFunction( propertyName, code ); } return propertyName; } public void RegisterGlobal( string name, string global ) { var result = ShaderResult; if ( result.Globals.ContainsKey( name ) ) return; result.Globals.Add( name, global ); } /// /// Register a texture and return the name of it /// public (string, int) ResultTexture( Sampler sampler, TextureInput input, Texture texture ) { var name = CleanName( input.Name ); name = string.IsNullOrWhiteSpace( name ) ? $"Texture_{StageName}_{ShaderResult.TextureInputs.Count}" : name; var id = name; int count = 0; var result = ShaderResult; while ( result.TextureInputs.ContainsKey( id ) ) { id = $"{name}_{count++}"; } if ( !result.SamplerStates.Contains( sampler ) ) result.SamplerStates.Add( sampler ); OnAttribute?.Invoke( id, texture ); result.TextureInputs.Add( id, input ); if ( CurrentResultInput == "Albedo" ) { result.RepresentativeTexture = $"g_t{id}"; } return new( $"g_t{id}", result.SamplerStates.IndexOf( sampler ) ); } /// /// Get result of an input with an optional default value if it failed to resolve /// public NodeResult ResultOrDefault( NodeInput input, T defaultValue ) { var result = Result( input ); return result.IsValid ? result : ResultValue( defaultValue ); } /// /// Get result of an input /// public NodeResult Result( NodeInput input ) { if ( !input.IsValid ) return default; BaseNode node = null; if ( string.IsNullOrEmpty( input.Subgraph ) ) { if ( Subgraph is not null ) { var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) ); return Result( new() { Identifier = input.Identifier, Output = input.Output, Subgraph = Subgraph.Path, SubgraphNode = nodeId } ); } node = Graph.FindNode( input.Identifier ); } else { var subgraph = Subgraphs.FirstOrDefault( x => x.Path == input.Subgraph ); if ( subgraph is not null ) { node = subgraph.FindNode( input.Identifier ); } } if ( ShaderResult.InputResults.TryGetValue( input, out var result ) ) { return result; } if ( node == null ) { return default; } var nodeType = node.GetType(); var property = nodeType.GetProperty( input.Output ); if ( property == null ) { // Search for alias var allProperties = nodeType.GetProperties(); foreach ( var prop in allProperties ) { var alias = prop.GetCustomAttribute(); if ( alias is null ) continue; foreach ( var al in alias.Value ) { if ( al == input.Output ) { property = prop; break; } } if ( property != null ) break; } } object value = null; if ( node is not IRerouteNode && InputStack.Contains( input ) ) { NodeErrors[node] = new List { "Circular reference detected" }; return default; } InputStack.Add( input ); if ( Subgraph is not null && node.Graph != Subgraph ) { if ( node.Graph != Graph ) { Subgraph = node.Graph as ShaderGraph; } else { Subgraph = null; } } if ( node is SubgraphNode subgraphNode ) { var newStack = (subgraphNode, Subgraph); var lastNode = SubgraphNode; SubgraphStack.Add( newStack ); Subgraph = subgraphNode.Subgraph; SubgraphNode = subgraphNode; if ( !Subgraphs.Contains( Subgraph ) ) { Subgraphs.Add( Subgraph ); } var resultNode = Subgraph.Nodes.FirstOrDefault( x => x is FunctionResult ) as FunctionResult; var resultInput = resultNode.Inputs.FirstOrDefault( x => x.Identifier == input.Output ); if ( resultInput?.ConnectedOutput is not null ) { var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) ); var newConnection = new NodeInput() { Identifier = resultInput.ConnectedOutput.Node.Identifier, Output = resultInput.ConnectedOutput.Identifier, Subgraph = Subgraph.Path, SubgraphNode = nodeId }; var newResult = Result( newConnection ); InputStack.Remove( input ); SubgraphStack.RemoveAt( SubgraphStack.Count - 1 ); Subgraph = newStack.Item2; SubgraphNode = lastNode; return newResult; } else { value = GetDefaultValue( subgraphNode, input.Output, resultInput.Type ); SubgraphStack.RemoveAt( SubgraphStack.Count - 1 ); Subgraph = newStack.Item2; SubgraphNode = lastNode; } //if ( value is null ) //{ // value = GetDefaultValue( subgraphNode, input.Output, resultType ); //} } else { if ( Subgraph is not null ) { if ( node is SubgraphInput subgraphInput && !string.IsNullOrWhiteSpace( subgraphInput.InputName ) ) { var newResult = ResolveSubgraphInput( subgraphInput, ref value ); if ( newResult.IsValid ) { InputStack.Remove( input ); return newResult; } } } else if ( Graph.IsSubgraph ) { if ( node is SubgraphInput subgraphInput ) { if ( subgraphInput.PreviewInput.IsValid ) { var paramResult = Result( subgraphInput.PreviewInput ); InputStack.Remove( input ); return paramResult; } } } if ( value is null ) { if ( property == null ) { InputStack.Remove( input ); return default; } value = property.GetValue( node ); } if ( value == null ) { InputStack.Remove( input ); return default; } } if ( value is NodeResult nodeResult ) { InputStack.Remove( input ); return nodeResult; } else if ( value is NodeInput nodeInput ) { if ( nodeInput == input ) { InputStack.Remove( input ); return default; } var newResult = Result( nodeInput ); InputStack.Remove( input ); return newResult; } else if ( value is NodeResult.Func resultFunc ) { var funcResult = resultFunc.Invoke( this ); if ( !funcResult.IsValid ) { if ( !NodeErrors.TryGetValue( node, out var errors ) ) { errors = new(); NodeErrors.Add( node, errors ); } if ( funcResult.Errors is null || funcResult.Errors.Length == 0 ) { errors.Add( $"Missing input" ); } else { foreach ( var error in funcResult.Errors ) errors.Add( error ); } InputStack.Remove( input ); return default; } // We can return this result without making it a local variable because it's constant if ( funcResult.Constant ) { InputStack.Remove( input ); return funcResult; } var id = ShaderResult.InputResults.Count; var varName = $"l_{id}"; var localResult = new NodeResult( funcResult.Components, varName ); ShaderResult.InputResults.Add( input, localResult ); ShaderResult.Results.Add( (localResult, funcResult) ); if ( IsPreview ) { Nodes.Add( node ); } InputStack.Remove( input ); return localResult; } var resultVal = ResultValue( value ); InputStack.Remove( input ); return resultVal; } /// /// Get result of two inputs and cast to the largest component of the two (a float2 and float3 will both become float3 results) /// public (NodeResult, NodeResult) Result( NodeInput a, NodeInput b, float defaultA = 0.0f, float defaultB = 1.0f ) { var resultA = ResultOrDefault( a, defaultA ); var resultB = ResultOrDefault( b, defaultB ); if ( resultA.Components == resultB.Components ) return (resultA, resultB); if ( resultA.Components < resultB.Components ) return (new( resultB.Components, resultA.Cast( resultB.Components ) ), resultB); return (resultA, new( resultA.Components, resultB.Cast( resultA.Components ) )); } /// /// Get result of a value that can be set in material editor /// public NodeResult ResultParameter( string name, T value, T min = default, T max = default, bool isRange = false, bool isAttribute = false, ParameterUI ui = default ) { if ( IsPreview || string.IsNullOrWhiteSpace( name ) || Subgraph is not null ) return ResultValue( value ); var attribName = name; name = CleanName( name ); var prefix = (value is float) ? "g_fl" : (value is bool) ? "g_b" : "g_v"; if ( !name.StartsWith( prefix ) ) name = prefix + name; if ( ShaderResult.Parameters.TryGetValue( name, out var parameter ) ) return parameter.Result; parameter.Result = ResultValue( value, name ); var options = new StringWriter(); // If we're an attribute, we don't care about the UI options if ( isAttribute ) { options.Write( $"Attribute( \"{attribName}\" ); " ); if ( value is bool boolValue ) { options.Write( $"Default( {(boolValue ? 1 : 0)} ); " ); } else { options.Write( $"Default{parameter.Result.Components}( {value} ); " ); } } else { if ( ui.Type != UIType.Default ) { options.Write( $"UiType( {ui.Type} ); " ); } if ( ui.Step > 0.0f ) { options.Write( $"UiStep( {ui.Step} ); " ); } options.Write( $"UiGroup( \"{ui.UIGroup}\" ); " ); if ( value is bool boolValue ) { options.Write( $"Default( {(boolValue ? 1 : 0)} ); " ); } else { options.Write( $"Default{parameter.Result.Components}( {value} ); " ); } if ( parameter.Result.Components > 0 && isRange ) { options.Write( $"Range{parameter.Result.Components}( {min}, {max} ); " ); } } parameter.Options = options.ToString().Trim(); ShaderResult.Parameters.Add( name, parameter ); return parameter.Result; } /// /// Get result of a value, in preview mode an attribute will be registered and returned /// Only supports float, Vector2, Vector3, Vector4, Color, bool /// public NodeResult ResultValue( T value, string name = null, bool previewOverride = false ) { if ( value is NodeInput nodeInput ) return Result( nodeInput ); bool isConstant = IsPreview && !previewOverride; bool isNamed = isConstant || !string.IsNullOrWhiteSpace( name ); name = isConstant ? $"g_{StageName}_{ShaderResult.Attributes.Count}" : name; if ( isConstant ) { OnAttribute?.Invoke( name, value ); ShaderResult.Attributes[name] = value; } return value switch { float v => isNamed ? new NodeResult( 1, $"{name}" ) : new NodeResult( 1, $"{v}", true ), Vector2 v => isNamed ? new NodeResult( 2, $"{name}" ) : new NodeResult( 2, $"float2( {v.x}, {v.y} )" ), Vector3 v => isNamed ? new NodeResult( 3, $"{name}" ) : new NodeResult( 3, $"float3( {v.x}, {v.y}, {v.z} )" ), Vector4 v => isNamed ? new NodeResult( 4, $"{name}" ) : new NodeResult( 4, $"float4( {v.x}, {v.y}, {v.z}, {v.w} )" ), Color v => isNamed ? new NodeResult( 4, $"{name}" ) : new NodeResult( 4, $"float4( {v.r}, {v.g}, {v.b}, {v.a} )" ), bool v => isNamed ? new NodeResult( 0, $"{name}" ) : new NodeResult( 0, $"{v}" ), _ => throw new ArgumentException( "Unsupported attribute type", nameof( value ) ) }; } private static object GetDefaultValue( SubgraphNode node, string name, Type type ) { if ( !node.DefaultValues.TryGetValue( name, out var value ) ) { switch ( type ) { case Type t when t == typeof( Vector2 ): return Vector2.Zero; case Type t when t == typeof( Vector3 ): return Vector3.Zero; case Type t when t == typeof( Vector4 ): return Vector4.Zero; case Type t when t == typeof( Color ): return Color.White; case Type t when t == typeof( int ): return 0; case Type t when t == typeof( float ): return 0.0f; case Type t when t == typeof( bool ): return false; } } if ( value is JsonElement el ) { if ( type == typeof( float ) ) { value = el.GetSingle(); } else if ( type == typeof( Vector2 ) ) { value = Vector2.Parse( el.GetString() ); } else if ( type == typeof( Vector3 ) ) { value = Vector3.Parse( el.GetString() ); } else if ( type == typeof( Vector4 ) ) { value = Vector4.Parse( el.GetString() ); } else if ( type == typeof( Color ) ) { value = Color.Parse( el.GetString() ) ?? Color.White; } else if ( type == typeof( bool ) ) { value = el.GetBoolean(); } } return value; } private NodeResult ResolveSubgraphInput( SubgraphInput node, ref object value ) { var lastStack = SubgraphStack.LastOrDefault(); var lastNodeEntered = lastStack.Item1; if ( lastNodeEntered is not null ) { var parentInput = lastNodeEntered.InputReferences.FirstOrDefault( x => x.Key.Identifier == node.InputName ); if ( parentInput.Key is not null ) { var lastSubgraph = Subgraph; var lastNode = SubgraphNode; Subgraph = lastStack.Item2; SubgraphNode = (Subgraph is null) ? null : lastNodeEntered; SubgraphStack.RemoveAt( SubgraphStack.Count - 1 ); var connectedPlug = parentInput.Key.ConnectedOutput; if ( connectedPlug is not null ) { var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) ); var newResult = Result( new() { Identifier = connectedPlug.Node.Identifier, Output = connectedPlug.Identifier, Subgraph = Subgraph?.Path, SubgraphNode = nodeId } ); SubgraphStack.Add( lastStack ); Subgraph = lastSubgraph; SubgraphNode = lastNode; return newResult; } else { value = GetDefaultValue( lastNodeEntered, node.InputName, parentInput.Value.Item2 ); SubgraphStack.Add( lastStack ); Subgraph = lastSubgraph; SubgraphNode = lastNode; } } } return new(); } private NodeResult ResolveParameterNode( IParameterNode node, ref object value ) { var lastStack = SubgraphStack.LastOrDefault(); var lastNodeEntered = lastStack.Item1; if ( lastNodeEntered is not null ) { var parentInput = lastNodeEntered.InputReferences.FirstOrDefault( x => x.Key.Identifier == node.Name ); if ( parentInput.Key is not null ) { var lastSubgraph = Subgraph; var lastNode = SubgraphNode; Subgraph = lastStack.Item2; SubgraphNode = (Subgraph is null) ? null : lastNodeEntered; SubgraphStack.RemoveAt( SubgraphStack.Count - 1 ); var connectedPlug = parentInput.Key.ConnectedOutput; if ( connectedPlug is not null ) { var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) ); var newResult = Result( new() { Identifier = connectedPlug.Node.Identifier, Output = connectedPlug.Identifier, Subgraph = Subgraph?.Path, SubgraphNode = nodeId } ); SubgraphStack.Add( lastStack ); Subgraph = lastSubgraph; SubgraphNode = lastNode; return newResult; } else { value = GetDefaultValue( lastNodeEntered, node.Name, parentInput.Value.Item2 ); SubgraphStack.Add( lastStack ); Subgraph = lastSubgraph; SubgraphNode = lastNode; } } } return new(); } private static int GetComponentCount( Type inputType ) { return inputType switch { Type t when t == typeof( Vector4 ) || t == typeof( Color ) => 4, Type t when t == typeof( Vector3 ) => 3, Type t when t == typeof( Vector2 ) => 2, Type t when t == typeof( float ) => 1, _ => 0 }; } private static IEnumerable GetNodeInputProperties( Type type ) { return type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) .Where( property => property.GetSetMethod() != null && property.PropertyType == typeof( NodeInput ) && property.IsDefined( typeof( BaseNode.InputAttribute ), false ) ); } private string GenerateCommon() { var sb = new StringBuilder(); var blendMode = Graph.BlendMode; var alphaTest = blendMode == BlendMode.Masked ? 1 : 0; var translucent = blendMode == BlendMode.Translucent ? 1 : 0; sb.AppendLine( $"#ifndef S_ALPHA_TEST" ); sb.AppendLine( $"#define S_ALPHA_TEST {alphaTest}" ); sb.AppendLine( $"#endif" ); sb.AppendLine( $"#ifndef S_TRANSLUCENT" ); sb.AppendLine( $"#define S_TRANSLUCENT {translucent}" ); sb.AppendLine( $"#endif" ); return sb.ToString(); } /// /// Generate shader code, will evaluate the graph if it hasn't already. /// Different code is generated for preview and not preview. /// public string Generate() { // May have already evaluated and there's errors if ( Errors.Any() ) return null; var material = GenerateMaterial(); var pixelOutput = GeneratePixelOutput(); // If we have any errors after evaluating, no point going further if ( Errors.Any() ) return null; return string.Format( ShaderTemplate.Code, Graph.Description, IndentString( GenerateCommon(), 1 ), IndentString( GenerateGlobals(), 1 ), IndentString( GenerateLocals(), 2 ), IndentString( material, 2 ), IndentString( GenerateVertex(), 2 ), IndentString( GenerateGlobals(), 1 ), IndentString( GenerateVertexComboRules(), 1 ), IndentString( GeneratePixelComboRules(), 1 ), IndentString( GenerateFunctions( PixelResult ), 1 ), IndentString( GenerateFunctions( VertexResult ), 1 ), IndentString( GeneratePixelInit(), 2 ), IndentString( pixelOutput, 2 ) ); } private static string GenerateFunctions( CompileResult result ) { if ( !result.Functions.Any() ) return null; var sb = new StringBuilder(); foreach ( var function in result.Functions ) { if ( ShaderTemplate.TryGetFunction( function, out var code ) ) { sb.Append( code ); } } return sb.ToString(); } private static string IndentString( string input, int tabCount ) { if ( string.IsNullOrWhiteSpace( input ) ) return input; var tabs = new string( '\t', tabCount ); var lines = input.Split( '\n' ); for ( int i = 0; i < lines.Length; i++ ) { lines[i] = tabs + lines[i]; } return string.Join( "\n", lines ); } private string GenerateVertexComboRules() { var sb = new StringBuilder(); foreach ( var include in VertexIncludes ) { sb.AppendLine( $"#include \"{include}\"" ); } if ( IsNotPreview ) return sb.ToString(); sb.AppendLine(); return sb.ToString(); } private string GeneratePixelComboRules() { var sb = new StringBuilder(); var pixelIncludes = new HashSet( PixelIncludes ); if ( Graph.Domain == ShaderDomain.PostProcess ) { pixelIncludes.Add( "postprocess/functions.hlsl" ); pixelIncludes.Add( "postprocess/common.hlsl" ); } foreach ( var include in pixelIncludes ) { sb.AppendLine( $"#include \"{include}\"" ); } if ( !IsNotPreview ) { sb.AppendLine(); sb.AppendLine( "DynamicCombo( D_RENDER_BACKFACES, 0..1, Sys( ALL ) );" ); sb.AppendLine( "RenderState( CullMode, D_RENDER_BACKFACES ? NONE : BACK );" ); } else { sb.AppendLine( "RenderState( CullMode, F_RENDER_BACKFACES ? NONE : DEFAULT );" ); } return sb.ToString(); } private string GeneratePixelInit() { Stage = ShaderStage.Pixel; if ( Graph.ShadingModel == ShadingModel.Lit && Graph.Domain != ShaderDomain.PostProcess ) return ShaderTemplate.Material_init; return ""; } private string GeneratePixelOutput() { Stage = ShaderStage.Pixel; Subgraph = null; SubgraphStack.Clear(); if ( Graph.ShadingModel == ShadingModel.Unlit || Graph.Domain == ShaderDomain.PostProcess ) { var resultNode = Graph.Nodes.OfType().FirstOrDefault(); if ( resultNode == null ) return null; var albedoResult = resultNode.GetAlbedoResult( this ); string albedo = albedoResult.Cast( GetComponentCount( typeof( Vector3 ) ) ) ?? "float3(1.00,1.00,1.00)"; var opacityResult = resultNode.GetOpacityResult( this ); string opacity = opacityResult.Cast( 1 ) ?? "1.00"; return $"return float4( {albedo}, {opacity} );"; } else if ( Graph.ShadingModel == ShadingModel.Lit ) { return ShaderTemplate.Material_output; } return null; } private string GetPropertyValue( PropertyInfo property, Result resultNode ) { NodeResult result; var inputAttribute = property.GetCustomAttribute(); var componentCount = GetComponentCount( inputAttribute.Type ); if ( property.GetValue( resultNode ) is NodeInput connection && connection.IsValid() ) { result = Result( connection ); } else { var editorAttribute = property.GetCustomAttribute(); if ( editorAttribute == null ) return null; var valueProperty = resultNode.GetType().GetProperty( editorAttribute.ValueName ); if ( valueProperty == null ) return null; result = ResultValue( valueProperty.GetValue( resultNode ), previewOverride: true ); } if ( Errors.Any() ) return null; if ( !result.IsValid() ) return null; if ( string.IsNullOrWhiteSpace( result.Code ) ) return null; return $"{result.Cast( componentCount )}"; } private string GenerateGlobals() { var sb = new StringBuilder(); foreach ( var global in ShaderResult.Globals ) { sb.AppendLine( global.Value ); } for ( int i = 0; i < ShaderResult.SamplerStates.Count; ++i ) { var sampler = ShaderResult.SamplerStates[i]; sb.Append( $"SamplerState g_sSampler{i} <" ) .Append( $" Filter( {sampler.Filter.ToString().ToUpper()} );" ) .Append( $" AddressU( {sampler.AddressU.ToString().ToUpper()} );" ) .Append( $" AddressV( {sampler.AddressV.ToString().ToUpper()} ); >;" ) .AppendLine(); } // Support for color buffer in post-process shaders if ( IsPs && Graph.Domain is ShaderDomain.PostProcess ) { sb.AppendLine( "Texture2D g_tColorBuffer < Attribute( \"ColorBuffer\" ); SrgbRead ( true ); >;" ); } if ( IsPreview ) { foreach ( var result in ShaderResult.TextureInputs ) { sb.Append( $"{result.Value.CreateTexture( result.Key )} <" ) .Append( $" Attribute( \"{result.Key}\" );" ) .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" ) .AppendLine(); } foreach ( var result in ShaderResult.Attributes ) { var typeName = result.Value switch { Color _ => "float4", Vector4 _ => "float4", Vector3 _ => "float3", Vector2 _ => "float2", float _ => "float", bool _ => "bool", _ => null }; sb.AppendLine( $"{typeName} {result.Key} < Attribute( \"{result.Key}\" ); >;" ); } sb.AppendLine( "float g_flPreviewTime < Attribute( \"g_flPreviewTime\" ); >;" ); sb.AppendLine( $"int g_iStageId < Attribute( \"g_iStageId\" ); >;" ); } else { foreach ( var result in ShaderResult.TextureInputs ) { // If we're an attribute, we don't care about texture inputs if ( result.Value.IsAttribute ) continue; var defaultTex = result.Value.DefaultTexture; sb.Append( $"{result.Value.CreateInput}( {result.Key}, {result.Value.ColorSpace}, 8," ) .Append( $" \"{result.Value.Processor.ToString()}\"," ) .Append( $" \"_{result.Value.ExtensionString.ToLower()}\"," ) .Append( $" \"{result.Value.UIGroup}\"," ) .Append( string.IsNullOrEmpty( defaultTex ) ? $" Default4( {result.Value.Default} ) );" : $" DefaultFile( \"{defaultTex}\" ) );" ) .AppendLine(); } foreach ( var result in ShaderResult.TextureInputs ) { // If we're an attribute, we don't care about the UI options if ( result.Value.IsAttribute ) { sb.AppendLine( $"{result.Value.CreateTexture( result.Key )} < Attribute( \"{result.Key}\" ); >;" ); } else { sb.Append( $"{result.Value.CreateTexture( result.Key )} < Channel( RGBA, Box( {result.Key} ), {(result.Value.SrgbRead ? "Srgb" : "Linear")} );" ) .Append( $" OutputFormat( {result.Value.ImageFormat} );" ) .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" ) .AppendLine(); } } if ( !string.IsNullOrWhiteSpace( ShaderResult.RepresentativeTexture ) ) { sb.AppendLine( $"TextureAttribute( LightSim_DiffuseAlbedoTexture, {ShaderResult.RepresentativeTexture} )" ); sb.AppendLine( $"TextureAttribute( RepresentativeTexture, {ShaderResult.RepresentativeTexture} )" ); } foreach ( var parameter in ShaderResult.Parameters ) { sb.AppendLine( $"{parameter.Value.Result.TypeName} {parameter.Key} < {parameter.Value.Options} >;" ); } } if ( sb.Length > 0 ) { sb.Insert( 0, "\n" ); } return sb.ToString(); } private string GenerateLocals() { var sb = new StringBuilder(); if ( ShaderResult.Results.Any() ) { sb.AppendLine(); } if ( IsPreview ) { int localId = 1; foreach ( var result in ShaderResult.Results ) { sb.AppendLine( $"{result.Item2.TypeName} {result.Item1} = {result.Item2.Code};" ); sb.AppendLine( $"if ( g_iStageId == {localId++} ) return {result.Item1.Cast( 4, 1.0f )};" ); } } else { foreach ( var result in ShaderResult.Results ) { sb.AppendLine( $"{result.Item2.TypeName} {result.Item1} = {result.Item2.Code};" ); } } return sb.ToString(); } private string GenerateMaterial() { Stage = ShaderStage.Pixel; Subgraph = null; SubgraphStack.Clear(); if ( Graph.ShadingModel != ShadingModel.Lit || Graph.Domain == ShaderDomain.PostProcess ) return ""; var resultNode = Graph.Nodes.OfType().FirstOrDefault(); if ( resultNode == null ) return null; var sb = new StringBuilder(); var visited = new HashSet(); foreach ( var property in GetNodeInputProperties( resultNode.GetType() ) ) { if ( property.Name == "PositionOffset" ) continue; CurrentResultInput = property.Name; visited.Add( property.Name ); NodeResult result; if ( property.GetValue( resultNode ) is NodeInput connection && connection.IsValid() ) { result = Result( connection ); } else { var editorAttribute = property.GetCustomAttribute(); if ( editorAttribute == null ) continue; var valueProperty = resultNode.GetType().GetProperty( editorAttribute.ValueName ); if ( valueProperty == null ) continue; result = ResultValue( valueProperty.GetValue( resultNode ) ); } if ( Errors.Any() ) return null; if ( !result.IsValid() ) continue; if ( string.IsNullOrWhiteSpace( result.Code ) ) continue; var inputAttribute = property.GetCustomAttribute(); var componentCount = GetComponentCount( inputAttribute.Type ); sb.AppendLine( $"m.{property.Name} = {result.Cast( componentCount )};" ); } if ( resultNode is FunctionResult functionResult ) { functionResult.AddMaterialOutputs( this, sb, visited ); } visited.Clear(); CurrentResultInput = null; return sb.ToString(); } private string GenerateVertex() { Stage = ShaderStage.Vertex; var resultNode = Graph.Nodes.OfType().FirstOrDefault(); if ( resultNode == null ) return null; var positionOffsetInput = resultNode.GetPositionOffset(); var sb = new StringBuilder(); switch ( Graph.Domain ) { case ShaderDomain.Surface: sb.AppendLine( @" PixelInput i = ProcessVertex( v ); i.vPositionOs = v.vPositionOs.xyz; i.vColor = v.vColor; ExtraShaderData_t extraShaderData = GetExtraPerInstanceShaderData( v.nInstanceTransformID ); i.vTintColor = extraShaderData.vTint; VS_DecodeObjectSpaceNormalAndTangent( v, i.vNormalOs, i.vTangentUOs_flTangentVSign );" ); break; case ShaderDomain.PostProcess: sb.AppendLine( @" PixelInput i; i.vPositionPs = float4(v.vPositionOs.xy, 0.0f, 1.0f ); i.vPositionWs = float3(v.vTexCoord, 0.0f); " ); break; } NodeResult result; if ( positionOffsetInput is NodeInput connection && connection.IsValid() ) { result = Result( connection ); if ( !Errors.Any() && result.IsValid() && !string.IsNullOrWhiteSpace( result.Code ) ) { var componentCount = GetComponentCount( typeof( Vector3 ) ); sb.AppendLine(); foreach ( var local in ShaderResult.Results ) { sb.AppendLine( $"{local.Item2.TypeName} {local.Item1} = {local.Item2.Code};" ); } sb.AppendLine( $"i.vPositionWs.xyz += {result.Cast( componentCount )};" ); sb.AppendLine( "i.vPositionPs.xyzw = Position3WsToPs( i.vPositionWs.xyz );" ); } } switch ( Graph.Domain ) { case ShaderDomain.Surface: sb.AppendLine( "return FinalizeVertex( i );" ); break; case ShaderDomain.PostProcess: sb.AppendLine( "return i;" ); break; } return sb.ToString(); } }