Vertex Shader
struct OUT
{
float4 Position:POSITION0;
float4 EyeVec:TEXCOORD0;
float2 TexCoord:TEXCOORD1;
};
OUT main (
float4 Position:POSITION0,
float4 FrustPos:NORMAL0,
float2 TexCoord:TEXCOORD0
)
{
OUT Out;
Out.Position=Position;
Out.EyeVec=FrustPos;
Out.TexCoord=TexCoord;
return Out;
}
Pixel Shader
float4 main(
uniform float4 Params:register(c0),
float4 EyeVec:TEXCOORD0,
float2 UV:TEXCOORD1,
uniform sampler SampleMap:register(s0),
uniform sampler RandNorms:register(s1),
uniform float4 SphereNorms[16]:register(c2)
):COLOR
{
float Strength=Params.x;// Somewhere around 0.5 to 3 or more
float4 Sample=tex2D(SampleMap,UV);
// Rebuild the normalised depth value
float Depth=(Sample.z+Sample.w/255);
float Rad=Params.y;// Radius of effect, in world units (ie metres)
// Build the camera space position
float3 CamPos=EyeVec.xyz*Depth;
// Scale up to the actual depth value
Depth*=Params.w;
// Rebuild the camera space normal
float3 Normal;
Normal.xy=Sample.xy*2-1;
Normal.z=-sqrt(1-dot(Normal.xy,Normal.xy));
// Get a random normal to start with
float3 Ref=normalize((tex2D(RandNorms,UV*20).xyz*2.0)-1);
float AO=0;
// Cast some rays to see how occluded we are
for (int i=0;i<16;i++)
{
// Reflection trick to reduce sampling jitter
float3 Ray=Rad*reflect(SphereNorms[i].xyz,Ref);
float RayDot=dot(normalize(Ray),Normal);
// Reject samples that don't point away from the normal by a fair bit
if (abs(RayDot)<0.5)
continue;
if (RayDot<0)
Ray=-Ray;
// Calculate camera space postion at end of sampling ray
float3 SamplePos=CamPos+Ray;
// Back project back into 2D so we can sample 'under' the ray
float2 SampleUV=(SamplePos.xy/SamplePos.z)*0.5+0.5;
SampleUV.y=1-SampleUV.y;
float4 TestSample=tex2D(SampleMap,SampleUV);
float TestDepth=(TestSample.z+TestSample.w/255)*Params.w;
// Rebuild normal under the sample point if you want to work
// that into your occlusion darkness term. I've found it adds little
//float3 TestNormal;
//TestNormal.xy=TestSample.xy*2-1;
//TestNormal.z=-sqrt(1-dot(TestNormal.xy,TestNormal.xy));
// Accrue a level of AO based on how occluded we are, if at all
float DepthDiff=(SamplePos.z-TestDepth);
if (DepthDiff>0 && DepthDiff<Rad)
AO+=1.0-smoothstep(0,Rad,DepthDiff);
}
float Val=Strength*AO/16;
return float4(Val,Val,Val,1);
That’s about it for now. Hope you find this useful in some small way…
PS. A quick note about the pics attached to this article. To use this effect nicely in a real engine, you’ll want to blur the output using a standard separable Gaussian filter that respects depth changes to prevent bleed. (This might come in a later article). Because of all the blurring, there’s little visible degradation from doing both the passes described at half size, them sampling up during the blur. This gives a sizeable speed boost that’s well worth having. The pics don’t have the blur applied but they were still drawn half-size which explains why the dots look so chunky. After a good blur, this whole thing looks sweet!