
#ifndef LAB_PBR
    #undef POM
#endif
const float MAX_OCCLUSION_DISTANCE = MAX_DIST;
const float MIX_OCCLUSION_DISTANCE = MAX_DIST * 0.9;
const int MAX_OCCLUSION_POINTS = MAX_ITERATIONS;

const float mincoord = 1.0 / 4096.0;

varying vec4 vtexcoordam; // .st for add, .pq for mul
varying vec2 vtexcoord;

vec2 dcdx = dFdx(vtexcoord.st * vtexcoordam.pq) * exp2(0);
vec2 dcdy = dFdy(vtexcoord.st * vtexcoordam.pq) * exp2(0);

vec4 readNormal(in vec2 coord)
{
    return texture2DGradARB(normals, fract(coord) * vtexcoordam.pq + vtexcoordam.st, dcdx, dcdy);
}
vec4 readTexture(in vec2 coord)
{
    return texture2DGradARB(texture, fract(coord) * vtexcoordam.pq + vtexcoordam.st, dcdx, dcdy);
}

vec2 pomColor(vec2 uv, vec3 fragpos, float noise, mat3 tbn)
{

    vec3 coord = vec3(vtexcoord.st, 1.0);
    vec2 adjustedTexCoord = uv;
    vec3 viewVector = normalize(tbn * fragpos);
    float dist = length(fragpos);
    gl_FragDepth = gl_FragCoord.z;

    if (dist < MAX_OCCLUSION_DISTANCE)
    {

        if (viewVector.z < 0.0 && readNormal(vtexcoord.st).a < 0.9999 && readNormal(vtexcoord.st).a > 0.00001)
        {

            vec3 interval = viewVector.xyz / -viewVector.z / MAX_OCCLUSION_POINTS * POM_DEPTH;
            vec2 atlasAspect = vec2(atlasSize.y / float(atlasSize.x), atlasSize.x / float(atlasSize.y));
            vec2 viewCorrection = max(vec2((vtexcoordam.q) / (vtexcoordam.p) * atlasAspect.x, 1.0),
                                      vec2(1.0, (vtexcoordam.p) / (vtexcoordam.q) * atlasAspect.y));
            interval.xy *= viewCorrection;

            coord += noise * interval;
            float sumVec = noise;
            for (int loopCount = 0; (loopCount < MAX_OCCLUSION_POINTS) &&
                                    (1.0 - POM_DEPTH + POM_DEPTH * readNormal(coord.st).a < coord.p) && coord.p >= 0.0;
                 ++loopCount)
            {
                coord = coord + interval;
                sumVec += 1.0;
            }

            if (coord.t < mincoord)
            {
                if (readTexture(vec2(coord.s, mincoord)).a == 0.0)
                {

                    coord.t = mincoord;

                    discard;
                }
            }

            adjustedTexCoord =
                mix(fract(coord.st) * vtexcoordam.pq + vtexcoordam.st, adjustedTexCoord,
                    max(dist - MIX_OCCLUSION_DISTANCE, 0.0) / (MAX_OCCLUSION_DISTANCE - MIX_OCCLUSION_DISTANCE));

            vec3 truePos = fragpos + sumVec * inverse(tbn) * interval;
            gl_FragDepth = toClipSpace3(truePos).z;
        }
    }
    return adjustedTexCoord;
}

float GetParallaxShadow(float parallaxFade, vec2 coord)
{

    vec3 tangent2 = normalize(cross(tangent.rgb, normalMat2.rgb) * tangent.w);
    mat3 tbn = mat3(
        tangent.x, tangent2.x, normalMat2.x,
        tangent.y, tangent2.y, normalMat2.y,
        tangent.z, tangent2.z, normalMat2.z);

    float parallaxshadow = 1.0;
    float dither = noise_standard(gl_FragCoord.xy);
    vec3 parallaxdir = tbn * sunVec;
    parallaxdir.xy *= 1.0 * POM_DEPTH;
    float height = texture(normals, coord, 0).a;
    for (int i = 0; i < 4 && parallaxshadow >= 0.01; i++)
    {
        float stepLC = 0.025 * (i + dither);

        float currentHeight = height + parallaxdir.z * stepLC;

        vec2 parallaxCoord = fract(coord + parallaxdir.xy * stepLC) * vtexcoordam.pq + vtexcoordam.st;
        float offsetHeight = textureGrad(normals, parallaxCoord, dcdx, dcdy).a;

        parallaxshadow *= clamp(1.0 - (offsetHeight - currentHeight) * 4.0, 0.0, 1.0);
    }

    return mix(parallaxshadow, 1.0, 0.25);
}

vec3 calculatePOMNormal(vec2 uv, mat3 tbn)
{
    vec2 texel = 1.0 / vec2(textureSize(texture, 0));

    vec2 size = (tileSize * vec2(textureSize(texture, 0)) * 0.5);

    vec2 boundary = texel * size;
    float mult = 16.0;

#ifdef PIXEL_PACK
    texel *= 0.13;
#endif
    // texel *= 2;

    vec2 offsets[4] = vec2[](
        vec2(texel.x, 0.0),  // Right
        vec2(-texel.x, 0.0), // Left
        vec2(0.0, texel.y),  // Up
        vec2(0.0, -texel.y)  // Down
    );

    float hR = texture(normals, abs(uv + offsets[0] - midtex).x > boundary.x ? uv : uv + offsets[0]).a;
    float hL = texture(normals, abs(uv + offsets[1] - midtex).x > boundary.x ? uv : uv + offsets[1]).a;
    float hU = texture(normals, abs(uv + offsets[2] - midtex).y > boundary.y ? uv : uv + offsets[2]).a;
    float hD = texture(normals, abs(uv + offsets[3] - midtex).y > boundary.y ? uv : uv + offsets[3]).a;

    vec3 tangentNormal = normalize(vec3((hL - hR) * mult, (hD - hU) * mult, 2.0 - POM_DEPTH));
    return normalize(tangentNormal * tbn);
}

vec2 pomColorAndNormal(vec2 uv, vec3 fragPos, float noise, mat3 tbn, inout vec3 normalOut)
{
    gl_FragDepth = gl_FragCoord.z;

    // Early out if heightmap is missing or invalid
    float encodedHeight = readNormal(vtexcoord.st).a;
    if (encodedHeight <= 0.00001 || encodedHeight >= 0.9999)
    {
        return uv;
    }
    
    // Base UV coordinates from your original attributes
    vec3 coord = vec3(vtexcoord.st, 1.0);
    vec2 baseTexCoord = fract(vtexcoord.st) * vtexcoordam.pq + vtexcoordam.st;

    // Compute view direction (in tangent space) and distance
    vec3 viewDir = normalize(tbn * fragPos);
    float viewDist = length(fragPos);
    // Check if we're within the range and we need to apply POM
    bool withinDistance = viewDist < MAX_OCCLUSION_DISTANCE;
    bool needsPOM = viewDir.z < 0.0;

    // Start with the base UV as the final result
    vec2 adjustedTexCoord = baseTexCoord;

    if (withinDistance && needsPOM)
    {
        // Calculate the step interval in tangent space.
        float maxSteps = float(MAX_OCCLUSION_POINTS);
        vec3 interval = viewDir / -viewDir.z / maxSteps * POM_DEPTH;

        // Correct for texture atlas aspect ratio differences
        vec2 atlasAspect = vec2(atlasSize.y / float(atlasSize.x), atlasSize.x / float(atlasSize.y));
        vec2 viewCorrection = max(vec2(vtexcoordam.q / vtexcoordam.p * atlasAspect.x, 1.0), vec2(1.0, vtexcoordam.p / vtexcoordam.q * atlasAspect.y));
        interval.xy *= viewCorrection;

        // Offset the initial coordinate by a noise-based amount for variation
        coord += noise * interval;
        float stepsTaken = noise;

        // Iterative parallax search (ray marching)
        int i;
        for (i = 0; i < MAX_OCCLUSION_POINTS; ++i)
        {
            float heightFromMap = readNormal(coord.st).a;
            float surfaceDepth = 1.0 - POM_DEPTH + POM_DEPTH * heightFromMap;

            if (coord.p < 0.0 || surfaceDepth >= coord.p)
            {
                break;
            }
            coord += interval;
            stepsTaken += 1.0;
        }

        // Binary search refinement
        if (i > 0)
        {
            vec3 lowCoord = coord - interval;
            vec3 highCoord = coord;
            const int NUM_REFINE_ITERATIONS = 4;
            for (int j = 0; j < NUM_REFINE_ITERATIONS; j++)
            {
                vec3 midCoord = (lowCoord + highCoord) * 0.5;
                float midHeight = readNormal(midCoord.st).a;
                float midSurfaceDepth = 1.0 - POM_DEPTH + POM_DEPTH * midHeight;
                if (midSurfaceDepth < midCoord.p)
                {
                    lowCoord = midCoord;
                }
                else
                {
                    highCoord = midCoord;
                }
            }
            coord = highCoord;
        }

        // Handle out-of-range texel with alpha == 0
        if (coord.t < mincoord)
        {
            if (readTexture(vec2(coord.s, mincoord)).a == 0.0)
            {
                coord.t = mincoord;
                discard;
            }
        }

        // Blend displaced UV with base UV
        float fadeFactor = clamp((viewDist - MIX_OCCLUSION_DISTANCE) / (MAX_OCCLUSION_DISTANCE - MIX_OCCLUSION_DISTANCE), 0.0, 1.0);
        adjustedTexCoord = mix(fract(coord.st) * vtexcoordam.pq + vtexcoordam.st, baseTexCoord, fadeFactor);
#ifndef HAND
        // Update depth
        vec3 displacedPos = fragPos + stepsTaken * inverse(tbn) * interval;
        gl_FragDepth = toClipSpace3(displacedPos).z;
#endif

        // Compute new normal

        normalOut = calculatePOMNormal(adjustedTexCoord, tbn);
    }

    return adjustedTexCoord;
}
float interleaved_gradientNoise()
{
    return fract(52.9829189 * fract(0.06711056 * gl_FragCoord.x + 0.00583715 * gl_FragCoord.y) + frameTimeCounter * 51.9521);
}
vec2 applyPOM(vec2 tcoord, vec3 fragpos, inout vec3 pomnormal)
{
    // Compute TBN matrix
    vec3 tangent2 = normalize(cross(tangent.rgb, normalMat2.rgb) * tangent.w);
    mat3 tbnMatrix2 = mat3(
        tangent.x, tangent2.x, normalMat2.x,
        tangent.y, tangent2.y, normalMat2.y,
        tangent.z, tangent2.z, normalMat2.z);

    // Apply POM
    vec2 newTcoord = pomColorAndNormal(tcoord, fragpos, interleaved_gradientNoise(), tbnMatrix2, pomnormal);

    return newTcoord;
}