namespace Editor.ShaderGraph.Nodes;
public abstract class TextureSamplerBase : ShaderNode, ITextureParameterNode, IErroringNode
{
///
/// Texture to sample in preview
///
[ImageAssetPath]
public string Image
{
get => _image;
set
{
_image = value;
_asset = AssetSystem.FindByPath( _image );
if ( _asset == null )
return;
CompileTexture();
}
}
private Asset _asset;
private string _texture;
private string _image;
private string _resourceText;
[JsonIgnore, Hide]
protected Asset Asset => _asset;
[JsonIgnore, Hide]
protected string TexturePath => _texture;
///
/// How the texture is filtered and wrapped when sampled
///
[InlineEditor( Label = false ), Group( "Sampler" )]
public Sampler Sampler { get; set; }
protected void CompileTexture()
{
if ( _asset == null )
return;
if ( string.IsNullOrWhiteSpace( _image ) )
return;
var ui = UI;
ui.DefaultTexture = _image;
UI = ui;
var resourceText = string.Format( ShaderTemplate.TextureDefinition,
_image,
UI.ColorSpace,
UI.ImageFormat,
UI.Processor );
if ( _resourceText == resourceText )
return;
_resourceText = resourceText;
var assetPath = $"shadergraph/{_image.Replace( ".", "_" )}_shadergraph.generated.vtex";
var resourcePath = FileSystem.Root.GetFullPath( "/.source2/temp" );
resourcePath = System.IO.Path.Combine( resourcePath, assetPath );
if ( AssetSystem.CompileResource( resourcePath, resourceText ) )
{
_texture = assetPath;
}
else
{
Log.Warning( $"Failed to compile {_image}" );
}
}
///
/// Settings for how this texture shows up in material editor
///
[InlineEditor( Label = false ), Group( "UI" )]
public TextureInput UI { get; set; } = new TextureInput
{
ImageFormat = TextureFormat.DXT5,
SrgbRead = true,
Default = Color.White,
};
[Hide]
public override string Title => string.IsNullOrWhiteSpace( UI.Name ) ? null : $"{DisplayInfo.For( this ).Name} {UI.Name}";
protected TextureSamplerBase() : base()
{
Image = "materials/dev/white_color.tga";
ExpandSize = new Vector2( 0, 8 + Inputs.Count() * 24 );
}
public override void OnPaint( Rect rect )
{
rect = rect.Align( 130, TextFlag.LeftBottom ).Shrink( 3 );
Paint.SetBrush( "/image/transparent-small.png" );
Paint.DrawRect( rect.Shrink( 2 ), 2 );
Paint.SetBrush( Theme.ControlBackground.WithAlpha( 0.7f ) );
Paint.DrawRect( rect, 2 );
if ( Asset != null )
{
Paint.Draw( rect.Shrink( 2 ), Asset.GetAssetThumb( true ) );
}
}
protected NodeResult Component( string component, GraphCompiler compiler )
{
var result = compiler.Result( new NodeInput { Identifier = Identifier, Output = nameof( Result ) } );
return result.IsValid ? new( 1, $"{result}.{component}", true ) : new( 1, "0.0f", true );
}
public List GetErrors()
{
var errors = new List();
if ( Graph is ShaderGraph sg && sg.IsSubgraph )
{
if ( string.IsNullOrWhiteSpace( UI.Name ) )
{
errors.Add( $"Texture parameter \"{DisplayInfo.For( this ).Name}\" is missing a name" );
}
foreach ( var node in sg.Nodes )
{
if ( node is ITextureParameterNode tpn && tpn != this && tpn.UI.Name == UI.Name )
{
errors.Add( $"Duplicate texture parameter name \"{UI.Name}\" on {DisplayInfo.For( this ).Name}" );
}
}
}
return errors;
}
}
///
/// Sample a 2D Texture
///
[Title( "Texture 2D" ), Category( "Textures" ), Icon( "image" )]
public sealed class TextureSampler : TextureSamplerBase
{
///
/// Coordinates to sample this texture (Defaults to vertex coordinates)
///
[Title( "Coordinates" )]
[Input( typeof( Vector2 ) )]
[Hide]
public NodeInput Coords { get; set; }
///
/// RGBA color result
///
[Hide]
[Output( typeof( Color ) ), Title( "RGBA" )]
public NodeResult.Func Result => ( GraphCompiler compiler ) =>
{
var input = UI;
input.Type = TextureType.Tex2D;
CompileTexture();
var texture = string.IsNullOrWhiteSpace( TexturePath ) ? null : Texture.Load( TexturePath );
texture ??= Texture.White;
var result = compiler.ResultTexture( Sampler, input, texture );
var coords = compiler.Result( Coords );
if ( compiler.Stage == GraphCompiler.ShaderStage.Vertex )
{
return new NodeResult( 4, $"{result.Item1}.SampleLevel(" +
$" g_sSampler{result.Item2}," +
$" {(coords.IsValid ? $"{coords.Cast( 2 )}" : "i.vTextureCoords.xy")}, 0 )" );
}
else
{
return new NodeResult( 4, $"Tex2DS( {result.Item1}," +
$" g_sSampler{result.Item2}," +
$" {(coords.IsValid ? $"{coords.Cast( 2 )}" : "i.vTextureCoords.xy")} )" );
}
};
///
/// Red component of result
///
[Output( typeof( float ) ), Hide, Title( "R" )]
public NodeResult.Func R => ( GraphCompiler compiler ) => Component( "r", compiler );
///
/// Green component of result
///
[Output( typeof( float ) ), Hide, Title( "G" )]
public NodeResult.Func G => ( GraphCompiler compiler ) => Component( "g", compiler );
///
/// Blue component of result
///
[Output( typeof( float ) ), Hide, Title( "B" )]
public NodeResult.Func B => ( GraphCompiler compiler ) => Component( "b", compiler );
///
/// Alpha (Opacity) component of result
///
[Output( typeof( float ) ), Hide, Title( "A" )]
public NodeResult.Func A => ( GraphCompiler compiler ) => Component( "a", compiler );
}
///
/// Sample a Cube Texture
///
[Title( "Texture Cube" ), Category( "Textures" ), Icon( "view_in_ar" )]
public sealed class TextureCube : ShaderNode
{
///
/// Coordinates to sample this cubemap
///
[Title( "Coordinates" )]
[Input( typeof( Vector3 ) )]
[Hide]
public NodeInput Coords { get; set; }
///
/// Texture to sample in preview
///
[ImageAssetPath]
public string Texture { get; set; }
///
/// How the texture is filtered and wrapped when sampled
///
[InlineEditor( Label = false ), Group( "Sampler" )]
public Sampler Sampler { get; set; }
///
/// Settings for how this texture shows up in material editor
///
[InlineEditor( Label = false ), Group( "UI" )]
public TextureInput UI { get; set; } = new TextureInput
{
ImageFormat = TextureFormat.DXT5,
SrgbRead = true,
Default = Color.White,
};
public TextureCube() : base()
{
Texture = "materials/skybox/skybox_workshop.vtex";
ExpandSize = new Vector2( 0, 8 + Inputs.Count() * 24 );
}
public override void OnPaint( Rect rect )
{
rect = rect.Align( 130, TextFlag.LeftBottom ).Shrink( 3 );
Paint.SetBrush( "/image/transparent-small.png" );
Paint.DrawRect( rect.Shrink( 2 ), 2 );
Paint.SetBrush( Theme.ControlBackground.WithAlpha( 0.7f ) );
Paint.DrawRect( rect, 2 );
if ( !string.IsNullOrEmpty( Texture ) )
{
var tex = Sandbox.Texture.Find( Texture );
if ( tex is null ) return;
var pixmap = Pixmap.FromTexture( tex );
Paint.Draw( rect.Shrink( 2 ), pixmap );
}
}
///
/// RGBA color result
///
[Hide]
[Output( typeof( Color ) ), Title( "RGBA" )]
public NodeResult.Func Result => ( GraphCompiler compiler ) =>
{
var input = UI;
input.Type = TextureType.TexCube;
var result = compiler.ResultTexture( Sampler, input, Sandbox.Texture.Load( Texture ) );
var coords = compiler.Result( Coords );
return new NodeResult( 4, $"TexCubeS( {result.Item1}," +
$" g_sSampler{result.Item2}," +
$" {(coords.IsValid ? $"{coords.Cast( 3 )}" : ViewDirection.Result.Invoke( compiler ))} )" );
};
private NodeResult Component( string component, GraphCompiler compiler )
{
var result = compiler.Result( new NodeInput { Identifier = Identifier, Output = nameof( Result ) } );
return result.IsValid ? new( 1, $"{result}.{component}", true ) : new( 1, "0.0f", true );
}
///
/// Red component of result
///
[Output( typeof( float ) ), Hide, Title( "R" )]
public NodeResult.Func R => ( GraphCompiler compiler ) => Component( "r", compiler );
///
/// Green component of result
///
[Output( typeof( float ) ), Hide, Title( "G" )]
public NodeResult.Func G => ( GraphCompiler compiler ) => Component( "g", compiler );
///
/// Blue component of result
///
[Output( typeof( float ) ), Hide, Title( "B" )]
public NodeResult.Func B => ( GraphCompiler compiler ) => Component( "b", compiler );
///
/// Alpha (Opacity) component of result
///
[Output( typeof( float ) ), Hide, Title( "A" )]
public NodeResult.Func A => ( GraphCompiler compiler ) => Component( "a", compiler );
}
///
/// Sample a 2D texture from 3 directions, then blend based on a normal vector.
///
[Title( "Texture Triplanar" ), Category( "Textures" ), Icon( "photo_library" )]
public sealed class TextureTriplanar : TextureSamplerBase
{
///
/// Coordinates to sample this texture (Defaults to vertex position)
///
[Title( "Coordinates" )]
[Input( typeof( Vector3 ) )]
[Hide]
public NodeInput Coords { get; set; }
///
/// Normal to use when blending between each sampled direction (Defaults to vertex normal)
///
[Title( "Normal" )]
[Input( typeof( Vector3 ) )]
[Hide]
public NodeInput Normal { get; set; }
///
/// RGBA color result
///
[Hide]
[Output( typeof( Color ) ), Title( "RGBA" )]
public NodeResult.Func Result => ( GraphCompiler compiler ) =>
{
var input = UI;
input.Type = TextureType.Tex2D;
CompileTexture();
var texture = string.IsNullOrWhiteSpace( TexturePath ) ? null : Texture.Load( TexturePath );
texture ??= Texture.White;
var (tex, sampler) = compiler.ResultTexture( Sampler, input, texture );
var coords = compiler.Result( Coords );
var normal = compiler.Result( Normal );
var result = compiler.ResultFunction( "TexTriplanar_Color",
tex,
$"g_sSampler{sampler}",
coords.IsValid ? coords.Cast( 3 ) : "(i.vPositionWithOffsetWs.xyz + g_vHighPrecisionLightingOffsetWs.xyz) / 39.3701",
normal.IsValid ? normal.Cast( 3 ) : "normalize( i.vNormalWs.xyz )" );
return new NodeResult( 4, result );
};
///
/// Red component of result
///
[Output( typeof( float ) ), Hide, Title( "R" )]
public NodeResult.Func R => ( GraphCompiler compiler ) => Component( "r", compiler );
///
/// Green component of result
///
[Output( typeof( float ) ), Hide, Title( "G" )]
public NodeResult.Func G => ( GraphCompiler compiler ) => Component( "g", compiler );
///
/// Blue component of result
///
[Output( typeof( float ) ), Hide, Title( "B" )]
public NodeResult.Func B => ( GraphCompiler compiler ) => Component( "b", compiler );
///
/// Alpha (Opacity) component of result
///
[Output( typeof( float ) ), Hide, Title( "A" )]
public NodeResult.Func A => ( GraphCompiler compiler ) => Component( "a", compiler );
}
///
/// Sample a 2D texture from 3 directions, then blend based on a normal vector.
///
[Title( "Normal Map Triplanar" ), Category( "Textures" ), Icon( "texture" )]
public sealed class NormapMapTriplanar : TextureSamplerBase
{
///
/// Coordinates to sample this texture (Defaults to vertex position)
///
[Title( "Coordinates" )]
[Input( typeof( Vector3 ) )]
[Hide]
public NodeInput Coords { get; set; }
///
/// Normal to use when blending between each sampled direction (Defaults to vertex normal)
///
[Title( "Normal" )]
[Input( typeof( Vector3 ) )]
[Hide]
public NodeInput Normal { get; set; }
public NormapMapTriplanar()
{
ExpandSize = new Vector2( 0f, 128f );
UI = new TextureInput
{
ImageFormat = TextureFormat.DXT5,
SrgbRead = false,
ColorSpace = TextureColorSpace.Linear,
Extension = TextureExtension.Normal,
Processor = TextureProcessor.NormalizeNormals,
Default = new Color( 0.5f, 0.5f, 1f, 1f )
};
}
///
/// RGBA color result
///
[Hide]
[Output( typeof( Vector3 ) ), Title( "XYZ" )]
public NodeResult.Func Result => ( GraphCompiler compiler ) =>
{
var input = UI;
input.Type = TextureType.Tex2D;
CompileTexture();
var texture = string.IsNullOrWhiteSpace( TexturePath ) ? null : Texture.Load( TexturePath );
texture ??= Texture.White;
var (tex, sampler) = compiler.ResultTexture( Sampler, input, texture );
var coords = compiler.Result( Coords );
var normal = compiler.Result( Normal );
var result = compiler.ResultFunction( "TexTriplanar_Normal",
tex,
$"g_sSampler{sampler}",
coords.IsValid ? coords.Cast( 3 ) : "(i.vPositionWithOffsetWs.xyz + g_vHighPrecisionLightingOffsetWs.xyz) / 39.3701",
normal.IsValid ? normal.Cast( 3 ) : "normalize( i.vNormalWs.xyz )" );
return new NodeResult( 3, result );
};
}
///
/// Texture Coordinate from vertex data
///
[Title( "Texture Coordinate" ), Category( "Variables" ), Icon( "texture" )]
public sealed class TextureCoord : ShaderNode
{
///
/// Use the secondary vertex coordinate
///
public bool UseSecondaryCoord { get; set; } = false;
///
/// How many times this coordinate repeats itself to give a tiled effect
///
public Vector2 Tiling { get; set; } = 1;
[Hide]
public override string Title => $"{DisplayInfo.For( this ).Name}{(UseSecondaryCoord ? " 2" : "")}";
///
/// Coordinate result
///
[Output( typeof( Vector2 ) )]
[Hide]
public NodeResult.Func Result => ( GraphCompiler compiler ) =>
{
if ( compiler.IsPreview )
{
var result = $"{compiler.ResultValue( UseSecondaryCoord )} ? i.vTextureCoords.zw : i.vTextureCoords.xy";
return new( 2, $"{compiler.ResultValue( Tiling.IsNearZeroLength )} ? {result} : ({result}) * {compiler.ResultValue( Tiling )}" );
}
else
{
var result = UseSecondaryCoord ? "i.vTextureCoords.zw" : "i.vTextureCoords.xy";
return Tiling.IsNearZeroLength ? new( 2, result ) : new( 2, $"{result} * {compiler.ResultValue( Tiling )}" );
}
};
}