So I've been working on SSAO for awhile, and I thought I had it right, untill I loaded up the sponza scene that accompanied the SAO technique I'm implementing (http://graphics.cs.williams.edu/papers/SAOHPG12/)

Here's the fully rendered picture:

And here's the SAO texture (couldn't disable the other channels for previewing, so its very red) enter image description here

The crease in the cloth in the middle should be smooth, not all jaggedy. Even after the blurring, it's still jaggy, compared to the screenshots in the document.

I can't quite fathom why it is nor why these symptoms occur. I've implemented it pretty much as the technical document says.

The normals are reconstructed, but so are they in their shader code, so that shouldnt be the problem.


#include "Constants.h"
#include "Common.hlsl"

static const float gNumSamples = 11.0;
static const float gRadius = 1.0;
static const float gRadius2 = gRadius * gRadius;
static const float gProjScale = 500.0;
static const float gNumSpiralTurns = 7;
static const float gBias = 0.01;
static const float gIntensity = 1.0;
// If using depth mip levels, the log of the maximum pixel offset before we need to switch to a lower 
// miplevel to maintain reasonable spatial locality in the cache
// If this number is too small (< 3), too many taps will land in the same pixel, and we'll get bad variance that manifests as flashing.
// If it is too high (> 5), we'll get bad performance because we're not using the MIP levels effectively
static const uint gMaxOffset = 3;
static const uint gMaxMipLevel = 5;

cbuffer SSAOConstants : register(CBUFFER_REGISTER_PIXEL)
    float4x4 gInvProjMatrix;
    float2 gScreenSize;

Texture2D gDepthTexture : register(TEXTURE_REGISTER_DEPTH);
SamplerState gPointSampler : register(SAMPLER_REGISTER_POINT);

float3 reconstructNormal(float3 positionViewSpace)
    return normalize(cross(ddy(positionViewSpace), ddx(positionViewSpace)));

/** Read the camera - space position of the point at screen - space pixel ssP + unitOffset * ssR.Assumes length(unitOffset) == 1 */
float3 getOffsetPosition(const int2 ssC, const float2 unitOffset, const float ssR) {
    // Derivation:
    //  mipLevel = floor(log(ssR / MAX_OFFSET));

    // TODO: mip levels
    int mipLevel = 0; //TODO: clamp((int)floor(log2(ssR)) - LOG_MAX_OFFSET, 0, MAX_MIP_LEVEL);

    uint2 ssP = uint2(ssR*unitOffset) + ssC;

    const float2 texCoord = float2(ssP.x / gScreenSize.x, ssP.y / gScreenSize.y);

    float depth = gDepthTexture.Sample(gPointSampler, texCoord).r;
    float3 P = reconstructViewPosition(depth, texCoord, gInvProjMatrix);

    // Divide coordinate by 2^mipLevel
    //P = gPositionTexture.Load(int3(ssP >> mipLevel, mipLevel)).xyz;
    //P = mul(gViewMatrix, float4(P, 1.0)).xyz;

    return P;

float2 tapLocation(int sampleNumber, float spinAngle, out float ssR)
    // Radius relative to ssR
    float alpha = float(sampleNumber + 0.5) * (1.0 / gNumSamples);
    float angle = alpha * (gNumSpiralTurns * 6.28) + spinAngle;

    ssR = alpha;
    return float2(cos(angle), sin(angle));

float sampleAO(uint2 screenSpacePos, float3 originPos, float3 normal, float ssDiskRadius, int tapIndex, float randomPatternRotationAngle)
    float ssR;
    float2 unitOffset = tapLocation(tapIndex, randomPatternRotationAngle, ssR);
    ssR *= ssDiskRadius;

    // The occluding point in camera space
    float3 Q = getOffsetPosition(screenSpacePos, unitOffset, ssR);

    float3 v = Q - originPos;

    float vv = dot(v, v);
    float vn = dot(v, normal);

    const float epsilon = 0.01;
    float f = max(gRadius2 - vv, 0.0); 

    return f * f * f * max((vn - gBias) / (epsilon + vv), 0.0);

float ps_main(const float4 position : SV_Position) : SV_Target0
    const uint2 screenSpacePos = (uint2)position.xy;
    const float2 texCoord = float2(screenSpacePos.x / gScreenSize.x, screenSpacePos.y / gScreenSize.y);

    float depth = gDepthTexture.Sample(gPointSampler, texCoord).r;
    float3 originPos = reconstructViewPosition(depth, texCoord, gInvProjMatrix);
    float3 normal = reconstructNormal(originPos);

    // Hash function used in the HPG12 AlchemyAO paper
    float randomPatternRotationAngle = (3 * screenSpacePos.x ^ screenSpacePos.y + screenSpacePos.x * screenSpacePos.y) * 10;
    float ssDiskRadius = -gProjScale * gRadius / originPos.z;

    float ao = 0.0;
    for (int i = 0; i < gNumSamples; i++)
        ao += sampleAO(screenSpacePos, originPos, normal, ssDiskRadius, i, randomPatternRotationAngle);

    float temp = gRadius2 * gRadius;
    ao /= temp * temp;

    float A = max(0.0, 1.0 - ao * gIntensity * (5.0 / gNumSamples));

    return A;


EDIT: added additional picture, SSAO of a lions head. Surely it looks abit off? does it say anything of what could be wrong?

enter image description here


2 Answers 2


A little late I guess but anyway it can be helpful, so here's my thought.

The problem

I think your problem might be caused by the fact that you project a ray from your hemisphere (or sphere) to sample depth, like this in theory :

enter image description here

But in practice, your surface isn't going to be a smooth line (like shown above), but it's going to be jagged like so :

enter image description here

In red, you can see what the surface actually looks like when you sample from it. Here, a problem arises : you'll have rays that are, in theory, above the surface but in practice will be under the surface and then get occluded.

Of course, the problem gets worse when the resolution of the depth buffer is low.

Potential solution

A solution to this problem would be to limit the area of where to account for rays under or above the surface. Here's a picture of what I'm talking about :

enter image description here

(Yes, every pictures are done with MS Paint...)

The areas in gray are the surfaces that you're not going to discard in your computation of the effect.

To do that I would compute the dot product of the ray and the surface normal and discard the ray if it is under a certain threshold set beforehand. Also, try playing with this value until you get a decent result.

In doing so, you'll not get falsely occluded rays and you'll avoid your heavily occluded areas.

Hope this helps anyone ;)


this looks like a depth encoding precision issue. check your near and far. this is just causing some steps because the quantization is bad, your AO is evaluating corners where there are none. I doubt the problem is caused by ddx and ddy where I first (and I guess you too) suspected, because the patterns are rotating ! this is impossible with ddx, you would get screen aligned artefacts. (2.2 quads) so definitely here, check your framebuffer formats, and the projection matrix to reduce the depth range (most importantly, push the near away).
ALso check that your HLSL is executing fp32 (single) and not fp16 (half float).

  • \$\begingroup\$ My depthbuffer format is DXGI_FORMAT_D24_UNORM_S8_UINT, which should be enough precision? I'm sure it has been achieved with such format. As for the near/far it is 0.1f/100.0f. \$\endgroup\$ Nov 11, 2014 at 8:57
  • \$\begingroup\$ check in your drivers if you can improve the precision of ddx ddy. I know nvidia has a better technique than ATI by default for example. I think recently some settings appeared so we can manually change the quality of the derivatives, see if you can find that setting and put it to high. \$\endgroup\$
    – v.oddou
    Nov 12, 2014 at 0:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .