I am building a 3d renderer in C++ using Vulkan.hpp. I am in the process of implementing a SSAO stage into the rendering pipeline. The shader takes in a depth buffer and reconstructs the position information using this buffer. This means the shader needs to manage different projections and the near and far planes of the projection. The output looks like something resembling SSAO, but I don't believe it is handling sampling, or possibly normals, correctly. The output does not change with the direction I am looking at an object and there is occlusion occurring on the convex portion of meshes, which doesn't see right.

Can you take a look at the following the shader code to diagnose and fix any issues? Please provide the entire updated code in your response.

#version 450

layout(location = 0) out vec4 FragColor;

layout(location = 0) in vec2 TexCoords;

layout(set = 0, binding = 0) uniform sampler2D gDepth;
layout(set = 0, binding = 1) uniform sampler2D gNormal;
layout(set = 0, binding = 2) uniform sampler2D texNoise;

layout(binding = 3) buffer SamplesBufferObject {
    vec3 samples[];
} samplesUBO;

layout(binding = 4) uniform InputsBufferObject {
    int kernelSize;
    float radius;
    float bias;
    vec2 noiseScale;
} inputsUBO;

layout(binding = 5) uniform ProjBufferObject {
    mat4 proj;
    mat4 projInv;
} projUBO;

void main() {
    float nearPlane = 0.1;
    float farPlane = 100.0;

    // Read depth from depth buffer
    float depth = texture(gDepth, TexCoords).r;

    // Linearize depth
    float linearDepth = (2.0 * nearPlane) / 
                       (farPlane + nearPlane - 
                        depth * (farPlane - nearPlane));

    // Reconstruct view-space position
    vec4 fragPosCS = vec4(TexCoords * 2.0 - 1.0, linearDepth, 1.0);
    vec4 fragPosVS = projUBO.projInv * fragPosCS;
    fragPosVS /= fragPosVS.w;  // Perspective division

    // Calculate fragment's distance from camera
    float fragDistance = length(fragPosVS.xyz);

    // Get normal and transform to view space
    vec3 normal = texture(gNormal, TexCoords).rgb;
    normal = normalize(normal);

    // Calculate noise vector
    vec2 noiseVec = texture(texNoise, TexCoords * inputsUBO.noiseScale).xy;
    noiseVec = 2.0 * noiseVec - 1.0;
    noiseVec *= 0.5; // Reduce noise intensity

    // Adaptive radius based on distance
    float distanceFactor = pow(fragDistance / 100.0, 2.0); // Adjust divisor as needed
    float adaptiveRadius = inputsUBO.radius * (1.0 - 0.1 * distanceFactor);
    float radius = adaptiveRadius;

    // Calculate occlusion
    float occlusion = 0.0;

    for (int i = 0; i < inputsUBO.kernelSize; i++) {
        // Get sample position
        vec3 samplePos = samplesUBO.samples[i];
        samplePos *= radius;

        // Perturb sample position with noise
        vec3 perturbedSample = samplePos + noiseVec.xyy * radius * 0.5;

        // Calculate sample's texture coordinates
        vec2 sampleTexCoords = TexCoords + perturbedSample.xy * (1.0 / 2000.0);

        // Ensure sampleTexCoords are within [0, 1] range
        sampleTexCoords = clamp(sampleTexCoords, 0.001, 0.999);

        // Get depth of sample position
        float sampleDepth = texture(gDepth, sampleTexCoords).r;

        // Linearize sample depth
        float linearSampleDepth = (2.0 * nearPlane) / 
                                 (farPlane + nearPlane - 
                                  sampleDepth * (farPlane - nearPlane));

        // Reconstruct sample's view space position
        vec4 sampleDepthVS = vec4(sampleTexCoords * 2.0 - 1.0, linearSampleDepth, 1.0);
        sampleDepthVS = projUBO.projInv * sampleDepthVS;
        sampleDepthVS /= sampleDepthVS.w;

        // Calculate distance between fragment and sample
        float distance = length(fragPosVS.xyz - sampleDepthVS.xyz);

        // Normal-based weighting using absolute value
        vec3 sampleDir = normalize(perturbedSample);
        float normalDot = abs(dot(normal, sampleDir));

        // Occlusion accumulation
        if (distance < radius) {
            float falloff = 1.0 - (distance / radius);
            falloff = max(falloff, 0.0);
            occlusion += falloff * normalDot;
        }
    }

    // Normalize occlusion
    occlusion /= float(inputsUBO.kernelSize);

    // Apply bias
    occlusion = max(occlusion - inputsUBO.bias, 0.0);

    FragColor = vec4(occlusion, occlusion, occlusion, 1.0);
}