Issues sampling position from depth buffer for SSAO shader (source code included)

I’ve been implementing SSAO based on the article here and I’ve been running into several issues, mainly with sampling the position from the depth buffer. I’ve tried numerous methods but all seem to give me undesirable effects, though they all seem to have something in common: the SSAO effect looks correct at a small portion of the center of the screen but begins to distort around that center.

I am thinking maybe it may have something to do with how I am setting up my projection matrix, but I can’t say for sure. I’d appreciate to anyone that can explain to me what I am doing wrong.

Here’s the code for setting up the SSAO shader:

    // setup SSAO shader
    kexVec2 noiseScale((float)sysMain.VideoWidth() / 4.0f,
                       (float)sysMain.VideoHeight() / 4.0f);
    
    kexMatrix invProj = kexMatrix::Invert(projection);
    float uFov = kexMath::Tan(74.0f / 2.0f); // 74.0f is the camera's FOV which is also set in the projection matrix
    
    ssaoTest->Enable();
    ssaoTest->SetUniform("uDepthBuffer", 0);
    ssaoTest->SetUniform("uNoiseTexture", 1);
    ssaoTest->SetUniform("zNear", zNear); // 0.1f
    ssaoTest->SetUniform("zFar", zFar); // 2500.0f
    ssaoTest->SetUniform("uSampleKernelSize", kernelSize);
    ssaoTest->SetUniform("uSampleKernel", kernel, kernelSize);
    ssaoTest->SetUniform("uRadius", 0.01f);
    ssaoTest->SetUniform("uNoiseScale", noiseScale);
    ssaoTest->SetUniform("uProjection", projection);
    ssaoTest->SetUniform("uInverseMatrix", invProj);
    ssaoTest->SetUniform("uWidth", (float)sysMain.VideoWidth());
    ssaoTest->SetUniform("uHeight", (float)sysMain.VideoHeight());
    ssaoTest->SetUniform("uFov", uFov);
    
    renderBackend.SetTextureUnit(1);
    dglBindTexture(GL_TEXTURE_2D, noiseTexture);
    renderBackend.SetTextureUnit(0);
    renderBackend.depthBuffer->BindDepthBuffer();
    
    // draw full screen quad with shader enabled
    DrawScreenQuad();

And here’s the SSAO fragment shader: (Note that I tried using the function ‘positionFromDepth’ to get the position from the depth but it doesn’t seem to work at all)


uniform sampler2D uDepthBuffer;
uniform sampler2D uNoiseTexture;


uniform float uWidth;
uniform float uHeight;


uniform mat4 uInverseMatrix;
uniform mat4 uProjection;
uniform vec2 uNoiseScale;


uniform float zNear;
uniform float zFar;
uniform float uFov;


const int MAX_KERNEL_SIZE = 128;


uniform int uSampleKernelSize;
uniform vec3 uSampleKernel[MAX_KERNEL_SIZE];


uniform float uRadius;


float linearizeDepth(float near, float far, float depth) {
    return 2.0 * near / (far + near - depth * (far - near));
}


vec3 normal_from_depth(float depth, vec2 texcoords) {
  
  const vec2 offset1 = vec2(0.0, 0.001);
  const vec2 offset2 = vec2(0.001, 0.0);
  
  float depth1 = linearizeDepth(zNear, zFar, texture2D(uDepthBuffer, texcoords + offset1).r);
  float depth2 = linearizeDepth(zNear, zFar, texture2D(uDepthBuffer, texcoords + offset2).r);
  
  vec3 p1 = vec3(offset1, depth1 - depth);
  vec3 p2 = vec3(offset2, depth2 - depth);
  
  vec3 normal = cross(p1, p2);
  normal.z = -normal.z;
  
  return normalize(normal);
}


vec3 positionFromDepth(vec2 texcoords, float depth) {
    vec4 pos = uInverseMatrix * vec4(texcoords.x * 2.0 - 1.0,
                                     texcoords.y * 2.0 - 1.0,
                                     -depth, 1.0);
                                     
    pos.xyz /= pos.w;
    
    return pos.xyz;
}


void main() {
    vec2 texcoord = gl_TexCoord[0].st;
    
    float thfov = uFov;
    vec2 ndc = texcoord * 2.0 - 1.0;
    float aspect = (uWidth / uHeight);
    
    vec3 viewRay = vec3(ndc.x * thfov * aspect, ndc.y * thfov, 1.0);
    
    float d = texture2D(uDepthBuffer, texcoord).r;
    
    vec3 origin = viewRay * -linearizeDepth(zNear, zFar, d);
    
    vec3 normal = normal_from_depth(linearizeDepth(zNear, zFar, d), texcoord) * 2.0 - 1.0;
    normal = normalize(normal);
    
    vec3 rvec = texture2D(uNoiseTexture, texcoord * uNoiseScale).rgb * 2.0 - 1.0;
    vec3 tangent = normalize(rvec - normal * dot(rvec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 tbn = mat3(tangent, bitangent, normal);
    
    float occlusion = 0.0;
    
    for(int i = 0; i < uSampleKernelSize; i++) {
        vec3 sample = tbn * uSampleKernel[i];
        sample = sample * uRadius + origin.xyz;
        
        vec4 offset = vec4(sample, 1.0);
        offset = uProjection * offset;
        offset.xy /= offset.w;
        offset.xy = offset.xy * 0.5 + 0.5;
        
        d = texture2D(uDepthBuffer, offset.xy).r;
        
        float sampleDepth = -linearizeDepth(zNear, zFar, d);
        occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0);
    }
    
    occlusion = 1.0 - (occlusion / float(uSampleKernelSize));
    gl_FragColor = vec4(occlusion, occlusion, occlusion, 1.0);
}

[b]
Source code

[/b]Here’s the source code for those who would like to dabble with it: https://dl.dropboxusercontent.com/u/18609/troubleshoot_ssao.7z
It’s a simple scene that renders a flat floor and the suzanne head model. The code of interest can be found in renderMain.cpp. The kernel and noise texture initialization begins at line #135 and the rendering begins at line #232. I apologize for the mass commented lines of code scattered everywhere; I’ve had to strip out a lot of irrelevant code just for this.
[b]
Notes on compiling

[/b]The only dependency used is SDL2 and compiling should not be an issue. I’ve included both MSVC 2008 and XCode workspaces though both may need some minor config tweaks.
[b]
Notes on running application

[/b]The engine expects to find files under the /defs, /fonts, /materials, /progs, and /textures directories. It’s important to make sure that the executable that you’re running is located where these folders are, if not the app will crash due to missing resources. You can open config.cfg and set the path for kf_basepath if you want the engine to look for these resources elsewhere. To exit the application, press ‘~’ to bring down the console and type quit to exit. The configs also has this binded to Q.
[b]
Controls

[/b]Holding down left mouse button moves camera forward and sideways, while right mouse button rotates the camera. Holding down both will make the camera move vertically.

Anyways I hope this is enough for anyone to help troubleshoot this problem. I frequently lurk on these forums so please don’t hesitate to ask any questions.