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 )}" ); } }; }