Screen space ambient occlusion

Main pass

Language: HLSL

This shader computes a screen space ambient occlusion effect.
This is the now standard implementation, with a few deviation:

  • The normal buffer is sampled:
    • Avoids self-occlusion by sampling only in an hemisphere around the surface normal vector
    • Normal mapping adds detailled occlusion
  • The occluders weight depends on their angle to the surface
  • Due to my renderer implementation, the shader depends on world space positions and normals

Code sample

//Matrix used by this shader pass
float4x4 World;
float4x4 View;
float4x4 Projection;
 
//The ViewProjection matrix used to render the inital scene
float4x4 SceneViewProj;
 
//The render target half pixel
float2 halfPixel;
 
//The camera forward vector
float3 viewForward;
 
//Samples points distributed in the unit sphere using energy minimisation
float3 samples[32] =
   {
        float3(-0.970699, -0.184458, 0.154010),
        float3(-0.831648, -0.217331, -0.511008),
        float3(-0.034957, 0.797863, -0.601824),
        ...
        float3(-0.305365, 0.806397, 0.506435)
   };
 
//A random texture used for dithering
texture RandomTexture;
sampler randomSampler = sampler_state
{
    Texture = (RandomTexture);
    MAGFILTER = POINT;
    MINFILTER = POINT;
    MIPFILTER = POINT;
    AddressU = Mirror;
    AddressV = Mirror;
};
 
//Geometric information (World position)
texture RT1;
sampler textureSampler = sampler_state
{
    Texture = (RT1);
    MAGFILTER = Linear;
    MINFILTER = Linear;
    MIPFILTER = Linear;
    AddressU = Border;
    AddressV = Border;
};
 
//Geometric information (World normal)
texture RT3;
sampler normalSampler = sampler_state
{
    Texture = (RT3);
    MAGFILTER = Linear;
    MINFILTER = Linear;
    MIPFILTER = Linear;
    AddressU = Border;
    AddressV = Border;
};
 
//The number of samples used by the SSAO
const int nbrSamples = 16;
 
//The world scale 
float minScale = 3;
float maxScale = 30;
 
//Exponent used to enhance the final contrast
float exponent = 1;
 
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};
 
// Simple vertex shader
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    float4x4 worldViewProjection = mul( mul(World, View), Projection);
    output.Position = mul(input.Position, worldViewProjection);
 
    output.TexCoord = input.TexCoord + halfPixel;
 
    return output;
}
 
// The SSAO pixel shader
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{    
    //Obtain the normalized forward vector
    float3 forward = normalize(viewForward);
 
    //We sample the different buffers
    float4 fragmentPos = tex2D(textureSampler, input.TexCoord);
    float3 fragmentNormal = tex2D(normalSampler, input.TexCoord);
    fragmentNormal = normalize((fragmentNormal - 0.5f) * 2.0f);    
 
    //Pick a random vector for this pixel
    float3 random = normalize(tex2D(randomSampler, input.TexCoord * 250));
 
    float totalWeight = 0;
    float totalOcclusion = 0;
    for(int i = 0; fragmentPos.a > 0.5 && i<nbrSamples && i < 32; i++)
    {
        //Distribute samples scale between minScale and maxScale 
        float k = (float)i / (float)nbrSamples;
        float sampleScale = minScale * k  + maxScale * (1-k);
 
        //Compute a sample vector. We use a random reflection plane for dithering
        float3 sampleOffset = sampleScale * reflect(samples[i%32].rgb , random);
 
        //We reflect the sample vector if needed to avoid self-occlusion
        if( dot( sampleOffset, fragmentNormal) < 0)
            sampleOffset = reflect(sampleOffset , fragmentNormal);    
 
        //Compute the sample position
        float3 samplePos = fragmentPos.rgb + sampleOffset.rgb;
        //The corresponding depth
        float sampleDepth = dot(samplePos, forward);
 
        //The corresponding offset in pixel, in the screen space
        float4 screenOffset = mul(float4(sampleOffset,1), SceneViewProj);
        float2 uvOffset = float2( screenOffset.x, -screenOffset.y) / screenOffset.w;
 
        //Read the corresponding pixel position in the scene
        float4 scenePos = tex2D(textureSampler, input.TexCoord + uvOffset);
        //The corresponding depth
        float sceneDepth = dot(scenePos, forward);
 
        //Put depth to infinity if no geometry is drawn (scenePos.a=0), to avoid occlusion from background
        if(scenePos.a < 0.5)
            sceneDepth = -100000;    
 
        //Computing the relative depth
        float relativeDepth = (sampleDepth - sceneDepth) / (maxScale);
 
        //The sample contribution depends on its angle with surface normal
        float weight = dot( normalize(samplePos - fragmentPos), normalize(fragmentNormal));
        totalWeight += weight;
 
        //The final occlusion        
        float occlusionFactor =  weight * 1 / (1 + relativeDepth * relativeDepth);
 
        totalOcclusion += step(relativeDepth, -1/(sampleScale * 1000)) * occlusionFactor;
    }        
 
    //Renormalize the total occlusion
    totalOcclusion = totalOcclusion / totalWeight;
 
    //Enhance the final contrast
    float finalOcclusion = pow(totalOcclusion, exponent);
 
    return 1 - finalOcclusion;
}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License