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, but I am running into some issues. The output of the shader is a black image. After extensive debugging, I believe I have narrowed down the issue to the sampling, offset or rangecheck components of the shader. There is some occlusion being created by the shader, but it is very minor. It is more visible on very close objects, but not noticeable at all on even medium distance objects. I do wonder whether the range check is working correctly or whether it need to be adjusted by the nearPlane and farPlane values. 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. I don't believe there are any issues with this, but it is worth noting for reference.

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.

Key variable values:

ssaoSamples = 64 ssaoNoiseSize = 8

ssaoUBO.kernelSize = 64; ssaoUBO.bias = 0.025; ssaoUBO.noiseScale = swapChainExtent / 2; ssaoUBO.radius = 0.5f;

SSAO fragment shader code:

#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;

vec3 reconstructPosition(vec2 texCoords, float depth, mat4 projInv) {
    vec4 fragPosCS = vec4(texCoords * 2.0 - 1.0, depth, 1.0);
    vec4 fragPosVS = projInv * fragPosCS;
    fragPosVS /= fragPosVS.w;
    return fragPosVS.xyz;
}

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

    float depth = texture(gDepth, TexCoords).r;
    float linearDepth = (2.0 * nearPlane * farPlane) /
                       (farPlane + nearPlane - depth * (farPlane - nearPlane));
    vec3 fragPos = reconstructPosition(TexCoords, linearDepth, projUBO.projInv);

    vec3 normal = texture(gNormal, TexCoords).rgb;
    normal = normalize(normal);

    // Improved noise sampling and TBN construction
    vec2 noiseUV = fract(TexCoords * inputsUBO.noiseScale); // Ensure noise wraps seamlessly
    vec3 randomVec = texture(texNoise, noiseUV).xyz * 2.0 - 1.0;
    randomVec = normalize(randomVec);

    // Better TBN matrix construction
    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);

    float occlusion = 0.0;
    float adjustedRadius = inputsUBO.radius * (linearDepth / farPlane);

    for (int i = 0; i < inputsUBO.kernelSize; i++) {
        vec3 sampleDir = TBN * samplesUBO.samples[i];
        sampleDir *= adjustedRadius;

        vec3 samplePos = fragPos + sampleDir;

        // Transform sample position to clip space
        vec4 offsetCS = projUBO.proj * vec4(samplePos, 1.0);
        offsetCS.xyz /= offsetCS.w;
        offsetCS.xyz = offsetCS.xyz * 0.5 + 0.5;

        // Improved range checking and boundary handling
        if (offsetCS.x < 0.0 || offsetCS.x > 1.0 || offsetCS.y < 0.0 || offsetCS.y > 1.0) {
            continue;
        }

        float sampleDepth = texture(gDepth, offsetCS.xy).r;
        float sampleLinearDepth = (2.0 * nearPlane * farPlane) /
                                 (farPlane + nearPlane - sampleDepth * (farPlane - nearPlane));

        float distance = length(sampleDir);
        float rangeCheck = smoothstep(adjustedRadius - 0.02, adjustedRadius, distance);

        // Increased bias to better catch occluded samples
        float depthCompare = sampleLinearDepth >= (linearDepth + 0.05) ? 1.0 : 0.0;

        occlusion += (1.0 - depthCompare) * rangeCheck;
    }

    occlusion = occlusion / float(inputsUBO.kernelSize);
    // Scale occlusion to enhance visibility
    occlusion = 1.0 - occlusion;

    // Additional contrast boost
    occlusion = 1.0 - pow(occlusion, 0.75);

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