Generators/ChannelCombineTextureGenerator.cs
using System.Text.Json.Serialization;
using System.Threading;
namespace Sandbox.Resources;
/// <summary>
/// Which channel to read from the source image.
/// </summary>
public enum ChannelSource
{
Red,
Green,
Blue,
Alpha,
Luminance
}
/// <summary>
/// Combines separate image files into a single texture by assigning each to an output R, G, B, or A channel.
/// </summary>
[Order( -90 )]
[Title( "RGBA Channels" )]
[Icon( "tune" )]
[ClassName( "channelcombine" )]
public class ChannelCombineGenerator : TextureGenerator
{
/// <summary>
/// The image file used for the Red output channel.
/// </summary>
[Header( "Red Channel" )]
[Title( "File Path" )]
[TextureImagePath]
public string RedFilePath { get; set; }
/// <summary>
/// Which channel to read from the red source image.
/// </summary>
[Title( "Source" )]
public ChannelSource RedSource { get; set; } = ChannelSource.Red;
/// <summary>
/// The default value for the Red channel when no image is provided (0-1).
/// </summary>
[Title( "Default" )]
[Range( 0, 1 )]
public float RedDefault { get; set; } = 0.0f;
/// <summary>
/// The image file used for the Green output channel.
/// </summary>
[Header( "Green Channel" )]
[Title( "File Path" )]
[TextureImagePath]
public string GreenFilePath { get; set; }
/// <summary>
/// Which channel to read from the green source image.
/// </summary>
[Title( "Source" )]
public ChannelSource GreenSource { get; set; } = ChannelSource.Green;
/// <summary>
/// The default value for the Green channel when no image is provided (0-1).
/// </summary>
[Title( "Default" )]
[Range( 0, 1 )]
public float GreenDefault { get; set; } = 0.0f;
/// <summary>
/// The image file used for the Blue output channel.
/// </summary>
[Header( "Blue Channel" )]
[Title( "File Path" )]
[TextureImagePath]
public string BlueFilePath { get; set; }
/// <summary>
/// Which channel to read from the blue source image.
/// </summary>
[Title( "Source" )]
public ChannelSource BlueSource { get; set; } = ChannelSource.Blue;
/// <summary>
/// The default value for the Blue channel when no image is provided (0-1).
/// </summary>
[Title( "Default" )]
[Range( 0, 1 )]
public float BlueDefault { get; set; } = 0.0f;
/// <summary>
/// The image file used for the Alpha output channel.
/// </summary>
[Header( "Alpha Channel" )]
[Title( "File Path" )]
[TextureImagePath]
public string AlphaFilePath { get; set; }
/// <summary>
/// Which channel to read from the alpha source image.
/// </summary>
[Title( "Source" )]
public ChannelSource AlphaSource { get; set; } = ChannelSource.Luminance;
/// <summary>
/// The default value for the Alpha channel when no image is provided (0-1).
/// </summary>
[Title( "Default" )]
[Range( 0, 1 )]
public float AlphaDefault { get; set; } = 1.0f;
/// <summary>
/// The maximum output size in pixels. Source images are resized to a common resolution before combining.
/// </summary>
[Header( "Output" )]
public int MaxSize { get; set; } = 2048;
[Hide]
public override bool CacheToDisk => true;
protected override async ValueTask<Texture> CreateTexture( Options options, CancellationToken ct )
{
var redBitmap = await LoadBitmap( RedFilePath );
var greenBitmap = await LoadBitmap( GreenFilePath );
var blueBitmap = await LoadBitmap( BlueFilePath );
var alphaBitmap = await LoadBitmap( AlphaFilePath );
try
{
// Register compile references
if ( options.Compiler is not null )
{
if ( !string.IsNullOrWhiteSpace( RedFilePath ) ) options.Compiler.Context.AddCompileReference( RedFilePath );
if ( !string.IsNullOrWhiteSpace( GreenFilePath ) ) options.Compiler.Context.AddCompileReference( GreenFilePath );
if ( !string.IsNullOrWhiteSpace( BlueFilePath ) ) options.Compiler.Context.AddCompileReference( BlueFilePath );
if ( !string.IsNullOrWhiteSpace( AlphaFilePath ) ) options.Compiler.Context.AddCompileReference( AlphaFilePath );
}
// Determine output size from the largest source image
int width = 1;
int height = 1;
foreach ( var bmp in (Bitmap[])[redBitmap, greenBitmap, blueBitmap, alphaBitmap] )
{
if ( bmp is null ) continue;
width = Math.Max( width, bmp.Width );
height = Math.Max( height, bmp.Height );
}
// Clamp to max size
if ( width > MaxSize || height > MaxSize )
{
float scale = Math.Min( (float)MaxSize / width, (float)MaxSize / height );
width = Math.Max( 1, (int)(width * scale) );
height = Math.Max( 1, (int)(height * scale) );
}
// Resize all source bitmaps to common resolution
redBitmap = ResizeIfNeeded( redBitmap, width, height );
greenBitmap = ResizeIfNeeded( greenBitmap, width, height );
blueBitmap = ResizeIfNeeded( blueBitmap, width, height );
alphaBitmap = ResizeIfNeeded( alphaBitmap, width, height );
// Read pixel arrays
var redPixels = redBitmap?.GetPixels();
var greenPixels = greenBitmap?.GetPixels();
var bluePixels = blueBitmap?.GetPixels();
var alphaPixels = alphaBitmap?.GetPixels();
// Combine into output
using var output = new Bitmap( width, height );
var outputPixels = new Color[width * height];
for ( int i = 0; i < outputPixels.Length; i++ )
{
float r = SampleChannel( redPixels, i, RedSource, RedDefault );
float g = SampleChannel( greenPixels, i, GreenSource, GreenDefault );
float b = SampleChannel( bluePixels, i, BlueSource, BlueDefault );
float a = SampleChannel( alphaPixels, i, AlphaSource, AlphaDefault );
outputPixels[i] = new Color( r, g, b, a );
}
output.SetPixels( outputPixels );
return output.ToTexture();
}
finally
{
redBitmap?.Dispose();
greenBitmap?.Dispose();
blueBitmap?.Dispose();
alphaBitmap?.Dispose();
}
}
private static float SampleChannel( Color[] pixels, int index, ChannelSource source, float defaultValue )
{
if ( pixels is null ) return defaultValue;
var c = pixels[index];
return source switch
{
ChannelSource.Red => c.r,
ChannelSource.Green => c.g,
ChannelSource.Blue => c.b,
ChannelSource.Alpha => c.a,
ChannelSource.Luminance => c.r * 0.2126f + c.g * 0.7152f + c.b * 0.0722f,
_ => defaultValue
};
}
private static Bitmap ResizeIfNeeded( Bitmap bitmap, int width, int height )
{
if ( bitmap is null ) return null;
if ( bitmap.Width == width && bitmap.Height == height ) return bitmap;
var resized = bitmap.Resize( width, height );
bitmap.Dispose();
return resized;
}
private static async Task<Bitmap> LoadBitmap( string filePath )
{
if ( string.IsNullOrWhiteSpace( filePath ) ) return null;
var path = filePath.NormalizeFilename();
if ( !FileSystem.Mounted.FileExists( path ) )
{
Log.Warning( $"ChannelCombineGenerator could not find file: {path}" );
return null;
}
var bytes = await FileSystem.Mounted.ReadAllBytesAsync( path );
return Bitmap.CreateFromBytes( bytes );
}
}