Editor/ShaderGraphPlus/Nodes/Core/Result.cs

Editor shader graph node for the material result. Defines a concrete Result node and an abstract BaseResult that expose material inputs (albedo, emission, opacity, normal, roughness, metalness, ambient occlusion, position offset), default values, and logic to build runtime input plugs based on serialized properties and graph shading model/domain.

Reflection
using Editor;


namespace ShaderGraphPlus;

/// <summary>
/// Final result
/// </summary>
[Title( "Material" ), Icon( "tonality" )]
[InternalNode]
public sealed class Result : BaseResult
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.GraphResultNode;

	[Hide]
	public override string Title
	{
		get
		{
			var graph = Graph as ShaderGraphPlus;
			return $"{DisplayInfo.For( this ).Name} ( {graph.ShadingModel} )";
		}
	}

	[Hide]
	private bool IsLit => (Graph is ShaderGraphPlus shaderGraph && shaderGraph.ShadingModel == ShadingModel.Lit && shaderGraph.Domain != ShaderDomain.PostProcess);

	[Hide]
	private bool IsPostProcess => (Graph is ShaderGraphPlus shaderGraph && shaderGraph.Domain == ShaderDomain.PostProcess);

	// TODO :
	//[Hide]
	//private bool IsCustomLighting => (Graph is ShaderGraphPlus shaderGraph && shaderGraph.ShadingModel == ShadingModel.Custom );

	[Hide, JsonIgnore]
	public override bool CanPreview => false;

	[Hide]
	[Input( typeof( Vector3 ) )]
	public NodeInput Albedo { get; set; }

	[Hide]
	[Input( typeof( Vector3 ) )]
	[ShowIf( nameof( this.IsLit ), true )]
	public NodeInput Emission { get; set; }

	[Hide, NodeValueEditor( nameof( DefaultOpacity ) )]
	[Input( typeof( float ) )]
	public NodeInput Opacity { get; set; }

	[Hide]
	[Title( "Normal (Tangent)" )]
	[Input( typeof( Vector3 ) )]
	[ShowIf( nameof( this.IsLit ), true )]
	public NodeInput Normal { get; set; }

	[Hide, NodeValueEditor( nameof( DefaultRoughness ) )]
	[Input( typeof( float ) )]
	[ShowIf( nameof( this.IsLit ), true )]
	public NodeInput Roughness { get; set; }

	[Hide, NodeValueEditor( nameof( DefaultMetalness ) )]
	[Input( typeof( float ) )]
	[ShowIf( nameof( this.IsLit ), true )]
	public NodeInput Metalness { get; set; }

	[Hide, NodeValueEditor( nameof( DefaultAmbientOcclusion ) )]
	[Input( typeof( float ) )]
	[ShowIf( nameof( this.IsLit ), true )]
	public NodeInput AmbientOcclusion { get; set; }

	[InputDefault( nameof( Opacity ) )]
	public float DefaultOpacity { get; set; } = 1.0f;

	[InputDefault( nameof( Roughness ) )]
	public float DefaultRoughness { get; set; } = 1.0f;
	[InputDefault( nameof( Metalness ) )]
	public float DefaultMetalness { get; set; } = 0.0f;
	[InputDefault( nameof( AmbientOcclusion ) )]
	public float DefaultAmbientOcclusion { get; set; } = 1.0f;

	[Hide, JsonIgnore]
	int _lastHashCode = 0;

	public override void OnFrame()
	{
		var hashCode = new HashCode();
		if ( Graph is ShaderGraphPlus shaderGraph )
		{
			hashCode.Add( shaderGraph.ShadingModel );
			hashCode.Add( shaderGraph.Domain );
		}
		var hc = hashCode.ToHashCode();
		if ( hc != _lastHashCode )
		{
			_lastHashCode = hc;

			CreateInputs();
			Update();
		}
	}

	[Hide]
	[Input( typeof( Vector3 ) )]
	[HideIf( nameof( IsPostProcess ), true )]
	public NodeInput PositionOffset { get; set; }

	// TODO :
	//[Hide]
	//[Input( typeof( Vector3 ) )]
	//[HideIf( nameof( IsCustomLighting ), true )]
	//public NodeInput CustomLighting { get; set; }

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

	public override NodeInput GetAlbedo() => Albedo;
	public override NodeInput GetEmission() => Emission;
	public override NodeInput GetOpacity() => Opacity;
	public override NodeInput GetNormal() => Normal;
	public override NodeInput GetRoughness() => Roughness;
	public override NodeInput GetMetalness() => Metalness;
	public override NodeInput GetAmbientOcclusion() => AmbientOcclusion;
	public override NodeInput GetPositionOffset() => PositionOffset;

	private void CreateInputs()
	{
		var plugs = new List<IPlugIn>();
		var serialized = this.GetSerialized();
		foreach ( var property in serialized )
		{
			if ( property.TryGetAttribute<InputAttribute>( out var inputAttr ) )
			{
				if ( property.TryGetAttribute<ConditionalVisibilityAttribute>( out var conditionalVisibilityAttr ) )
				{
					if ( conditionalVisibilityAttr.TestCondition( this.GetSerialized() ) )
					{
						continue;
					}
				}
				var propertyInfo = typeof( Result ).GetProperty( property.Name );
				if ( propertyInfo is null ) continue;
				var info = new PlugInfo( propertyInfo );
				var displayInfo = info.DisplayInfo;
				displayInfo.Name = property.DisplayName;
				info.DisplayInfo = displayInfo;
				var plug = new BasePlugIn( this, info, info.Type );
				// Match by Info.Name (the C# property name) - matching by DisplayInfo.Name breaks
				// inputs whose display name differs from the property name (e.g. [Title])
				var oldPlug = Inputs.FirstOrDefault( x => x is BasePlugIn plugIn && plugIn.Info.Name == property.Name ) as BasePlugIn;
				if ( oldPlug is not null )
				{
					oldPlug.Info.Name = info.Name;
					oldPlug.Info.Type = info.Type;
					oldPlug.Info.DisplayInfo = info.DisplayInfo;
					var nodeInput = property.GetValue<NodeInput>();
					if ( nodeInput.IsValid && plug is IPlugIn plugIn )
					{
						var connectedNode = Graph.Nodes.FirstOrDefault( x => x is BaseNodePlus node && node.Identifier == nodeInput.Identifier ) as BaseNodePlus;
						plugIn.ConnectedOutput = connectedNode?.Outputs.FirstOrDefault( x => x.Identifier == nodeInput.Output );
					}
					plugs.Add( oldPlug );
				}
				else
				{
					var nodeInput = property.GetValue<NodeInput>();
					if ( nodeInput.IsValid && plug is IPlugIn plugIn )
					{
						var connectedNode = Graph.Nodes.FirstOrDefault( x => x is BaseNodePlus node && node.Identifier == nodeInput.Identifier ) as BaseNodePlus;
						plugIn.ConnectedOutput = connectedNode?.Outputs.FirstOrDefault( x => x.Identifier == nodeInput.Output );
					}
					plugs.Add( plug );
				}
			}
		}
		Inputs = plugs;
	}

}


public abstract class BaseResult : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override bool CanRemove => Graph.Nodes.Count( x => x is BaseResult ) > 1;

	public virtual NodeInput GetAlbedo() => new();
	public virtual NodeInput GetEmission() => new();
	public virtual NodeInput GetOpacity() => new();
	public virtual NodeInput GetNormal() => new();
	public virtual NodeInput GetRoughness() => new();
	public virtual NodeInput GetMetalness() => new();
	public virtual NodeInput GetAmbientOcclusion() => new();
	public virtual NodeInput GetPositionOffset() => new();

	public virtual Color GetDefaultAlbedo() => Color.White;
	public virtual Color GetDefaultEmission() => Color.Black;
	public virtual float GetDefaultOpacity() => 1.0f;
	public virtual Vector3 GetDefaultNormal() => new( 0, 0, 0 );
	public virtual float GetDefaultRoughness() => 1.0f;
	public virtual float GetDefaultMetalness() => 0.0f;
	public virtual float GetDefaultAmbientOcclusion() => 1.0f;
	public virtual Vector3 GetDefaultPositionOffset() => new( 0, 0, 0 );

	public NodeResult GetAlbedoResult( GraphCompiler compiler )
	{
		var albedoInput = GetAlbedo();
		if ( albedoInput.IsValid )
		{
			return compiler.ResultValue( albedoInput );
		}


		return compiler.ResultValue( GetDefaultAlbedo() );
	}

	public NodeResult GetEmissionResult( GraphCompiler compiler )
	{
		var emissionInput = GetEmission();
		if ( emissionInput.IsValid )
			return compiler.ResultValue( emissionInput );
		return compiler.ResultValue( GetDefaultEmission() );
	}

	public NodeResult GetOpacityResult( GraphCompiler compiler )
	{
		var opacityInput = GetOpacity();
		if ( opacityInput.IsValid )
			return compiler.ResultValue( opacityInput );
		return compiler.ResultValue( GetDefaultOpacity() );
	}

	public NodeResult GetNormalResult( GraphCompiler compiler )
	{
		var normalInput = GetNormal();
		if ( normalInput.IsValid )
			return compiler.ResultValue( normalInput );
		return compiler.ResultValue( GetDefaultNormal() );
	}

	public NodeResult GetRoughnessResult( GraphCompiler compiler )
	{
		var roughnessInput = GetRoughness();
		if ( roughnessInput.IsValid )
			return compiler.ResultValue( roughnessInput );
		return compiler.ResultValue( GetDefaultRoughness() );
	}

	public NodeResult GetMetalnessResult( GraphCompiler compiler )
	{
		var metalnessInput = GetMetalness();
		if ( metalnessInput.IsValid )
			return compiler.ResultValue( metalnessInput );
		return compiler.ResultValue( GetDefaultMetalness() );
	}

	public NodeResult GetAmbientOcclusionResult( GraphCompiler compiler )
	{
		var ambientOcclusionInput = GetAmbientOcclusion();
		if ( ambientOcclusionInput.IsValid )
			return compiler.ResultValue( ambientOcclusionInput );
		return compiler.ResultValue( GetDefaultAmbientOcclusion() );
	}

	public NodeResult GetPositionOffsetResult( GraphCompiler compiler )
	{
		var positionOffsetInput = GetPositionOffset();
		if ( positionOffsetInput.IsValid )
			return compiler.ResultValue( positionOffsetInput );
		return compiler.ResultValue( GetDefaultPositionOffset() );
	}
}