using Sandbox.UI; using System.Text.Json.Serialization; using System.Threading; namespace Sandbox.Resources; /// /// Load images from disk and convert them to textures /// [Order( -100 )] [Title( "Image File" )] [Icon( "image" )] [ClassName( "imagefile" )] public class ImageFileGenerator : TextureGenerator { /// /// The path to the image file, relative to any other assets in the project. /// [Header( "Image" )] [KeyProperty, TextureImagePath] public string FilePath { get; set; } /// /// The maximum size of the image in pixels. If the imported image is larger than this (after cropping), it will be downscaled to fit. /// public int MaxSize { get; set; } = 4096; /// /// When enabled, the output texture will be a normal map generated from the heightmap of the image. /// [Header( "Normal Map" ), Title( "Height to Normal" )] public bool ConvertHeightToNormals { get; set; } /// /// The scale of the normal map when using . If negative, the normal map will be inverted. /// [ShowIf( "ConvertHeightToNormals", true )] public float NormalScale { get; set; } = 1; /// /// How much to rotate the image by, in degrees. This is applied after cropping and padding. /// [Header( "Adjust" ), Range( 0, 360 )] public float Rotate { get; set; } = 0.0f; /// /// Whether or not to flip the image vertically. This is done after everything else has been applied. /// public bool FlipVertical { get; set; } = false; /// /// Whether or not to flip the image horizontally. This is done after everything else has been applied. /// public bool FlipHorizontal { get; set; } = false; /// /// How many pixels from each edge to crop from the image. If negative values are used, the image will be expanded instead of cropped. /// public Margin Cropping { get; set; } /// /// How many pixels of padding from each edge. After the image has been cropped, /// padding is added without affecting the size of the image (scaling the original image down to fit padded margins). /// public Margin Padding { get; set; } /// /// Whether or not to invert the colors of the image. /// [Header( "Effects" )] public bool InvertColor { get; set; } = false; /// /// The color the image should be tinted. This effectively multiplies the color of each pixel by this color (including alpha). /// public Color Tint { get; set; } = Color.White; /// /// The intensity of the blur effect. If 0, no blur is applied. /// [Range( 0, 32 )] public float Blur { get; set; } = 0.0f; /// /// The intensity of the sharpen effect. If 0, no sharpening is applied. /// [Range( 0, 10 )] public float Sharpen { get; set; } = 0.0f; /// /// The brightness of the image. /// [Range( 0, 2 )] public float Brightness { get; set; } = 1.0f; /// /// The contrast of the image. /// [Range( 0, 2 )] public float Contrast { get; set; } = 1.0f; /// /// The saturation of the image. /// [Range( 0, 2 )] public float Saturation { get; set; } = 1.0f; /// /// How much to adjust the hue of the image, in degrees. If 0, no hue adjustment is applied. /// [Range( 0, 360 )] public float Hue { get; set; } = 0.0f; /// /// When enabled, every pixel in the image will be re-colored to the (interpolated by the alpha). /// [ToggleGroup( "Colorize" )] public bool Colorize { get; set; } /// /// When is enabled, this is the target color that every pixel in the image will be re-colored to. /// [Group( "Colorize" )] public Color TargetColor { get; set; } = Color.White; [Hide] public override bool CacheToDisk => true; // Don't continually load from disk, cache that shit [JsonIgnore, Hide] int _loadedCacheHash; [JsonIgnore, Hide] Bitmap _cacheLoaded; [JsonIgnore, Hide] int loadCount = 0; async Task LoadCached() { if ( string.IsNullOrWhiteSpace( FilePath ) ) return default; var path = FilePath.NormalizeFilename(); if ( string.IsNullOrEmpty( FilePath ) ) return default; if ( !EngineFileSystem.Mounted.FileExists( path ) ) { Log.Warning( $"ImageFileGenerator could not find file: {path}" ); return default; } var size = EngineFileSystem.Mounted.FileSize( path ); var hash = HashCode.Combine( size, FilePath, Cropping ); if ( hash == _loadedCacheHash ) return _cacheLoaded?.Clone(); var bytes = await EngineFileSystem.Mounted.ReadAllBytesAsync( path ); // Create the bitmap and crop it if needed var bitmap = Bitmap.CreateFromBytes( bytes ); var newRect = new Rect( Cropping.Left, Cropping.Top, bitmap.Width - Cropping.Right - Cropping.Left, bitmap.Height - Cropping.Bottom - Cropping.Top ); if ( newRect.Width > 0 && newRect.Height > 0 ) { bitmap = bitmap.Crop( newRect.SnapToGrid() ); } if ( loadCount > 3 ) { _loadedCacheHash = hash; _cacheLoaded = bitmap?.Clone(); } loadCount++; return bitmap; } protected override async ValueTask CreateTexture( Options options, CancellationToken ct ) { if ( string.IsNullOrWhiteSpace( FilePath ) ) return null; // // A regular texture! // // Trim compiled names! if ( FilePath.EndsWith( "_c", StringComparison.OrdinalIgnoreCase ) ) FilePath = FilePath[..^2]; if ( FilePath.EndsWith( ".vtex", StringComparison.OrdinalIgnoreCase ) ) { await MainThread.Wait(); return Texture.Load( FilePath ); } var bitmap = await LoadCached(); if ( bitmap is null ) return Texture.Invalid; // // Tell the compiler we're using this file, so to add it as a compile reference // if ( options.Compiler is not null ) { options.Compiler.Context.AddCompileReference( FilePath ); } if ( !Padding.IsNearlyZero() ) { bitmap.InsertPadding( Padding ); } if ( Rotate != 0 ) bitmap = bitmap.Rotate( Rotate ); if ( bitmap.Width > MaxSize || bitmap.Height > MaxSize ) { float scale = Math.Min( (float)MaxSize / bitmap.Width, (float)MaxSize / bitmap.Height ); int newWidth = (int)(bitmap.Width * scale); int newHeight = (int)(bitmap.Height * scale); newWidth = newWidth.Clamp( 1, 1024 * 8 ); newHeight = newHeight.Clamp( 1, 1024 * 8 ); bitmap = bitmap.Resize( newWidth, newHeight ); } if ( Tint != Color.White || Tint.a != 1 ) { bitmap.Tint( Tint ); } if ( Sharpen > 0.0f ) bitmap.Sharpen( Sharpen, true ); if ( Blur > 0.0f ) bitmap.Blur( Blur, true ); if ( Brightness != 1 || Contrast != 1 || Saturation != 1 || Hue != 0 ) bitmap.Adjust( Brightness, Contrast, Saturation, Hue ); if ( InvertColor ) bitmap.InvertColor(); if ( Colorize ) { bitmap.Colorize( TargetColor ); } // if ( Posterize != 0 ) bitmap.Posterize( Posterize ); if ( FlipHorizontal ) bitmap = bitmap.FlipHorizontal(); if ( FlipVertical ) bitmap = bitmap.FlipVertical(); if ( ConvertHeightToNormals ) { bitmap = bitmap.HeightmapToNormalMap( NormalScale ); } return bitmap.ToTexture(); } public override EmbeddedResource? CreateEmbeddedResource() { // if we're a vtex, don't create an embedded resource if ( FilePath.EndsWith( ".vtex", StringComparison.OrdinalIgnoreCase ) ) return null; return base.CreateEmbeddedResource(); } }