Documentation
// Shared functions for all shaders in the engine. Contents of this
// file will be *automatically* included in all shaders!

const float PI = 3.14159;

// Tries to solve quadratic equation. Returns true iff there are any real roots.
bool S_SolveQuadraticEq(float a, float b, float c, out float minT, out float maxT)
{
    float twoA = 2.0 * a;
    float det = b * b - 2.0 * twoA * c;

    if (det < 0.0)
    {
        minT = 0.0;
        maxT = 0.0;

        return false;
    }

    float sqrtDet = sqrt(det);

    float root1 = (-b - sqrtDet) / twoA;
    float root2 = (-b + sqrtDet) / twoA;

    minT = min(root1, root2);
    maxT = max(root1, root2);

    return true;
}

// Returns attenuation in inverse square model. It falls to zero at given radius.
float S_LightDistanceAttenuation(float distance, float radius)
{
    float attenuation = clamp(1.0 - distance * distance / (radius * radius), 0.0, 1.0);
    return attenuation;
}

// Projects world space position (typical use case) by given matrix.
vec3 S_Project(vec3 worldPosition, mat4 matrix)
{
    vec4 screenPos = matrix * vec4(worldPosition, 1);

    screenPos.xyz /= screenPos.w;

    return screenPos.xyz * 0.5 + 0.5;
}

// Returns matrix-space position from given screen position.
// Real space of returned value is defined by matrix and can
// be any, but there are few common use cases:
//  - To get position in view space pass inverse projection matrix.
//  - To get position in world space pass inverse view-projection matrix.
vec3 S_UnProject(vec3 screenPos, mat4 matrix)
{
    vec4 clipSpacePos = vec4(screenPos * 2.0 - 1.0, 1.0);

    vec4 position = matrix * clipSpacePos;

    return position.xyz / position.w;
}

float S_DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH * NdotH;

    float nom = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return nom / denom;
}

float S_GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r * r) / 8.0;

    float nom = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}

// Calculates occlusion factor using given material properties (normal + roughness),
// viewer position (V) and light-to-fragment vector (L).
float S_GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = S_GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = S_GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

// Fresnel law approximation using Fresnel-Schlick formula.
vec3 S_FresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0);
}

struct TPBRContext {
    vec3 lightColor;
    vec3 viewVector;
    vec3 fragmentToLight;
    vec3 fragmentNormal;
    float metallic;
    float roughness;
    vec3 albedo;
};

// Calculates physically-correct lighting using provided light and fragment parameters.
// Does not apply any distance or direction attenuation! Attenuation depends on the
// light source and appied in separate shaders.
vec3 S_PBR_CalculateLight(TPBRContext ctx) {
    vec3 F0 = mix(vec3(0.04), ctx.albedo, ctx.metallic);

    vec3 L = ctx.fragmentToLight;
    vec3 H = normalize(ctx.viewVector + L);

    // Cook-Torrance BRDF
    float NDF = S_DistributionGGX(ctx.fragmentNormal, H, ctx.roughness);
    float G = S_GeometrySmith(ctx.fragmentNormal, ctx.viewVector, L, ctx.roughness);
    vec3 F = S_FresnelSchlick(max(dot(H, ctx.viewVector), 0.0), F0);

    vec3 numerator = NDF * G * F;
    float denominator = 4.0 * max(dot(ctx.fragmentNormal, ctx.viewVector), 0.0) * max(dot(ctx.fragmentNormal, L), 0.0) + 0.001; // 0.001 to prevent divide by zero.
    vec3 specular = numerator / denominator;

    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - ctx.metallic;

    float NdotL = max(dot(ctx.fragmentNormal, L), 0.0);

    return (kD * ctx.albedo / PI + specular) * ctx.lightColor * NdotL;
}

// Returns scatter amount for given parameters.
// https://cseweb.ucsd.edu/~ravir/papers/singlescat/scattering.pdf
// https://blog.mmacklin.com/2010/05/29/in-scattering-demo/
float S_InScatter(vec3 start, vec3 dir, vec3 lightPos, float d)
{
    // light to ray origin
    vec3 q = start - lightPos;

    // coefficients
    float b = dot(dir, q);
    float c = dot(q, q);

    // evaluate integral
    float s = 1.0 / sqrt(c - b*b);
    float l = s * (atan((d + b) * s) - atan(b*s));

    return l;
}

// https://en.wikipedia.org/wiki/Rayleigh_scattering
vec3 S_RayleighScatter(vec3 start, vec3 dir, vec3 lightPos, float d)
{
    float scatter = S_InScatter(start, dir, lightPos, d);

    // Apply simple version of Rayleigh scattering. Just increase
    // intensity of blue light over other colors.
    return vec3(0.55, 0.75, 1.0) * scatter;
}

// Tries to find intersection of given ray with specified sphere. If there is an intersection, returns true.
// In out parameters minT, maxT will be min and max ray parameters of intersection.
bool S_RaySphereIntersection(vec3 origin, vec3 dir, vec3 center, float radius, out float minT, out float maxT)
{
    vec3 d = origin - center;
    float a = dot(dir, dir);
    float b = 2.0 * dot(dir, d);
    float c = dot(d, d) - radius * radius;
    return S_SolveQuadraticEq(a, b, c, minT, maxT);
}

// Calculates point shadow factor where 1.0 - no shadow, 0.0 - fully in shadow.
// Why value is inversed? To be able to directly multiply color to shadow factor.
float S_PointShadow(
    bool shadowsEnabled,
    bool softShadows,
    float fragmentDistance,
    float shadowBias,
    vec3 toLight,
    in samplerCube shadowMap)
{
    if (shadowsEnabled)
    {
        float biasedFragmentDistance = fragmentDistance - shadowBias;

        if (softShadows)
        {
            const int samples = 20;

            const vec3 directions[samples] = vec3[samples] (
            vec3(1, 1, 1), vec3(1, -1, 1), vec3(-1, -1, 1), vec3(-1, 1, 1),
            vec3(1, 1, -1), vec3(1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
            vec3(1, 1, 0), vec3(1, -1, 0), vec3(-1, -1, 0), vec3(-1, 1, 0),
            vec3(1, 0, 1), vec3(-1, 0, 1), vec3(1, 0, -1), vec3(-1, 0, -1),
            vec3(0, 1, 1), vec3(0, -1, 1), vec3(0, -1, -1), vec3(0, 1, -1)
            );

            const float diskRadius = 0.0025;

            float accumulator = 0.0;

            for (int i = 0; i < samples; ++i)
            {
                vec3 fetchDirection = -toLight + directions[i] * diskRadius;
                float shadowDistanceToLight = texture(shadowMap, fetchDirection).r;
                if (biasedFragmentDistance > shadowDistanceToLight)
                {
                    accumulator += 1.0;
                }
            }

            return clamp(1.0 - accumulator / float(samples), 0.0, 1.0);
        }
        else
        {
            float shadowDistanceToLight = texture(shadowMap, -toLight).r;
            return biasedFragmentDistance > shadowDistanceToLight ? 0.0 : 1.0;
        }
    } else {
        return 1.0; // No shadow
    }
}

// Calculates spot light shadow factor where 1.0 - no shadow, 0.0 - fully in shadow.
// Why value is inversed? To be able to directly multiply color to shadow factor.
float S_SpotShadowFactor(
    bool shadowsEnabled,
    bool softShadows,
    float shadowBias,
    vec3 fragmentPosition,
    mat4 lightViewProjMatrix,
    float shadowMapInvSize,
    in sampler2D spotShadowTexture)
{
    if (shadowsEnabled)
    {
        vec3 lightSpacePosition = S_Project(fragmentPosition, lightViewProjMatrix);

        float biasedLightSpaceFragmentDepth = lightSpacePosition.z - shadowBias;

        if (softShadows)
        {
            float accumulator = 0.0;

            for (float y = -0.5; y <= 0.5; y += 0.5)
            {
                for (float x = -0.5; x <= 0.5; x += 0.5)
                {
                    vec2 fetchTexCoord = lightSpacePosition.xy + vec2(x, y) * shadowMapInvSize;
                    if (biasedLightSpaceFragmentDepth > texture(spotShadowTexture, fetchTexCoord).r)
                    {
                        accumulator += 1.0;
                    }
                }
            }

            return clamp(1.0 - accumulator / 9.0, 0.0, 1.0);
        }
        else
        {
            return biasedLightSpaceFragmentDepth > texture(spotShadowTexture, lightSpacePosition.xy).r ? 0.0 : 1.0;
        }
    } else {
        return 1.0; // No shadow
    }
}

float Internal_FetchHeight(in sampler2D heightTexture, vec2 texCoords) {
    return texture(heightTexture, texCoords).r;
}

vec2 S_ComputeParallaxTextureCoordinates(in sampler2D heightTexture, vec3 eyeVec, vec2 texCoords, vec3 normal) {
    const float minLayers = 8.0;
    const float maxLayers = 15.0;
    const int maxIterations = 15;
    const float parallaxScale = 0.05;

    float numLayers = mix(maxLayers, minLayers, abs(dot(normal, eyeVec)));

    float layerHeight = 1.0 / numLayers;
    float curLayerHeight = 0.0;
    vec2 dtex = parallaxScale * eyeVec.xy / numLayers;

    vec2 currentTexCoords = texCoords;

    float height = Internal_FetchHeight(heightTexture, currentTexCoords);

    for (int i = 0; i < maxIterations; i++) {
        if (height > curLayerHeight) {
            curLayerHeight += layerHeight;
            currentTexCoords -= dtex;
            height = Internal_FetchHeight(heightTexture, currentTexCoords);
        } else {
            break;
        }
    }

    vec2 prev = currentTexCoords + dtex;
    float nextH = height - curLayerHeight;
    float prevH = Internal_FetchHeight(heightTexture, prev) - curLayerHeight + layerHeight;

    float weight = nextH / (nextH - prevH);

    return prev * weight + currentTexCoords * (1.0 - weight);
}

vec4 S_LinearToSRGB(vec4 color) {
    vec3 a = 12.92 * color.rgb;
    vec3 b = 1.055 * pow(color.rgb, vec3(1.0 / 2.4)) - 0.055;
    vec3 c = step(vec3(0.0031308), color.rgb);
    vec3 rgb = mix(a, b, c);
    return vec4(rgb, color.a);
}

vec4 S_SRGBToLinear(vec4 color) {
    vec3 a = color.rgb / 12.92;
    vec3 b = pow((color.rgb + 0.055) / 1.055, vec3(2.4));
    vec3 c = step(vec3(0.04045), color.rgb);
    vec3 rgb = mix(a, b, c);
    return vec4(rgb, color.a);
}

float S_Luminance(vec3 x) {
    return dot(x, vec3(0.299, 0.587, 0.114));
}