Editor Prism material editor nodes for texture sampling and helpers. Declares enums, shader helper functions (HLSL bodies) and many node classes that resolve textures/samplers, emit IR calls for sampling (Sample, SampleLevel, SampleGrad, comparisons), triplanar mapping, parallax, normal decoding and resource queries (size, texel size). It builds GlobalDecls for texture and sampler fallbacks and exposes node validation and port definitions.
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;
namespace Editor.Prism.Nodes;
// ---------------------------------------------------------------------------------------------------
// Texture sampling.
//
// Every sampling node takes an explicit Texture port and an explicit Sampler port, so a graph can bind
// one texture to several samplers or one sampler to several textures. Both have a fallback: the texture
// falls back to a blackboard parameter (or, failing that, to a material slot the node declares itself),
// and the sampler falls back to a shared state built from this node's own filter and addressing
// properties. That is what makes a freshly dropped Sample Texture 2D show something immediately.
//
// Sampling with an implicit mip level needs screen-space derivatives, so those nodes declare
// StageMask.Pixel through IStageConstrained. The explicit-LOD and gradient variants are legal
// everywhere. Nothing here emits text: every body is a Prism_* HelperFunction.
// ---------------------------------------------------------------------------------------------------
/// <summary>Filtering mode of a node's fallback sampler state.</summary>
public enum PrismTextureFilter
{
/// <summary>Anisotropic filtering, the engine's default for surface maps.</summary>
Anisotropic,
/// <summary>Trilinear: bilinear within a mip, linear between mips.</summary>
Trilinear,
/// <summary>Bilinear within a single mip.</summary>
Bilinear,
/// <summary>Nearest texel, no filtering.</summary>
Point
}
/// <summary>Addressing mode of a node's fallback sampler state.</summary>
public enum PrismTextureAddress
{
/// <summary>Repeat the texture.</summary>
Wrap,
/// <summary>Clamp to the edge texel.</summary>
Clamp,
/// <summary>Mirror on every repeat.</summary>
Mirror,
/// <summary>Return the border colour outside 0..1.</summary>
Border
}
/// <summary>Which channel a gather reads.</summary>
public enum PrismGatherChannel
{
/// <summary>Red.</summary>
Red,
/// <summary>Green.</summary>
Green,
/// <summary>Blue.</summary>
Blue,
/// <summary>Alpha.</summary>
Alpha
}
/// <summary>The space a normal-producing node returns its result in.</summary>
public enum PrismNormalSpace
{
/// <summary>Tangent space, which is what the surface output's Normal input expects.</summary>
Tangent,
/// <summary>World space.</summary>
World
}
/// <summary>
/// The primitives shared by every node in this package: the small maths bodies that more than one
/// family needs.
/// <para>
/// Helper names are global, so there is exactly one canonical definition of each of these and every
/// node calls that one. A same-name, different-body collision is a hard compile error, which is why
/// this class exists at all rather than each file declaring its own <c>Prism_Hash12</c>.
/// </para>
/// </summary>
internal static class PrismCommon
{
/// <summary>Unpack a 0..1 encoded normal into a unit vector.</summary>
internal static readonly HelperFunction DecodeNormal = new( "Prism_DecodeNormal", ShaderType.Float3,
[new HelperParam( "vEncoded", ShaderType.Float3 )] )
{
Hlsl = """
float3 Prism_DecodeNormal( float3 vEncoded )
{
return normalize( 2.0f * vEncoded - 1.0f );
}
"""
};
/// <summary>Rotate a world-space vector into tangent space.</summary>
internal static readonly HelperFunction WorldToTangent = new( "Prism_Vec3WsToTs", ShaderType.Float3,
[
new HelperParam( "vVectorWs", ShaderType.Float3 ),
new HelperParam( "vNormalWs", ShaderType.Float3 ),
new HelperParam( "vTangentUWs", ShaderType.Float3 ),
new HelperParam( "vTangentVWs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_Vec3WsToTs( float3 vVectorWs, float3 vNormalWs, float3 vTangentUWs, float3 vTangentVWs )
{
float3 vVectorTs;
vVectorTs.x = dot( vVectorWs.xyz, vTangentUWs.xyz );
vVectorTs.y = dot( vVectorWs.xyz, vTangentVWs.xyz );
vVectorTs.z = dot( vVectorWs.xyz, vNormalWs.xyz );
return vVectorTs.xyz;
}
"""
};
/// <summary>Rotate a tangent-space vector into world space and normalise it.</summary>
internal static readonly HelperFunction TangentToWorld = new( "Prism_Vec3TsToWs", ShaderType.Float3,
[
new HelperParam( "vVectorTs", ShaderType.Float3 ),
new HelperParam( "vNormalWs", ShaderType.Float3 ),
new HelperParam( "vTangentUWs", ShaderType.Float3 ),
new HelperParam( "vTangentVWs", ShaderType.Float3 )
] )
{
Hlsl = """
float3 Prism_Vec3TsToWs( float3 vVectorTs, float3 vNormalWs, float3 vTangentUWs, float3 vTangentVWs )
{
return normalize( vVectorTs.x * vTangentUWs + vVectorTs.y * vTangentVWs + vVectorTs.z * vNormalWs );
}
"""
};
/// <summary>Rec.709 relative luminance of a linear colour.</summary>
internal static readonly HelperFunction Luminance = new( "Prism_Luminance", ShaderType.Float,
[new HelperParam( "vColor", ShaderType.Float3 )] )
{
Hlsl = """
float Prism_Luminance( float3 vColor )
{
return dot( vColor, float3( 0.2126729f, 0.7151522f, 0.0721750f ) );
}
"""
};
/// <summary>Hash a scalar into 0..1.</summary>
internal static readonly HelperFunction Hash11 = new( "Prism_Hash11", ShaderType.Float,
[new HelperParam( "p", ShaderType.Float )] )
{
Hlsl = """
float Prism_Hash11( float p )
{
return frac( sin( p * 127.1f ) * 43758.5453123f );
}
"""
};
/// <summary>Hash a 2D point into 0..1.</summary>
internal static readonly HelperFunction Hash12 = new( "Prism_Hash12", ShaderType.Float,
[new HelperParam( "p", ShaderType.Float2 )] )
{
Hlsl = """
float Prism_Hash12( float2 p )
{
return frac( sin( dot( p, float2( 12.9898f, 78.233f ) ) ) * 43758.5453123f );
}
"""
};
/// <summary>Hash a 3D point into 0..1.</summary>
internal static readonly HelperFunction Hash13 = new( "Prism_Hash13", ShaderType.Float,
[new HelperParam( "p", ShaderType.Float3 )] )
{
Hlsl = """
float Prism_Hash13( float3 p )
{
return frac( sin( dot( p, float3( 12.9898f, 78.233f, 37.719f ) ) ) * 43758.5453123f );
}
"""
};
/// <summary>Hash a 2D point into a 2D value in 0..1.</summary>
internal static readonly HelperFunction Hash22 = new( "Prism_Hash22", ShaderType.Float2,
[new HelperParam( "p", ShaderType.Float2 )] )
{
Hlsl = """
float2 Prism_Hash22( float2 p )
{
p = float2( dot( p, float2( 127.1f, 311.7f ) ), dot( p, float2( 269.5f, 183.3f ) ) );
return frac( sin( p ) * 43758.5453123f );
}
"""
};
/// <summary>Hash a 3D point into a 3D value in 0..1.</summary>
internal static readonly HelperFunction Hash33 = new( "Prism_Hash33", ShaderType.Float3,
[new HelperParam( "p", ShaderType.Float3 )] )
{
Hlsl = """
float3 Prism_Hash33( float3 p )
{
p = float3( dot( p, float3( 127.1f, 311.7f, 74.7f ) ),
dot( p, float3( 269.5f, 183.3f, 246.1f ) ),
dot( p, float3( 113.5f, 271.9f, 124.6f ) ) );
return frac( sin( p ) * 43758.5453123f );
}
"""
};
/// <summary>Antialias a signed distance field into a 0..1 coverage mask. Pixel stage only.</summary>
internal static readonly HelperFunction SdfMask = new( "Prism_SdfMask", ShaderType.Float,
[new HelperParam( "flDistance", ShaderType.Float )] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.PixelDerivatives],
Hlsl = """
float Prism_SdfMask( float flDistance )
{
return saturate( 0.5f - flDistance / max( fwidth( flDistance ), 1e-6f ) );
}
"""
};
/// <summary>Turn a signed distance field into a soft mask without touching derivatives.</summary>
internal static readonly HelperFunction SdfMaskSoft = new( "Prism_SdfMaskSoft", ShaderType.Float,
[
new HelperParam( "flDistance", ShaderType.Float ),
new HelperParam( "flSoftness", ShaderType.Float )
] )
{
Hlsl = """
float Prism_SdfMaskSoft( float flDistance, float flSoftness )
{
float flHalf = max( flSoftness, 1e-6f ) * 0.5f;
return 1.0f - smoothstep( -flHalf, flHalf, flDistance );
}
"""
};
}
/// <summary>The HLSL bodies the texture family needs: triplanar, parallax, POM and height-to-normal.</summary>
internal static class PrismTextureHelpers
{
/// <summary>Triplanar colour sample with an exposed tile factor and blend sharpness.</summary>
internal static readonly HelperFunction TriplanarColor = new( "Prism_TriplanarColor", ShaderType.Float4,
[
new HelperParam( "tTex", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vPosition", ShaderType.Float3 ),
new HelperParam( "vNormal", ShaderType.Float3 ),
new HelperParam( "flTile", ShaderType.Float ),
new HelperParam( "flBlend", ShaderType.Float )
] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.ImplicitLodSampling],
Hlsl = """
float4 Prism_TriplanarColor( Texture2D tTex, SamplerState sSampler, float3 vPosition, float3 vNormal, float flTile, float flBlend )
{
float3 vUv = vPosition * flTile;
float3 vWeights = pow( abs( normalize( vNormal ) ), max( flBlend, 1e-3f ) );
vWeights /= max( dot( vWeights, float3( 1.0f, 1.0f, 1.0f ) ), 1e-4f );
// step() rather than a vector ternary: HLSL 2021 and Slang only accept a scalar condition.
float3 vAxisSign = lerp( float3( 1.0f, 1.0f, 1.0f ), float3( -1.0f, -1.0f, -1.0f ), step( vNormal, 0.0f ) );
float2 uvX = float2( vUv.z * vAxisSign.x, vUv.y );
float2 uvY = float2( vUv.x * vAxisSign.y, vUv.z );
float2 uvZ = float2( vUv.x * -vAxisSign.z, vUv.y );
float4 cX = tTex.Sample( sSampler, uvX );
float4 cY = tTex.Sample( sSampler, uvY );
float4 cZ = tTex.Sample( sSampler, uvZ );
return cX * vWeights.x + cY * vWeights.y + cZ * vWeights.z;
}
"""
};
/// <summary>
/// Triplanar normal sample. Returns a world-space normal, built with the whiteout swizzle blend so
/// the three projections agree along their seams.
/// </summary>
internal static readonly HelperFunction TriplanarNormal = new( "Prism_TriplanarNormal", ShaderType.Float3,
[
new HelperParam( "tTex", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vPosition", ShaderType.Float3 ),
new HelperParam( "vNormal", ShaderType.Float3 ),
new HelperParam( "flTile", ShaderType.Float ),
new HelperParam( "flBlend", ShaderType.Float )
] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.ImplicitLodSampling],
Requires = [PrismCommon.DecodeNormal],
Hlsl = """
float3 Prism_TriplanarNormal( Texture2D tTex, SamplerState sSampler, float3 vPosition, float3 vNormal, float flTile, float flBlend )
{
float3 vUv = vPosition * flTile;
float3 vN = normalize( vNormal );
float3 vWeights = pow( abs( vN ), max( flBlend, 1e-3f ) );
vWeights /= max( dot( vWeights, float3( 1.0f, 1.0f, 1.0f ) ), 1e-4f );
float3 vAxisSign = lerp( float3( 1.0f, 1.0f, 1.0f ), float3( -1.0f, -1.0f, -1.0f ), step( vN, 0.0f ) );
float2 uvX = float2( vUv.z * vAxisSign.x, vUv.y );
float2 uvY = float2( vUv.x * vAxisSign.y, vUv.z );
float2 uvZ = float2( vUv.x * -vAxisSign.z, vUv.y );
float3 nX = Prism_DecodeNormal( tTex.Sample( sSampler, uvX ).xyz );
float3 nY = Prism_DecodeNormal( tTex.Sample( sSampler, uvY ).xyz );
float3 nZ = Prism_DecodeNormal( tTex.Sample( sSampler, uvZ ).xyz );
nX.x *= vAxisSign.x;
nY.x *= vAxisSign.y;
nZ.x *= -vAxisSign.z;
nX = float3( nX.xy + vN.zy, abs( nX.z ) * vN.x );
nY = float3( nY.xy + vN.xz, abs( nY.z ) * vN.y );
nZ = float3( nZ.xy + vN.xy, abs( nZ.z ) * vN.z );
return normalize( nX.zyx * vWeights.x + nY.xzy * vWeights.y + nZ.xyz * vWeights.z );
}
"""
};
/// <summary>Offset-limited single-step parallax. Cheap, stable, and enough for shallow detail.</summary>
internal static readonly HelperFunction ParallaxOffset = new( "Prism_ParallaxOffset", ShaderType.Float2,
[
new HelperParam( "tHeight", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vViewDirTs", ShaderType.Float3 ),
new HelperParam( "flAmplitude", ShaderType.Float ),
new HelperParam( "flLimit", ShaderType.Float )
] )
{
Hlsl = """
float2 Prism_ParallaxOffset( Texture2D tHeight, SamplerState sSampler, float2 vUv, float3 vViewDirTs, float flAmplitude, float flLimit )
{
float flHeight = tHeight.SampleLevel( sSampler, vUv, 0.0f ).r - 0.5f;
float3 V = normalize( vViewDirTs );
V.z += flLimit;
return vUv + flHeight * flAmplitude * ( V.xy / max( abs( V.z ), 1e-4f ) );
}
"""
};
/// <summary>
/// Parallax occlusion mapping with an adaptive step count, stable mip selection and linear
/// intersection refinement.
/// <para>
/// Returns <c>xy</c> = the displaced UV, <c>z</c> = the surface height that was hit, and <c>w</c> =
/// the pixel depth offset that height implies, so a graph can drive occlusion, self-shadowing and
/// depth from one node.
/// </para>
/// </summary>
internal static readonly HelperFunction ParallaxOcclusion = new( "Prism_ParallaxOcclusion", ShaderType.Float4,
[
new HelperParam( "tHeight", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vViewDirTs", ShaderType.Float3 ),
new HelperParam( "flAmplitude", ShaderType.Float ),
new HelperParam( "nMinSteps", ShaderType.Int ),
new HelperParam( "nMaxSteps", ShaderType.Int ),
new HelperParam( "flRefPlane", ShaderType.Float )
] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.PixelDerivatives, Capability.Loops],
Hlsl = """
float4 Prism_ParallaxOcclusion( Texture2D tHeight, SamplerState sSampler, float2 vUv, float3 vViewDirTs, float flAmplitude, int nMinSteps, int nMaxSteps, float flRefPlane )
{
float3 V = normalize( vViewDirTs );
// Keep the derivatives of the original UV so mip selection stays stable inside the loop.
float2 dUVdx = ddx( vUv );
float2 dUVdy = ddy( vUv );
// Head-on needs fewer samples than grazing.
float flSteps = max( lerp( (float)nMaxSteps, (float)nMinSteps, saturate( abs( V.z ) ) ), 1.0f );
float flLayerDepth = 1.0f / flSteps;
float flCurrentDepth = 0.0f;
float2 vMaxOffset = ( V.xy / max( abs( V.z ), 1e-4f ) ) * flAmplitude;
float2 vDeltaUv = vMaxOffset * flLayerDepth;
float2 vUvCurrent = vUv + vMaxOffset * flRefPlane;
float flCurrentSample = 1.0f - tHeight.SampleGrad( sSampler, vUvCurrent, dUVdx, dUVdy ).r;
[loop]
for ( int nStep = 0; nStep < nMaxSteps; nStep++ )
{
if ( flCurrentSample <= flCurrentDepth )
break;
vUvCurrent -= vDeltaUv;
flCurrentDepth += flLayerDepth;
flCurrentSample = 1.0f - tHeight.SampleGrad( sSampler, vUvCurrent, dUVdx, dUVdy ).r;
}
float2 vUvPrev = vUvCurrent + vDeltaUv;
float flPrevSample = 1.0f - tHeight.SampleGrad( sSampler, vUvPrev, dUVdx, dUVdy ).r;
float flAfter = flCurrentSample - flCurrentDepth;
float flBefore = flPrevSample - ( flCurrentDepth - flLayerDepth );
float flWeight = flAfter / ( flAfter - flBefore + 1e-6f );
float2 vUvFinal = lerp( vUvCurrent, vUvPrev, flWeight );
float flHeight = 1.0f - lerp( flCurrentDepth, flCurrentDepth - flLayerDepth, flWeight );
float flDepthOffset = ( flAmplitude - flHeight * flAmplitude ) / max( abs( V.z ), 1e-4f );
return float4( vUvFinal, flHeight, flDepthOffset );
}
"""
};
/// <summary>Soft self-shadowing along the light direction, marched over the same height field.</summary>
internal static readonly HelperFunction ParallaxSelfShadow = new( "Prism_ParallaxSelfShadow", ShaderType.Float,
[
new HelperParam( "tHeight", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "flHeight", ShaderType.Float ),
new HelperParam( "vLightDirTs", ShaderType.Float3 ),
new HelperParam( "flAmplitude", ShaderType.Float ),
new HelperParam( "nSteps", ShaderType.Int ),
new HelperParam( "flSoftness", ShaderType.Float )
] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.PixelDerivatives, Capability.Loops],
Hlsl = """
float Prism_ParallaxSelfShadow( Texture2D tHeight, SamplerState sSampler, float2 vUv, float flHeight, float3 vLightDirTs, float flAmplitude, int nSteps, float flSoftness )
{
float3 L = normalize( vLightDirTs );
if ( L.z <= 0.0f )
return 0.0f;
float2 dUVdx = ddx( vUv );
float2 dUVdy = ddy( vUv );
float flCount = max( (float)nSteps, 1.0f );
float2 vStepUv = ( L.xy / max( L.z, 1e-4f ) ) * flAmplitude / flCount;
float flStepH = ( 1.0f - flHeight ) / flCount;
float flShadow = 0.0f;
float flH = flHeight;
float2 vUvS = vUv;
[loop]
for ( int n = 1; n <= nSteps; n++ )
{
vUvS += vStepUv;
flH += flStepH;
float flSample = tHeight.SampleGrad( sSampler, vUvS, dUVdx, dUVdy ).r;
float flDiff = flSample - flH;
if ( flDiff > 0.0f )
flShadow = max( flShadow, flDiff * ( 1.0f - (float)n / flCount ) );
}
return saturate( 1.0f - flShadow * flSoftness );
}
"""
};
/// <summary>
/// Screen-space surface-gradient normal from a scalar height, in world space. The height may come
/// from anywhere in the graph, which is what makes it work on procedural detail as well as textures.
/// </summary>
internal static readonly HelperFunction NormalFromHeight = new( "Prism_NormalFromHeight", ShaderType.Float3,
[
new HelperParam( "flHeight", ShaderType.Float ),
new HelperParam( "flStrength", ShaderType.Float ),
new HelperParam( "vPositionWs", ShaderType.Float3 ),
new HelperParam( "vNormalWs", ShaderType.Float3 )
] )
{
Stages = StageMask.Pixel,
Capabilities = [Capability.PixelDerivatives],
Hlsl = """
float3 Prism_NormalFromHeight( float flHeight, float flStrength, float3 vPositionWs, float3 vNormalWs )
{
float3 vDerivativeX = ddx( vPositionWs );
float3 vDerivativeY = ddy( vPositionWs );
float3 vN = normalize( vNormalWs );
float3 vCrossX = cross( vN, vDerivativeX );
float3 vCrossY = cross( vDerivativeY, vN );
float flD = dot( vDerivativeX, vCrossY );
float flSign = flD < 0.0f ? -1.0f : 1.0f;
float flSurface = flSign / max( 1.192093e-15f, abs( flD ) );
float dHdx = ddx( flHeight );
float dHdy = ddy( flHeight );
float3 vSurfaceGradient = flSurface * ( dHdx * vCrossY + dHdy * vCrossX );
return normalize( vN - flStrength * vSurfaceGradient );
}
"""
};
/// <summary>
/// Sobel-filtered tangent-space normal from a height texture. Deterministic, derivative-free and
/// higher quality than the screen-space form, at the cost of eight taps.
/// </summary>
internal static readonly HelperFunction NormalFromHeightMap = new( "Prism_NormalFromHeightMap", ShaderType.Float3,
[
new HelperParam( "tHeight", ShaderType.Texture2D ),
new HelperParam( "sSampler", ShaderType.Sampler ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "vTexelSize", ShaderType.Float2 ),
new HelperParam( "flStrength", ShaderType.Float )
] )
{
Hlsl = """
float3 Prism_NormalFromHeightMap( Texture2D tHeight, SamplerState sSampler, float2 vUv, float2 vTexelSize, float flStrength )
{
float h00 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( -1.0f, -1.0f ), 0.0f ).r;
float h10 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( 0.0f, -1.0f ), 0.0f ).r;
float h20 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( 1.0f, -1.0f ), 0.0f ).r;
float h01 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( -1.0f, 0.0f ), 0.0f ).r;
float h21 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( 1.0f, 0.0f ), 0.0f ).r;
float h02 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( -1.0f, 1.0f ), 0.0f ).r;
float h12 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( 0.0f, 1.0f ), 0.0f ).r;
float h22 = tHeight.SampleLevel( sSampler, vUv + vTexelSize * float2( 1.0f, 1.0f ), 0.0f ).r;
float gx = ( h00 + 2.0f * h01 + h02 ) - ( h20 + 2.0f * h21 + h22 );
float gy = ( h00 + 2.0f * h10 + h20 ) - ( h02 + 2.0f * h12 + h22 );
return normalize( float3( gx * flStrength, gy * flStrength, 1.0f ) );
}
"""
};
/// <summary>
/// A depth comparison sample.
/// <para>
/// The comparison state is declared in the helper body rather than as a module global because a
/// <c>SamplerComparisonState</c> is not something the material editor can expose, and because the
/// filter and comparison function are fixed by what a shadow lookup means.
/// </para>
/// </summary>
internal static readonly HelperFunction SampleCompare = new( "Prism_SampleCompare", ShaderType.Float,
[
new HelperParam( "tTex", ShaderType.Texture2D ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "flCompare", ShaderType.Float )
] )
{
Capabilities = [Capability.ComparisonSampling],
Hlsl = """
SamplerComparisonState g_sPrismCompare < Filter( BILINEAR ); ComparisonFunc( LESSEQUAL ); AddressU( CLAMP ); AddressV( CLAMP ); >;
float Prism_SampleCompare( Texture2D tTex, float2 vUv, float flCompare )
{
return tTex.SampleCmpLevelZero( g_sPrismCompare, vUv, flCompare );
}
""",
// The VFX annotation grammar is not Slang, so the portable module declares the state bare and
// lets the host application supply the filter and comparison function.
Slang = """
SamplerComparisonState g_sPrismCompare;
float Prism_SampleCompare( Texture2D tTex, float2 vUv, float flCompare )
{
return tTex.SampleCmpLevelZero( g_sPrismCompare, vUv, flCompare );
}
"""
};
/// <summary>A 3x3 percentage-closer-filtered comparison sample, for soft shadow edges.</summary>
internal static readonly HelperFunction SampleComparePcf = new( "Prism_SampleComparePcf", ShaderType.Float,
[
new HelperParam( "tTex", ShaderType.Texture2D ),
new HelperParam( "vUv", ShaderType.Float2 ),
new HelperParam( "flCompare", ShaderType.Float ),
new HelperParam( "vTexelSize", ShaderType.Float2 )
] )
{
Capabilities = [Capability.ComparisonSampling],
Requires = [SampleCompare],
Hlsl = """
float Prism_SampleComparePcf( Texture2D tTex, float2 vUv, float flCompare, float2 vTexelSize )
{
float flSum = 0.0f;
[unroll]
for ( int y = -1; y <= 1; y++ )
{
[unroll]
for ( int x = -1; x <= 1; x++ )
{
flSum += Prism_SampleCompare( tTex, vUv + float2( x, y ) * vTexelSize, flCompare );
}
}
return flSum * ( 1.0f / 9.0f );
}
"""
};
}
/// <summary>
/// Resolves the texture and sampler a sampling node should use.
/// <para>
/// Three sources, in order: the port, when something is connected to it; the blackboard parameter the
/// node names; and finally a declaration the node makes for itself, which is what gives a freshly
/// dropped node a working material slot instead of a red error badge.
/// </para>
/// </summary>
internal static class PrismTextureBinding
{
/// <summary>The texture a node samples, or an invalid value when it cannot be resolved.</summary>
internal static IrValue Texture( EmitContext ctx, TextureNode node, string port, ObjectKind kind )
{
if ( ctx is null || node is null ) return IrValue.Invalid;
if ( ctx.TryIn( port, out var connected ) ) return connected;
var fromParameter = FromParameter( ctx, node.TextureParameter, kind );
if ( fromParameter is not null ) return ctx.Global( fromParameter );
return ctx.Global( Declare( ctx, node, kind ) );
}
/// <summary>The sampler a node samples with. Falls back to a shared state built from its properties.</summary>
internal static IrValue Sampler( EmitContext ctx, TextureNode node, string port )
{
if ( ctx is null || node is null ) return IrValue.Invalid;
if ( ctx.TryIn( port, out var connected ) ) return connected;
return ctx.Global( SamplerState( node.Filter, node.AddressU, node.AddressV ) );
}
/// <summary>The declaration a blackboard texture parameter resolves to, or null when there is none.</summary>
internal static GlobalDecl FromParameter( EmitContext ctx, ParamId id, ObjectKind kind )
{
if ( !id.IsValid || ctx?.Graph is not ICompilableGraph graph ) return null;
foreach ( var parameter in graph.Parameters ?? Array.Empty<IGraphParameter>() )
{
if ( parameter is null || parameter.Id != id ) continue;
if ( !parameter.Type.IsTexture ) return null;
var name = GraphCompiler.ParameterSymbol( parameter );
if ( string.IsNullOrEmpty( name ) ) return null;
// Mirrors what the compiler already produced for this parameter, so the module-level
// declaration is reused rather than a second one being invented under the same name.
return new GlobalDecl( name, parameter.Type, GlobalKind.Texture )
{
Parameter = parameter.Id,
AttributeName = parameter.AttributeName,
DefaultAsset = parameter.DefaultAsset,
Srgb = parameter.Srgb,
Ui = parameter.Ui
};
}
_ = kind;
return null;
}
/// <summary>The material slot a node declares for itself when nothing else supplies a texture.</summary>
internal static GlobalDecl Declare( EmitContext ctx, TextureNode node, ObjectKind kind )
{
var slot = string.IsNullOrWhiteSpace( node.TextureName )
? "PrismTex" + Model.Parameter.Sanitize( ctx?.Node?.Id.Value )
: Model.Parameter.Sanitize( node.TextureName );
var asset = string.IsNullOrWhiteSpace( node.DefaultTexture ) ? null : node.DefaultTexture;
return new GlobalDecl( "g_t" + slot, ShaderType.Obj( kind ), GlobalKind.Texture )
{
DefaultAsset = asset,
Srgb = node.Srgb,
Ui = new UiHints
{
Control = UiControl.Texture,
Group = string.IsNullOrWhiteSpace( node.TextureGroup ) ? "Textures" : node.TextureGroup
}
};
}
/// <summary>
/// A shared sampler state. Named after what it does, so two nodes asking for bilinear clamp get one
/// declaration and one sampler slot rather than two.
/// </summary>
internal static GlobalDecl SamplerState( PrismTextureFilter filter, PrismTextureAddress u, PrismTextureAddress v )
{
var name = "g_sPrism" + filter + u + ( u == v ? string.Empty : v.ToString() );
var options = new List<string>
{
$"Filter( {FilterToken( filter )} )",
$"AddressU( {AddressToken( u )} )",
$"AddressV( {AddressToken( v )} )",
$"AddressW( {AddressToken( u )} )"
};
if ( filter == PrismTextureFilter.Anisotropic ) options.Add( "MaxAniso( 8 )" );
return new GlobalDecl( name, ShaderType.Sampler, GlobalKind.Sampler )
{
Ui = new UiHints { Options = options }
};
}
static string FilterToken( PrismTextureFilter filter ) => filter switch
{
PrismTextureFilter.Trilinear => "TRILINEAR",
PrismTextureFilter.Bilinear => "BILINEAR",
PrismTextureFilter.Point => "POINT",
_ => "ANISOTROPIC"
};
static string AddressToken( PrismTextureAddress address ) => address switch
{
PrismTextureAddress.Clamp => "CLAMP",
PrismTextureAddress.Mirror => "MIRROR",
PrismTextureAddress.Border => "BORDER",
_ => "WRAP"
};
/// <summary>Read a coordinate port, falling back to the mesh's first UV set.</summary>
internal static IrValue Uv( EmitContext ctx, string port ) =>
ctx.TryIn( port, out var value ) ? value : ctx.Builtin( Builtin.TexCoord0 );
/// <summary>Publish the four scalar channels of a sampled colour.</summary>
internal static void Channels( EmitContext ctx, IrValue rgba )
{
ctx.Out( "R", ctx.Swizzle( rgba, "x" ) );
ctx.Out( "G", ctx.Swizzle( rgba, "y" ) );
ctx.Out( "B", ctx.Swizzle( rgba, "z" ) );
ctx.Out( "A", ctx.Swizzle( rgba, "w" ) );
}
}
/// <summary>
/// The shared state of every texture node: which texture it reads, and how it filters when no explicit
/// sampler is wired to it.
/// </summary>
public abstract class TextureNode : PrismNode
{
/// <summary>
/// The blackboard texture parameter this node samples when nothing is wired to its Texture port.
/// Typed <see cref="ParamId"/> so copy, paste and re-import remap it exactly.
/// </summary>
public ParamId TextureParameter { get; set; }
/// <summary>
/// The name of the material slot this node declares when it has neither a connection nor a
/// blackboard parameter. Leave it empty for a slot private to this node.
/// </summary>
public string TextureName { get; set; } = string.Empty;
/// <summary>
/// The asset a self-declared slot falls back to.
/// <remarks>
/// Deliberately <em>not</em> marked <c>[InlineValue]</c>, even though the node card edits it as if it
/// were the Texture port's literal. That attribute enrols a property in the inline machinery, and
/// <c>ResetInline</c> — which runs on every load and every undo restore — puts every inline property
/// back to its prototype value. A path read from the document would be wiped to the stock white
/// texture microseconds later. The card reaches it through
/// <c>InlineEditorFactory.AssetProperty</c> instead, which reads the property directly.
/// </remarks>
/// </summary>
public string DefaultTexture { get; set; } = "materials/dev/white_color.tga";
/// <summary>Heading the self-declared slot appears under in the material editor.</summary>
public string TextureGroup { get; set; } = "Textures";
/// <summary>True when a self-declared slot holds sRGB-encoded colour rather than linear data.</summary>
public bool Srgb { get; set; } = true;
/// <summary>Filtering of the fallback sampler.</summary>
public PrismTextureFilter Filter { get; set; } = PrismTextureFilter.Anisotropic;
/// <summary>Horizontal addressing of the fallback sampler.</summary>
public PrismTextureAddress AddressU { get; set; } = PrismTextureAddress.Wrap;
/// <summary>Vertical addressing of the fallback sampler.</summary>
public PrismTextureAddress AddressV { get; set; } = PrismTextureAddress.Wrap;
/// <summary>The resource kind this node samples. Drives the declaration it makes for itself.</summary>
protected virtual ObjectKind Kind => ObjectKind.Texture2D;
/// <summary>Resolve the texture: the port, then the blackboard parameter, then a slot of our own.</summary>
protected IrValue ResolveTexture( EmitContext ctx, string port = "Texture" ) =>
PrismTextureBinding.Texture( ctx, this, port, Kind );
/// <summary>Resolve the sampler: the port, then a shared state built from this node's properties.</summary>
protected IrValue ResolveSampler( EmitContext ctx, string port = "Sampler" ) =>
PrismTextureBinding.Sampler( ctx, this, port );
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
if ( ctx is null ) return;
if ( TextureParameter.IsValid || ctx.IsConnected( "Texture" ) ) return;
if ( !string.IsNullOrWhiteSpace( DefaultTexture ) ) return;
ctx.Warn( "No texture: wire the Texture port, pick a blackboard parameter, or set a default asset.",
"Texture" );
}
/// <summary>
/// Point out a mip level that is wired but switched off. Silently ignoring a connection is the kind
/// of thing that costs an hour, so the node says so instead.
/// </summary>
protected void ValidateExplicitLod( ValidationContext ctx, bool explicitLod )
{
if ( ctx is null || explicitLod ) return;
if ( !ctx.IsConnected( "Lod" ) ) return;
ctx.Info( "The LOD input is ignored until Explicit LOD is switched on.", "Lod" );
}
}
// ---------------------------------------------------------------------------------------------------
// 2D sampling
// ---------------------------------------------------------------------------------------------------
/// <summary>
/// Samples a 2D texture with the mip level the hardware picks from screen-space derivatives.
/// </summary>
[NodeInfo( Id = "prism.texture.sample2d", Title = "Sample Texture 2D", Category = "Texture/Sample",
Icon = "image", Keywords = new[] { "tex2d", "sample", "texture", "map" },
Description = "Samples a 2D texture at the mip level the hardware chooses. Pixel stage only." )]
[NodeVersion( 1 )]
public sealed class SampleTexture2DNode : TextureNode, IStageConstrained
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it. Defaults to the mesh's first UV set.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Filtering state. Defaults to this node's own filter and address settings.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <summary>Red channel.</summary>
[Out( "float", Name = "R" )] public PortRef R { get; set; }
/// <summary>Green channel.</summary>
[Out( "float", Name = "G" )] public PortRef G { get; set; }
/// <summary>Blue channel.</summary>
[Out( "float", Name = "B" )] public PortRef B { get; set; }
/// <summary>Alpha channel.</summary>
[Out( "float", Name = "A" )] public PortRef A { get; set; }
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var texture = ResolveTexture( ctx );
var sampler = ResolveSampler( ctx );
var uv = PrismTextureBinding.Uv( ctx, nameof( UV ) );
var rgba = ctx.Call( Intrinsic.Sample, texture, sampler, uv );
ctx.Out( nameof( RGBA ), rgba );
PrismTextureBinding.Channels( ctx, rgba );
}
}
/// <summary>Samples a 2D texture at an explicit mip level. Legal in every stage.</summary>
[NodeInfo( Id = "prism.texture.sample2dLod", Title = "Sample Texture 2D LOD", Category = "Texture/Sample",
Icon = "layers", Keywords = new[] { "tex2dlod", "samplelevel", "mip" },
Description = "Samples a 2D texture at a mip level you choose. Works in the vertex stage." )]
[NodeVersion( 1 )]
public sealed class SampleTexture2DLodNode : TextureNode
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The mip level to read.</summary>
[In( "float", Name = "LOD" )] public PortRef Lod { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The mip level used when nothing is connected.</summary>
[InlineValue( nameof( Lod ) )] public float DefaultLod { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <summary>Red channel.</summary>
[Out( "float", Name = "R" )] public PortRef R { get; set; }
/// <summary>Green channel.</summary>
[Out( "float", Name = "G" )] public PortRef G { get; set; }
/// <summary>Blue channel.</summary>
[Out( "float", Name = "B" )] public PortRef B { get; set; }
/// <summary>Alpha channel.</summary>
[Out( "float", Name = "A" )] public PortRef A { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var rgba = ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ), ctx.In( nameof( Lod ), ctx.Const( DefaultLod ) ) );
ctx.Out( nameof( RGBA ), rgba );
PrismTextureBinding.Channels( ctx, rgba );
}
}
/// <summary>Samples a 2D texture with a bias applied to the mip level the hardware chooses.</summary>
[NodeInfo( Id = "prism.texture.sample2dBias", Title = "Sample Texture 2D Bias", Category = "Texture/Sample",
Icon = "exposure", Keywords = new[] { "samplebias", "mip", "sharpen", "blur" },
Description = "Samples a 2D texture, nudging the automatic mip level up or down. Pixel stage only." )]
[NodeVersion( 1 )]
public sealed class SampleTexture2DBiasNode : TextureNode, IStageConstrained
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How far to push the chosen mip level. Negative sharpens, positive blurs.</summary>
[In( "float", Name = "Bias" )] public PortRef Bias { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The bias used when nothing is connected.</summary>
[InlineValue( nameof( Bias ) )] public float DefaultBias { get; set; } = -0.5f;
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( RGBA ), ctx.Call( Intrinsic.SampleBias, ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ), ctx.In( nameof( Bias ), ctx.Const( DefaultBias ) ) ) );
}
}
/// <summary>Samples a 2D texture with the UV gradients you supply, so filtering stays correct across a discontinuity.</summary>
[NodeInfo( Id = "prism.texture.sample2dGrad", Title = "Sample Texture 2D Grad", Category = "Texture/Sample",
Icon = "gradient", Keywords = new[] { "samplegrad", "ddx", "ddy", "anisotropic" },
Description = "Samples a 2D texture using explicit UV derivatives. Fixes seams on wrapped or warped coordinates." )]
[NodeVersion( 1 )]
public sealed class SampleTexture2DGradNode : TextureNode
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The horizontal UV gradient.</summary>
[In( "float2", Name = "DDX" )] public PortRef Ddx { get; set; }
/// <summary>The vertical UV gradient.</summary>
[In( "float2", Name = "DDY" )] public PortRef Ddy { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var uv = PrismTextureBinding.Uv( ctx, nameof( UV ) );
var zero = ctx.Const( new Vector2( 0f, 0f ) );
ctx.Out( nameof( RGBA ), ctx.Call( Intrinsic.SampleGrad, ResolveTexture( ctx ), ResolveSampler( ctx ),
uv, ctx.In( nameof( Ddx ), zero ), ctx.In( nameof( Ddy ), zero ) ) );
}
}
/// <summary>Samples one slice of a 2D texture array.</summary>
[NodeInfo( Id = "prism.texture.sample2dArray", Title = "Sample Texture 2D Array", Category = "Texture/Sample",
Icon = "burst_mode", Keywords = new[] { "array", "slice", "atlas" },
Description = "Samples one slice of a texture array. Pixel stage only unless you supply a mip level." )]
[NodeVersion( 1 )]
public sealed class SampleTexture2DArrayNode : TextureNode, IStageConstrained
{
/// <summary>The array to read.</summary>
[In( "Texture2DArray", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Which slice to read.</summary>
[In( "float", Name = "Index" )] public PortRef Index { get; set; }
/// <summary>An explicit mip level. Connect it to make this node legal in the vertex stage.</summary>
[In( "float", Name = "LOD" )] public PortRef Lod { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The slice used when nothing is connected.</summary>
[InlineValue( nameof( Index ) )] public float DefaultIndex { get; set; }
/// <summary>True when the mip level is chosen explicitly rather than by the hardware.</summary>
public bool ExplicitLod { get; set; }
/// <summary>The mip level used when <see cref="ExplicitLod"/> is on and nothing is connected.</summary>
[InlineValue( nameof( Lod ) )] public float DefaultLod { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <inheritdoc/>
protected override ObjectKind Kind => ObjectKind.Texture2DArray;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
base.OnValidate( ctx );
ValidateExplicitLod( ctx, ExplicitLod );
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => ExplicitLod ? StageMask.All : StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var uv = PrismTextureBinding.Uv( ctx, nameof( UV ) );
var index = ctx.In( nameof( Index ), ctx.Const( DefaultIndex ) );
var coord = ctx.Construct( ShaderType.Float3, uv, index );
var rgba = ExplicitLod
? ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ), coord,
ctx.In( nameof( Lod ), ctx.Const( DefaultLod ) ) )
: ctx.Call( Intrinsic.Sample, ResolveTexture( ctx ), ResolveSampler( ctx ), coord );
ctx.Out( nameof( RGBA ), rgba );
}
}
/// <summary>Samples a volume texture.</summary>
[NodeInfo( Id = "prism.texture.sample3d", Title = "Sample Texture 3D", Category = "Texture/Sample",
Icon = "view_in_ar", Keywords = new[] { "volume", "3d", "lut" },
Description = "Samples a volume texture at a 3D coordinate." )]
[NodeVersion( 1 )]
public sealed class SampleTexture3DNode : TextureNode, IStageConstrained
{
/// <summary>The volume to read.</summary>
[In( "Texture3D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float3", Name = "UVW" )] public PortRef Uvw { get; set; }
/// <summary>An explicit mip level, used when <see cref="ExplicitLod"/> is on.</summary>
[In( "float", Name = "LOD" )] public PortRef Lod { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>True when the mip level is chosen explicitly rather than by the hardware.</summary>
public bool ExplicitLod { get; set; }
/// <summary>The mip level used when <see cref="ExplicitLod"/> is on and nothing is connected.</summary>
[InlineValue( nameof( Lod ) )] public float DefaultLod { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <inheritdoc/>
protected override ObjectKind Kind => ObjectKind.Texture3D;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
base.OnValidate( ctx );
ValidateExplicitLod( ctx, ExplicitLod );
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => ExplicitLod ? StageMask.All : StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var coord = ctx.In( nameof( Uvw ), ctx.Const( new Vector3( 0.5f, 0.5f, 0.5f ) ) );
var rgba = ExplicitLod
? ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ), coord,
ctx.In( nameof( Lod ), ctx.Const( DefaultLod ) ) )
: ctx.Call( Intrinsic.Sample, ResolveTexture( ctx ), ResolveSampler( ctx ), coord );
ctx.Out( nameof( RGBA ), rgba );
}
}
/// <summary>Samples a cube map along a direction.</summary>
[NodeInfo( Id = "prism.texture.sampleCube", Title = "Sample Cubemap", Category = "Texture/Sample",
Icon = "panorama_photosphere", Keywords = new[] { "cube", "envmap", "reflection", "skybox" },
Description = "Samples a cube map along a direction vector." )]
[NodeVersion( 1 )]
public sealed class SampleCubemapNode : TextureNode, IStageConstrained
{
/// <summary>The cube map to read.</summary>
[In( "TextureCube", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>The direction to read along. Defaults to the world normal.</summary>
[In( "float3", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>An explicit mip level, used when <see cref="ExplicitLod"/> is on.</summary>
[In( "float", Name = "LOD" )] public PortRef Lod { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>True when the mip level is chosen explicitly, as a roughness-driven blur usually is.</summary>
public bool ExplicitLod { get; set; }
/// <summary>The mip level used when <see cref="ExplicitLod"/> is on and nothing is connected.</summary>
[InlineValue( nameof( Lod ) )] public float DefaultLod { get; set; }
/// <summary>The sampled colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <summary>Red channel.</summary>
[Out( "float", Name = "R" )] public PortRef R { get; set; }
/// <summary>Green channel.</summary>
[Out( "float", Name = "G" )] public PortRef G { get; set; }
/// <summary>Blue channel.</summary>
[Out( "float", Name = "B" )] public PortRef B { get; set; }
/// <summary>Alpha channel.</summary>
[Out( "float", Name = "A" )] public PortRef A { get; set; }
/// <inheritdoc/>
protected override ObjectKind Kind => ObjectKind.TextureCube;
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => ExplicitLod ? StageMask.All : StageMask.Pixel;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
base.OnValidate( ctx );
ValidateExplicitLod( ctx, ExplicitLod );
}
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var direction = ctx.TryIn( nameof( UV ), out var wired ) ? wired : ctx.Builtin( Builtin.WorldNormal );
var rgba = ExplicitLod
? ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ), direction,
ctx.In( nameof( Lod ), ctx.Const( DefaultLod ) ) )
: ctx.Call( Intrinsic.Sample, ResolveTexture( ctx ), ResolveSampler( ctx ), direction );
ctx.Out( nameof( RGBA ), rgba );
PrismTextureBinding.Channels( ctx, rgba );
}
}
/// <summary>Samples a cube map along the direction the view reflects off a surface.</summary>
[NodeInfo( Id = "prism.texture.sampleCubeReflected", Title = "Sample Reflected Cubemap", Category = "Texture/Sample",
Icon = "flip", Keywords = new[] { "reflection", "envmap", "specular", "mirror" },
Description = "Reflects the view direction about a normal and samples a cube map along it." )]
[NodeVersion( 1 )]
public sealed class SampleReflectedCubemapNode : TextureNode, IStageConstrained
{
/// <summary>The cube map to read.</summary>
[In( "TextureCube", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>The surface normal. Defaults to the world normal.</summary>
[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <summary>The direction from the surface to the eye. Defaults to the view direction.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>Mip level, usually driven by roughness.</summary>
[In( "float", Name = "LOD" )] public PortRef Lod { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The mip level used when nothing is connected.</summary>
[InlineValue( nameof( Lod ) )] public float DefaultLod { get; set; }
/// <summary>The reflected colour.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <summary>The reflection vector, for feeding other lookups.</summary>
[Out( "float3", Name = "Reflection" )] public PortRef Reflection { get; set; }
/// <inheritdoc/>
protected override ObjectKind Kind => ObjectKind.TextureCube;
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.All;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var normal = ctx.TryIn( nameof( Normal ), out var n ) ? n : ctx.Builtin( Builtin.WorldNormal );
var view = ctx.TryIn( nameof( ViewDirection ), out var v ) ? v : ctx.Builtin( Builtin.ViewDirection );
var incident = ctx.Un( UnaryOp.Negate, ctx.Call( Intrinsic.Normalize, view ) );
var reflection = ctx.Call( Intrinsic.Reflect, incident, ctx.Call( Intrinsic.Normalize, normal ) );
ctx.Out( nameof( Reflection ), reflection );
ctx.Out( nameof( RGBA ), ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ),
reflection, ctx.In( nameof( Lod ), ctx.Const( DefaultLod ) ) ) );
}
}
/// <summary>Samples a tangent-space normal map and unpacks it into a unit vector.</summary>
[NodeInfo( Id = "prism.texture.sampleNormal", Title = "Sample Normal Map", Category = "Texture/Sample",
Icon = "texture", Keywords = new[] { "normal", "bump", "tangent", "decode" },
Description = "Samples a normal map and unpacks it from 0..1 into a signed unit vector." )]
[NodeVersion( 1 )]
public sealed class SampleNormalMapNode : TextureNode, IStageConstrained
{
/// <summary>The normal map to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How far to push the perturbation. One leaves the map untouched.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 1f;
/// <summary>The tangent-space normal.</summary>
[Out( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <inheritdoc/>
public SampleNormalMapNode()
{
Srgb = false;
DefaultTexture = "materials/default/default_normal.tga";
TextureGroup = "Normal";
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var rgba = ctx.Call( Intrinsic.Sample, ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ) );
var decoded = ctx.Helper( PrismCommon.DecodeNormal, ctx.Swizzle( rgba, "xyz" ) );
var strength = ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) );
ctx.Out( nameof( Normal ), ctx.Helper( PrismNormalHelpers.NormalStrength, decoded, strength ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Triplanar
// ---------------------------------------------------------------------------------------------------
/// <summary>Projects a texture down all three world axes and blends by the surface normal.</summary>
[NodeInfo( Id = "prism.texture.triplanar", Title = "Triplanar", Category = "Texture/Triplanar",
Icon = "photo_library", Keywords = new[] { "triplanar", "world", "projection", "terrain" },
Description = "Samples a texture three times along the world axes and blends the result by the normal. No UVs needed." )]
[NodeVersion( 1 )]
public sealed class TriplanarNode : TextureNode, IStageConstrained
{
/// <summary>The tile factor used when nothing is connected.</summary>
public float DefaultTile { get; set; } = 1f;
/// <summary>The blend sharpness used when nothing is connected.</summary>
public float DefaultBlend { get; set; } = 4f;
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <summary>
/// Ports are declared here rather than through <c>[In]</c> properties because one of them is called
/// <c>Position</c>, which is also the name of every node's canvas position. Declaring the set by hand
/// keeps the port id the importer expects without shadowing the base member.
/// </summary>
protected override void OnDefinePorts( PortBuilder b )
{
base.OnDefinePorts( b );
PrismTriplanarSupport.DefinePorts( b, nameof( DefaultTile ), nameof( DefaultBlend ) );
b?.Output( "RGBA", "float4", "RGBA" );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( "RGBA", ctx.Helper( PrismTextureHelpers.TriplanarColor,
ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTriplanarSupport.Position( ctx, "Position" ),
ctx.TryIn( "Normal", out var n ) ? n : ctx.Builtin( Builtin.WorldNormal ),
ctx.In( "Tile", ctx.Const( DefaultTile ) ),
ctx.In( "Blend", ctx.Const( DefaultBlend ) ) ) );
}
}
/// <summary>Projects a normal map down all three world axes and blends by the surface normal.</summary>
[NodeInfo( Id = "prism.texture.triplanarNormal", Title = "Triplanar Normal", Category = "Texture/Triplanar",
Icon = "texture", Keywords = new[] { "triplanar", "normal", "world", "projection" },
Description = "Triplanar normal mapping. Outputs tangent space by default, so it drops straight into the surface output." )]
[NodeVersion( 1 )]
public sealed class TriplanarNormalNode : TextureNode, IStageConstrained
{
/// <summary>The tile factor used when nothing is connected.</summary>
public float DefaultTile { get; set; } = 1f;
/// <summary>The blend sharpness used when nothing is connected.</summary>
public float DefaultBlend { get; set; } = 4f;
/// <summary>Which space to return the normal in.</summary>
public PrismNormalSpace OutputSpace { get; set; } = PrismNormalSpace.Tangent;
/// <inheritdoc/>
public TriplanarNormalNode()
{
Srgb = false;
DefaultTexture = "materials/default/default_normal.tga";
TextureGroup = "Normal";
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
protected override void OnDefinePorts( PortBuilder b )
{
base.OnDefinePorts( b );
PrismTriplanarSupport.DefinePorts( b, nameof( DefaultTile ), nameof( DefaultBlend ) );
b?.Output( "XYZ", "float3", "XYZ" );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var world = ctx.Helper( PrismTextureHelpers.TriplanarNormal,
ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTriplanarSupport.Position( ctx, "Position" ),
ctx.TryIn( "Normal", out var n ) ? n : ctx.Builtin( Builtin.WorldNormal ),
ctx.In( "Tile", ctx.Const( DefaultTile ) ),
ctx.In( "Blend", ctx.Const( DefaultBlend ) ) );
if ( OutputSpace == PrismNormalSpace.World )
{
ctx.Out( "XYZ", world );
return;
}
ctx.Out( "XYZ", ctx.Helper( PrismCommon.WorldToTangent, world,
ctx.Builtin( Builtin.WorldNormal ), ctx.Builtin( Builtin.WorldTangentU ),
ctx.Builtin( Builtin.WorldTangentV ) ) );
}
}
/// <summary>Shared plumbing for the triplanar family.</summary>
internal static class PrismTriplanarSupport
{
/// <summary>Inches per metre. Source works in inches, so triplanar tiling needs converting or it looks microscopic.</summary>
internal const float InchesPerMetre = 39.3701f;
/// <summary>The input ports both triplanar nodes share, in socket order.</summary>
internal static void DefinePorts( PortBuilder b, string tileProperty, string blendProperty )
{
if ( b is null ) return;
b.Input( "Texture", "Texture2D", "Texture" );
b.Input( "Position", "float3", "Position" );
b.Input( "Normal", "float3", "Normal" );
b.Add( new PortDef( PortId.Parse( "Tile" ), "Tile", "float", PortDirection.Input )
{
InlineValueProperty = tileProperty
} );
b.Add( new PortDef( PortId.Parse( "Blend" ), "Blend", "float", PortDirection.Input )
{
InlineValueProperty = blendProperty
} );
b.Input( "Sampler", "SamplerState", "Sampler" );
}
/// <summary>The projection position: the port when wired, otherwise the world position in metres.</summary>
internal static IrValue Position( EmitContext ctx, string port )
{
if ( ctx.TryIn( port, out var wired ) ) return wired;
return ctx.Bin( BinaryOp.Div, ctx.Builtin( Builtin.WorldPosition ), ctx.Const( InchesPerMetre ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Dimensions
// ---------------------------------------------------------------------------------------------------
/// <summary>Reports the pixel dimensions of a texture.</summary>
[NodeInfo( Id = "prism.texture.size", Title = "Texture Size", Category = "Texture/Resource",
Icon = "straighten", Keywords = new[] { "dimensions", "width", "height", "resolution" },
Description = "The width and height of a texture, in texels." )]
[NodeVersion( 1 )]
public sealed class TextureSizeNode : TextureNode
{
/// <summary>The texture to measure.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Width and height together.</summary>
[Out( "float2", Name = "Size" )] public PortRef Size { get; set; }
/// <summary>Width in texels.</summary>
[Out( "float", Name = "Width" )] public PortRef Width { get; set; }
/// <summary>Height in texels.</summary>
[Out( "float", Name = "Height" )] public PortRef Height { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var size = ctx.Call( Intrinsic.TextureSize, ResolveTexture( ctx ) );
ctx.Out( nameof( Size ), size );
ctx.Out( nameof( Width ), ctx.Swizzle( size, "x" ) );
ctx.Out( nameof( Height ), ctx.Swizzle( size, "y" ) );
}
}
/// <summary>Reports the size of one texel of a texture in UV space.</summary>
[NodeInfo( Id = "prism.texture.texelSize", Title = "Texel Size", Category = "Texture/Resource",
Icon = "grid_4x4", Keywords = new[] { "texel", "pixel", "offset", "blur" },
Description = "One over the texture's dimensions: the UV distance between neighbouring texels." )]
[NodeVersion( 1 )]
public sealed class TexelSizeNode : TextureNode
{
/// <summary>The texture to measure.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>The UV size of one texel.</summary>
[Out( "float2", Name = "Texel Size" )] public PortRef TexelSize { get; set; }
/// <summary>The UV width of one texel.</summary>
[Out( "float", Name = "Texel Width" )] public PortRef TexelWidth { get; set; }
/// <summary>The UV height of one texel.</summary>
[Out( "float", Name = "Texel Height" )] public PortRef TexelHeight { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var size = ctx.Call( Intrinsic.TextureSize, ResolveTexture( ctx ) );
var texel = ctx.Bin( BinaryOp.Div, ctx.Const( new Vector2( 1f, 1f ) ), size );
ctx.Out( nameof( TexelSize ), texel );
ctx.Out( nameof( TexelWidth ), ctx.Swizzle( texel, "x" ) );
ctx.Out( nameof( TexelHeight ), ctx.Swizzle( texel, "y" ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Parallax
// ---------------------------------------------------------------------------------------------------
/// <summary>Offsets UVs by a height map read, in one step. The cheap parallax.</summary>
[NodeInfo( Id = "prism.texture.parallaxOffset", Title = "Parallax Mapping", Category = "Texture/Parallax",
Icon = "layers", Keywords = new[] { "parallax", "bumpoffset", "height", "depth" },
Description = "Shifts UVs along the tangent-space view direction by a height map read. One tap, offset-limited." )]
[NodeVersion( 1 )]
public sealed class ParallaxMappingNode : TextureNode
{
/// <summary>The height map. Height is read from the red channel.</summary>
[In( "Texture2D", Name = "Height Map" )] public PortRef Texture { get; set; }
/// <summary>The UVs to shift.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The direction from the surface towards the eye, in tangent space.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>How deep the displacement reads, in UV units.</summary>
[In( "float", Name = "Amplitude" )] public PortRef Amplitude { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The amplitude used when nothing is connected.</summary>
[InlineValue( nameof( Amplitude ) )] public float DefaultAmplitude { get; set; } = 0.05f;
/// <summary>
/// Offset limiting. Adding a little to the tangent-space view Z tames the swim at grazing angles;
/// zero gives the strict, unlimited form.
/// </summary>
public float OffsetLimit { get; set; } = 0.42f;
/// <summary>The shifted UVs.</summary>
[Out( "float2", Name = "UV" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public ParallaxMappingNode()
{
Srgb = false;
DefaultTexture = "materials/dev/white_color.tga";
TextureGroup = "Height";
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Out ), ctx.Helper( PrismTextureHelpers.ParallaxOffset,
ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ),
ctx.In( nameof( ViewDirection ), ctx.Const( new Vector3( 0f, 0f, 1f ) ) ),
ctx.In( nameof( Amplitude ), ctx.Const( DefaultAmplitude ) ),
ctx.Const( OffsetLimit ) ) );
}
}
/// <summary>Ray-marches a height map to produce properly occluded UVs, a hit height and a depth offset.</summary>
[NodeInfo( Id = "prism.texture.parallaxOcclusion", Title = "Parallax Occlusion Mapping", Category = "Texture/Parallax",
Icon = "terrain", Keywords = new[] { "pom", "parallax", "occlusion", "displacement", "height" },
Description = "Ray-marches a height field along the tangent-space view direction, with adaptive stepping and linear refinement." )]
[NodeVersion( 1 )]
public sealed class ParallaxOcclusionNode : TextureNode, IStageConstrained
{
/// <summary>The height map. Height is read from the red channel; one is the highest point.</summary>
[In( "Texture2D", Name = "Height Map" )] public PortRef Texture { get; set; }
/// <summary>The UVs to displace.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The direction from the surface towards the eye, in tangent space.</summary>
[In( "float3", Name = "View Direction" )] public PortRef ViewDirection { get; set; }
/// <summary>How deep the displacement reads, in UV units.</summary>
[In( "float", Name = "Amplitude" )] public PortRef Amplitude { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The amplitude used when nothing is connected.</summary>
[InlineValue( nameof( Amplitude ) )] public float DefaultAmplitude { get; set; } = 0.05f;
/// <summary>Steps taken when looking straight down at the surface.</summary>
public int MinSteps { get; set; } = 8;
/// <summary>Steps taken at grazing angles, where the ray travels furthest.</summary>
public int MaxSteps { get; set; } = 32;
/// <summary>Where the reference plane sits: 0 pushes in, 1 pops out, 0.5 centres the displacement.</summary>
public float ReferencePlane { get; set; }
/// <summary>The displaced UVs.</summary>
[Out( "float2", Name = "UV" )] public PortRef Out { get; set; }
/// <summary>The height of the surface the ray hit.</summary>
[Out( "float", Name = "Height" )] public PortRef Height { get; set; }
/// <summary>The depth offset the hit implies, for driving a pixel depth offset.</summary>
[Out( "float", Name = "Depth Offset" )] public PortRef DepthOffset { get; set; }
/// <inheritdoc/>
public ParallaxOcclusionNode()
{
Srgb = false;
DefaultTexture = "materials/dev/white_color.tga";
TextureGroup = "Height";
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void OnValidate( ValidationContext ctx )
{
base.OnValidate( ctx );
if ( ctx is null ) return;
if ( MaxSteps < MinSteps )
{
ctx.Warn( "Max Steps is below Min Steps, so the march never adapts.", null );
}
if ( !ctx.IsConnected( "ViewDirection" ) )
{
ctx.Info( "Without a tangent-space view direction the march runs straight down and nothing shifts.",
"ViewDirection" );
}
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var packed = ctx.Helper( PrismTextureHelpers.ParallaxOcclusion,
ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ),
ctx.In( nameof( ViewDirection ), ctx.Const( new Vector3( 0f, 0f, 1f ) ) ),
ctx.In( nameof( Amplitude ), ctx.Const( DefaultAmplitude ) ),
ctx.Const( Math.Max( 1, MinSteps ) ),
ctx.Const( Math.Max( 1, Math.Max( MinSteps, MaxSteps ) ) ),
ctx.Const( ReferencePlane ) );
ctx.Out( nameof( Out ), ctx.Swizzle( packed, "xy" ) );
ctx.Out( nameof( Height ), ctx.Swizzle( packed, "z" ) );
ctx.Out( nameof( DepthOffset ), ctx.Swizzle( packed, "w" ) );
}
}
/// <summary>Marches a height map towards the light to shadow the valleys a parallax surface carves.</summary>
[NodeInfo( Id = "prism.texture.parallaxShadow", Title = "Parallax Self Shadow", Category = "Texture/Parallax",
Icon = "wb_shade", Keywords = new[] { "parallax", "shadow", "self", "occlusion", "pom" },
Description = "Soft self-shadowing for a parallax-occluded surface. Feed it the UV and height that POM returned." )]
[NodeVersion( 1 )]
public sealed class ParallaxSelfShadowNode : TextureNode, IStageConstrained
{
/// <summary>The same height map the displacement used.</summary>
[In( "Texture2D", Name = "Height Map" )] public PortRef Texture { get; set; }
/// <summary>The displaced UVs.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The height the displacement hit.</summary>
[In( "float", Name = "Height" )] public PortRef Height { get; set; }
/// <summary>The direction from the surface towards the light, in tangent space.</summary>
[In( "float3", Name = "Light Direction" )] public PortRef LightDirection { get; set; }
/// <summary>The amplitude the displacement used.</summary>
[In( "float", Name = "Amplitude" )] public PortRef Amplitude { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The height used when nothing is connected.</summary>
[InlineValue( nameof( Height ) )] public float DefaultHeight { get; set; } = 1f;
/// <summary>The amplitude used when nothing is connected.</summary>
[InlineValue( nameof( Amplitude ) )] public float DefaultAmplitude { get; set; } = 0.05f;
/// <summary>How many taps the shadow march takes.</summary>
public int Steps { get; set; } = 16;
/// <summary>How hard the shadow edge is. Higher is darker and harder.</summary>
public float Softness { get; set; } = 8f;
/// <summary>The shadow term: one is lit, zero is fully occluded.</summary>
[Out( "float", Name = "Shadow" )] public PortRef Shadow { get; set; }
/// <inheritdoc/>
public ParallaxSelfShadowNode()
{
Srgb = false;
DefaultTexture = "materials/dev/white_color.tga";
TextureGroup = "Height";
}
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Shadow ), ctx.Helper( PrismTextureHelpers.ParallaxSelfShadow,
ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ),
ctx.In( nameof( Height ), ctx.Const( DefaultHeight ) ),
ctx.In( nameof( LightDirection ), ctx.Const( new Vector3( 0f, 0f, 1f ) ) ),
ctx.In( nameof( Amplitude ), ctx.Const( DefaultAmplitude ) ),
ctx.Const( Math.Max( 1, Steps ) ),
ctx.Const( Softness ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Height to normal
// ---------------------------------------------------------------------------------------------------
/// <summary>Turns any scalar height into a normal, using screen-space derivatives.</summary>
[NodeInfo( Id = "prism.texture.heightToNormal", Title = "Normal From Height", Category = "Texture/Normal",
Icon = "terrain", Keywords = new[] { "bump", "height", "normal", "derivative", "surface gradient" },
Description = "Builds a normal from a scalar height using the surface gradient. Works on procedural detail, not just textures." )]
[NodeVersion( 1 )]
public sealed class NormalFromHeightNode : PrismNode, IStageConstrained
{
/// <summary>The height to differentiate.</summary>
[In( "float", Name = "Height" )] public PortRef Height { get; set; }
/// <summary>How far the normal tilts.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>The height used when nothing is connected.</summary>
[InlineValue( nameof( Height ) )] public float DefaultHeight { get; set; }
/// <summary>The strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 0.01f;
/// <summary>Which space to return the normal in.</summary>
public PrismNormalSpace OutputSpace { get; set; } = PrismNormalSpace.Tangent;
/// <summary>The reconstructed normal.</summary>
[Out( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <inheritdoc/>
StageMask IStageConstrained.RequiredStages => StageMask.Pixel;
/// <inheritdoc/>
ShaderStage IStageConstrained.PreferredStage => ShaderStage.None;
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var world = ctx.Helper( PrismTextureHelpers.NormalFromHeight,
ctx.In( nameof( Height ), ctx.Const( DefaultHeight ) ),
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ),
ctx.Builtin( Builtin.WorldPosition ),
ctx.Builtin( Builtin.WorldNormal ) );
if ( OutputSpace == PrismNormalSpace.World )
{
ctx.Out( nameof( Normal ), world );
return;
}
ctx.Out( nameof( Normal ), ctx.Helper( PrismCommon.WorldToTangent, world,
ctx.Builtin( Builtin.WorldNormal ), ctx.Builtin( Builtin.WorldTangentU ),
ctx.Builtin( Builtin.WorldTangentV ) ) );
}
}
/// <summary>Turns a height texture into a tangent-space normal with a Sobel filter.</summary>
[NodeInfo( Id = "prism.texture.heightMapToNormal", Title = "Normal From Height Map", Category = "Texture/Normal",
Icon = "filter_hdr", Keywords = new[] { "sobel", "bump", "height", "normal", "heightmap" },
Description = "Eight-tap Sobel normal reconstruction from a height texture. Deterministic and derivative-free." )]
[NodeVersion( 1 )]
public sealed class NormalFromHeightMapNode : TextureNode
{
/// <summary>The height map. Height is read from the red channel.</summary>
[In( "Texture2D", Name = "Height Map" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>How far apart the taps sit. Defaults to the texture's own texel size.</summary>
[In( "float2", Name = "Texel Size" )] public PortRef TexelSize { get; set; }
/// <summary>How far the normal tilts.</summary>
[In( "float", Name = "Strength" )] public PortRef Strength { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>The strength used when nothing is connected.</summary>
[InlineValue( nameof( Strength ) )] public float DefaultStrength { get; set; } = 4f;
/// <summary>The reconstructed tangent-space normal.</summary>
[Out( "float3", Name = "Normal" )] public PortRef Normal { get; set; }
/// <inheritdoc/>
public NormalFromHeightMapNode()
{
Srgb = false;
TextureGroup = "Height";
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var texture = ResolveTexture( ctx );
var texel = ctx.TryIn( nameof( TexelSize ), out var wired )
? wired
: ctx.Bin( BinaryOp.Div, ctx.Const( new Vector2( 1f, 1f ) ), ctx.Call( Intrinsic.TextureSize, texture ) );
ctx.Out( nameof( Normal ), ctx.Helper( PrismTextureHelpers.NormalFromHeightMap,
texture, ResolveSampler( ctx ), PrismTextureBinding.Uv( ctx, nameof( UV ) ), texel,
ctx.In( nameof( Strength ), ctx.Const( DefaultStrength ) ) ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Comparison, gather and fetch
// ---------------------------------------------------------------------------------------------------
/// <summary>Compares a value against a depth texture, the way a shadow lookup does.</summary>
[NodeInfo( Id = "prism.texture.sampleCmp", Title = "Sample Compare", Category = "Texture/Sample",
Icon = "compare", Keywords = new[] { "shadow", "compare", "depth", "pcf", "samplecmp" },
Description = "Hardware depth comparison against a texture, optionally with a 3x3 percentage-closer filter." )]
[NodeVersion( 1 )]
public sealed class SampleCompareNode : TextureNode
{
/// <summary>The depth texture to compare against.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>The depth to compare. Result is one where this passes.</summary>
[In( "float", Name = "Compare" )] public PortRef Compare { get; set; }
/// <summary>Tap spacing for the filtered variant. Defaults to the texture's texel size.</summary>
[In( "float2", Name = "Texel Size" )] public PortRef TexelSize { get; set; }
/// <summary>The comparison value used when nothing is connected.</summary>
[InlineValue( nameof( Compare ) )] public float DefaultCompare { get; set; } = 0.5f;
/// <summary>True to average a 3x3 neighbourhood instead of taking a single tap.</summary>
public bool Filtered { get; set; }
/// <summary>The comparison result.</summary>
[Out( "float", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public SampleCompareNode()
{
Srgb = false;
TextureGroup = "Depth";
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var texture = ResolveTexture( ctx );
var uv = PrismTextureBinding.Uv( ctx, nameof( UV ) );
var compare = ctx.In( nameof( Compare ), ctx.Const( DefaultCompare ) );
if ( !Filtered )
{
ctx.Out( nameof( Out ), ctx.Helper( PrismTextureHelpers.SampleCompare, texture, uv, compare ) );
return;
}
var texel = ctx.TryIn( nameof( TexelSize ), out var wired )
? wired
: ctx.Bin( BinaryOp.Div, ctx.Const( new Vector2( 1f, 1f ) ), ctx.Call( Intrinsic.TextureSize, texture ) );
ctx.Out( nameof( Out ), ctx.Helper( PrismTextureHelpers.SampleComparePcf, texture, uv, compare, texel ) );
}
}
/// <summary>Reads the four texels a bilinear filter would blend, unfiltered.</summary>
[NodeInfo( Id = "prism.texture.gather", Title = "Gather", Category = "Texture/Sample",
Icon = "grid_view", Keywords = new[] { "gather", "gather4", "fetch4", "quad" },
Description = "Returns one channel from each of the four texels around a UV, in counter-clockwise order." )]
[NodeVersion( 1 )]
public sealed class GatherNode : TextureNode
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>Where to read it.</summary>
[In( "float2", Name = "UV" )] public PortRef UV { get; set; }
/// <summary>Filtering state.</summary>
[In( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <summary>Which channel to gather.</summary>
public PrismGatherChannel Channel { get; set; } = PrismGatherChannel.Red;
/// <summary>The four gathered texels.</summary>
[Out( "float4", Name = "Out" )] public PortRef Out { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var intrinsic = Channel switch
{
PrismGatherChannel.Green => Intrinsic.GatherGreen,
PrismGatherChannel.Blue => Intrinsic.GatherBlue,
PrismGatherChannel.Alpha => Intrinsic.GatherAlpha,
_ => Intrinsic.GatherRed
};
ctx.Out( nameof( Out ), ctx.Call( intrinsic, ResolveTexture( ctx ), ResolveSampler( ctx ),
PrismTextureBinding.Uv( ctx, nameof( UV ) ) ) );
}
}
/// <summary>Fetches one texel by integer coordinate, with no filtering and no sampler.</summary>
[NodeInfo( Id = "prism.texture.load", Title = "Load Texel", Category = "Texture/Sample",
Icon = "pin_drop", Keywords = new[] { "load", "fetch", "texel", "unfiltered" },
Description = "Reads a single texel at an integer coordinate and mip level. No sampler is involved." )]
[NodeVersion( 1 )]
public sealed class LoadTexelNode : TextureNode
{
/// <summary>The texture to read.</summary>
[In( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <summary>The texel coordinate.</summary>
[In( "float2", Name = "Coord" )] public PortRef Coord { get; set; }
/// <summary>The mip level to read from.</summary>
[In( "float", Name = "Mip" )] public PortRef Mip { get; set; }
/// <summary>The mip level used when nothing is connected.</summary>
[InlineValue( nameof( Mip ) )] public float DefaultMip { get; set; }
/// <summary>The fetched texel.</summary>
[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var coord = ctx.In( nameof( Coord ), ctx.Const( new Vector2( 0f, 0f ) ) );
var mip = ctx.In( nameof( Mip ), ctx.Const( DefaultMip ) );
var location = ctx.Cast( ctx.Construct( ShaderType.Float3, coord, mip ), ShaderType.Int3 );
ctx.Out( nameof( RGBA ), ctx.Call( Intrinsic.Load, ResolveTexture( ctx ), location ) );
}
}
// ---------------------------------------------------------------------------------------------------
// Resource objects
// ---------------------------------------------------------------------------------------------------
/// <summary>
/// A texture object, with no sampling attached. Wire it into as many sampling nodes as you like.
/// </summary>
[NodeInfo( Id = "prism.texture.object", Title = "Texture Object", Category = "Texture/Resource",
Icon = "collections", Keywords = new[] { "texture", "asset", "object", "bindless", "slot" },
Description = "Declares a texture and hands it out as a value, so several nodes can share one slot." )]
[NodeVersion( 1 )]
public sealed class TextureObjectNode : TextureNode
{
/// <summary>Which resource kind to declare.</summary>
public enum TextureKind
{
/// <summary>A flat 2D texture.</summary>
Texture2D,
/// <summary>An array of 2D textures.</summary>
Texture2DArray,
/// <summary>A volume texture.</summary>
Texture3D,
/// <summary>A cube map.</summary>
TextureCube
}
/// <summary>The resource kind this node declares.</summary>
public TextureKind Resource { get; set; } = TextureKind.Texture2D;
/// <summary>The declared texture.</summary>
[Out( "Texture2D", Name = "Texture" )] public PortRef Texture { get; set; }
/// <inheritdoc/>
protected override ObjectKind Kind => Resource switch
{
TextureKind.Texture2DArray => ObjectKind.Texture2DArray,
TextureKind.Texture3D => ObjectKind.Texture3D,
TextureKind.TextureCube => ObjectKind.TextureCube,
_ => ObjectKind.Texture2D
};
/// <inheritdoc/>
protected override void OnDefinePorts( PortBuilder b )
{
base.OnDefinePorts( b );
b?.Retype( nameof( Texture ), ShaderType.ObjectName( Kind ) );
}
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
var parameter = PrismTextureBinding.FromParameter( ctx, TextureParameter, Kind );
ctx.Out( nameof( Texture ),
ctx.Global( parameter ?? PrismTextureBinding.Declare( ctx, this, Kind ) ) );
}
}
/// <summary>A named sampler state, so several sampling nodes can share one filtering setup.</summary>
[NodeInfo( Id = "prism.texture.samplerState", Title = "Sampler State", Category = "Texture/Resource",
Icon = "tune", Keywords = new[] { "sampler", "filter", "wrap", "clamp", "aniso" },
Description = "Declares a sampler state and hands it out as a value." )]
[NodeVersion( 1 )]
public sealed class SamplerStateNode : PrismNode
{
/// <summary>Filtering mode.</summary>
public PrismTextureFilter Filter { get; set; } = PrismTextureFilter.Anisotropic;
/// <summary>Horizontal addressing.</summary>
public PrismTextureAddress AddressU { get; set; } = PrismTextureAddress.Wrap;
/// <summary>Vertical addressing.</summary>
public PrismTextureAddress AddressV { get; set; } = PrismTextureAddress.Wrap;
/// <summary>The declared sampler.</summary>
[Out( "SamplerState", Name = "Sampler" )] public PortRef Sampler { get; set; }
/// <inheritdoc/>
public override void Emit( EmitContext ctx )
{
if ( ctx is null ) return;
ctx.Out( nameof( Sampler ),
ctx.Global( PrismTextureBinding.SamplerState( Filter, AddressU, AddressV ) ) );
}
}