// This file is part of the OGRE project.
// code adapted from Google Filament
// SPDX-License-Identifier: Apache-2.0

#define PI 3.14159265359

#ifdef OGRE_GLSLES
    // min roughness such that (MIN_PERCEPTUAL_ROUGHNESS^4) > 0 in fp16 (i.e. 2^(-14/4), rounded up)
    #define MIN_PERCEPTUAL_ROUGHNESS 0.089
#else
    #define MIN_PERCEPTUAL_ROUGHNESS 0.045
#endif

#define MEDIUMP_FLT_MAX    65504.0
#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)

#define MIN_N_DOT_V 1e-4

struct PixelParams
{
    vec3 baseColor;
    vec3 diffuseColor;
    float perceptualRoughness;
    float roughness;
    vec3  f0;
    vec3  dfg;
    vec3  energyCompensation;
};

float clampNoV(float NoV) {
    // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"
    return max(NoV, MIN_N_DOT_V);
}

// Computes x^5 using only multiply operations.
float pow5(float x) {
    float x2 = x * x;
    return x2 * x2 * x;
}

// https://google.github.io/filament/Filament.md.html#materialsystem/diffusebrdf
float Fd_Lambert() {
    return 1.0 / PI;
}

// https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/fresnel(specularf)
vec3 F_Schlick(const vec3 f0, float f90, float VoH) {
    // Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"
    return f0 + (f90 - f0) * pow5(1.0 - VoH);
}

vec3 computeDiffuseColor(const vec3 baseColor, float metallic) {
    return baseColor.rgb * (1.0 - metallic);
}

vec3 computeF0(const vec3 baseColor, float metallic, float reflectance) {
    return baseColor.rgb * metallic + (reflectance * (1.0 - metallic));
}

float perceptualRoughnessToRoughness(float perceptualRoughness) {
    return perceptualRoughness * perceptualRoughness;
}

// https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg)
float V_SmithGGXCorrelated(float roughness, float NoV, float NoL) {
    // Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"
    float a2 = roughness * roughness;
    // TODO: lambdaV can be pre-computed for all the lights, it should be moved out of this function
    float lambdaV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2);
    float lambdaL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2);
    float v = 0.5 / (lambdaV + lambdaL);
    // a2=0 => v = 1 / 4*NoL*NoV   => min=1/4, max=+inf
    // a2=1 => v = 1 / 2*(NoL+NoV) => min=1/4, max=+inf
    // clamp to the maximum value representable in mediump
    return saturateMediump(v);
}

// https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/normaldistributionfunction(speculard)
float D_GGX(float roughness, float NoH, const vec3 h, const vec3 n) {
    // Walter et al. 2007, "Microfacet Models for Refraction through Rough Surfaces"

    // In mediump, there are two problems computing 1.0 - NoH^2
    // 1) 1.0 - NoH^2 suffers floating point cancellation when NoH^2 is close to 1 (highlights)
    // 2) NoH doesn't have enough precision around 1.0
    // Both problem can be fixed by computing 1-NoH^2 in highp and providing NoH in highp as well

    // However, we can do better using Lagrange's identity:
    //      ||a x b||^2 = ||a||^2 ||b||^2 - (a . b)^2
    // since N and H are unit vectors: ||N x H||^2 = 1.0 - NoH^2
    // This computes 1.0 - NoH^2 directly (which is close to zero in the highlights and has
    // enough precision).
    // Overall this yields better performance, keeping all computations in mediump
#ifdef OGRE_GLSLES
    vec3 NxH = cross(n, h);
    float oneMinusNoHSquared = dot(NxH, NxH);
#else
    float oneMinusNoHSquared = 1.0 - NoH * NoH;
#endif

    float a = NoH * roughness;
    float k = roughness / (oneMinusNoHSquared + a * a);
    float d = k * k * (1.0 / PI);
    return saturateMediump(d);
}

float getDistanceAttenuation(const vec3 params, float distance)
{
    return 1.0 / (params.x + params.y * distance + params.z * distance * distance);
}

float getAngleAttenuation(const vec3 params, const vec3 lightDir, const vec3 toLight)
{
    float rho		= dot(-lightDir, toLight);
    float fSpotE	= saturate((rho - params.y) / (params.x - params.y));
    return pow(fSpotE, params.z);
}

void evaluateLight(
                in vec3 vNormal,
                in vec3 viewPos,
                in vec4 lightPos,
                in vec3 lightColor,
                in vec4 pointParams,
                in vec4 vLightDirView,
                in vec4 spotParams,
                in PixelParams pixel,
                inout vec3 vOutColour)
{
    vec3 vLightView = lightPos.xyz;
    float fLightD = 0.0;

    if (lightPos.w != 0.0)
    {
        vLightView -= viewPos; // to light
        fLightD     = length(vLightView);

        if(fLightD > pointParams.x)
            return;
    }

	vLightView		   = normalize(vLightView);

	vec3 vNormalView = normalize(vNormal);
	float NoL		 = saturate(dot(vNormalView, vLightView));

    if(NoL <= 0.0)
        return; // not lit by this light

    // https://google.github.io/filament/Filament.md.html#toc5.6.2
    float f90 = saturate(dot(pixel.f0, vec3_splat(50.0 * 0.33)));

	vec3 vView       = -normalize(viewPos);

    // https://google.github.io/filament/Filament.md.html#materialsystem/standardmodelsummary
    vec3 h    = normalize(vView + vLightView);
    float NoH = saturate(dot(vNormalView, h));
    float NoV = clampNoV(abs(dot(vNormalView, vView)));

    float V = V_SmithGGXCorrelated(pixel.roughness, NoV, NoL);
    vec3 F  = F_Schlick(pixel.f0, f90, NoH);
    float D = D_GGX(pixel.roughness, NoH, h, vNormalView);

    vec3 Fr = (D * V) * F;
    vec3 Fd = pixel.diffuseColor * Fd_Lambert();

    // https://google.github.io/filament/Filament.md.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance
    vec3 color = NoL * lightColor * (Fr * pixel.energyCompensation + Fd);

    color *= getDistanceAttenuation(pointParams.yzw, fLightD);

    if(spotParams.w != 0.0)
    {
        color *= getAngleAttenuation(spotParams.xyz, vLightDirView.xyz, vLightView);
    }

    vOutColour += color;
}

void PBR_MakeParams(in vec3 baseColor, in vec2 mrParam, inout PixelParams pixel)
{
#ifdef DEBUG_PSSM
	baseColor += pssm_lod_info;
#endif
    baseColor = pow(baseColor, vec3_splat(2.2));
    pixel.baseColor = baseColor;

    float perceptualRoughness = mrParam.x;
    // Clamp the roughness to a minimum value to avoid divisions by 0 during lighting
    pixel.perceptualRoughness = clamp(perceptualRoughness, MIN_PERCEPTUAL_ROUGHNESS, 1.0);
    // Remaps the roughness to a perceptually linear roughness (roughness^2)
    pixel.roughness = perceptualRoughnessToRoughness(pixel.perceptualRoughness);

    float metallic = saturate(mrParam.y);
    pixel.f0 = computeF0(baseColor, metallic, 0.04);
    pixel.diffuseColor = computeDiffuseColor(baseColor, metallic);

    pixel.dfg = vec3_splat(0.5); // use full f0 for energy compensation
    pixel.energyCompensation = vec3_splat(0.0); // will be set later
}

#if LIGHT_COUNT > 0
void PBR_Lights(
#ifdef HAVE_SHADOW_FACTOR
                in float shadowFactor,
#endif
                in vec3 vNormal,
                in vec3 viewPos,
                in vec4 ambient,
                in vec4 lightPos[LIGHT_COUNT],
                in vec4 lightColor[LIGHT_COUNT],
                in vec4 pointParams[LIGHT_COUNT],
                in vec4 vLightDirView[LIGHT_COUNT],
                in vec4 spotParams[LIGHT_COUNT],
                in PixelParams pixel,
                inout vec3 vOutColour)
{
    vOutColour = pow(vOutColour, vec3_splat(2.2)); // gamma to linear

    // Energy compensation for multiple scattering in a microfacet model
    // See "Multiple-Scattering Microfacet BSDFs with the Smith Model"
    pixel.energyCompensation = 1.0 + pixel.f0 * (1.0 / pixel.dfg.y - 1.0);

    for(int i = 0; i < LIGHT_COUNT; i++)
    {
        evaluateLight(vNormal, viewPos, lightPos[i], lightColor[i].xyz, pointParams[i], vLightDirView[i], spotParams[i],
                      pixel, vOutColour);

#ifdef HAVE_SHADOW_FACTOR
        if(i == 0) // directional lights always come first
            vOutColour *= shadowFactor;
#endif
    }

    vOutColour += pixel.baseColor * pow(ambient.rgb, vec3_splat(2.2));

    // linear to gamma
    vOutColour = pow(vOutColour, vec3_splat(1.0/2.2));

    vOutColour = saturate(vOutColour);
}
#endif