Editor/UI/MaterialAssets.Formats.cs
using SkiaSharp;
namespace HumanoidRigger.Editor;

internal static partial class MaterialAssets
{
    // Work on copies in the output/cache directory. glTF packs roughness in G
    // and metalness in B; Source 2 and MTL consume separate grayscale maps.
    internal static SourceMaterial[] PrepareFormats(SourceMaterial[] source,string directory,ExportFormats formats)
    {
        bool portable=(formats&(ExportFormats.Gltf|ExportFormats.Glb))!=0;
        var converted=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        string Png(string path)
        {
            if(path is null||Path.GetExtension(path).ToLowerInvariant() is ".png" or ".jpg" or ".jpeg")return path;
            if(converted.TryGetValue(path,out var result))return result;
            using var bitmap=Decode(path);result="textures/portable_"+converted.Count+".png";Save(bitmap,result);converted.Add(path,result);return result;
        }
        SKBitmap Decode(string path)=>SKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException("Cannot decode texture '"+path+"'.");
        void Save(SKBitmap bitmap,string path)
        {
            string output=Path.Combine(directory,path);Directory.CreateDirectory(Path.GetDirectoryName(output));
            using var image=SKImage.FromBitmap(bitmap);using var data=image.Encode(SKEncodedImageFormat.Png,100);using var stream=File.Create(output);data.SaveTo(stream);
        }
        static SKColor Sample(SKBitmap bitmap,int x,int y,int width,int height,SKColor fallback)=>bitmap is null?fallback:bitmap.GetPixel(Math.Min(bitmap.Width-1,x*bitmap.Width/width),Math.Min(bitmap.Height-1,y*bitmap.Height/height));
        static byte Channel(float value)=>(byte)Math.Clamp((int)MathF.Round(value),0,255);
        return source.Select((original,index)=>
        {
            var m=original with{};
            string WriteMap(string suffix,int width,int height,Func<int,int,SKColor> pixel)
            {
                using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);
                for(int y=0;y<height;y++)for(int x=0;x<width;x++)bitmap.SetPixel(x,y,pixel(x,y));
                string path="textures/pbr_"+index+"_"+suffix+".png";Save(bitmap,path);return path;
            }
            if(m.AuthoredPbr)
            {
                using var packed=m.MetallicRoughnessTexture is null?null:Decode(m.MetallicRoughnessTexture);
                int w=packed?.Width??1,h=packed?.Height??1;
                m.RoughnessTexture=WriteMap("roughness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});
                m.MetalnessTexture=WriteMap("metalness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});
            }
            else if(portable&&(m.RoughnessTexture is not null||m.MetalnessTexture is not null))
            {
                using var rough=m.RoughnessTexture is null?null:Decode(m.RoughnessTexture);using var metal=m.MetalnessTexture is null?null:Decode(m.MetalnessTexture);
                int w=Math.Max(rough?.Width??1,metal?.Width??1),h=Math.Max(rough?.Height??1,metal?.Height??1);
                m.MetallicRoughnessTexture=WriteMap("metallic_roughness",w,h,(x,y)=>new(255,Sample(rough,x,y,w,h,SKColors.White).Red,Sample(metal,x,y,w,h,SKColors.Black).Red));
            }
            if(portable)
            {
                if(m.OpacityTexture is not null)
                {
                    using var color=m.ColorTexture is null?null:Decode(m.ColorTexture);using var opacity=Decode(m.OpacityTexture);
                    int w=Math.Max(color?.Width??1,opacity.Width),h=Math.Max(color?.Height??1,opacity.Height);
                    bool packedAlpha=string.Equals(m.OpacityTexture,m.ColorTexture,StringComparison.OrdinalIgnoreCase);
                    m.ColorTexture=WriteMap("rgba",w,h,(x,y)=>{var c=Sample(color,x,y,w,h,SKColors.White);var a=Sample(opacity,x,y,w,h,SKColors.White);return new(c.Red,c.Green,c.Blue,packedAlpha?a.Alpha:Channel(c.Alpha*a.Red/255f));});
                }
                m.ColorTexture=Png(m.ColorTexture);m.NormalTexture=Png(m.NormalTexture);m.MetallicRoughnessTexture=Png(m.MetallicRoughnessTexture);m.OcclusionTexture=Png(m.OcclusionTexture);m.EmissiveTexture=Png(m.EmissiveTexture);
            }
            return m;
        }).ToArray();
    }
}