Editor Prism lighting nodes and helper definitions for the shader graph. Declares helper functions that lower to HLSL/Slang bodies and a set of Prism nodes (sun, ambient, envmap, SSAO, clustered light access, BRDF terms, fog, tonemap) which emit IR via EmitContext to build shader code.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// Lighting.
//
// Almost everything here is a thin wrapper over one of the engine's shading classes — Light::,
// EnvMap::, AmbientLight::, Fog::, ScreenSpaceAmbientOcclusion:: — because re-deriving clustered
// lighting, cube-map parallax or volumetric fog in a graph would be both slower and wrong.
//
// Those classes only exist in the pixel program and only in an s&box .shader, so every node that uses
// one is IStageConstrained to the pixel stage and every helper carries a neutral Slang body: a
// standalone .slang module has no engine behind it and still has to compile.
//
// The BRDF nodes are the exception. They are self-contained maths, identical on both backends, and
// exist so a graph can build a custom shading model without leaving the editor.
// ---------------------------------------------------------------------------------------------------
/// <summary>The tone curves the tonemap node offers.</summary>
public enum PrismTonemapMode
{
/// <summary>No curve; the colour passes through.</summary>
None,
/// <summary>The engine's own linear tone-map scalar.</summary>
Engine,
/// <summary>Reinhard: <c>c / (1 + c)</c>.</summary>
Reinhard,
/// <summary>The ACES filmic approximation.</summary>
Aces,
/// <summary>The Hable / Uncharted 2 filmic curve.</summary>
Filmic
}
/// <summary>The helper functions the lighting nodes lower to.</summary>
internal static class PrismLightingHelpers
{
/// <summary>Indirect diffuse light at a point, from whichever ambient source the level uses.</summary>
public static readonly HelperFunction AmbientLight = new( "Prism_AmbientLight", ShaderType.Float3,
new[]
{
new HelperParam( "vPositionWs", ShaderType.Float3 ),
new HelperParam( "vPositionSs", ShaderType.Float4 ),
new HelperParam( "vNormalWs", ShaderType.Float3 )
} )
{
Hlsl =
"""
float3 Prism_AmbientLight( float3 vPositionWs, float4 vPositionSs, float3 vNormalWs )
{
return AmbientLight::From( vPositionWs, vPositionSs, vNormalWs );
}
""",
Slang =
"""
float3 Prism_AmbientLight( float3 vPositionWs, float4 vPositionSs, float3 vNormalWs )
{
// A standalone module has no probe volume; a hemispherical constant is the honest stand-in.
float sky = saturate( vNormalWs.z * 0.5 + 0.5 );
return lerp( float3( 0.05, 0.06, 0.08 ), float3( 0.35, 0.40, 0.50 ), sky );
}
""",
Stages = StageMask.Pixel
};
/// <summary>The reflection colour arriving from the nearest environment map.</summary>
public static readonly HelperFunction EnvironmentMap = new( "Prism_EnvironmentMap", ShaderType.Float3,
new[]
{
new HelperParam( "vPositionWs", ShaderType.Float3 ),
new HelperParam( "vPositionSs", ShaderType.Float4 ),
new HelperParam( "vNormalWs", ShaderType.Float3 ),
new HelperParam( "flRoughness", ShaderType.Float )
} )
{
Hlsl =
"""
float3 Prism_EnvironmentMap( float3 vPositionWs, float4 vPositionSs, float3 vNormalWs, float flRoughness )
{
return EnvMap::From( vPositionWs, vPositionSs, vNormalWs, float2( flRoughness, flRoughness ) );
}
""",
Slang =
"""
float3 Prism_EnvironmentMap( float3 vPositionWs, float4 vPositionSs, float3 vNormalWs, float flRoughness )
{
return lerp( float3( 0.30, 0.34, 0.40 ), float3( 0.08, 0.09, 0.11 ), saturate( flRoughness ) );
}
""",
Stages = StageMask.Pixel
};
/// <summary>How many lights the cluster this fragment belongs to holds.</summary>
public static readonly HelperFunction LightCount = new( "Prism_LightCount", ShaderType.Int,
new[] { new HelperParam( "vPositionSs", ShaderType.Float4 ) } )
{
Hlsl =
"""
int Prism_LightCount( float4 vPositionSs )
{
return (int)Light::Count( vPositionSs );
}
""",
Slang =
"""
int Prism_LightCount( float4 vPositionSs )
{
return 1;
}
""",
Stages = StageMask.Pixel
};
/// <summary>The colour of one binned light.</summary>
public static readonly HelperFunction LightColor = Light( "Prism_LightColor", ShaderType.Float3,
"l.Color", "gPrismEnv.Frame.SunColor" );
/// <summary>The direction from the shaded point towards one binned light.</summary>
public static readonly HelperFunction LightDirection = Light( "Prism_LightDirection", ShaderType.Float3,
"l.Direction", "-gPrismEnv.Frame.SunDirection" );
/// <summary>Where one binned light is, in world space.</summary>
public static readonly HelperFunction LightPosition = Light( "Prism_LightPosition", ShaderType.Float3,
"l.Position", "gPrismEnv.Frame.CameraPosition" );
/// <summary>The distance and cone falloff of one binned light.</summary>
public static readonly HelperFunction LightAttenuation = Light( "Prism_LightAttenuation", ShaderType.Float,
"l.Attenuation", "1.0" );
/// <summary>How much of one binned light survives its shadow map.</summary>
public static readonly HelperFunction LightVisibility = Light( "Prism_LightVisibility", ShaderType.Float,
"l.Visibility", "1.0" );
/// <summary>
/// One accessor onto the engine's clustered light list. Each field gets its own helper because a
/// helper returns a single value; the compiler collapses the repeated <c>Light::From</c> calls.
/// </summary>
static HelperFunction Light( string name, ShaderType type, string field, string fallback )
{
var spelling = type.Hlsl;
return new HelperFunction( name, type,
new[]
{
new HelperParam( "vPositionWs", ShaderType.Float3 ),
new HelperParam( "vPositionSs", ShaderType.Float4 ),
new HelperParam( "nIndex", ShaderType.Int )
} )
{
Hlsl =
$"{spelling} {name}( float3 vPositionWs, float4 vPositionSs, int nIndex )\r\n" +
"{\r\n" +
"\tLight l = Light::From( vPositionWs, vPositionSs, (uint)max( nIndex, 0 ) );\r\n" +
$"\treturn {field};\r\n" +
"}",
Slang =
$"{spelling} {name}( float3 vPositionWs, float4 vPositionSs, int nIndex )\r\n" +
"{\r\n" +
$"\treturn {fallback};\r\n" +
"}",
Stages = StageMask.Pixel
};
}
/// <summary>Blend a colour towards the level's fog, exactly as the engine's own surfaces do.</summary>
public static readonly HelperFunction ApplyFog = new( "Prism_ApplyFog", ShaderType.Float3,
new[]
{
new HelperParam( "vPositionWs", ShaderType.Float3 ),
new HelperParam( "vPositionSs", ShaderType.Float2 ),
new HelperParam( "vColor", ShaderType.Float3 )
} )
{
Hlsl =
"""
float3 Prism_ApplyFog( float3 vPositionWs, float2 vPositionSs, float3 vColor )
{
return Fog::Apply( vPositionWs, vPositionSs, vColor );
}
""",
Slang =
"""
float3 Prism_ApplyFog( float3 vPositionWs, float2 vPositionSs, float3 vColor )
{
return vColor;
}
""",
Stages = StageMask.Pixel
};
/// <summary>The screen-space ambient occlusion term the renderer already computed.</summary>
public static readonly HelperFunction ScreenSpaceOcclusion = new( "Prism_ScreenSpaceOcclusion",
ShaderType.Float, new[] { new HelperParam( "vPositionSs", ShaderType.Float4 ) } )
{
Hlsl =
"""
float Prism_ScreenSpaceOcclusion( float4 vPositionSs )
{
return ScreenSpaceAmbientOcclusion::Sample( vPositionSs );
}
""",
Slang =
"""
float Prism_ScreenSpaceOcclusion( float4 vPositionSs )
{
return 1.0;
}
""",
Stages = StageMask.Pixel
};
/// <summary>
/// A complete Cook-Torrance GGX specular lobe: Trowbridge-Reitz distribution, Smith-Schlick
/// visibility and a Schlick Fresnel, already multiplied by N·L.
/// </summary>
public static readonly HelperFunction Ggx = new( "Prism_GgxSpecular", ShaderType.Float3,
new[]
{
new HelperParam( "vNormal", ShaderType.Float3 ),
new HelperParam( "vLight", ShaderType.Float3 ),
new HelperParam( "vView", ShaderType.Float3 ),
new HelperParam( "flRoughness", ShaderType.Float ),
new HelperParam( "vF0", ShaderType.Float3 )
} )
{
Hlsl =
"""
float3 Prism_GgxSpecular( float3 vNormal, float3 vLight, float3 vView, float flRoughness, float3 vF0 )
{
float3 N = normalize( vNormal );
float3 L = normalize( vLight );
float3 V = normalize( vView );
float3 H = normalize( L + V );
float a = max( flRoughness * flRoughness, 1.0e-3 );
float a2 = a * a;
float NdotL = saturate( dot( N, L ) );
float NdotV = saturate( dot( N, V ) ) + 1.0e-5;
float NdotH = saturate( dot( N, H ) );
float VdotH = saturate( dot( V, H ) );
float denominator = ( NdotH * NdotH ) * ( a2 - 1.0 ) + 1.0;
float D = a2 / max( 3.14159265 * denominator * denominator, 1.0e-7 );
float k = a * 0.5;
float gv = NdotV / ( NdotV * ( 1.0 - k ) + k );
float gl = NdotL / ( NdotL * ( 1.0 - k ) + k );
float G = gv * gl;
float3 F = vF0 + ( 1.0 - vF0 ) * pow( 1.0 - VdotH, 5.0 );
return ( ( D * G ) / max( 4.0 * NdotV * NdotL, 1.0e-5 ) ) * F * NdotL;
}
"""
};
/// <summary>The engine's own linear tone map, so a graph matches what the rest of the frame does.</summary>
public static readonly HelperFunction TonemapEngine = new( "Prism_TonemapEngine", ShaderType.Float3,
new[] { new HelperParam( "vColor", ShaderType.Float3 ) } )
{
Hlsl =
"""
float3 Prism_TonemapEngine( float3 vColor )
{
return ToneMapLinear( vColor );
}
""",
Slang =
"""
float3 Prism_TonemapEngine( float3 vColor )
{
return vColor / ( 1.0 + vColor );
}
"""
};
/// <summary>Reinhard.</summary>
public static readonly HelperFunction TonemapReinhard = new( "Prism_TonemapReinhard", ShaderType.Float3,
new[] { new HelperParam( "vColor", ShaderType.Float3 ) } )
{
Hlsl =
"""
float3 Prism_TonemapReinhard( float3 vColor )
{
return vColor / ( 1.0 + max( vColor, 0.0 ) );
}
"""
};
/// <summary>The ACES filmic approximation.</summary>
public static readonly HelperFunction TonemapAces = new( "Prism_TonemapAces", ShaderType.Float3,
new[] { new HelperParam( "vColor", ShaderType.Float3 ) } )
{
Hlsl =
"""
float3 Prism_TonemapAces( float3 vColor )
{
float3 x = max( vColor, 0.0 );
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return saturate( ( x * ( a * x + b ) ) / ( x * ( c * x + d ) + e ) );
}
"""
};
/// <summary>The Hable / Uncharted 2 filmic curve, normalised against a white point of 11.2.</summary>
public static readonly HelperFunction TonemapFilmic = new( "Prism_TonemapFilmic", ShaderType.Float3,
new[] { new HelperParam( "vColor", ShaderType.Float3 ) } )
{
Hlsl =
"""
float3 Prism_TonemapFilmic( float3 vColor )
{
float A = 0.15;
float B = 0.50;
float C = 0.10;
float D = 0.20;
float E = 0.02;
float F = 0.30;
float W = 11.2;
float3 x = max( vColor, 0.0 );
float3 curve = ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;
float white = ( ( W * ( A * W + C * B ) + D * E ) / ( W * ( A * W + B ) + D * F ) ) - E / F;
return saturate( curve / max( white, 1.0e-5 ) );
}
"""
};
}
// ---- environment ----------------------------------------------------------------------------------
/// <summary>The level's directional light — the sun.</summary>
[NodeInfo( Id = SunNode.TypeId, Title = "Sun", Category = "Lighting",
Icon = "light_mode", Keywords = new[] { "sun", "directional", "main light", "sky", "key light" },
Description = "The level's directional light. Direction is the vector the engine stores; colour " +
"is linear and already carries the light's brightness." )]
[NodeVersion( 1 )]
public sealed class SunNode : PrismNode
{
/// <summary>The stable type id.</summary>
public const string TypeId = "prism.lighting.sun";
/// <summary>The directional light's direction vector.</summary>
[Out( "float3", Name = "Direction" )] public PortRef Direction { get; set; }
/// <summary>The directional light's linear colour.</summary>
[Out( "float3", Name = "Color" )] public PortRef Color { get; set; }
/// <summary>The colour's Rec. 709 luminance, as a convenience.</summary>
[Out( "float", Name = "Intensity" )] public PortRef Intensity { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var color = ctx.Builtin( Builtin.SunColor );
ctx.Out( nameof( Direction ), ctx.Builtin( Builtin.SunDirection ) );
ctx.Out( nameof( Color ), color );
ctx.Out( nameof( Intensity ),
ctx.Call( Intrinsic.Dot, color, ctx.Const( new Vector3( 0.2126f, 0.7152f, 0.0722f ) ) ) );
}
}
/// <summary>The indirect diffuse light arriving at a point.</summary>
[NodeInfo( Id = "prism.lighting.ambient", Title = "Ambient Light", Category = "Lighting",
Icon = "wb_twilight", Keywords = new[] { "ambient", "indirect", "gi", "probe", "irradiance", "ddgi" },
Description = "Indirect diffuse light from whichever ambient source the level uses: DDGI, a " +
"lightmap probe volume or an environment probe." )]
[NodeVersion( 1 )]
public sealed class AmbientLightNode : PrismNode, IStageConstrained
{
/// <summary>The normal to gather along. Defaults to the surface normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>The point to gather at. Defaults to the shaded point.</summary>
[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }
/// <summary>The indirect diffuse colour.</summary>
[Out( "float3", Name = "Color" )] 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( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
var normal = ctx.In( nameof( Normal ), ctx.Builtin( Builtin.WorldNormal ) );
var screen = ctx.Builtin( Builtin.ClipPosition );
ctx.Out( nameof( Result ), ctx.Helper( PrismLightingHelpers.AmbientLight, position, screen, normal ) );
}
}
/// <summary>Reflections from the environment probe covering this point.</summary>
[NodeInfo( Id = "prism.lighting.envmap", Title = "Environment Map", Category = "Lighting",
Icon = "panorama_photosphere", Keywords = new[] { "envmap", "cubemap", "reflection", "probe", "ibl", "specular" },
Description = "The reflection colour from the nearest environment probe, with box or sphere " +
"parallax already applied. Roughness selects the mip." )]
[NodeVersion( 1 )]
public sealed class EnvironmentMapNode : PrismNode, IStageConstrained
{
/// <summary>The normal to reflect about. Defaults to the surface normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>How blurred the reflection is.</summary>
[In( "float", Name = "Roughness" )] public PortRef Roughness { get; set; }
/// <summary>The point to sample at. Defaults to the shaded point.</summary>
[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }
/// <summary>The roughness used when <see cref="Roughness"/> is unconnected.</summary>
[InlineValue( nameof( Roughness ) )] public float DefaultRoughness { get; set; }
/// <summary>The reflected colour.</summary>
[Out( "float3", Name = "Specular" )] public PortRef Specular { get; set; }
/// <summary>The indirect diffuse colour at the same point, for convenience.</summary>
[Out( "float3", Name = "Diffuse" )] public PortRef Diffuse { 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( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
var normal = ctx.In( nameof( Normal ), ctx.Builtin( Builtin.WorldNormal ) );
var roughness = ctx.In( nameof( Roughness ), ctx.Const( DefaultRoughness ) );
var screen = ctx.Builtin( Builtin.ClipPosition );
ctx.Out( nameof( Specular ),
ctx.Helper( PrismLightingHelpers.EnvironmentMap, position, screen, normal, roughness ) );
ctx.Out( nameof( Diffuse ),
ctx.Helper( PrismLightingHelpers.AmbientLight, position, screen, normal ) );
}
}
/// <summary>The screen-space ambient occlusion the renderer already computed for this fragment.</summary>
[NodeInfo( Id = "prism.lighting.ssao", Title = "Screen Space Occlusion", Category = "Lighting",
Icon = "blur_on", Keywords = new[] { "ssao", "occlusion", "ambient", "screen space", "ao" },
Description = "The renderer's SSAO term at this fragment. Multiply it into ambient occlusion to " +
"combine it with a baked map." )]
[NodeVersion( 1 )]
public sealed class ScreenSpaceOcclusionNode : PrismNode, IStageConstrained
{
/// <summary>The occlusion term. 1 is unoccluded.</summary>
[Out( "float", Name = "Occlusion" )] 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.Helper( PrismLightingHelpers.ScreenSpaceOcclusion, ctx.Builtin( Builtin.ClipPosition ) ) );
}
}
// ---- clustered lights -----------------------------------------------------------------------------
/// <summary>How many lights affect this fragment.</summary>
[NodeInfo( Id = "prism.lighting.lightCount", Title = "Light Count", Category = "Lighting",
Icon = "format_list_numbered", Tier = NodeTier.Advanced,
Keywords = new[] { "light", "count", "cluster", "loop", "forward" },
Description = "How many lights the renderer binned into this fragment's cluster. Drive a loop " +
"with it and read each one through the Light node." )]
[NodeVersion( 1 )]
public sealed class LightCountNode : PrismNode, IStageConstrained
{
/// <summary>The number of lights.</summary>
[Out( "int", Name = "Count" )] 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.Helper( PrismLightingHelpers.LightCount, ctx.Builtin( Builtin.ClipPosition ) ) );
}
}
/// <summary>Everything about one of the lights affecting this fragment.</summary>
[NodeInfo( Id = "prism.lighting.light", Title = "Light", Category = "Lighting",
Icon = "lightbulb", Tier = NodeTier.Advanced,
Keywords = new[] { "light", "index", "cluster", "shadow", "attenuation", "custom lighting" },
Description = "Reads one binned light by index: its colour, the direction towards it, its " +
"attenuation and how much of it survives its shadow map." )]
[NodeVersion( 1 )]
public sealed class LightNode : PrismNode, IStageConstrained
{
/// <summary>Which light to read. 0 up to Light Count minus one.</summary>
[In( "int", Name = "Index" )] public PortRef Index { get; set; }
/// <summary>The point to evaluate the light at. Defaults to the shaded point.</summary>
[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }
/// <summary>The index used when <see cref="Index"/> is unconnected.</summary>
[InlineValue( nameof( Index ) )] public int DefaultIndex { get; set; }
/// <summary>The light's linear colour.</summary>
[Out( "float3", Name = "Color" )] public PortRef Color { get; set; }
/// <summary>The direction from the shaded point towards the light.</summary>
[Out( "float3", Name = "Direction" )] public PortRef Direction { get; set; }
/// <summary>Where the light is, in world space.</summary>
[Out( "float3", Name = "Position" )] public PortRef LightPosition { get; set; }
/// <summary>Distance and cone falloff, before shadowing.</summary>
[Out( "float", Name = "Attenuation" )] public PortRef Attenuation { get; set; }
/// <summary>How much of the light survives its shadow map. 1 is fully lit.</summary>
[Out( "float", Name = "Visibility" )] public PortRef Visibility { 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( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
var index = ctx.In( nameof( Index ), ctx.Const( DefaultIndex ) );
var screen = ctx.Builtin( Builtin.ClipPosition );
ctx.Out( nameof( Color ),
ctx.Helper( PrismLightingHelpers.LightColor, position, screen, index ) );
ctx.Out( nameof( Direction ),
ctx.Helper( PrismLightingHelpers.LightDirection, position, screen, index ) );
ctx.Out( nameof( LightPosition ),
ctx.Helper( PrismLightingHelpers.LightPosition, position, screen, index ) );
ctx.Out( nameof( Attenuation ),
ctx.Helper( PrismLightingHelpers.LightAttenuation, position, screen, index ) );
ctx.Out( nameof( Visibility ),
ctx.Helper( PrismLightingHelpers.LightVisibility, position, screen, index ) );
}
}
/// <summary>How much of a light survives its shadow map at this point.</summary>
[NodeInfo( Id = "prism.lighting.shadow", Title = "Shadow Attenuation", Category = "Lighting",
Icon = "dark_mode", Keywords = new[] { "shadow", "attenuation", "visibility", "occlusion", "cast" },
Description = "1 where the light reaches this point, 0 where it is fully shadowed. Index 0 is " +
"the directional light in almost every level." )]
[NodeVersion( 1 )]
public sealed class ShadowAttenuationNode : PrismNode, IStageConstrained
{
/// <summary>Which light to test against.</summary>
[In( "int", Name = "Light Index" )] public PortRef Index { get; set; }
/// <summary>The point to test. Defaults to the shaded point.</summary>
[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }
/// <summary>The index used when <see cref="Index"/> is unconnected.</summary>
[InlineValue( nameof( Index ) )] public int DefaultIndex { get; set; }
/// <summary>The shadow term.</summary>
[Out( "float", Name = "Attenuation" )] 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( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
var index = ctx.In( nameof( Index ), ctx.Const( DefaultIndex ) );
var screen = ctx.Builtin( Builtin.ClipPosition );
var visibility = ctx.Helper( PrismLightingHelpers.LightVisibility, position, screen, index );
var attenuation = ctx.Helper( PrismLightingHelpers.LightAttenuation, position, screen, index );
ctx.Out( nameof( Result ), ctx.Bin( BinaryOp.Mul, visibility, attenuation ) );
}
}
// ---- BRDF terms -----------------------------------------------------------------------------------
/// <summary>Lambertian diffuse: the cosine of the angle between the normal and the light.</summary>
[NodeInfo( Id = "prism.lighting.lambert", Title = "Lambert", Category = "Lighting/BRDF",
Icon = "wb_sunny", Keywords = new[] { "lambert", "diffuse", "ndotl", "dot", "shading" },
Description = "saturate( dot( N, L ) ). The cheapest diffuse term there is." )]
[NodeVersion( 1 )]
public sealed class LambertNode : PrismNode
{
/// <summary>The surface normal. Defaults to the interpolated normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>The direction towards the light. Defaults to the sun.</summary>
[In( "float3", Name = "Light Direction" )] public PortRef LightDirection { get; set; }
/// <summary>The diffuse term.</summary>
[Out( "float", Name = "Diffuse" )] 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 light = ctx.Call( Intrinsic.Normalize,
ctx.In( nameof( LightDirection ), ctx.Un( UnaryOp.Negate, ctx.Builtin( Builtin.SunDirection ) ) ) );
ctx.Out( nameof( Result ),
ctx.Call( Intrinsic.Saturate, ctx.Call( Intrinsic.Dot, normal, light ) ) );
}
}
/// <summary>Blinn-Phong: a Lambert diffuse plus a half-vector specular lobe.</summary>
[NodeInfo( Id = "prism.lighting.blinnPhong", Title = "Blinn-Phong", Category = "Lighting/BRDF",
Icon = "flare", Keywords = new[] { "blinn", "phong", "specular", "highlight", "shading" },
Description = "The classic half-vector model. Cheap, stylised, and still the right answer for a " +
"non-photoreal look." )]
[NodeVersion( 1 )]
public sealed class BlinnPhongNode : PrismNode
{
/// <summary>The surface normal. Defaults to the interpolated normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>The direction towards the light. Defaults to the sun.</summary>
[In( "float3", Name = "Light Direction" )] public PortRef LightDirection { get; set; }
/// <summary>The direction towards the camera. Defaults to the view direction.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>How tight the highlight is.</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; } = 32f;
/// <summary>The diffuse term.</summary>
[Out( "float", Name = "Diffuse" )] public PortRef Diffuse { get; set; }
/// <summary>The specular term.</summary>
[Out( "float", Name = "Specular" )] public PortRef Specular { 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 light = ctx.Call( Intrinsic.Normalize,
ctx.In( nameof( LightDirection ), ctx.Un( UnaryOp.Negate, ctx.Builtin( Builtin.SunDirection ) ) ) );
var view = ctx.Call( Intrinsic.Normalize,
ctx.In( nameof( ViewDirection ), ctx.Builtin( Builtin.ViewDirection ) ) );
var power = ctx.In( nameof( Power ), ctx.Const( DefaultPower ) );
var diffuse = ctx.Call( Intrinsic.Saturate, ctx.Call( Intrinsic.Dot, normal, light ) );
var half = ctx.Call( Intrinsic.Normalize, ctx.Bin( BinaryOp.Add, light, view ) );
var highlight = ctx.Call( Intrinsic.Saturate, ctx.Call( Intrinsic.Dot, normal, half ) );
ctx.Out( nameof( Diffuse ), diffuse );
ctx.Out( nameof( Specular ),
ctx.Bin( BinaryOp.Mul, ctx.Call( Intrinsic.Pow, highlight, power ), diffuse ) );
}
}
/// <summary>A physically based specular lobe, for a graph building its own shading model.</summary>
[NodeInfo( Id = "prism.lighting.ggx", Title = "GGX Specular", Category = "Lighting/BRDF",
Icon = "auto_awesome", Tier = NodeTier.Advanced,
Keywords = new[] { "ggx", "brdf", "specular", "cook torrance", "pbr", "microfacet" },
Description = "Cook-Torrance with a GGX distribution, Smith-Schlick visibility and a Schlick " +
"Fresnel, already multiplied by N·L." )]
[NodeVersion( 1 )]
public sealed class GgxSpecularNode : PrismNode
{
/// <summary>The surface normal. Defaults to the interpolated normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>The direction towards the light. Defaults to the sun.</summary>
[In( "float3", Name = "Light Direction" )] public PortRef LightDirection { get; set; }
/// <summary>The direction towards the camera. Defaults to the view direction.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>Microfacet roughness.</summary>
[In( "float", Name = "Roughness" )] public PortRef Roughness { get; set; }
/// <summary>Reflectance at normal incidence. 0.04 is the dielectric default.</summary>
[In( "float3", Name = "F0" )] public PortRef F0 { get; set; }
/// <summary>The roughness used when <see cref="Roughness"/> is unconnected.</summary>
[InlineValue( nameof( Roughness ) )] public float DefaultRoughness { get; set; } = 0.5f;
/// <summary>The reflectance used when <see cref="F0"/> is unconnected.</summary>
[InlineValue( nameof( F0 ) )] public Vector3 DefaultF0 { get; set; } = new( 0.04f, 0.04f, 0.04f );
/// <summary>The specular term.</summary>
[Out( "float3", Name = "Specular" )] public PortRef Result { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var normal = ctx.In( nameof( Normal ), ctx.Builtin( Builtin.WorldNormal ) );
var light = ctx.In( nameof( LightDirection ),
ctx.Un( UnaryOp.Negate, ctx.Builtin( Builtin.SunDirection ) ) );
var view = ctx.In( nameof( ViewDirection ), ctx.Builtin( Builtin.ViewDirection ) );
var roughness = ctx.In( nameof( Roughness ), ctx.Const( DefaultRoughness ) );
var f0 = ctx.In( nameof( F0 ), ctx.Const( DefaultF0 ) );
ctx.Out( nameof( Result ),
ctx.Helper( PrismLightingHelpers.Ggx, normal, light, view, roughness, f0 ) );
}
}
// ---- post -----------------------------------------------------------------------------------------
/// <summary>Blends a colour towards the level's fog.</summary>
[NodeInfo( Id = "prism.lighting.fog", Title = "Apply Fog", Category = "Lighting",
Icon = "foggy", Keywords = new[] { "fog", "atmosphere", "haze", "depth", "volumetric" },
Description = "Runs a colour through whichever fog the level has: gradient, cube-map or " +
"volumetric. The lit surface path already does this; use it on an unlit or custom output." )]
[NodeVersion( 1 )]
public sealed class ApplyFogNode : PrismNode, IStageConstrained
{
/// <summary>The colour to fog.</summary>
[In( "float3", Name = "Color" )] public PortRef Color { get; set; }
/// <summary>The point being shaded. Defaults to the shaded point.</summary>
[In( "float3", Name = "Position" )] public PortRef WorldPosition { get; set; }
/// <summary>The fogged colour.</summary>
[Out( "float3", Name = "Color" )] 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 color = ctx.In( nameof( Color ) );
if ( !color.IsValid )
{
ctx.Out( nameof( Result ), IrValue.Invalid );
return;
}
var position = ctx.In( nameof( WorldPosition ), ctx.Builtin( Builtin.WorldPosition ) );
var screen = ctx.Builtin( Builtin.PixelPosition );
ctx.Out( nameof( Result ), ctx.Helper( PrismLightingHelpers.ApplyFog, position, screen, color ) );
}
}
/// <summary>Compresses high dynamic range down to something a display can show.</summary>
[NodeInfo( Id = "prism.lighting.tonemap", Title = "Tonemap", Category = "Lighting",
Icon = "exposure", Keywords = new[] { "tonemap", "aces", "reinhard", "filmic", "exposure", "hdr" },
Description = "Applies an exposure multiplier and a tone curve. Useful on a post-process graph, " +
"or on an unlit output that has to sit alongside tone-mapped geometry." )]
[NodeVersion( 1 )]
public sealed class TonemapNode : PrismNode
{
/// <summary>Which tone curve to apply.</summary>
public PrismTonemapMode Mode { get; set; } = PrismTonemapMode.Aces;
/// <summary>The colour to compress.</summary>
[In( "float3", Name = "Color" )] public PortRef Color { get; set; }
/// <summary>A linear multiplier applied before the curve.</summary>
[In( "float", Name = "Exposure" )] public PortRef Exposure { get; set; }
/// <summary>The exposure used when <see cref="Exposure"/> is unconnected.</summary>
[InlineValue( nameof( Exposure ) )] public float DefaultExposure { get; set; } = 1f;
/// <summary>The compressed colour.</summary>
[Out( "float3", Name = "Color" )] public PortRef Result { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var color = ctx.In( nameof( Color ) );
if ( !color.IsValid )
{
ctx.Out( nameof( Result ), IrValue.Invalid );
return;
}
var exposure = ctx.In( nameof( Exposure ), ctx.Const( DefaultExposure ) );
var exposed = ctx.Bin( BinaryOp.Mul, color, exposure );
var value = Mode switch
{
PrismTonemapMode.Engine => ctx.Helper( PrismLightingHelpers.TonemapEngine, exposed ),
PrismTonemapMode.Reinhard => ctx.Helper( PrismLightingHelpers.TonemapReinhard, exposed ),
PrismTonemapMode.Aces => ctx.Helper( PrismLightingHelpers.TonemapAces, exposed ),
PrismTonemapMode.Filmic => ctx.Helper( PrismLightingHelpers.TonemapFilmic, exposed ),
_ => exposed
};
ctx.Out( nameof( Result ), value );
}
}