Mystery SSAO Artifacts?


So, I have an SSAO setup working, though I’m not blurring it at all so it is still rather ugly.

The issue I’m running into is that when I render the SSAO pass to the screen with gl::draw( ssaoFbo->getTexture2D(GL_COLOR_ATTACHMENT1), getWindowBounds() ); I get a bunch of odd artifacts.

Each of the artifacts appears to consist of a 4x4 pixel texture that I’m using to apply noise to the SSAO Samples. I’ve attached a GIF below. I also see these same artifacts when I run the “DeferredShadingAdvanced” sample that comes with Cinder, but only when “Fog” is turned on. Since it shows up in a sample as well, is this just a GPU issue, or some kind of easily recognizable OpenGL glitch that I’m just not familiar with yet?

Any ideas?


Nothing obvious immediately springs to mind i’m afraid but are you running this on an AMD graphics card by any chance?

Ive seen similar glitches on AMD hardware that didn’t appear on NVidia hardware. Perhaps double check your fragment shaders for possible divisions by zero or negative/strange numbers in ‘pow’ functions.

Good luck.


It might be easier to help if you post your SSAO shader code too.


I would have assumed an error in your kernel (used to perturb the light vector), but if this also happens in the sample then it must be something else. @felixfaire may be on to something with his AMD remark.


@felixfaire @paul.houx

It’s actually an Intel Iris Pro GPU (1536 MB VRAM) on a MacBook Pro. Obviously not ideal, but seems to be keeping up. Here’re the relevent shaders -

  1. The gBuffer vertex shader that provides vPosition, vNormal, vTexCoord0, and vColor to the fragment shader.
uniform mat4 ciModelViewProjection;
uniform mat4 ciModelView;
uniform mat3 ciNormalMatrix;

in vec4 ciPosition;
in vec3 ciNormal;
in vec2 ciTexCoord0;
in vec3 ciColor;

out vec4 vPosition;
out vec3 vNormal;
out vec2 vTexCoord0;
out vec3 vColor;

void main()
    vPosition = ciModelView * ciPosition;
    vNormal = ciNormalMatrix * ciNormal;
    vTexCoord0 = ciTexCoord0;
    vColor = ciColor;

    gl_Position = ciModelViewProjection * ciPosition;

Then the gBuffer fragment shader, which renders a position buffer, a normal buffer, and a color buffer (using the pickingColor uniform which is passed in per-mesh).

uniform sampler2D	tex0;
uniform vec3 		pickingColor;

in vec4 vPosition;
in vec3 vNormal;
in vec2 vTexCoord0;
in vec3 vColor;

out vec4 [3] FragColor;

void main()
    FragColor[0] = vPosition;
    FragColor[1] = vec4( vNormal, 1.0 );
    FragColor[2] = vec4( pickingColor, 1.0 );

And then finally the SSAO pass fragment shader, which outputs a single float value for the occlusion, and is given the Position and Normal passes from the gBuffer, in addition to a 4x4 pixel noiseTexture.

out float FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D noiseTexture;

uniform vec3 samples[64];
uniform mat4 ciProjectionMatrix;
uniform vec2 screenSize;

int kernelSize = 64;
float radius = 0.5;
float bias = 0.025;

void main()
    // tile noise texture over screen based on screen dimensions divided by noise size
    vec2 noiseScale = screenSize / 4.0;
    // get input for SSAO algorithm
    vec3 fragPos = texture(gPosition, TexCoords).xyz;
    vec3 normal = normalize(texture(gNormal, TexCoords).rgb);
    vec3 randomVec = normalize(texture(noiseTexture, TexCoords * noiseScale).xyz);
    // create TBN change-of-basis matrix: from tangent-space to view-space
    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);
    // iterate over the sample kernel and calculate occlusion factor
    float occlusion = 0.0;
    for(int i = 0; i < kernelSize; ++i)
        // get sample position
        vec3 sample = TBN * samples[i]; // from tangent to view-space
        sample = fragPos + sample * radius;
        // project sample position (to sample texture) (to get position on screen/texture)
        vec4 offset = vec4(sample, 1.0);
        offset = ciProjectionMatrix * offset; // from view to clip-space /= offset.w; // perspective divide = * 0.5 + 0.5; // transform to range 0.0 - 1.0
        // get sample depth
        float sampleDepth = texture(gPosition, offset.xy).z; // get depth value of kernel sample
        // range check & accumulate
        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0) * rangeCheck;
    occlusion = 1.0 - (occlusion / kernelSize);
    FragColor = occlusion;


I’ll have to do this from memory, since I can not test your shader myself at the moment, but if I were you I’d focus on the following parts of the shader to see if it might fix things:

  • Before writing the normal to the gBuffer, try to normalize it. Even though you normalize it when reading it from the buffer later on, this will likely increase precision, especially when your gBuffer is not a 32-bit floating point texture.
  • Construction of the TBN matrix may fail if the normal is pointing in the same direction as randomVec, although this is probably quite rare.
  • The perspective divide might fail if offset.w is zero. Add a check.
  • Try to add parentheses to your range check, so that it reads
    occlusion += (( sampleDepth >= sample.z + bias) ? 1.0 : 0.0) * rangeCheck;
    Intel’s GLSL compiler might otherwise interpret it as
    occlusion += (sampleDepth >= sample.z + (bias ? 1.0 : 0.0)) * rangeCheck;.

If none of this helps, you may have to resort to debugging the shader code by outputting intermediate results to a debug color texture. You know, the usual way to debug a shader.



There it was!

For some reason multiplying the offset by the automatically provided ciProjectionMatrix uniform resulted in an offset.w of 0. Passing in a projection uniform from my camera using .getProjectionMatrix(); seems to have fixed it. I was under the impression that it should be the same as the ciProjectionMatrix, but I guess not.

Thanks so much for the help, I don’t think I ever would’ve found that one on my own.