Editor/Prism/Nodes/InputNodes.cs

Shader graph input node definitions for the Prism editor. Declares enums and many PrismNode subclasses that expose engine builtins, helpers and globals (positions, normals, tangents, UVs, camera, time, depth, scene color, matrices, etc.) and emit IR via Emit/OnValidate implementations.

Native Interop
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;

namespace Editor.Prism.Nodes;

// ---------------------------------------------------------------------------------------------------
// Engine inputs.
//
// Every value here is either a Builtin — which the backend lowers per stage, because world position is
// i.vPositionWs in the vertex program and i.vPositionWithOffsetWs + g_vHighPrecisionLightingOffsetWs in
// the pixel one — or a HelperFunction wrapping an engine class such as Depth:: or Fog::. Nothing in
// this file spells HLSL inline, and nothing bakes a frame-varying value into a constant.
// ---------------------------------------------------------------------------------------------------

/// <summary>Which coordinate space a position or direction is expressed in.</summary>
public enum PrismSpace
{
	/// <summary>Model space, before any instance transform.</summary>
	Object,
	/// <summary>World space.</summary>
	World,
	/// <summary>Camera space.</summary>
	View,
	/// <summary>Clip / screen space.</summary>
	Clip,
	/// <summary>Surface tangent space.</summary>
	Tangent
}

/// <summary>Which of the engine's transform matrices a matrix node reads.</summary>
public enum PrismMatrixKind
{
	/// <summary>Object to world.</summary>
	ObjectToWorld,
	/// <summary>World to object.</summary>
	WorldToObject,
	/// <summary>World to view.</summary>
	View,
	/// <summary>View to projection.</summary>
	Projection,
	/// <summary>World to projection.</summary>
	ViewProjection
}

// ---- position -------------------------------------------------------------------------------------

/// <summary>
/// The surface position, in the space the node is set to.
/// </summary>
[NodeInfo( Id = "prism.input.worldPosition", Title = "Position", Category = "Input/Geometry",
	Icon = "public", Keywords = new[] { "position", "world", "object", "view", "wpos", "vertex" },
	Description = "The shaded point's position. World space is camera-relative-corrected for you; " +
		"object space is only available on a surface graph." )]
[NodeVersion( 1 )]
public sealed class PositionNode : PrismNode
{
	/// <summary>Which space the position is reported in.</summary>
	public PrismSpace Space { get; set; } = PrismSpace.World;

	/// <summary>The position.</summary>
	[Out( "float3", Name = "Position" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = Space switch
		{
			PrismSpace.Object => ctx.Builtin( Builtin.ObjectPosition ),
			PrismSpace.View => PrismHelpers.WorldToView( ctx, ctx.Builtin( Builtin.WorldPosition ) ),
			PrismSpace.Clip => ctx.Swizzle( ctx.Builtin( Builtin.ClipPosition ), "xyz" ),
			PrismSpace.Tangent => PrismHelpers.WorldToTangent( ctx, ctx.Builtin( Builtin.WorldPosition ) ),
			_ => ctx.Builtin( Builtin.WorldPosition )
		};

		ctx.Out( nameof( Result ), value );
	}
}

/// <summary>The interpolated surface normal.</summary>
[NodeInfo( Id = "prism.input.worldNormal", Title = "Normal", Category = "Input/Geometry",
	Icon = "north", Keywords = new[] { "normal", "world", "object", "tangent" },
	Description = "The shaded point's normal. Tangent space is the identity (0,0,1), which is what a " +
		"normal-map output expects." )]
[NodeVersion( 1 )]
public sealed class NormalNode : PrismNode
{
	/// <summary>Which space the normal is reported in.</summary>
	public PrismSpace Space { get; set; } = PrismSpace.World;

	/// <summary>The normal.</summary>
	[Out( "float3", Name = "Normal" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = Space switch
		{
			PrismSpace.Object => ctx.Builtin( Builtin.ObjectNormal ),
			PrismSpace.Tangent => ctx.Const( new Vector3( 0f, 0f, 1f ) ),
			PrismSpace.View => PrismHelpers.WorldToViewDirection( ctx, ctx.Builtin( Builtin.WorldNormal ) ),
			_ => ctx.Builtin( Builtin.WorldNormal )
		};

		ctx.Out( nameof( Result ), value );
	}
}

/// <summary>The surface tangent basis.</summary>
[NodeInfo( Id = "prism.input.worldTangent", Title = "Tangent", Category = "Input/Geometry",
	Icon = "east", Keywords = new[] { "tangent", "bitangent", "binormal", "basis", "tbn" },
	Description = "The world-space tangent basis the engine decoded for this surface." )]
[NodeVersion( 1 )]
public sealed class TangentNode : PrismNode
{
	/// <summary>The tangent along U.</summary>
	[Out( "float3", Name = "Tangent U" )] public PortRef U { get; set; }

	/// <summary>The tangent along V — the bitangent.</summary>
	[Out( "float3", Name = "Tangent V" )] public PortRef V { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( U ), ctx.Builtin( Builtin.WorldTangentU ) );
		ctx.Out( nameof( V ), ctx.Builtin( Builtin.WorldTangentV ) );
	}
}

/// <summary>The bitangent, as its own node so a graph does not have to know it is tangent V.</summary>
[NodeInfo( Id = "prism.input.bitangent", Title = "Bitangent", Category = "Input/Geometry",
	Icon = "south_east", Keywords = new[] { "bitangent", "binormal", "tangent v" },
	Description = "The world-space bitangent: the third axis of the surface's tangent basis." )]
[NodeVersion( 1 )]
public sealed class BitangentNode : PrismNode
{
	/// <summary>The bitangent.</summary>
	[Out( "float3", Name = "Bitangent" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.WorldTangentV ) );
	}
}

/// <summary>The object-space normal, kept as a separate node for parity with the built-in editor.</summary>
[NodeInfo( Id = "prism.input.objectNormal", Title = "Object Space Normal", Category = "Input/Geometry",
	Icon = "view_in_ar", Keywords = new[] { "normal", "object", "model" },
	Description = "The surface normal in the model's own space, before the instance transform." )]
[NodeVersion( 1 )]
public sealed class ObjectNormalNode : PrismNode
{
	/// <summary>The object-space normal.</summary>
	[Out( "float3", Name = "Normal" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.ObjectNormal ) );
	}
}

/// <summary>The object-space position, kept as a separate node for parity with the built-in editor.</summary>
[NodeInfo( Id = "prism.input.objectPosition", Title = "Object Space Position", Category = "Input/Geometry",
	Icon = "view_in_ar", Keywords = new[] { "position", "object", "model", "local" },
	Description = "The shaded point in the model's own space, before the instance transform." )]
[NodeVersion( 1 )]
public sealed class ObjectPositionNode : PrismNode
{
	/// <summary>The object-space position.</summary>
	[Out( "float3", Name = "Position" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.ObjectPosition ) );
	}
}

/// <summary>
/// The instance's origin in world space.
/// <para>
/// Vertex-only: the engine derives it from the instancing transform, which the pixel program never
/// sees. Reading it from the pixel stage is legal — the stage planner moves the read into the vertex
/// program and allocates an interpolator for it.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.objectOrigin", Title = "Object Origin", Category = "Input/Geometry",
	Icon = "my_location", Keywords = new[] { "object", "origin", "pivot", "instance", "position" },
	Description = "Where this instance's pivot sits in the world. Computed per vertex and interpolated." )]
[NodeVersion( 1 )]
public sealed class ObjectOriginNode : PrismNode, IStageConstrained
{
	/// <summary>The instance origin, in world space.</summary>
	[Out( "float3", Name = "Origin" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Vertex;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.Vertex;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.ObjectOrigin ) );
	}
}

// ---- vertex attributes ----------------------------------------------------------------------------

/// <summary>The mesh's vertex colour stream.</summary>
[NodeInfo( Id = "prism.input.vertexColor", Title = "Vertex Color", Category = "Input/Vertex",
	Icon = "format_color_fill", Keywords = new[] { "vertex", "colour", "color", "col0" },
	Description = "The COLOR0 vertex stream, interpolated across the triangle." )]
[NodeVersion( 1 )]
public sealed class VertexColorNode : PrismNode
{
	/// <summary>The colour channels.</summary>
	[Out( "float3", Name = "RGB" )] public PortRef RGB { get; set; }

	/// <summary>The alpha channel.</summary>
	[Out( "float", Name = "Alpha" )] public PortRef Alpha { get; set; }

	/// <summary>All four channels.</summary>
	[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.Builtin( Builtin.VertexColor );

		ctx.Out( nameof( RGB ), ctx.Swizzle( value, "xyz" ) );
		ctx.Out( nameof( Alpha ), ctx.Swizzle( value, "w" ) );
		ctx.Out( nameof( RGBA ), value );
	}
}

/// <summary>The per-instance tint the renderer pushed for this draw.</summary>
[NodeInfo( Id = "prism.input.tint", Title = "Tint", Category = "Input/Vertex",
	Icon = "palette", Keywords = new[] { "tint", "instance", "colour", "color" },
	Description = "The per-instance tint colour. TintMask on the output node controls where it lands." )]
[NodeVersion( 1 )]
public sealed class TintNode : PrismNode
{
	/// <summary>The tint colour.</summary>
	[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( RGBA ), ctx.Builtin( Builtin.TintColor ) );
	}
}

/// <summary>
/// The mesh's texture coordinates.
/// <para>
/// The engine packs UV0 and UV1 into one <c>float4</c> interpolant, so both channels are free; a
/// higher channel has no stream behind it and reports an error instead of silently reading zero.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.texcoord", Title = "Texture Coordinate", Category = "Input/Geometry",
	Icon = "grid_on", Keywords = new[] { "uv", "texcoord", "texture coordinate", "uv2", "lightmap" },
	Tier = NodeTier.Deprecated, DeprecatedBy = "prism.uv.texcoord",
	Description = "A UV set from the mesh. Channel 0 is the main set; channel 1 is the second set, " +
		"which the graph must also enable in its settings. Superseded by the UV node, which reads the " +
		"same streams and can tile and offset them; this id stays registered because it is what the " +
		"legacy ShaderGraph importer and the document schema name." )]
[NodeVersion( 1 )]
public sealed class TexCoordNode : PrismNode
{
	/// <summary>Which UV set to read. Only 0 and 1 exist.</summary>
	public int Channel { get; set; }

	/// <summary>The texture coordinate.</summary>
	[Out( "float2", Name = "UV" )] public PortRef UV { get; set; }

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;

		if ( Channel is not ( 0 or 1 ) )
		{
			ctx.Error( $"UV channel {Channel} does not exist; the engine's vertex format carries channels 0 and 1",
				nameof( UV ), DiagnosticCode.StageViolation );

			return;
		}

		if ( Channel != 1 ) return;
		if ( ctx.Graph is not ICompilableGraph graph || graph.UsesUv2 ) return;

		ctx.Warn( "This graph does not declare a second UV set, so channel 1 reads whatever the mesh " +
			"happened to leave there; turn UV2 on in the graph's settings",
			nameof( UV ), DiagnosticCode.BackendUnsupported );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( UV ), ctx.Builtin( Channel == 1 ? Builtin.TexCoord1 : Builtin.TexCoord0 ) );
	}
}

/// <summary>The instance index of the primitive being drawn.</summary>
[NodeInfo( Id = "prism.input.instanceId", Title = "Instance ID", Category = "Input/Vertex",
	Icon = "tag", Keywords = new[] { "instance", "id", "index", "batch" },
	Description = "The index of this instance inside the draw call. Read per vertex and interpolated " +
		"flat when the pixel stage asks for it." )]
[NodeVersion( 1 )]
public sealed class InstanceIdNode : PrismNode, IStageConstrained
{
	/// <summary>The instance index.</summary>
	[Out( "int", Name = "Index" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Vertex;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.Vertex;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Cast( ctx.Builtin( Builtin.InstanceId ), ShaderType.Int ) );
	}
}

/// <summary>
/// The index of the vertex being processed.
/// <para>
/// <c>common/vertexinput.hlsl</c> declares no <c>SV_VertexID</c> stream, so the s&amp;box backend takes
/// it as a second parameter on <c>MainVs</c> — the shape the engine's own sprite and UI shaders use —
/// and only for a graph that actually reads it. The Slang backend carries it on its <c>VsIn</c> block.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.vertexId", Title = "Vertex ID", Category = "Input/Vertex",
	Icon = "tag", Tier = NodeTier.Advanced, Keywords = new[] { "vertex", "id", "index" },
	Description = "The index of the vertex inside the draw call. Vertex stage only; pass it to the " +
		"pixel stage through a flat interpolator if you need it there." )]
[NodeVersion( 1 )]
public sealed class VertexIdNode : PrismNode, IStageConstrained
{
	/// <summary>The vertex index.</summary>
	[Out( "int", Name = "Index" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Vertex;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.Vertex;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Cast( ctx.Builtin( Builtin.VertexId ), ShaderType.Int ) );
	}
}

/// <summary>Whether the triangle being shaded faces the camera.</summary>
[NodeInfo( Id = "prism.input.isFrontFace", Title = "Is Front Face", Category = "Input/Geometry",
	Icon = "flip", Keywords = new[] { "front", "back", "face", "facing", "two sided", "backface" },
	Description = "True on front faces. The signed form is +1 / -1, which is what you multiply a " +
		"normal by to make a two-sided material." )]
[NodeVersion( 1 )]
public sealed class IsFrontFaceNode : PrismNode, IStageConstrained
{
	/// <summary>True when this fragment belongs to a front face.</summary>
	[Out( "bool", Name = "Front Face" )] public PortRef Result { get; set; }

	/// <summary>+1 on a front face, -1 on a back face.</summary>
	[Out( "float", Name = "Sign" )] public PortRef Sign { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var facing = ctx.Builtin( Builtin.IsFrontFace );

		ctx.Out( nameof( Result ), facing );
		ctx.Out( nameof( Sign ), ctx.Select( facing, ctx.Const( 1f ), ctx.Const( -1f ) ) );
	}
}

// ---- camera / view --------------------------------------------------------------------------------

/// <summary>Everything about the camera this pass is rendering from.</summary>
[NodeInfo( Id = "prism.input.camera", Title = "Camera", Category = "Input/Camera",
	Icon = "photo_camera", Keywords = new[] { "camera", "eye", "near", "far", "view" },
	Description = "The camera's world-space position and direction, plus its near and far planes." )]
[NodeVersion( 1 )]
public sealed class CameraNode : PrismNode
{
	/// <summary>Where the camera is.</summary>
	[Out( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }

	/// <summary>Which way the camera looks.</summary>
	[Out( "float3", Name = "Direction" )] public PortRef Direction { get; set; }

	/// <summary>The near clip distance.</summary>
	[Out( "float", Name = "Near Plane" )] public PortRef NearPlane { get; set; }

	/// <summary>The far clip distance.</summary>
	[Out( "float", Name = "Far Plane" )] public PortRef FarPlane { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( WorldPosition ), ctx.Builtin( Builtin.CameraPosition ) );
		ctx.Out( nameof( Direction ), ctx.Builtin( Builtin.CameraForward ) );
		ctx.Out( nameof( NearPlane ), ctx.Builtin( Builtin.CameraNear ) );
		ctx.Out( nameof( FarPlane ), ctx.Builtin( Builtin.CameraFar ) );
	}
}

/// <summary>The unit vector from the shaded point towards the camera.</summary>
[NodeInfo( Id = "prism.input.viewDirection", Title = "View Direction", Category = "Input/Camera",
	Icon = "cameraswitch", Keywords = new[] { "view", "eye", "direction", "camera" },
	Description = "Points from the surface towards the camera, already normalised and corrected for " +
		"the engine's high-precision lighting offset." )]
[NodeVersion( 1 )]
public sealed class ViewDirectionNode : PrismNode
{
	/// <summary>Which space to report the direction in.</summary>
	public PrismSpace Space { get; set; } = PrismSpace.World;

	/// <summary>The direction towards the camera.</summary>
	[Out( "float3", Name = "Direction" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var world = ctx.Builtin( Builtin.ViewDirection );

		var value = Space switch
		{
			PrismSpace.View => PrismHelpers.WorldToViewDirection( ctx, world ),
			PrismSpace.Tangent => PrismHelpers.WorldToTangentDirection( ctx, world ),
			_ => world
		};

		ctx.Out( nameof( Result ), value );
	}
}

/// <summary>
/// The camera-to-surface ray expressed in tangent space, the way parallax mapping wants it.
/// </summary>
[NodeInfo( Id = "prism.input.tangentViewVector", Title = "Tangent View Vector", Category = "Input/Camera",
	Icon = "visibility", Keywords = new[] { "tangent", "view", "parallax", "pom" },
	Description = "The camera-to-surface direction rotated into tangent space. This is the vector a " +
		"parallax or POM node steps along." )]
[NodeVersion( 1 )]
public sealed class TangentViewVectorNode : PrismNode
{
	/// <summary>The surface position. Defaults to the shaded point.</summary>
	[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }

	/// <summary>The surface normal. Defaults to the interpolated normal.</summary>
	[In( "float3", Name = "Normal" )] public PortRef WorldNormal { get; set; }

	/// <summary>Tangent U. Defaults to the interpolated tangent.</summary>
	[In( "float3", Name = "Tangent U" )] public PortRef TangentUWs { get; set; }

	/// <summary>Tangent V. Defaults to the interpolated bitangent.</summary>
	[In( "float3", Name = "Tangent V" )] public PortRef TangentVWs { get; set; }

	/// <summary>The tangent-space view vector.</summary>
	[Out( "float3", Name = "Vector" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var position = ctx.In( nameof( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
		var normal = ctx.In( nameof( WorldNormal ), ctx.Builtin( Builtin.WorldNormal ) );
		var tangentU = ctx.In( nameof( TangentUWs ), ctx.Builtin( Builtin.WorldTangentU ) );
		var tangentV = ctx.In( nameof( TangentVWs ), ctx.Builtin( Builtin.WorldTangentV ) );

		ctx.Out( nameof( Result ),
			ctx.Helper( PrismHelpers.TangentViewVector, position, normal, tangentU, tangentV ) );
	}
}

/// <summary>The classic rim term: how edge-on the surface is to the viewer.</summary>
[NodeInfo( Id = "prism.input.fresnel", Title = "Fresnel", Category = "Input/Camera",
	Icon = "blur_circular", Keywords = new[] { "fresnel", "rim", "edge", "grazing", "schlick" },
	Description = "Rises towards 1 where the surface turns away from the camera. Power 1 is a straight " +
		"grazing term; 5 matches the Schlick approximation." )]
[NodeVersion( 1 )]
public sealed class FresnelNode : PrismNode
{
	/// <summary>The surface normal. Defaults to the interpolated normal.</summary>
	[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }

	/// <summary>The view direction. Defaults to the direction towards the camera.</summary>
	[In( "float3", Name = "Direction" )] public PortRef Direction { get; set; }

	/// <summary>The falloff exponent.</summary>
	[In( "float", Name = "Power" )] public PortRef Power { get; set; }

	/// <summary>The exponent used when <see cref="Power"/> is unconnected.</summary>
	[InlineValue( nameof( Power ) )] public float DefaultPower { get; set; } = 5f;

	/// <summary>The rim term.</summary>
	[Out( "float", Name = "Fresnel" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var normal = ctx.Call( Intrinsic.Normalize,
			ctx.In( nameof( Normal ), ctx.Builtin( Builtin.WorldNormal ) ) );

		var direction = ctx.Call( Intrinsic.Normalize,
			ctx.In( nameof( Direction ), ctx.Builtin( Builtin.ViewDirection ) ) );

		var power = ctx.In( nameof( Power ), ctx.Const( DefaultPower ) );

		var facing = ctx.Call( Intrinsic.Saturate, ctx.Call( Intrinsic.Dot, normal, direction ) );
		var rim = ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), facing );

		ctx.Out( nameof( Result ), ctx.Call( Intrinsic.Pow, rim, power ) );
	}
}

// ---- screen ---------------------------------------------------------------------------------------

/// <summary>The raw clip- or screen-space position of the fragment.</summary>
[NodeInfo( Id = "prism.input.screenPosition", Title = "Screen Position", Category = "Input/Screen",
	Icon = "install_desktop", Keywords = new[] { "screen", "clip", "position", "sv_position", "ndc" },
	Description = "The homogeneous clip position in the vertex program and the rasteriser's pixel " +
		"position in the pixel program. W carries view depth." )]
[NodeVersion( 1 )]
public sealed class ScreenPositionNode : PrismNode
{
	/// <summary>The full XYZ.</summary>
	[Out( "float3", Name = "XYZ" )] public PortRef XYZ { get; set; }

	/// <summary>The XY pair.</summary>
	[Out( "float2", Name = "XY" )] public PortRef XY { get; set; }

	/// <summary>The Z component.</summary>
	[Out( "float", Name = "Z" )] public PortRef Z { get; set; }

	/// <summary>The W component — view-space depth in the pixel program.</summary>
	[Out( "float", Name = "W" )] public PortRef W { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.Builtin( Builtin.ClipPosition );

		ctx.Out( nameof( XYZ ), ctx.Swizzle( value, "xyz" ) );
		ctx.Out( nameof( XY ), ctx.Swizzle( value, "xy" ) );
		ctx.Out( nameof( Z ), ctx.Swizzle( value, "z" ) );
		ctx.Out( nameof( W ), ctx.Swizzle( value, "w" ) );
	}
}

/// <summary>The fragment's position as a 0..1 viewport coordinate.</summary>
[NodeInfo( Id = "prism.input.screenCoordinate", Title = "Screen Coordinate", Category = "Input/Screen",
	Icon = "tv", Keywords = new[] { "screen", "uv", "viewport", "coordinate", "ndc" },
	Description = "Where the fragment lands in the viewport, normalised to 0..1. This is the UV you " +
		"sample a full-screen texture with." )]
[NodeVersion( 1 )]
public sealed class ScreenCoordinateNode : PrismNode
{
	/// <summary>The viewport coordinate.</summary>
	[Out( "float2", Name = "UV" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.ScreenUv ) );
	}
}

/// <summary>The fragment's position in whole pixels.</summary>
[NodeInfo( Id = "prism.input.pixelPosition", Title = "Pixel Position", Category = "Input/Screen",
	Icon = "grid_4x4", Keywords = new[] { "pixel", "screen", "position", "raster", "dither" },
	Description = "The rasteriser's pixel coordinate. Feed it to a dither or interleaved-gradient node." )]
[NodeVersion( 1 )]
public sealed class PixelPositionNode : PrismNode, IStageConstrained
{
	/// <summary>The pixel coordinate.</summary>
	[Out( "float2", Name = "Pixel" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Result ), ctx.Builtin( Builtin.PixelPosition ) );
	}
}

/// <summary>The viewport's geometry.</summary>
[NodeInfo( Id = "prism.input.viewport", Title = "Viewport", Category = "Input/Screen",
	Icon = "aspect_ratio", Keywords = new[] { "viewport", "resolution", "size", "screen" },
	Description = "The viewport's size, reciprocal size, offset and depth range." )]
[NodeVersion( 1 )]
public sealed class ViewportNode : PrismNode
{
	/// <summary>The viewport size in pixels.</summary>
	[Out( "float2", Name = "Size" )] public PortRef ViewportSize { get; set; }

	/// <summary>One over the viewport size — a texel step.</summary>
	[Out( "float2", Name = "Inverse Size" )] public PortRef ViewportInverseSize { get; set; }

	/// <summary>The viewport's origin inside the render target.</summary>
	[Out( "float2", Name = "Offset" )] public PortRef ViewportOffset { get; set; }

	/// <summary>The near end of the depth range.</summary>
	[Out( "float", Name = "Min Z" )] public PortRef ViewportMinZ { get; set; }

	/// <summary>The far end of the depth range.</summary>
	[Out( "float", Name = "Max Z" )] public PortRef ViewportMaxZ { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( ViewportSize ), ctx.Builtin( Builtin.ViewportSize ) );
		ctx.Out( nameof( ViewportInverseSize ), ctx.Builtin( Builtin.ViewportInvSize ) );
		ctx.Out( nameof( ViewportOffset ), ctx.Builtin( Builtin.ViewportOffset ) );
		ctx.Out( nameof( ViewportMinZ ), ctx.Helper( PrismHelpers.ViewportMinZ ) );
		ctx.Out( nameof( ViewportMaxZ ), ctx.Helper( PrismHelpers.ViewportMaxZ ) );
	}
}

// ---- time -----------------------------------------------------------------------------------------

/// <summary>
/// The engine's clock.
/// <para>
/// Time is read from the view constant buffer every frame; nothing here is ever folded into a
/// constant, so an animated graph keeps animating after the shader is compiled.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.time", Title = "Time", Category = "Input/Time",
	Icon = "timer", Keywords = new[] { "time", "clock", "animate", "sine", "cosine", "delta" },
	Description = "Seconds since the level loaded, plus the usual derived forms." )]
[NodeVersion( 1 )]
public sealed class TimeNode : PrismNode
{
	/// <summary>How fast the clock runs for this node.</summary>
	[In( "float", Name = "Speed" )] public PortRef Speed { get; set; }

	/// <summary>The multiplier used when <see cref="Speed"/> is unconnected.</summary>
	[InlineValue( nameof( Speed ) )] public float DefaultSpeed { get; set; } = 1f;

	/// <summary>Seconds since the level loaded, scaled by speed.</summary>
	[Out( "float", Name = "Time" )] public PortRef Result { get; set; }

	/// <summary>The sine of the scaled time.</summary>
	[Out( "float", Name = "Sine" )] public PortRef Sine { get; set; }

	/// <summary>The cosine of the scaled time.</summary>
	[Out( "float", Name = "Cosine" )] public PortRef Cosine { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var time = ctx.Builtin( Builtin.Time );
		var speed = ctx.In( nameof( Speed ), ctx.Const( DefaultSpeed ) );
		var scaled = ctx.Bin( BinaryOp.Mul, time, speed );

		ctx.Out( nameof( Result ), scaled );
		ctx.Out( nameof( Sine ), ctx.Call( Intrinsic.Sin, scaled ) );
		ctx.Out( nameof( Cosine ), ctx.Call( Intrinsic.Cos, scaled ) );
	}
}

/// <summary>
/// The frame's timestep and index.
/// <para>
/// The engine's per-view constant buffer carries neither, so both arrive as render attributes the
/// host pushes each frame. They are declared with sane defaults, so a shader that is rendered without
/// anyone setting them still behaves.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.frame", Title = "Frame", Category = "Input/Time",
	Icon = "speed", Keywords = new[] { "delta", "frame", "time", "dt", "counter" },
	Description = "Seconds since the previous frame, and the frame counter. Driven by the " +
		"PrismDeltaTime and PrismFrameCount render attributes." )]
[NodeVersion( 1 )]
public sealed class FrameNode : PrismNode
{
	/// <summary>The render-attribute name carrying the frame's timestep.</summary>
	public const string DeltaTimeAttribute = "PrismDeltaTime";

	/// <summary>The render-attribute name carrying the frame counter.</summary>
	public const string FrameCountAttribute = "PrismFrameCount";

	/// <summary>Seconds since the previous frame.</summary>
	[Out( "float", Name = "Delta Time" )] public PortRef DeltaTime { get; set; }

	/// <summary>Frames rendered so far.</summary>
	[Out( "int", Name = "Frame Count" )] public PortRef FrameCount { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( DeltaTime ), ctx.Global( new GlobalDecl( "g_flPrismDeltaTime", ShaderType.Float,
			GlobalKind.Uniform )
		{
			AttributeName = DeltaTimeAttribute,
			Default = ConstValue.From( 1f / 60f )
		} ) );

		ctx.Out( nameof( FrameCount ), ctx.Global( new GlobalDecl( "g_nPrismFrameCount", ShaderType.Int,
			GlobalKind.Uniform )
		{
			AttributeName = FrameCountAttribute,
			Default = ConstValue.Zero
		} ) );
	}
}

// ---- depth and scene ------------------------------------------------------------------------------

/// <summary>How a depth read is expressed.</summary>
public enum PrismDepthMode
{
	/// <summary>The value stored in the depth buffer.</summary>
	Raw,
	/// <summary>The raw value remapped into 0..1 across the viewport's depth range.</summary>
	Normalized,
	/// <summary>Distance from the camera plane, in world units.</summary>
	Linear
}

/// <summary>Reads the scene's depth buffer.</summary>
[NodeInfo( Id = "prism.input.depth", Title = "Scene Depth", Category = "Input/Scene",
	Icon = "layers", Keywords = new[] { "depth", "z", "buffer", "scene", "linear" },
	Description = "Samples the depth chain at a screen position. Pixel stage only — the depth buffer " +
		"does not exist while vertices are being transformed." )]
[NodeVersion( 1 )]
public sealed class SceneDepthNode : PrismNode, IStageConstrained
{
	/// <summary>How the depth is expressed.</summary>
	public PrismDepthMode Mode { get; set; } = PrismDepthMode.Linear;

	/// <summary>The screen position to sample, in pixels. Defaults to this fragment's own.</summary>
	[In( "float2", Name = "Screen Position" )] public PortRef UV { get; set; }

	/// <summary>The depth.</summary>
	[Out( "float", Name = "Depth" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var position = ctx.In( nameof( UV ), ctx.Builtin( Builtin.PixelPosition ) );

		var helper = Mode switch
		{
			PrismDepthMode.Raw => PrismHelpers.SceneDepthRaw,
			PrismDepthMode.Normalized => PrismHelpers.SceneDepthNormalized,
			_ => PrismHelpers.SceneDepthLinear
		};

		ctx.Out( nameof( Out ), ctx.Helper( helper, position ) );
	}
}

/// <summary>Reads the scene's depth buffer, linearised. Kept for parity with the built-in editor.</summary>
[NodeInfo( Id = "prism.input.linearDepth", Title = "Linear Depth", Category = "Input/Scene",
	Icon = "straighten", Keywords = new[] { "depth", "linear", "eye", "distance" },
	Description = "Distance from the camera plane to whatever is already in the depth buffer." )]
[NodeVersion( 1 )]
public sealed class LinearDepthNode : PrismNode, IStageConstrained
{
	/// <summary>The screen position to sample, in pixels. Defaults to this fragment's own.</summary>
	[In( "float2", Name = "Screen Position" )] public PortRef UV { get; set; }

	/// <summary>The linear depth.</summary>
	[Out( "float", Name = "Depth" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var position = ctx.In( nameof( UV ), ctx.Builtin( Builtin.PixelPosition ) );

		ctx.Out( nameof( Out ), ctx.Helper( PrismHelpers.SceneDepthLinear, position ) );
	}
}

/// <summary>Reconstructs the world position already sitting in the depth buffer.</summary>
[NodeInfo( Id = "prism.input.sceneWorldPosition", Title = "Scene World Position", Category = "Input/Scene",
	Icon = "explore", Keywords = new[] { "depth", "world", "reconstruct", "position", "scene" },
	Description = "The world-space point the depth buffer holds at a screen position. Useful for " +
		"decals, projected effects and intersection highlights." )]
[NodeVersion( 1 )]
public sealed class SceneWorldPositionNode : PrismNode, IStageConstrained
{
	/// <summary>The screen position to sample, in pixels. Defaults to this fragment's own.</summary>
	[In( "float2", Name = "Screen Position" )] public PortRef ScreenPosition { get; set; }

	/// <summary>The reconstructed world position.</summary>
	[Out( "float3", Name = "Position" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var position = ctx.In( nameof( ScreenPosition ), ctx.Builtin( Builtin.PixelPosition ) );

		ctx.Out( nameof( Result ), ctx.Helper( PrismHelpers.SceneDepthWorldPosition, position ) );
	}
}

/// <summary>
/// How far this fragment is in front of whatever the depth buffer already holds, and the soft-particle
/// fade that follows from it.
/// </summary>
[NodeInfo( Id = "prism.input.depthFade", Title = "Depth Fade", Category = "Input/Scene",
	Icon = "gradient", Keywords = new[] { "soft particle", "depth", "fade", "intersection", "difference" },
	Description = "The gap between this fragment and the scene behind it. Fade goes to zero exactly " +
		"where the surface intersects geometry, which is what softens a particle or a water edge." )]
[NodeVersion( 1 )]
public sealed class DepthFadeNode : PrismNode, IStageConstrained
{
	/// <summary>Over how many world units the fade happens.</summary>
	[In( "float", Name = "Distance" )] public PortRef Distance { get; set; }

	/// <summary>The distance used when <see cref="Distance"/> is unconnected.</summary>
	[InlineValue( nameof( Distance ) )] public float DefaultDistance { get; set; } = 32f;

	/// <summary>0 at the intersection, 1 once the surface is clear of the scene.</summary>
	[Out( "float", Name = "Fade" )] public PortRef Fade { get; set; }

	/// <summary>The raw depth difference, in world units.</summary>
	[Out( "float", Name = "Difference" )] public PortRef Difference { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var screen = ctx.Builtin( Builtin.ClipPosition );
		var sceneDepth = ctx.Helper( PrismHelpers.SceneDepthLinear, ctx.Swizzle( screen, "xy" ) );
		var fragmentDepth = ctx.Swizzle( screen, "w" );
		var difference = ctx.Bin( BinaryOp.Sub, sceneDepth, fragmentDepth );

		var distance = ctx.In( nameof( Distance ), ctx.Const( DefaultDistance ) );
		var safe = ctx.Call( Intrinsic.Max, distance, ctx.Const( 1e-5f ) );

		ctx.Out( nameof( Difference ), difference );
		ctx.Out( nameof( Fade ), ctx.Call( Intrinsic.Saturate, ctx.Bin( BinaryOp.Div, difference, safe ) ) );
	}
}

/// <summary>
/// Reads the colour already rendered behind this fragment.
/// <para>
/// A post-process graph reads the engine's colour buffer directly. A translucent surface graph reads
/// the frame-buffer copy, which the engine only fills for a material that asks for it — declaring the
/// texture here makes the s&amp;box backend emit <c>BoolAttribute( bWantsFBCopyTexture, true )</c> into
/// the PS block, so the ask and the read can never drift apart.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.input.sceneColor", Title = "Scene Color", Category = "Input/Scene",
	Icon = "palette", Keywords = new[] { "scene", "colour", "color", "refraction", "grab", "frame buffer" },
	Description = "The colour already in the frame buffer at a screen position. Post-process graphs " +
		"read the colour buffer; translucent surfaces read the frame-buffer copy." )]
[NodeVersion( 1 )]
public sealed class SceneColorNode : PrismNode, IStageConstrained
{
	/// <summary>The declaration a post-process graph reads through.</summary>
	public const string ColorBufferSymbol = "g_tColorBuffer";

	/// <summary>The declaration a translucent surface graph reads through.</summary>
	public const string FrameBufferCopySymbol = "g_tFrameBufferCopyTexture";

	/// <summary>The screen coordinate to read, 0..1. Defaults to this fragment's own.</summary>
	[In( "float2", Name = "UV" )] public PortRef Coords { get; set; }

	/// <summary>The colour behind this fragment.</summary>
	[Out( "float3", Name = "RGB" )] public PortRef Result { get; set; }

	/// <summary>All four channels of the frame buffer.</summary>
	[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }

	/// <inheritdoc/>
	public StageMask RequiredStages => StageMask.Pixel;

	/// <inheritdoc/>
	public ShaderStage PreferredStage => ShaderStage.None;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var postProcess = ( ctx.Graph as ICompilableGraph )?.Domain == ShaderDomain.PostProcess;

		var declaration = postProcess
			? new GlobalDecl( ColorBufferSymbol, ShaderType.Texture2D, GlobalKind.Texture )
			{
				AttributeName = "ColorBuffer",
				Srgb = true,
				Stages = StageMask.Pixel
			}
			: new GlobalDecl( FrameBufferCopySymbol, ShaderType.Texture2D, GlobalKind.Texture )
			{
				AttributeName = "FrameBufferCopyTexture",
				Srgb = true,
				Stages = StageMask.Pixel
			};

		var texture = ctx.Global( declaration );
		var uv = ctx.In( nameof( Coords ), ctx.Builtin( Builtin.ScreenUv ) );
		var sampled = ctx.Helper( PrismHelpers.SampleSceneColor, texture, uv );

		ctx.Out( nameof( RGBA ), sampled );
		ctx.Out( nameof( Result ), ctx.Swizzle( sampled, "xyz" ) );
	}

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx?.Graph is not ICompilableGraph graph ) return;
		if ( graph.Domain == ShaderDomain.PostProcess ) return;

		if ( graph.BlendMode is SurfaceBlendMode.Translucent or SurfaceBlendMode.Additive
			or SurfaceBlendMode.Multiply ) return;

		ctx.Warn( "Scene Color reads the frame-buffer copy, which the engine only fills for a " +
			"translucent or blended material; on an opaque surface it will read stale contents",
			nameof( Result ), DiagnosticCode.BackendUnsupported );
	}
}

// ---- matrices -------------------------------------------------------------------------------------

/// <summary>One of the engine's transform matrices.</summary>
[NodeInfo( Id = "prism.input.matrix", Title = "Transform Matrix", Category = "Input/Matrix",
	Icon = "transform", Tier = NodeTier.Advanced,
	Keywords = new[] { "matrix", "transform", "view", "projection", "object to world" },
	Description = "A view or projection matrix from the engine's per-view constants. The object " +
		"matrices exist only while a vertex is being transformed." )]
[NodeVersion( 1 )]
public sealed class TransformMatrixNode : PrismNode
{
	/// <summary>Which matrix to read.</summary>
	public PrismMatrixKind Matrix { get; set; } = PrismMatrixKind.ViewProjection;

	/// <summary>The matrix.</summary>
	[Out( "float4x4", Name = "Matrix" )] public PortRef Result { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var id = Matrix switch
		{
			PrismMatrixKind.ObjectToWorld => Builtin.ObjectToWorld,
			PrismMatrixKind.WorldToObject => Builtin.WorldToObject,
			PrismMatrixKind.View => Builtin.ViewMatrix,
			PrismMatrixKind.Projection => Builtin.ProjectionMatrix,
			_ => Builtin.ViewProjectionMatrix
		};

		ctx.Out( nameof( Result ), ctx.Builtin( id ) );
	}

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;
		if ( Matrix is not ( PrismMatrixKind.ObjectToWorld or PrismMatrixKind.WorldToObject ) ) return;

		// The engine builds the object transform from the instancing table and never exposes it as a
		// global, so the backend generates a helper per stage. The vertex one reads the table directly
		// and is exact; the pixel one is rebuilt from the interpolated tangent frames, which recovers
		// rotation and translation exactly but not scale — the vertex program normalised the frame.
		ctx.Info( "In the pixel program this matrix is reconstructed from the interpolated tangent " +
			"frame: rotation and translation are exact, scale is not. Read it in the vertex program " +
			"if the object is non-uniformly scaled.", nameof( Result ), DiagnosticCode.SampleLowered );
	}
}