Screen space ambient occlusion in Merc Tactics

posted in Merc Tactics
Published May 14, 2024
Advertisement

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:

simple SSAO results

compared to with no SSAO

without SSAO

Then compare that against GTAO (this is output from reshader MXAO using 8 samples and radius of 1):

GTAO

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.

1 likes 6 comments

Comments

JoeJ

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.

May 14, 2024 10:48 PM
Ed Welch

That's not a bad idea

May 15, 2024 08:51 AM
Aressera

I agree that it's too dark. For the right-angle edges, only about half of the hemisphere is occluded, so the ambient occlusion value shouldn't be less than about half. For the corners, ambient should be no less than ¼.

May 17, 2024 03:13 AM
Ed Welch

@Aressera The ambient reduction due to SSAO is about 25%, but then you have the shadow map, which makes it darker. Also, there are the variables (scale, bias, etc.) which make it darker or lighter according to taste

May 17, 2024 01:51 PM
Aressera

@Ed Welch Why would the shadow map reduce ambient light? The shadow map only represents where direct light is occluded (not ambient/indirect light).

Maybe your SSAO is dark due to mishandling of gamma? Is your lighting pipeline operating fully in HDR linear RGB, with tone mapping at the end?

May 19, 2024 11:30 PM
Ed Welch

@Aressera ambient light is added to the shadow, so it does affect the shadow. I don't do HDR or tone mapping, but I use sRGB

May 21, 2024 09:58 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement