Editor/UpgradedShaderGraph/Core/FunctionResult.cs

Editor-side shader graph node representing a function result/output block. Manages a list of FunctionOutput entries, creates corresponding input plugs when outputs change, validates outputs for errors, and exposes methods to provide material outputs (albedo, emission, opacity, normal, roughness, metalness, AO, position offset) to the graph compiler.

Reflection
namespace Editor.ShaderGraphExtras;

public interface IErroringNode
{
	List<string> GetErrors();
}

/// <summary>
/// Final result
/// </summary>
[Title( "Result" ), Icon( "output" )]
public sealed class FunctionResult : BaseResult, IErroringNode
{
	[Title( "Outputs" )]
	public List<FunctionOutput> FunctionOutputs { get; set; }

	[Hide]
	private List<IPlugIn> InternalInputs = new();

	[Hide]
	public override IEnumerable<IPlugIn> Inputs => InternalInputs;

	[Hide, JsonIgnore]
	int _lastHashCode = 0;

	[Hide, JsonIgnore]
	public override Color PrimaryColor => Color.Lerp( Theme.Blue, Color.White, 0.25f );

	public override void OnFrame()
	{
		var hashCode = 0;
		int index = 1;
		foreach ( var output in FunctionOutputs )
		{
			hashCode += output.GetHashCode() * index;
			index++;
		}
		if ( hashCode != _lastHashCode )
		{
			_lastHashCode = hashCode;

			CreateInputs();
			Update();
		}
	}

	public void CreateInputs()
	{
		var plugs = new List<IPlugIn>();
		if ( FunctionOutputs == null )
		{
			InternalInputs = new();
		}
		else
		{
			foreach ( var output in FunctionOutputs.OrderBy( x => x.Priority ) )
			{
				var outputType = output.Type;
				if ( outputType == typeof( ColorTextureGenerator ) )
				{
					outputType = typeof( Color );
				}
				if ( outputType is null ) continue;
				var info = new PlugInfo()
				{
					Id = output.Id,
					Name = output.Name,
					Type = outputType,
					DisplayInfo = new()
					{
						Name = output.Name,
						Fullname = outputType.FullName
					}
				};
				var oldPlug = InternalInputs.FirstOrDefault( x => x is BasePlugIn plugIn && plugIn.Info.Id == info.Id ) as BasePlugIn;
				if ( oldPlug is not null )
				{
					oldPlug.Info.Name = info.Name;
					oldPlug.Info.Type = info.Type;
					oldPlug.Info.DisplayInfo = info.DisplayInfo;
					plugs.Add( oldPlug );
				}
				else
				{
					var plug = new BasePlugIn( this, info, info.Type );
					plugs.Add( plug );
				}
			}
			InternalInputs = plugs;
		}
	}

	public List<string> GetErrors()
	{
		var errors = new List<string>();
		if ( FunctionOutputs == null || FunctionOutputs.Count == 0 )
		{
			errors.Add( "No outputs defined" );
		}
		else
		{
			Dictionary<string, int> namesSoFar = new();
			foreach ( var output in FunctionOutputs )
			{
				if ( output.Name is null )
				{
					errors.Add( $"{output.TypeName} Output has no name" );
					continue;
				}
				if ( !namesSoFar.ContainsKey( output.Name ) )
				{
					namesSoFar.Add( output.Name, 1 );
				}
				else
				{
					namesSoFar[output.Name]++;
				}
				if ( string.IsNullOrEmpty( output.Name ) )
				{
					errors.Add( $"{output.TypeName} Output has no name" );
				}
				if ( string.IsNullOrEmpty( output.TypeName ) )
				{
					errors.Add( $"Output '{output.Name}' has no type" );
				}
			}
			foreach ( var name in namesSoFar )
			{
				if ( name.Value > 1 )
				{
					errors.Add( $"Output name '{name.Key}' is used {name.Value} times" );
				}
			}
		}
		return errors;
	}

	public override NodeInput GetAlbedo()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Albedo );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetEmission()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Emission );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetOpacity()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Opacity );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetNormal()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Normal );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetRoughness()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Roughness );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetMetalness()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.Metalness );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetAmbientOcclusion()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.AmbientOcclusion );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override NodeInput GetPositionOffset()
	{
		var input = GetInputFromPreview( FunctionOutput.PreviewType.PositionOffset );
		if ( input is not null )
		{
			return input.Value;
		}
		return default;
	}

	public override Color GetDefaultAlbedo()
	{
		return base.GetDefaultAlbedo();
	}

	public void AddMaterialOutputs( GraphCompiler compiler, StringBuilder sb, HashSet<string> visited )
	{
		if ( !visited.Contains( "Albedo" ) )
		{
			var albedoResult = GetAlbedoResult( compiler );
			sb.AppendLine( $"m.Albedo = {albedoResult.Cast( 3 )};" );
		}
		if ( !visited.Contains( "Emission" ) )
		{
			var emissionResult = GetEmissionResult( compiler );
			sb.AppendLine( $"m.Emission = {emissionResult.Cast( 3 )};" );
		}
		if ( !visited.Contains( "Opacity" ) )
		{
			var opacityResult = GetOpacityResult( compiler );
			sb.AppendLine( $"m.Opacity = {opacityResult.Cast( 1 )};" );
		}
		if ( !visited.Contains( "Normal" ) )
		{
			var normalResult = GetNormalResult( compiler );
			sb.AppendLine( $"m.Normal = {normalResult.Cast( 3 )};" );
		}
		if ( !visited.Contains( "Roughness" ) )
		{
			var roughnessResult = GetRoughnessResult( compiler );
			sb.AppendLine( $"m.Roughness = {roughnessResult.Cast( 1 )};" );
		}
		if ( !visited.Contains( "Metalness" ) )
		{
			var metalnessResult = GetMetalnessResult( compiler );
			sb.AppendLine( $"m.Metalness = {metalnessResult.Cast( 1 )};" );
		}
		if ( !visited.Contains( "AmbientOcclusion" ) )
		{
			var ambientOcclusionResult = GetAmbientOcclusionResult( compiler );
			sb.AppendLine( $"m.AmbientOcclusion = {ambientOcclusionResult.Cast( 1 )};" );
		}
	}

	private NodeInput? GetInputFromPreview( FunctionOutput.PreviewType previewType )
	{
		var albedoOutput = FunctionOutputs.FirstOrDefault( x => x.Preview == previewType );
		if ( albedoOutput is not null )
		{
			var input = Inputs.FirstOrDefault( x => x is BasePlugIn plugIn && plugIn.Info.Id == albedoOutput.Id );
			if ( input is BasePlugIn plugIn )
			{
				if ( plugIn.Info.ConnectedPlug is not null )
				{
					return new NodeInput
					{
						Identifier = plugIn.Info.ConnectedPlug.Node.Identifier,
						Output = plugIn.Info.ConnectedPlug.Identifier
					};
				}
				return plugIn.Info.GetInput( plugIn.Node );
			}
		}
		return null;
	}
}

public class FunctionOutput
{
	[Hide]
	public Guid Id { get; } = Guid.NewGuid();

	[KeyProperty]
	public string Name { get; set; }

	[Hide, JsonIgnore]
	public Type Type
	{
		get
		{
			if ( string.IsNullOrEmpty( TypeName ) ) return null;
			var typeName = TypeName;
			if ( typeName == "float" ) typeName = typeof( float ).FullName;
			if ( typeName == "int" ) typeName = typeof( int ).FullName;
			if ( typeName == "bool" ) typeName = typeof( bool ).FullName;
			var type = TypeLibrary.GetType( typeName ).TargetType;
			return type;
		}
	}

	[KeyProperty, Editor( "shadertype" ), JsonPropertyName( "Type" )]
	public string TypeName { get; set; }

	public PreviewType Preview { get; set; }

	public int Priority { get; set; }

	public override int GetHashCode()
	{
		return System.HashCode.Combine( Id, Name, TypeName, Priority );
	}

	public enum PreviewType
	{
		[Icon( "clear" )]
		None,
		[Icon( "palette" )]
		Albedo,
		[Icon( "brightness_5" )]
		Emission,
		[Icon( "opacity" )]
		Opacity,
		[Icon( "texture" )]
		Normal,
		[Icon( "terrain" )]
		Roughness,
		[Icon( "auto_awesome" )]
		Metalness,
		[Icon( "tonality" )]
		AmbientOcclusion,
		[Icon( "arrow_forward" )]
		PositionOffset
	}
}