I wanted to improve the graphic quality for Merc Tactics by adding a SSAO shader and I looked into several techniques. The best quality implementation of this is GTAO. There is also ASSAO, which is used in the Godot engine. Unfortunately, GTAO is a bit too heavy weight and I want Merc Tactics to be able to run on a PC with integrated graphics. I the end I used this very simple implementation: A Simple and Practical Approach to SSAO. For this approach he stores the vertex positions and normals in view space in two textures, then uses that data to generate the SSAO texture. However, I found that storing positions and normals in world space gave much better results. The view space version does not cast a shadow on the ground below objects, as you can see in the image below.
This is the fragment shader that I used:
out float fragColor;
in mediump vec2 uvOutA;
uniform sampler2D sPosition; // position texture
uniform sampler2D sNormal; // normals texture
uniform float scale;
uniform float bias;
uniform float intensity;
uniform float far;
uniform float near;
uniform float radius;
uniform vec3 posCamera;
// a simple random generator (taken from a post on StackOverflow). co is seed.
float rand(vec2 co)
{
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
float doAmbientOcclusion(vec2 tcoord, vec2 uv, vec3 pos, vec3 normal)
{
vec3 posOcculder = texture(sPosition, tcoord + uv).xyz;
vec3 diff = posOcculder - pos;
vec3 vectorToOcculder = normalize(diff);
float distToOcculder = length(diff) * radius;
float percentOcculded = max(0.0, dot(normal, vectorToOcculder) - bias);
return percentOcculded / (1.0 + distToOcculder) * intensity;
}
void main()
{
vec3 pos = texture(sPosition, uvOutA.xy).xyz;
vec3 normal = normalize(texture(sNormal, uvOutA.xy).xyz);
vec2 randomVec = normalize(vec2(rand(pos.xy), rand(normal.xy)));
float ao = 0.0;
float distToCamera = length(posCamera - pos);
float ratioDist = (distToCamera-near)/(far - near);
vec2 vec[8];
vec[0] = vec2(1.0, 0.0);
vec[1] = vec2(-1.0, 0.0);
vec[2] = vec2(0.0, 1.0);
vec[3] = vec2(0.0, -1.0);
vec[4] = vec2(1.0, 1.0);
vec[5] = vec2(-1.0, -1.0);
vec[6] = vec2(1.0, -1.0);
vec[7] = vec2(-1.0, 1.0);
int iterations = 4;
for (int j = 0; j < iterations; j++)
{
vec2 coord1 = reflect(vec[j], randomVec)*scale/ratioDist;
vec2 coord2 = vec2(coord1.x * 0.707 - coord1.y * 0.707,
coord1.x * 0.707 + coord1.y * 0.707);
ao += doAmbientOcclusion(uvOutA.xy, coord1 * 0.25, pos, normal);
ao += doAmbientOcclusion(uvOutA.xy, coord2 * 0.5, pos, normal);
ao += doAmbientOcclusion(uvOutA.xy, coord1 * 0.75, pos, normal);
ao += doAmbientOcclusion(uvOutA.xy, coord2, pos, normal);
}
ao = ao / (float(iterations) * 4.0);
fragColor = 1.0 - ao;
}
And this is how it looks:
compared to with no SSAO
Then compare that against GTAO (this is output from reshader MXAO using 8 samples and radius of 1):
Obviously, GTAO is much better quality, but it comes at a high cost. On a Vega 8 iGPU at 1080p GTAO takes 7ms of frame time, compared to only 1ms for the simple SSAO method. That's enough to drop it well below 60fps. The simple SSAO method has reasonable results and is definitely better than having no SSAO at all.
I also tried using implementing this by using only the depth buffer and calculating positions and normals from depth values. This turned out to be a bad idea for two reasons: firstly, performance is terrible… about 7ms. Secondly, the calculating normals always leads to artifacts that can never be fully removed.
Nice.
But it would be nicer if you add the intensity to the graphics options menu.
For me it's always too dark. But no game lets me tweak it to the very subtle amount i want.