Editor/Core/SpecularGlossinessMaterial.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
using Vector4=System.Numerics.Vector4;

/// <summary>Authored glTF specular/glossiness inputs, retained for texture baking
/// and lossless glTF material export. RGB samples supplied here are linear.</summary>
public sealed record SpecularGlossinessMaterial
{
    public Vector4 DiffuseFactor {get;init;}=Vector4.One;
    public Vector3 SpecularFactor {get;init;}=Vector3.One;
    public float GlossinessFactor {get;init;}=1;
    public string? DiffuseTexture {get;init;}
    public string? SpecularGlossinessTexture {get;init;}

    // The Khronos workflow conversion fits the fixed dielectric reflectance of
    // metallic/roughness to the authored diffuse and specular energy. It is an
    // approximation for materials outside that workflow's representable range.
    public (Vector3 Color,float Opacity,float Metallic,float Roughness) Evaluate(Vector4 diffuseSample,Vector4 specularSample)
    {
        var d=DiffuseFactor*diffuseSample;
        var diffuse=new Vector3(d.X,d.Y,d.Z);
        var specular=SpecularFactor*new Vector3(specularSample.X,specularSample.Y,specularSample.Z);
        float strength=1-Math.Max(specular.X,Math.Max(specular.Y,specular.Z));
        float Brightness(Vector3 v)=>MathF.Sqrt(Vector3.Dot(v*v,new(.299f,.587f,.114f)));
        const float dielectric=.04f,epsilon=1e-6f;
        float reflectance=Brightness(specular),metallic=0;
        if(reflectance>dielectric)
        {
            float b=Brightness(diffuse)*strength/(1-dielectric)+reflectance-2*dielectric;
            metallic=Math.Clamp((MathF.Sqrt(Math.Max(0,b*b-4*dielectric*(dielectric-reflectance)))-b)/(2*dielectric),0,1);
        }
        var fromDiffuse=diffuse*(strength/((1-dielectric)*Math.Max(1-metallic,epsilon)));
        var fromSpecular=(specular-new Vector3(dielectric*(1-metallic)))/Math.Max(metallic,epsilon);
        var color=Vector3.Clamp(Vector3.Lerp(fromDiffuse,fromSpecular,metallic*metallic),Vector3.Zero,Vector3.One);
        return(color,d.W,metallic,1-GlossinessFactor*specularSample.W);
    }
}