# Ambient occlusion : explanations

Hello there. I am sorry that I have not written since long time ago, but I had to pass my exams ^^.

So, what is ambient occlusion?

There, we can see respictively : No ambient occlusion, occlusion map, and rendering with ambient occlusion.

So, Ambient occlusion allow to improve shadow in the scene.

Generally, Ambient Occlusion is defined by

Ohhhh, it’s a very difficult formula, with a strange integrale.

We are going to see how we can get one formula in this kind.

Firstly, ambient occlusion is a « ray tracing » technique. The idea behind ambient occlusion is, you launch many rays throughout the hemisphere oriented by normal $\overrightarrow{n}$

Obviously, we can say that one orthogonal ray to normal is not influent compared to one parallel ray, so, we can introduce a dot product between ray and normal into the integral. We do not perform this integral in all hemisphere, but only in the hemisphere’s surface.

I remind you that the infinitesimal surface of sphere is $d\omega = R^{2}\sin(\theta) d\theta d\phi$.

So, we can lay down that :

$\displaystyle{ ka = K \cdot ka_{total} = \int_{\Omega} V(\overrightarrow{\omega})\cdot \cos(\overrightarrow{n},\overrightarrow{\omega})\cdot d\overrightarrow{\omega}}$

Where $\Omega$ is the hemisphere oriented by $\overrightarrow{n}$ and $\overrightarrow{\omega}$ is the ray « launched » in the hemisphere, $V(\overrightarrow{\omega})$ is the view function defined by

$\displaystyle{V(\overrightarrow{\omega}) = \left\{\begin{matrix} &0&\hspace{1mm}if\hspace{1mm}occluder\\ &1&\hspace{1mm}if\hspace{1mm}no\hspace{1mm}occluder \end{matrix}\right.}$

K is the constant as we have to compute, because ka have to be ranged between 0 and 1. So, now we can try to compute K, with the view function always to 1, because we compute the case with no occlusion is performed.

$\displaystyle{\begin{array}{lcl}&&\int_{\Omega} V(\overrightarrow{\omega})\cdot \cos(\overrightarrow{n},\overrightarrow{\omega})\cdot d\omega \\ &=&\int_{\Omega}\cos(\overrightarrow{n},\overrightarrow{\omega})\cdot d\omega\\ &=&\int_{0}^{2\pi}\int_{0}^{\frac{\pi}{2}}R^2\cdot\frac{\overrightarrow{n}}{{||\overrightarrow{n}||}}\cdot\frac{\overrightarrow{\omega}}{{||{\overrightarrow{w}||}}}\cdot \sin(\theta)\cdot d\theta d\phi \left(||\overrightarrow{\omega}||=||\overrightarrow{n}|| = R \right ) \\ &=&\int_{0}^{2\pi}\int_{0}^{\frac{\pi}{2}}\cos(\theta)\cdot \sin(\theta)\cdot d\theta d\phi\\ &=&\int_{0}^{2\pi}\frac{1}{2}\cdot d\phi\\ &=&\pi \end{array}}$

So, $K=\frac{1}{\pi}$, so, we have the same expression of the first integral in the beginning of this article :

$\displaystyle{ka=\frac{1}{\pi}\int_{\Omega}V(\overrightarrow{\omega})\cdot \cos(\theta)\cdot d\omega}$

For people who like so much rigourous mathematics, I know it’s not very rigourous, but it’s with this « method » that we will compute our occlusion factor :).
If you prefer a more accurate technique, you can integrate in all Hemisphere (with the variable radius and take a view function who return one value between 0 and 1 according to the distance of occluder from the origin of ray) and, you get exactly the same formula cause you do one thing like this : $\frac{1}{R}\int_{0}^{R} dr = 1$ with $\frac{1}{R}$ is from the View function to limit the return value from 0 to 1.

So, now, we can try to approximate this integral. We have two problems, we can’t perform « launching » infinite rays, so, we launch only few rays, and we have to use a « inverse » of view function $\bar{V}$

$\displaystyle{\begin{array}{lcl}&&\frac{1}{\pi}\int_{\Omega}V(\overrightarrow{\omega})\cdot\overrightarrow{n}\cdot\overrightarrow{\omega}\cdot d\omega \\ &=&1-\frac{1}{\pi}\int_{\Omega}\bar{V}(\overrightarrow{\omega})\cdot\overrightarrow{n}\cdot\overrightarrow{\omega}\cdot d\omega\\ &\approx&1-\frac{1}{N}\sum_{\mathbb{N}}\bar{V}(\overrightarrow{\omega})\cdot\overrightarrow{n}\cdot\overrightarrow{\omega} \end{array}}$

After that, we can blur the occlusion map to improve its rendering.

So we just use a simple blur.

#version 440 core

/* Uniform */
#define CONTEXT 0
#define MATRIX 1
#define MATERIAL 2
#define POINT_LIGHT 3

layout(local_size_x = 256)in;

layout(shared, binding = CONTEXT) uniform Context
{
uvec4 sizeScreenFrameBuffer;
vec4 posCamera;
mat4 invProjectionViewMatrix;
};

layout(binding = 4) uniform sampler2D AO;
layout(binding = 4, r32f) uniform image2D imageAO;

void main(void)
{
float blur = texture(AO, vec2(gl_GlobalInvocationID.xy) / sizeScreenFrameBuffer.zw).x;

for(int i = 4; i > 0; --i)
blur += texture(AO, vec2((ivec2(gl_GlobalInvocationID.xy) + ivec2(-i, 0))) / sizeScreenFrameBuffer.zw).x;

for(int i = 4; i > 0; --i)
blur += texture(AO, vec2((ivec2(gl_GlobalInvocationID.xy) + ivec2(i, 0))) / sizeScreenFrameBuffer.zw).x;

imageStore(imageAO, ivec2(gl_GlobalInvocationID.xy), vec4(blur / 9, 0, 0, 0));
}

Now, we have to code our ambient occlusion.

This picture is very good to understand how we can use our approximation.
Indeed, there, we can see that if the point is red, we have $V(\overrightarrow{\omega}) = 1$, so $\bar{V}\overrightarrow{\omega}) = 0$ You just have to test the depth buffer to know if the point is occluder or no.

#version 440 core

/* Uniform */
#define CONTEXT 0
#define MATRIX 1
#define MATERIAL 2
#define POINT_LIGHT 3

layout(shared, binding = CONTEXT) uniform Context
{
uvec4 sizeScreenFrameBuffer;
vec4 posCamera;
mat4 invProjectionViewMatrix;
};

layout(local_size_x = 16, local_size_y = 16) in;

layout(binding = 1) uniform sampler2D position;
layout(binding = 2) uniform sampler2D normal;
layout(binding = 3) uniform sampler2D distSquare;

writeonly layout(binding = 4, r16f) uniform image2D AO;

void main(void)
{
float ao = 0.0;

const ivec2 texCoord = ivec2(gl_GlobalInvocationID.xy);
const vec2 texCoordAO = vec2(texCoord) / sizeScreenFrameBuffer.zw;

vec3 positionAO = texture(position, texCoordAO).xyz;
vec3 normalAO = texture(normal, texCoordAO).xyz;
float distSquareAO = texture(distSquare, texCoordAO).x;

for(int j = -2; j < 3; ++j)
{
for(int i = -2; i < 3; ++i)
{
vec2 texCoordRay = vec2(texCoord + ivec2(i, j)) / sizeScreenFrameBuffer.zw;

vec3 positionRay = texture(position, texCoordRay).xyz;
float distSquareRay = texture(distSquare, texCoordRay).x;

float c = dot(normalAO, normalize(positionRay - positionAO));

if(c < 0.0)
c = -c;

if(distSquareRay < distSquareAO)
ao += c;
}
}

imageStore(AO, texCoord, vec4((1 - ao / 25), 0.0, 0.0, 0.0));
}


Strangely, in this case, if I use shared memory, I have badder result than just texture. Maybe the allocation of shared memory is longer and it’s not very efficient here :-).

I advise you to use texture instead of imageLoad, indeed, I get 8 times performance better with texture ^^.