

#ifdef DISTANT_HORIZONS

vec3 reprojectDH(vec3 sceneSpace, bool hand)
{
    vec3 prevScreenPos = hand ? vec3(0.0) : cameraPosition - previousCameraPosition;
    prevScreenPos = sceneSpace + prevScreenPos;
    prevScreenPos = viewMAD(gbufferPreviousModelView, prevScreenPos);
    prevScreenPos = viewMAD(dhPreviousProjection, prevScreenPos) * (0.5 / -prevScreenPos.z) + 0.5;

    return prevScreenPos;
}
#endif
float speculars(vec3 n, vec3 rd, vec3 l, float km, float f0)
{
    // km = max(pow(km, 2.0), 0.0001);

    vec3 h = normalize(l - rd);
    float ndh = dot(n, h);
    float g = ndh * ndh * (km * km - 1.0) + 1.0;
    float ggx = km * km / (PI * g * g);

    float fre = 1.0 + dot(rd, n);

    float kr = f0 + (1.0 - f0) * (1. - km) * (1.0 - km) * pow(fre, 5.0);

    return kr * ggx;
}
vec3 specularWater(vec3 n, vec3 rd)
{
    // Hardcoded values for demonstration, adjust as needed
    vec3 f0 = vec3(0.2); // Typical value for non-metallic surfaces
    float km = 0.5;      // Example roughness squared
    vec3 light = toScreenSpace(vec3(0, -1, 0));

    vec3 h = normalize(light - rd);
    float ndh = dot(n, h);
    float g = ndh * ndh * (km * km - 1.0) + 1.0;
    float ggx = km * km / (PI * g * g);

    float fre = 1.0 + dot(rd, n);

    vec3 kr = f0 + (1.0 - f0) * (1. - km) * (1.0 - km) * pow(fre, 5.0);

    return kr * ggx;
}
float fresnel0(vec3 l, vec3 n, float eta1, float eta2)
{
    float cosThetaI = abs(dot(l, n)); // cos(theta_i) - angle of incidence
    float eta = eta2 / eta1;          // relative index of refraction
    float sin2ThetaI = 1.0 - cosThetaI * cosThetaI;
    float sin2ThetaT = eta * eta * sin2ThetaI; // sin^2(theta_t) using Snell's law

    float cosThetaT = sqrt(max(0.0, 1.0 - sin2ThetaT)); // cos(theta_t)

    // Reflectance for s-polarized light
    float Rs = (eta1 * cosThetaI - eta2 * cosThetaT) / (eta1 * cosThetaI + eta2 * cosThetaT);
    Rs = Rs * Rs;

    // Reflectance for p-polarized light
    float Rp = (eta2 * cosThetaI - eta1 * cosThetaT) / (eta2 * cosThetaI + eta1 * cosThetaT);
    Rp = Rp * Rp;

    // Average of the s and p polarization reflectances
    return 0.5 * (Rs + Rp);
}

vec3 rayTrace(vec3 dir, vec3 position, float dither)
{
    const float quality = SSR_STEPS;

    vec3 clipPosition = toClipSpace3(position);
    vec3 direction = normalize(toClipSpace3(position + dir) - clipPosition);
    vec3 maxLengths = (step(0.0, direction) - clipPosition) / direction;
    float minLength = min(min(maxLengths.x, maxLengths.y), maxLengths.z);

#if SSR_STEPS > 4
    vec3 stepv = (direction * minLength) / quality;
#else
    vec3 stepv = (direction * minLength) / (dither * 0.25 + quality);

#endif

    float tolerance = max(abs(stepv.z) * 4.0, 0.01 / (position.z * position.z));

    for (int i = 0; i <= int(quality); ++i)
    {
#if SSR_STEPS > 4
        vec3 spos = clipPosition + stepv * (i + dither);
#else
        vec3 spos = clipPosition + stepv * (i);
#endif
        spos.xy += offsets[framemod8] * texelSize * 0.5;
#ifdef DH
        float depthSample = texelFetch2D(dhDepthTex0, ivec2(spos.xy / texelSize), 0).r;

#else
        float depthSample = texelFetch2D(depthtex1, ivec2(spos.xy / texelSize), 0).r;

#endif
        if (depthSample < spos.z && abs(spos.z - depthSample) < tolerance)
        {
            return vec3(spos.xy, depthSample);
        }
    }

    return vec3(1.1); // Indicates no intersection found
}
vec3 RT5(vec3 dir, vec3 position, float dither, float steps)
{

    float quality = steps;
#ifdef DH

    vec3 clipPosition = toClipSpace3DH(position);
    vec3 direction = normalize(toClipSpace3DH(position + dir) - clipPosition);
#else
    vec3 clipPosition = toClipSpace3(position);
    vec3 direction = normalize(toClipSpace3(position + dir) - clipPosition);

#endif
    vec3 maxLengths = (step(0.0, direction) - clipPosition) / direction;
    float minLength = min(min(maxLengths.x, maxLengths.y), maxLengths.z);

    vec3 stepv = (direction * minLength) / quality;

    float tolerance = max(abs(stepv.z) * 1.0, 0.5 / (position.z * position.z));
    // float tolerance = max(abs(stepv.z) * 2.0, 0.1 / (position.z * position.z));

    for (int i = 0; i <= int(quality); ++i)
    {

        vec3 spos = clipPosition + stepv * (i + dither);

        spos.xy += offsets[framemod8] * texelSize * 0.5;

#ifdef DH
        float depthSample = texelFetch2D(dhDepthTex1, ivec2(spos.xy / texelSize), 0).r;
#else
        float depthSample = texelFetch2D(depthtex1, ivec2(spos.xy / texelSize), 0).r;
#endif

        if (depthSample < spos.z && abs(spos.z - depthSample) < tolerance)
        {
            return vec3(spos.xy, depthSample);
        }
    }

    return vec3(1.1); // Indicates no intersection found
}
vec3 RT5LQ(vec3 dir, vec3 position, float dither, float steps)
{

    float quality = steps;
#ifdef DH

    vec3 clipPosition = toClipSpace3DH(position);
    vec3 direction = normalize(toClipSpace3DH(position + dir) - clipPosition);
#else
    vec3 clipPosition = toClipSpace3(position);
    vec3 direction = normalize(toClipSpace3(position + dir) - clipPosition);

#endif
    vec3 maxLengths = (step(0.0, direction) - clipPosition) / direction;
    float minLength = min(min(maxLengths.x, maxLengths.y), maxLengths.z);

    vec3 stepv = (direction * minLength) / quality;

    float tolerance = max(abs(stepv.z) * 4.0, 0.75 / (position.z * position.z));

    for (int i = 0; i <= int(quality); ++i)
    {

        vec3 spos = clipPosition + stepv * (i + dither);

#ifdef DH
        float depthSample = texelFetch2D(dhDepthTex1, ivec2(spos.xy / texelSize), 0).r;
#else
        float depthSample = texelFetch2D(depthtex1, ivec2(spos.xy / texelSize), 0).r;
#endif

        if (depthSample < spos.z && abs(spos.z - depthSample) < tolerance)
        {
            return vec3(spos.xy, depthSample);
        }
    }

    return vec3(1.1); // Indicates no intersection found
}
vec3 RT5XLQ(vec3 dir, vec3 position, float dither, float steps)
{
#ifdef DH
    vec3 clipPosition = toClipSpace3DH(position);
    vec3 direction = normalize(toClipSpace3DH(position + dir) - clipPosition);
#else
    vec3 clipPosition = toClipSpace3(position);
    vec3 direction = normalize(toClipSpace3(position + dir) - clipPosition);
#endif

    // Calculate step size in clip space
    vec3 maxLengths = (step(0.0, direction) - clipPosition) / direction;
    float minLength = min(min(maxLengths.x, maxLengths.y), maxLengths.z);
    vec3 stepv = direction * (minLength / steps);

    float tolerance = abs(stepv.z) * 4.0;

    // Precompute texel size scaling
    vec2 texCoordScale = 1.0 / texelSize;

    float scaledDither0 = mix(0.1, 1.0, dither);
    float scaledDither1 = mix(0.9, 1.0, dither);
    float scaledDither = mix(scaledDither1, scaledDither0, float(steps >= 4));
    for (int i = 0; i < steps; ++i)
    {
        vec3 samplePosition = clipPosition + stepv * (float(i) + scaledDither);

        // Use integer coordinates for sampling directly
        ivec2 texCoords = ivec2(samplePosition.xy * texCoordScale);
#ifdef DH
        float depthSample = texelFetch2D(dhDepthTex1, texCoords, 0).r;

#else
        float depthSample = texelFetch2D(depthtex1, texCoords, 0).r;
#endif
        if (depthSample < samplePosition.z && abs(samplePosition.z - depthSample) < tolerance)
        {
            return vec3(samplePosition.xy, depthSample);
        }
    }

    return vec3(1.1); // No valid depth found
}

// GGX distribution function G1
float g(float NdotL, float roughness)
{
    float k = (roughness * roughness) / 2.0;
    return NdotL / (NdotL * (1.0 - k) + k);
}
float g2(float NdotL)
{
    return NdotL / (NdotL * 0.9999 + 0.0001);
}

vec3 sampleGGXVNDF(vec3 V_, float alpha_x, float alpha_y, float U1, float U2)
{
    // stretch view
    vec3 V = normalize(vec3(alpha_x * V_.x, alpha_y * V_.y, V_.z));
    // orthonormal basis
    vec3 T1 = (V.z < 0.9999) ? normalize(cross(V, vec3(0, 0, 1))) : vec3(1, 0, 0);
    vec3 T2 = cross(T1, V);
    // sample point with polar coordinates (r, phi)
    float a = 1.0 / (1.0 + V.z);
    float r = sqrt(U1);
    float phi = (U2 < a) ? U2 / a * 3.141592653589793 : 3.141592653589793 + (U2 - a) / (1.0 - a) * 3.141592653589793;
    float P1 = r * cos(phi);
    float P2 = r * sin(phi) * ((U2 < a) ? 1.0 : V.z);
    // compute normal
    vec3 N = P1 * T1 + P2 * T2 + sqrt(max(0.0, 1.0 - P1 * P1 - P2 * P2)) * V;
    // unstretch
    N = normalize(vec3(alpha_x * N.x, alpha_y * N.y, max(0.0, N.z)));
    return N;
}

void frisvad(in vec3 n, out vec3 f, out vec3 r)
{
    if (n.z < -0.999999)
    {
        f = vec3(0., -1, 0);
        r = vec3(-1, 0, 0);
    }
    else
    {
        float a = 1. / (1. + n.z);
        float b = -n.x * n.y * a;
        f = vec3(1. - n.x * n.x * a, b, -n.x);
        r = vec3(b, 1. - n.y * n.y * a, -n.y);
    }
}
mat3 CoordBase(vec3 n)
{
    vec3 x, y;
    frisvad(n, x, y);
    return mat3(x, y, n);
}

vec2 R2_samples(int n)
{
    vec2 alpha = vec2(0.75487765, 0.56984026);
    return fract(alpha * n);
}

vec3 GGX2(vec3 normal, vec3 viewDir, vec3 lightDir, float roughness, vec3 baseReflectance)
{
    // Calculate the half-vector (H), which is halfway between the view direction and light direction
    vec3 halfVector = normalize(lightDir + viewDir);

    // Dot products for various terms
    float NdotH = max(dot(normal, halfVector), 0.0);  // Dot product between normal and half-vector
    float NdotL = max(dot(normal, lightDir), 0.0);    // Dot product between normal and light direction
    float NdotV = max(dot(normal, viewDir), 0.0);     // Dot product between normal and view direction
    float VdotH = max(dot(viewDir, halfVector), 0.0); // Dot product between view direction and half-vector

    // ----------------------------
    // Fresnel term (F)
    // ----------------------------
    // Schlick's approximation for the Fresnel effect
    float oneMinusVdotH = 1.0 - VdotH;
    float fresnelFactor = oneMinusVdotH * oneMinusVdotH * oneMinusVdotH * oneMinusVdotH * oneMinusVdotH; // (1 - VdotH)^5
    vec3 fresnel = mix(baseReflectance, vec3(1.0), fresnelFactor);                                       // F = F0 + (1 - F0) * (1 - VdotH)^5

    // ----------------------------
    // Distribution term (D)
    // ----------------------------
    // GGX normal distribution function (NDF)
    float adjustedRoughness = roughness + 1e-4;          // Ensure roughness is never zero to avoid division by zero
    float alpha = adjustedRoughness * adjustedRoughness; // Squared roughness
    float NdotH2 = NdotH * NdotH;                        // Square of the dot product between normal and half-vector
    float distributionDenom = NdotH2 * (alpha - 1.0) + 1.0;
    float distributionTerm = alpha / (distributionDenom * distributionDenom * PI);

    // ----------------------------
    // Geometry term (G)
    // ----------------------------
    // Smith's geometry function with Schlick-GGX approximation
    float k = (adjustedRoughness + 1.0) * (adjustedRoughness + 1.0) * 0.125; // k^2 / 8
    float geometryFactor = 1.0 - k;

    float geomVisibilityV = NdotV / (NdotV * geometryFactor + k); // Geometry term for view direction
    float geomVisibilityL = NdotL / (NdotL * geometryFactor + k); // Geometry term for light direction

    // ----------------------------
    // Final Specular Calculation
    // ----------------------------
    // Combine all terms: Distribution (D), Geometry (G), and Fresnel (F)
    float denominator = max(4.0 * NdotV * NdotL, 1e-7); // Prevent division by zero with a small epsilon
    vec3 specular = (distributionTerm * geomVisibilityV * geomVisibilityL * fresnel) * (NdotL / denominator);

    return specular; // Return the computed specular reflection
}

// Function to compute Fresnel reflectance using the Fresnel equations
float fresnelDielectric(float cosThetaI, float etaI, float etaT)
{
    float sinThetaI = sqrt(max(0.0, 1.0 - cosThetaI * cosThetaI));
    float sinThetaT = etaI / etaT * sinThetaI;

    // Total internal reflection
    if (sinThetaT >= 1.0)
    {
        return 1.0;
    }

    float cosThetaT = sqrt(max(0.0, 1.0 - sinThetaT * sinThetaT));
    cosThetaI = abs(cosThetaI);

    float Rs = ((etaT * cosThetaI) - (etaI * cosThetaT)) / ((etaT * cosThetaI) + (etaI * cosThetaT));
    float Rp = ((etaI * cosThetaI) - (etaT * cosThetaT)) / ((etaI * cosThetaI) + (etaT * cosThetaT));
    return 0.5 * (Rs * Rs + Rp * Rp);
}

vec3 GetN(int idx)
{
    switch (idx)
    {
    case 230:
        return vec3(2.9114, 2.9497, 2.5845);
    case 231:
        return vec3(0.18299, 0.42108, 1.3734);
    case 232:
        return vec3(1.3456, 0.96521, 0.61722);
    case 233:
        return vec3(3.1071, 3.1812, 2.3230);
    case 234:
        return vec3(0.27105, 0.67693, 1.3164);
    case 235:
        return vec3(1.9100, 1.8300, 1.4400);
    case 236:
        return vec3(2.3757, 2.0847, 1.8453);
    case 237:
        return vec3(0.15943, 0.14512, 0.13547);
    case 255:
        return vec3(1.0);
    default:
        return vec3(0.0);
    }
}

vec3 GetK(int idx)
{
    switch (idx)
    {
    case 230:
        return vec3(3.0893, 2.9318, 2.7670);
    case 231:
        return vec3(3.4242, 2.3459, 1.7704);
    case 232:
        return vec3(7.4746, 6.3995, 5.3031);
    case 233:
        return vec3(3.3314, 3.3291, 3.1350);
    case 234:
        return vec3(3.6092, 2.6248, 2.2921);
    case 235:
        return vec3(3.5100, 3.4000, 3.1800);
    case 236:
        return vec3(4.2655, 3.7153, 3.1365);
    case 237:
        return vec3(3.9291, 3.1900, 2.3808);
    case 255:
        return vec3(1.0);
    default:
        return vec3(1.0);
    }
}
const bool colortex4MipmapEnabled = true;
const bool colortex6MipmapEnabled = true;

vec4 calculateSpecularLightingHQ(
    vec3 normal,
    vec3 np3,
    vec3 albedo,
    float shading,
    float skylight,
    float roughness,
    vec3 f0,
    float porosity,
    vec3 fragpos,
    vec4 scene,
    float alpha0)
{
#ifdef WATER
    bool inWater = (isEyeInWater == 1);

    float refractiveIndexAir = 1.0;    // Refractive index of air
    float refractiveIndexWater = 1.33; // Refractive index of water

    // Determine the incident and transmitted refractive indices based on the environment
    float etaI = inWater ? refractiveIndexWater : refractiveIndexAir;
    float etaT = inWater ? refractiveIndexAir : refractiveIndexWater;
#endif
    ////////
    vec2 texcoord = gl_FragCoord.xy * viewResolution;
    vec2 texSize2 = vec2(1) / textureSize(colortex12, 0); // Get the size of the base mipmap level (LOD 0)
    float noise = blueNoise(gl_FragCoord.xy);

    // Compute metal index from f0.y
    int metalidx = int((f0.y * 255));
    bool isMetal = (metalidx > 229);
    bool isNotHardcoded = (metalidx == 255);
    //  roughness = 0.0;
    if (isMetal)
    {

#ifdef HARDCODED_METALS

        vec3 eta_k = GetK(metalidx);
        vec3 eta = GetN(metalidx);

        f0 = ((eta - vec3(1.0)) * (eta - vec3(1.0)) + (eta_k * eta_k)) / ((eta + vec3(1.0)) * (eta + vec3(1.0)) + (eta_k * eta_k));
        if (isNotHardcoded)
#endif
            f0 = albedo;
    }
    // Modify roughness based on wetness and porosity
    float rainMult = sqrt(skylight) * wetness * (1.0 - pow(porosity, 2.0));
    roughness = mix(roughness, 0.01, rainMult);
    // Sun specular term
    vec3 specTerm = shading * GGX2(normal, -np3, lightPos, roughness, f0);

    // Indirect specular calculation
    vec3 indirectSpecular = vec3(0.0);
    vec3 fresnelDiffuse = vec3(0.0);
    int nSpecularSamples = int(mix(float(REFLECTION_SAMPLES), float(REFLECTION_SAMPLES_MAX), roughness));

    // Coordinate basis for sampling
    mat3 basis = CoordBase(normal);
    vec3 normSpaceView = normalize(-np3 * basis);

    // Texture size and LOD calculation
    // ivec2 texSize = textureSize(colortex4, 0); // Get the size of the base mipmap level (LOD 0)
    // float maxLod = float(log2(float(max(texSize.x, texSize.y))));
    float lod = mix(1.5, 8.0, roughness);
    vec3 reflectionColorScene = scene.rgb;

    for (int i = 0; i < nSpecularSamples; i++)
    {
        // Generate random samples
        int seed = int(frameCounter) * nSpecularSamples + i;
        vec2 ij = fract(R2_samples(seed) + blueNoise4(gl_FragCoord.xy).rg);

        // Sample GGX VNDF
        vec3 H = sampleGGXVNDF(normSpaceView, roughness, roughness, ij.x, ij.y);
        vec3 Ln = reflect(-normSpaceView, H);
        vec3 L = basis * Ln;
        vec3 L2 = L;
        // Fresnel Schlick with TIR handling
        float cosThetaI = dot(-Ln, H); // cosine of the angle between incident light and half-vector

        // Calculate sin^2 of the transmitted angle using Snell's law
        float sinThetaTSq = etaI * etaI * (1.0 - cosThetaI * cosThetaI);

        // Fresnel-Schlick approximation with TIR handling
        vec3 F = f0 + (1.0 - f0) * pow(clamp(1.0 + dot(-Ln, H), 0.0, 1.0), 5.0);
#ifdef WATER
        if (sinThetaTSq > 1.0)
        {
            L2.y = -L2.y;
            F = vec3(1.0);
        }
#endif
        // Geometry term
        float NoL = clamp(dot(normal, L), 0.0, 1.0);
        float G1 = g(NoL, roughness);
        float bounceAmount = float(L.y > 0.0) + clamp(-L.y * 0.1 + 0.1, 0.0, 1.0);

        // Ray contribution
        vec3 rayContrib = F * G1;
        vec3 reflectionColor = vec3(0.0);

        // Skip calculations if ray does not contribute much to the lighting
        if (luma(rayContrib) > 0.02)
        {
            //   reflectionColor = vec3(1.0, 0, 0);
            float rayQuality = 35.0 * sqrt(luma(rayContrib));

            if (rayQuality > 5.0)
            {

                // Trace reflection ray
                vec3 rtPos = RT5(mat3(gbufferModelView) * L, fragpos, fract(ij.x + ij.y), rayQuality);
                // Reproject on previous frame

#ifndef DISTANT_HORIZONS
                if (rtPos.z < 1.0)
#else
                if (rtPos.z < 1.1)
#endif
                {
                    vec3 previousPosition = mat3(gbufferModelViewInverse) * toScreenSpace(rtPos) + gbufferModelViewInverse[3].xyz + cameraPosition - previousCameraPosition;
                    previousPosition = mat3(gbufferPreviousModelView) * previousPosition + gbufferPreviousModelView[3].xyz;
                    previousPosition.xy = projMAD(gbufferPreviousProjection, previousPosition).xy / -previousPosition.z * 0.5 + 0.5;

                    if (distance(texcoord, rtPos.xy) * 100 > 1)
                    {
                        if (previousPosition.x > 0.0 && previousPosition.y > 0.0 && previousPosition.x < 1.0 && previousPosition.y < 1.0)
                        {
                            reflectionColor = textureLod(colortex4, previousPosition.xy, lod).rgb;
                        }
                    }
                }
                else
                {
                    vec4 sky = skyFromTexLod(L2, colortex6, lod);
                    sky.rgb = (sky.rgb + (sky.a * sunLight));
                    reflectionColor = mix(reflectionColorScene, sky.rgb, skylight * bounceAmount);
                }
            }

            indirectSpecular += reflectionColor * rayContrib;

            // Accumulate Fresnel diffuse term
            fresnelDiffuse += rayContrib;
        }
    }

    // Average indirect specular and fresnel diffuse terms
    vec3 a = (indirectSpecular / float(nSpecularSamples)) + (specTerm * sunLight);
    vec3 b = (vec3(1.0) - fresnelDiffuse / float(nSpecularSamples));
    b = (isMetal ? vec3(0.0) : b);

    //////////////////
#ifdef WATER

    float fresnel = luma(fresnelDiffuse) / float(nSpecularSamples);
    float effectiveAlpha = scene.a * (1.0 - fresnel) + fresnel;

    a *= (inWater ? scene.a : 1.0);

    vec3 baseColorCorrected = scene.rgb * alpha0 * (1.0 - fresnel);
    if (!isMetal)
        albedo = vec3(1.0);
    vec3 outcolor = (a + b * baseColorCorrected.rgb) * albedo;
    float outalpha = effectiveAlpha;

    return vec4(outcolor, outalpha);
#else
    return encodeTwoVec3(a, b);
#endif
}

vec4 calculateSpecularLightingLQ(
    vec3 normal,
    vec3 np3,
    float shading,
    float skylight,
    vec3 fragpos,
    vec4 scene,
    float alpha0)
{
    // Precomputed constants
    const float f0 = 0.02;

#ifdef WATER
    bool inWater = (isEyeInWater == 1);
    const float refractiveIndexAir = 1.0;
    const float refractiveIndexWater = 1.333;
    float etaI = inWater ? refractiveIndexWater : refractiveIndexAir;
#endif

    // Precompute texture coordinates scaling
    vec2 texcoordScale = viewResolution;
    vec2 texcoord = gl_FragCoord.xy * texcoordScale;

    // Calculate the specular term using the GGX distribution with precomputed roughness
    vec3 specTerm = shading * GGX2(normal, -np3, lightPos, 0.025, vec3(f0));

    // Reflect the view direction over the normal
    vec3 L = reflect(np3, normal);
    vec3 L2 = L;

    // Calculate Fresnel term with precomputed f0
    float cosThetaI = dot(np3, normal);
    float clampedCosThetaI = clamp(1.0 + cosThetaI, 0.0, 1.0);
    float F = f0 + (1.0 - f0) * pow(clampedCosThetaI, 5.0);

#ifdef WATER
    // Precompute etaI squared
    float etaISq = etaI * etaI;
    float sinThetaTSq = etaISq * (1.0 - cosThetaI * cosThetaI);
    if (sinThetaTSq > 1.0)
    {
        L2.y = -L2.y;
        F = 1.0;
    }
#endif

    // Geometry term simplified for smooth surfaces with precomputed k
    float NoL = clamp(dot(normal, L), 0.0, 1.0);
    float G1 = NoL / (NoL * 0.99995 + 0.00005);

    // Calculate the ray's contribution to the final color
    float rayContrib = F * G1;
    vec3 reflectionColor = vec3(0.0);

    // Sample the sky color
    vec4 sky = skyFromTexLod(L2, colortex6, 0);
    sky.rgb += sky.a * sunLight;
    reflectionColor = mix(scene.rgb + sky.a * 0.1, sky.rgb, skylight);

#if SSR_STEPS > 0
    if (isEyeInWater != 1)
    {
        // Trace reflection ray for screen-space reflections
    #ifndef DISTANT_HORIZONS
        vec3 rtPos = RT5XLQ(mat3(gbufferModelView) * L, fragpos, noise_standard(gl_FragCoord.xy), SSR_STEPS);

        if (rtPos.z < 1.0)
    #else
        vec3 rtPos = RT5LQ(mat3(gbufferModelView) * L, fragpos, noise_standard(gl_FragCoord.xy), SSR_STEPS);

        if (rtPos.z < 1.1)
    #endif
        {
            vec3 previousPosition = mat3(gbufferModelViewInverse) * toScreenSpace(rtPos) + gbufferModelViewInverse[3].xyz + cameraPosition - previousCameraPosition;
            previousPosition = mat3(gbufferPreviousModelView) * previousPosition + gbufferPreviousModelView[3].xyz;
            vec2 previousUV = projMAD(gbufferPreviousProjection, previousPosition).xy / -previousPosition.z * 0.5 + 0.5;

            vec2 delta = texcoord - rtPos.xy;
            if (dot(delta, delta) > 0.0001)
            {
                if (all(greaterThan(previousUV, vec2(0.0))) && all(lessThan(previousUV, vec2(1.0))))
                {
                    reflectionColor = texture(colortex4, previousUV).rgb;
                }
            }
        }
    }
#endif

    // Calculate indirect specular reflection
    vec3 indirectSpecular = reflectionColor * rayContrib;
    float fresnelDiffuse = rayContrib;

    // Combine the specular and diffuse components
    vec3 a = indirectSpecular + specTerm * sunLight;
    float b = 1 - fresnelDiffuse;

#ifdef WATER
    float fresnel = fresnelDiffuse;
    float effectiveAlpha = mix(scene.a, 1.0, fresnel);

    a *= inWater ? scene.a : 1.0;

    vec3 baseColorCorrected = scene.rgb * alpha0 * (1.0 - fresnel);
    vec3 outcolor = a + b * baseColorCorrected;
    float outalpha = effectiveAlpha;

    return vec4(outcolor, outalpha);
#else
    return encodeTwoVec3(a, vec3(b));
#endif
}

#ifdef DISTANT_HORIZONS

vec4 calculateSpecularLightingDH(

    vec3 normal,
    vec3 np3,
    float shading,
    float skylight,
    vec3 fragpos,
    vec4 scene,
    float alpha0)
{
    // Precomputed constants
    const float f0 = 0.02;

    #ifdef WATER
    bool inWater = (isEyeInWater == 1);
    const float refractiveIndexAir = 1.0;
    const float refractiveIndexWater = 1.333;
    float etaI = inWater ? refractiveIndexWater : refractiveIndexAir;
    #endif

    // Precompute texture coordinates scaling
    vec2 texcoordScale = viewResolution;
    vec2 texcoord = gl_FragCoord.xy * texcoordScale;

    // Calculate the specular term using the GGX distribution with precomputed roughness
    vec3 specTerm = shading * GGX2(normal, -np3, lightPos, 0.025, vec3(f0));

    // Reflect the view direction over the normal
    vec3 L = reflect(np3, normal);
    vec3 L2 = L;

    // Calculate Fresnel term with precomputed f0
    float cosThetaI = dot(np3, normal);
    float clampedCosThetaI = clamp(1.0 + cosThetaI, 0.0, 1.0);
    float F = f0 + (1.0 - f0) * pow(clampedCosThetaI, 5.0);

    #ifdef WATER
    // Precompute etaI squared
    float etaISq = etaI * etaI;
    float sinThetaTSq = etaISq * (1.0 - cosThetaI * cosThetaI);
    if (sinThetaTSq > 1.0)
    {
        L2.y = -L2.y;
        F = 1.0;
    }
    #endif

    // Geometry term simplified for smooth surfaces with precomputed k
    float NoL = clamp(dot(normal, L), 0.0, 1.0);
    float G1 = NoL / (NoL * 0.99995 + 0.00005);

    // Calculate the ray's contribution to the final color
    float rayContrib = F * G1;
    vec3 reflectionColor = vec3(0.0);

    // Sample the sky color
    vec4 sky = skyFromTexLod(L2, colortex6, 0);
    sky.rgb += sky.a * sunLight;
    reflectionColor = mix(scene.rgb + sky.a * 0.1, sky.rgb, skylight);

    // Trace reflection ray
    vec3 rtPos = RT5(mat3(gbufferModelView) * L, fragpos, blueNoise(gl_FragCoord.xy), SSR_STEPS);
    // Reproject on previous frame

    if (rtPos.z < 1.0)
    {
        vec3 previousPosition =
            reprojectDH(mat3(gbufferModelViewInverse) * toScreenSpaceDH(rtPos) + gbufferModelViewInverse[3].xyz, false);
        if (distance(texcoord, rtPos.xy) * 100 > 1)
        {
            if (previousPosition.x > 0.0 && previousPosition.y > 0.0 && previousPosition.x < 1.0 && previousPosition.y < 1.0)
            {
                reflectionColor = texture(colortex4, previousPosition.xy).rgb;
            }
        }
    }

    // Calculate indirect specular reflection
    vec3 indirectSpecular = reflectionColor * rayContrib;
    float fresnelDiffuse = rayContrib;

    // Combine the specular and diffuse components
    vec3 a = indirectSpecular + specTerm * sunLight;
    float b = 1 - fresnelDiffuse;

    #ifdef WATER
    float fresnel = fresnelDiffuse;
    float effectiveAlpha = mix(scene.a, 1.0, fresnel);

    a *= inWater ? scene.a : 1.0;

    vec3 baseColorCorrected = scene.rgb * alpha0 * (1.0 - fresnel);
    vec3 outcolor = a + b * baseColorCorrected;
    float outalpha = effectiveAlpha;

    return vec4(outcolor, outalpha);
    #else
    return encodeTwoVec3(a, b);
    #endif
}
#endif
vec4 reflections_HQ(float skyLight, vec3 normal, vec3 fragpos, vec4 materialProps, vec4 outcolor, float shading, vec3 albedo, float alpha0)
{

    vec3 p3 = mat3(gbufferModelViewInverse) * fragpos;
    vec3 f0 = vec3(materialProps.z);

    float porosity = 0;

    vec4 reflection = calculateSpecularLightingHQ(normal, normalize(p3), albedo, shading, skyLight, pow(1.0 - materialProps.w, 2.0), f0, porosity, fragpos, outcolor, alpha0);

    return reflection;
}
vec4 reflections_LQ(float skyLight, vec3 normal, vec3 fragpos, vec4 materialProps, vec4 outcolor, float shading, vec3 albedo, float alpha0)
{

    vec3 p3 = mat3(gbufferModelViewInverse) * fragpos;
    vec4 reflection = calculateSpecularLightingLQ(normal, normalize(p3), shading, skyLight, fragpos, outcolor, alpha0);

    return reflection;
}
#ifdef DISTANT_HORIZONS

vec4 reflections_DH(float skyLight, vec3 normal, vec3 fragpos, vec4 materialProps, vec4 outcolor, float shading, vec3 albedo, float alpha0)
{

    vec3 p3 = mat3(gbufferModelViewInverse) * fragpos;
    vec3 f0 = vec3(materialProps.z);

    float porosity = 0;

    vec4 reflection = calculateSpecularLightingDH(normal, normalize(p3), shading, skyLight, fragpos, outcolor, alpha0);

    return reflection;
}
#endif
vec3 reflections(vec3 direct, float fresnel, float sunSpec, float skyLight, vec3 normal, float watermult, vec3 fragpos, vec4 materialProps)
{
    fresnel = pow(fresnel, 0.75);
    vec3 reflectedVector = reflect(normalize(fragpos), normal);
    vec3 reflection = vec3(0.0);
    vec4 tempreflection = vec4(0.0);
    vec4 sky = vec4(1.0);
    if (pow(skyLight, 3) > 0.0)
    {
        vec3 wrefl = mat3(gbufferModelViewInverse) * reflectedVector;

        wrefl.y *= watermult;

        sky = skyFromTex2((wrefl), gaux3);
        reflection.rgb = (sky.rgb + (sky.a * direct * 1)) * pow(skyLight, 2);
    }
#if SSR_STEPS > 0
    vec3 rtPos = rayTrace(reflectedVector, fragpos.xyz, noise_standard(gl_FragCoord.xy));

    vec3 previousPosition =
        reproject(mat3(gbufferModelViewInverse) * toScreenSpace(rtPos) + gbufferModelViewInverse[3].xyz, false);
    #ifndef DISTANT_HORIZONS
    if (all(greaterThanEqual(previousPosition.xy, vec2(0.0))) && rtPos.z < 1.0)
    #else
    if (all(greaterThanEqual(previousPosition.xy, vec2(0.0))) && rtPos.z < 1.1)
    #endif
    {

        tempreflection = vec4(texture(gaux1, previousPosition.xy).rgb, 1);
    }
#endif
    if (isEyeInWater == 1)
    {

        reflection = mix(
            mix(min(specularWater((normal), normalize(fragpos)), 16.0) * reflection * 8.0, vec3(0), 1 - fresnel),
            tempreflection.rgb, tempreflection.a);
    }
    else
    {
        reflection = mix(reflection.rgb, tempreflection.rgb, tempreflection.a) * fresnel;
    }

    reflection = (reflection.rgb) + sunSpec * direct;

    return reflection;
}

//////////
vec3 waterNormal(vec2 coord, vec3 normFragpos)
{
    coord = mod(coord / 128.0, 1.0);
    float strength = mix(2.0, 3.0, float(isEyeInWater == 1));

    float lod = clamp((pow(1.0 + dot(normFragpos, normal0), 4)), 0, 1);

    float deltaPos = 0.0015;
    deltaPos = mix(deltaPos, 0.0, lod * 1.0);
    strength = mix(strength, 0.0, lod * 0.9);

    vec2 offsets[4] = vec2[](vec2(deltaPos, 0.0), vec2(-deltaPos, 0.0), vec2(0.0, deltaPos), vec2(0.0, -deltaPos));

    float heights[4];
    for (int i = 0; i < 4; i++)
    {
        heights[i] = texture(gaux4, coord + offsets[i]).x;
    }

    float dx = (heights[1] - heights[0]) * strength;
    float dy = (heights[3] - heights[2]) * strength;
    vec3 full = normalize(vec3(dx, dy, 0.5)) + 0.5;
    // full = vec3(0.5, 0.5, 1.0);
    return full;
}
