Files
sbox-public/engine/Sandbox.Engine/Resources/Textures/Generators/ImageFileTextureGenerator.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

263 lines
7.4 KiB
C#

using Sandbox.UI;
using System.Text.Json.Serialization;
using System.Threading;
namespace Sandbox.Resources;
/// <summary>
/// Load images from disk and convert them to textures
/// </summary>
[Order( -100 )]
[Title( "Image File" )]
[Icon( "image" )]
[ClassName( "imagefile" )]
public class ImageFileGenerator : TextureGenerator
{
/// <summary>
/// The path to the image file, relative to any other assets in the project.
/// </summary>
[Header( "Image" )]
[KeyProperty, TextureImagePath]
public string FilePath { get; set; }
/// <summary>
/// The maximum size of the image in pixels. If the imported image is larger than this (after cropping), it will be downscaled to fit.
/// </summary>
public int MaxSize { get; set; } = 4096;
/// <summary>
/// When enabled, the output texture will be a normal map generated from the heightmap of the image.
/// </summary>
[Header( "Normal Map" ), Title( "Height to Normal" )]
public bool ConvertHeightToNormals { get; set; }
/// <summary>
/// The scale of the normal map when using <see cref="ConvertHeightToNormals"/>. If negative, the normal map will be inverted.
/// </summary>
[ShowIf( "ConvertHeightToNormals", true )]
public float NormalScale { get; set; } = 1;
/// <summary>
/// How much to rotate the image by, in degrees. This is applied after cropping and padding.
/// </summary>
[Header( "Adjust" ), Range( 0, 360 )]
public float Rotate { get; set; } = 0.0f;
/// <summary>
/// Whether or not to flip the image vertically. This is done after everything else has been applied.
/// </summary>
public bool FlipVertical { get; set; } = false;
/// <summary>
/// Whether or not to flip the image horizontally. This is done after everything else has been applied.
/// </summary>
public bool FlipHorizontal { get; set; } = false;
/// <summary>
/// How many pixels from each edge to crop from the image. If negative values are used, the image will be expanded instead of cropped.
/// </summary>
public Margin Cropping { get; set; }
/// <summary>
/// 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).
/// </summary>
public Margin Padding { get; set; }
/// <summary>
/// Whether or not to invert the colors of the image.
/// </summary>
[Header( "Effects" )]
public bool InvertColor { get; set; } = false;
/// <summary>
/// The color the image should be tinted. This effectively multiplies the color of each pixel by this color (including alpha).
/// </summary>
public Color Tint { get; set; } = Color.White;
/// <summary>
/// The intensity of the blur effect. If 0, no blur is applied.
/// </summary>
[Range( 0, 32 )]
public float Blur { get; set; } = 0.0f;
/// <summary>
/// The intensity of the sharpen effect. If 0, no sharpening is applied.
/// </summary>
[Range( 0, 10 )]
public float Sharpen { get; set; } = 0.0f;
/// <summary>
/// The brightness of the image.
/// </summary>
[Range( 0, 2 )]
public float Brightness { get; set; } = 1.0f;
/// <summary>
/// The contrast of the image.
/// </summary>
[Range( 0, 2 )]
public float Contrast { get; set; } = 1.0f;
/// <summary>
/// The saturation of the image.
/// </summary>
[Range( 0, 2 )]
public float Saturation { get; set; } = 1.0f;
/// <summary>
/// How much to adjust the hue of the image, in degrees. If 0, no hue adjustment is applied.
/// </summary>
[Range( 0, 360 )]
public float Hue { get; set; } = 0.0f;
/// <summary>
/// When enabled, every pixel in the image will be re-colored to the <see cref="TargetColor"/> (interpolated by the alpha).
/// </summary>
[ToggleGroup( "Colorize" )]
public bool Colorize { get; set; }
/// <summary>
/// When <see cref="Colorize"/> is enabled, this is the target color that every pixel in the image will be re-colored to.
/// </summary>
[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<Bitmap> 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<Texture> 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();
}
}