SSAO is now back…

and easier to use!

The small debug output windows are the SSAO before and after the blur process. I’ve made this a lot simpler to use as well. So, in your Game class, you can turn on SSAO with the following code:

SSAORenderer.Enabled = true;

There are two more properties that are now available in the SSAORenderer class. There are:

SSAORenderer.DistanceScale
SSAORenderer.SampleRadius

Set these values to get your desired look, and that’s it. The SSAORenderer is disabled by default, so you’ll have to enable it if you want to use it. Sourcecode available at Codeplex, changeset 15588.

SSAO Continued…

Following on from the last post, this will concentrate on adding the Effects needed for the Screen Space Ambient Occlusion. These shaders are taken from The Cansin’s Deferred Rendering engine, but we’ll look at altering these later on.

Firstly, in the “ProjectVanquishTestContent” project, locate the “Shaders” folder and create a new folder called “SSAO”. Add a new Effect file called “SSAO”. We’ll start by adding a few variables:

#define NUMSAMPLES 8

//Projection matrix
float4x4 Projection;

//Corner Fustrum
float3 cornerFustrum;

//Sample Radius
float sampleRadius;

//Distance Scale
float distanceScale;

//GBuffer Texture Size
float2 GBufferTextureSize;

//Samplers
sampler GBuffer1 : register(s1);
sampler GBuffer2 : register(s2);
sampler RandNormal : register(s3);

Now we’ll add the Vertex Input/Outputs and Function:

//Vertex Input Structure
struct VSI
{
	float3 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

//Vertex Output Structure
struct VSO
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 ViewDirection : TEXCOORD1;
};

//Vertex Shader
VSO VS(VSI input)
{
	//Initialize Output
	VSO output;

	//Just Straight Pass Position
	output.Position = float4(input.Position, 1);

	//Set up UV's
	output.UV = input.UV - float2(1.0f / GBufferTextureSize.xy);

	//Set up ViewDirection vector
	output.ViewDirection = float3(-cornerFustrum.x * input.Position.x, cornerFustrum.y * input.Position.y, cornerFustrum.z);

	//Return
	return output;
}

We’ll add the Normal decoding function:

float3 decode(float3 enc)
{
    return (2.0f * enc.xyz- 1.0f);
}

Finally, we’ll add the Pixel Shader Function and the Technique:

float4 PS(VSO input) : COLOR0
{
	//Sample Vectors
	float4 samples[8] =
	{
		float4(0.355512, 	-0.709318, 	-0.102371,	0.0 ),
		float4(0.534186, 	0.71511, 	-0.115167,	0.0 ),
		float4(-0.87866, 	0.157139, 	-0.115167,	0.0 ),
		float4(0.140679, 	-0.475516, 	-0.0639818,	0.0 ),
		float4(-0.207641, 	0.414286, 	0.187755,	0.0 ),
		float4(-0.277332, 	-0.371262, 	0.187755,	0.0 ),
		float4(0.63864, 	-0.114214, 	0.262857,	0.0 ),
		float4(-0.184051, 	0.622119, 	0.262857,	0.0 )
	};

	//Normalize the input ViewDirection
	float3 ViewDirection = normalize(input.ViewDirection);

	//Sample the depth
	float depth = tex2D(GBuffer2, input.UV).g;
	
	//Calculate the depth at this pixel along the view direction
	float3 se = depth * ViewDirection;

	//Sample a random normal vector
	float3 randNormal = tex2D(RandNormal, input.UV * 200.0f).xyz;

	//Sample the Normal for this pixel
	float3 normal = decode(tex2D(GBuffer1, input.UV).xyz);
	
	//No assymetry in HLSL, workaround
	float finalColor = 0.0f;
	
	//SSAO loop
	for (int i = 0; i < NUMSAMPLES; i++)
	{
		//Calculate the Reflection Ray
		float3 ray = reflect(samples[i].xyz, randNormal) * sampleRadius;
		
		//Test the Reflection Ray against the surface normal
		if(dot(ray, normal) < 0) ray += normal * sampleRadius;
		
		//Calculate the Sample vector
		float4 sample = float4(se + ray, 1.0f);
		
		//Project the Sample vector into ScreenSpace
		float4 ss = mul(sample, Projection);

		//Convert SS into UV space
		float2 sampleTexCoord = 0.5f * ss.xy / ss.w + float2(0.5f, 0.5f);
		
		//Sample the Depth along the ray
		float sampleDepth = tex2D(GBuffer2, sampleTexCoord).g;
		
		//Check the sampled depth value
		if (sampleDepth == 1.0)
		{
			//Non-Occluded sample
			finalColor++;
		}
		else
		{	
			//Calculate Occlusion
			float occlusion = distanceScale * max(sampleDepth - depth, 0.0f);
			
			//Accumulate to finalColor
			finalColor += 1.0f / (1.0f + occlusion * occlusion * 0.1);
		}
	}

	//Output the Average of finalColor
	return float4(finalColor / NUMSAMPLES, finalColor / NUMSAMPLES, finalColor / NUMSAMPLES, 1.0f);
}

technique SSAO
{
	pass p0
	{
		VertexShader = compile vs_3_0 VS();
		PixelShader = compile ps_3_0 PS();
	}
}

Excellent. One down, 2 to go. Create a new Effect file called “Blur” in the “SSAO” folder. Remove all contents and add the following variables:

float2 blurDirection;
float2 targetSize;

sampler GBuffer1 : register(s1);
sampler GBuffer2 : register(s2);
sampler SSAO : register(s3);

Next we’ll add the Vertex Shader info:

struct VSI
{
	float3 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

struct VSO
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

VSO VS(VSI input)
{
	//Initialize Output
	VSO output;

	//Just Straight Pass Position
	output.Position = float4(input.Position, 1);

	//Pass UV
	output.UV = input.UV - float2(1.0f / targetSize.xy);
    
	//Return
	return output;
}

We’ll add the Manual Linear Sample and Normal Decoding functions:

//Manually Linear Sample
float4 manualSample(sampler Sampler, float2 UV, float2 textureSize)
{
	float2 texelpos = textureSize * UV; 
	float2 lerps = frac(texelpos); 
	float texelSize = 1.0 / textureSize;                 
 
	float4 sourcevals[4]; 
	sourcevals[0] = tex2D(Sampler, UV); 
	sourcevals[1] = tex2D(Sampler, UV + float2(texelSize, 0)); 
	sourcevals[2] = tex2D(Sampler, UV + float2(0, texelSize)); 
	sourcevals[3] = tex2D(Sampler, UV + float2(texelSize, texelSize));   
         
	float4 interpolated = lerp(lerp(sourcevals[0], sourcevals[1], lerps.x), lerp(sourcevals[2], sourcevals[3], lerps.x ), lerps.y); 

	return interpolated;
}

//Normal Decoding Function
float3 decode(float3 enc)
{
	return (2.0f * enc.xyz- 1.0f);
}

Lastly, the Pixel Shader Function and technique:

float4 PS(float2 UV :TEXCOORD0) : COLOR0
{
	//Sample Depth
	float depth = manualSample(GBuffer2, UV, targetSize).y;
    
	//Sample Normal
	float3 normal = decode(tex2D(GBuffer1, UV).xyz);
    
	//Sample SSAO
	float ssao = tex2D(SSAO, UV).x;
   
	//Color Normalizer
    float ssaoNormalizer = 1;

	//Blur Samples to be done
    int blurSamples = 8; 
	
	//From the negative half of blurSamples to the positive half; almost like gaussian blur
	for(int i = -blurSamples / 2; i <= blurSamples / 2; i++)
	{
		//Calculate newUV as the current UV offset by the current sample
		float2 newUV = float2(UV.xy + i * blurDirection.xy);
		
		//Sample SSAO
		float sample = manualSample(SSAO, newUV, targetSize).y;
		
		//Sample Normal
		float3 samplenormal = decode(tex2D(GBuffer1, newUV).xyz);
			
		//Check Angle Between SampleNormal and Normal
		if (dot(samplenormal, normal) > 0.99)	
		{
			//Calculate this samples contribution
			float contribution = blurSamples / 2 - abs(i);
			
			//Accumulate to normalizer
			ssaoNormalizer += (blurSamples / 2 - abs(i));

			//Accumulate to SSAO
			ssao += sample * contribution;
		}
	}

	//Return Averaged Samples
	return ssao / ssaoNormalizer;
}

technique Blur
{
	pass p0
	{
		VertexShader = compile vs_3_0 VS();
		PixelShader = compile ps_3_0 PS();
	}
}

Now, we need to create a new Effect file called “Final” in the “SSAO” folder. Again, remove all default contents and add the following:

float2 halfPixel;

sampler Scene : register(s0);
sampler SSAO : register(s1);

Now for the Vertex Shader information:

struct VSI
{
	float3 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

struct VSO
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

VSO VS(VSI input)
{
	//Initialize Output
	VSO output;

	//Pass Position
	output.Position = float4(input.Position, 1);

	//Pass Texcoord's
	output.UV = input.UV - halfPixel;

	//Return
	return output;
}

And finally the Pixel Shader Function and the technique:

float4 PS(VSO input) : COLOR0
{
	//Sample Scene
	float4 scene = tex2D(Scene, input.UV);

	//Sample SSAO
	float4 ssao = tex2D(SSAO, input.UV);

	//Return
	return (scene * ssao);
}

technique SSAOFinal
{
	pass p0
	{
		VertexShader = compile vs_3_0 VS();
		PixelShader = compile ps_3_0 PS();
	}
}

Great. Compile the solution and it should build without errors. In the next couple of posts, I’m going to review some of the code previously written and rework some of it. Once finished, I’ll upload the project has a whole, so we are all on the same page 🙂

Screen Space Ambient Occlusion

Whilst there is still the on going problem with the Shadow Renderer, I didn’t want to hold the project up too long so I thought I’d implement Screen Space Ambient Occlusion (SSAO).

The last post had a link to the latest source code, so I would go ahead and download this before continuing, then we are all on the same page.

We’ll start with some new modifications to the “DeferredRenderer” class. Firstly, we need a new Scene RenderTarget and also a RenderTargetBinding array.

RenderTarget2D sceneRT;
RenderTargetBinding[] renderTargets;

Also, we can create our new “SSAORenderer” class object:

SSAORenderer ssaoRenderer;

Instantiate the new Scene RenderTarget, the RenderTargetBinding array and the SSAORenderer in the constructor:

sceneRT = new RenderTarget2D(device, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);

renderTargets = new RenderTargetBinding[] { colorRT, normalRT, depthRT };

ssaoRenderer = new SSAORenderer(device, content,
                                device.PresentationParameters.BackBufferWidth,
                                device.PresentationParameters.BackBufferHeight);

We will need to set this in the “CombineGBuffer” method:

void CombineGBuffer(ref RenderTarget2D shadowOcclusion)
{
    device.SetRenderTarget(sceneRT);

    finalEffect.Parameters["colorMap"].SetValue(colorRT);
    finalEffect.Parameters["lightMap"].SetValue(lightRT);
    finalEffect.Parameters["shadowMap"].SetValue(shadowOcclusion);
    finalEffect.Parameters["halfPixel"].SetValue(halfPixel);
    finalEffect.CurrentTechnique.Passes[0].Apply();
    fullscreenQuad.Draw();
}

This will set the Scene RenderTarget so that we can use it in the new “SSAORenderer” class that we’ll create shortly. We can also use this new RenderTarget for Post Processing.

The last change that we need to make to the “DeferredRenderer” class is in the “Draw” method, and that is calling the “SSAORenderer Draw” method after the “CombineGBuffer” method:

public void Draw(GameTime gameTime)
{
    SetGBuffer();
    ClearGBuffer();
    sceneManager.Draw();            
    ResolveGBuffer();
    DrawDepth();
    var shadowOcclusion = shadowRenderer.Draw(device, depthRT, sceneManager);
    DrawLights();
    CombineGBuffer(ref shadowOcclusion);
    ssaoRenderer.Draw(device, renderTargets, sceneRT, sceneManager, null);
    DrawDebug(ref shadowOcclusion);
}

As you can see we are using our new Scene RenderTarget. This is so we can pass a final deferred rendered scene into the SSAO Renderer ready for the Ambient Occlusion application.

Let’s create our new “SSAORenderer” class. In the “Renderers” folder, create a new class called “SSAORenderer”. Add the usual namespaces:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using ProjectVanquish.Core;

Add the following “Effect” declarations:

Effect ssao, ssaoBlur, composer;

Add the following RenderTargets:

RenderTarget2D ssaoRT, blurRT;

Just a few more declarations which we’ll wrap up in one code block:

float sampleRadius, distanceScale;
Texture2D randomNormals;
QuadRenderer fullscreenQuad;

Now, we can create our constructor:

public SSAORenderer(GraphicsDevice device, ContentManager content, int Width, int Height)
{
    // Load SSAO effects
    ssao = content.Load<Effect>("Shaders/SSAO/SSAO");
    ssaoBlur = content.Load<Effect>("Shaders/SSAO/Blur");
    composer = content.Load<Effect>("Shaders/SSAO/Final");

    // Create RenderTargets
    ssaoRT = new RenderTarget2D(device, Width, Height, false, SurfaceFormat.Color, DepthFormat.None);
    blurRT = new RenderTarget2D(device, Width, Height, false, SurfaceFormat.Color, DepthFormat.None);

    fullscreenQuad = new QuadRenderer(device);
    randomNormals = content.Load<Texture2D>("Textures/RandomNormal");

    // Set Sample Radius to Default
    sampleRadius = 0;

    // Set Distance Scale to Default
    distanceScale = 0;
}

You’ll see that we are loading some Effects, but we’ll come back to these once we’ve finished with the class. We’ll need to declare 4 new methods:

void BlurSSAO(GraphicsDevice device)
{
}

void Compose(GraphicsDevice device, RenderTarget2D scene, RenderTarget2D output)
{
}

public void Draw(GraphicsDevice device, RenderTargetBinding[] gBuffer, RenderTarget2D sceneRT, SceneManager scene, RenderTarget2D output)
{
}

void RenderSSAO(GraphicsDevice device, RenderTargetBinding[] gBuffer, SceneManager scene)
{
}

Starting with the “Draw” method, we’ll call these new methods in order so we build our Ambient Occlusion onto the Scene RenderTarget:

public void Draw(GraphicsDevice device, RenderTargetBinding[] gBuffer, RenderTarget2D sceneRT, 
                 SceneManager scene, RenderTarget2D output)
{
    device.BlendState = BlendState.Opaque;
    device.DepthStencilState = DepthStencilState.Default;
    device.RasterizerState = RasterizerState.CullCounterClockwise;

    RenderSSAO(device, gBuffer, scene);
    BlurSSAO(device);
    Compose(device, sceneRT, output);
}

So you can see that we are rendering the SSAO first, then applying a blur and then composing the final scene. Ok, we’ll start by adding the code for the “RenderSSAO” scene:

void RenderSSAO(GraphicsDevice device, RenderTargetBinding[] gBuffer, SceneManager scene)
{
    device.SetRenderTarget(ssaoRT);
    device.Clear(Color.White);

    device.Textures[2] = gBuffer[2].RenderTarget;
    device.SamplerStates[2] = SamplerState.PointClamp;
    device.Textures[3] = randomNormals;
    device.SamplerStates[3] = SamplerState.LinearWrap;

    // Calculate Frustum Corner of the Camera
    Vector3 cornerFrustum = Vector3.Zero;
    cornerFrustum.Y = (float)Math.Tan(Math.PI / 3.0 / 2.0) * scene.Camera.FarClip;
    cornerFrustum.X = cornerFrustum.Y * scene.Camera.AspectRatio;
    cornerFrustum.Z = scene.Camera.FarClip;

    // Set SSAO parameters
    ssao.Parameters["Projection"].SetValue(scene.Camera.ProjectionMatrix);
    ssao.Parameters["cornerFustrum"].SetValue(cornerFrustum);
    ssao.Parameters["sampleRadius"].SetValue(sampleRadius);
    ssao.Parameters["distanceScale"].SetValue(distanceScale);
    ssao.Parameters["GBufferTextureSize"].SetValue(new Vector2(ssaoRT.Width, ssaoRT.Height));

    // Apply Effect
    ssao.CurrentTechnique.Passes[0].Apply();

    // Draw
    fullscreenQuad.Draw();
}

As mentioned before, we are setting a lot of Effect parameters, but we will come back to the Effects later. On to the “BlurSSAO” method:

void BlurSSAO(GraphicsDevice device)
{
    device.SetRenderTarget(blurRT);
    device.Clear(Color.White);

    device.Textures[3] = ssaoRT;
    device.SamplerStates[3] = SamplerState.LinearClamp;

    // Set Blur parameters
    ssaoBlur.Parameters["blurDirection"].SetValue(Vector2.One);
    ssaoBlur.Parameters["targetSize"].SetValue(new Vector2(ssaoRT.Width, ssaoRT.Height));

    // Apply Effect
    ssaoBlur.CurrentTechnique.Passes[0].Apply();

    // Draw
    fullscreenQuad.Draw();
}

Now we just need to fill out the “Compose” method:

void Compose(GraphicsDevice device, RenderTarget2D sceneRT, RenderTarget2D output)
{
    device.SetRenderTarget(output);
    device.Clear(Color.White);

    device.Textures[0] = sceneRT;
    device.SamplerStates[0] = SamplerState.LinearClamp;
    device.Textures[1] = blurRT;
    device.SamplerStates[1] = SamplerState.LinearClamp;

    // Set Composition Parameters
    composer.Parameters["halfPixel"].SetValue(new Vector2(1.0f / ssaoRT.Width, 1.0f / ssaoRT.Height));

    // Apply Effect
    composer.CurrentTechnique.Passes[0].Apply();

    // Draw
    fullscreenQuad.Draw();
}

That’s the last of the class code. I’ll add a new post to include the Shaders to make the posts more manageable.