BGE HBAO Ambient Occlusion Shader

Update 1: See this post

Original post:

Hi,

the following shader is an implementation of the very beautiful and up-to-date “Horizon Based Ambient Occlusion” AO method for the BGE.

Some test-scene results:


(test scene with shader)


(the pure HBAO shader pass)


(comparison of HBAO and SSAO)

To set it up, just add it as a 2D filter and change the shader’s variables to match your scene.
I would also recommend to use the UPBGE branch for the best performance (it really does make a huge difference!).

Shader download: REMOVED - use version in update post

Shader code:


uniform sampler2D bgl_DepthTexture;
uniform sampler2D bgl_RenderedTexture;
uniform float bgl_RenderedTextureWidth;
uniform float bgl_RenderedTextureHeight;








//THIS NEEDS TO MATCH YOUR CAMERA SETTINGS---------------------
const float znear = 0.1;    //Z-near
const float zfar = 100.0;   //Z-far
const float fov = 50.0;     //FoV
//-------------------------------------------------------------


//USER VARIABLES-----------------------------------------------
const float intensity = 1.2;            //Intensity of the AO effect
const float sampleRadius = 9.0;         //Radius of the AO, bigger values need more performance


const float sampleDirections = 10.0;    //Main sample count, affects performance heavily 
const float sampleSteps = 32.0;         //SubSample count, sometimes higher values are faster


const bool useAttenuation = false;      //Applies attenuation to each AO sample, different values look better depending on your scene
const float attenuationScale = 1.0;     //Depth scale of the attenuation, different values look better depending on your scene


const float angleBias = 0.2;            //Brightens up the AO effect to reduce noise and artifacts


const bool noise = false;               //Use noise instead of pattern for sample dithering, much slower but more accurate
const float noiseamount = 1.0;          //Per-Pixel noise amount, bigger values need more performance
const float jitterAmount = 1.0;         //Per-Sample noise amount, bigger values need more performance


const float lumInfluence = 0.3;         //Influence of the luminance on the AO effect


const bool onlyAO = false;               //Only show AO pass for debugging
const bool externalBlur = false;         //Store AO in alpha pass for a later blur
//-------------------------------------------------------------












float width = bgl_RenderedTextureWidth;
float height = bgl_RenderedTextureHeight;


vec2 texCoord = gl_TexCoord[0].st;


float aspectratio = width/height;
float thfov = tan(fov * 0.0087266462597222);
const float TWO_PI = 6.283185307;


vec3 getLinearColor(vec2 coord)
{    
    vec3 C = vec3(texture2D(bgl_RenderedTexture, coord));
    C.r = pow(C.r, 2.2);
    C.g = pow(C.g, 2.2);
    C.b = pow(C.b, 2.2);
    return C.rgb;
}


vec3 sRGBToLinear(vec3 C)
{
    C.r = pow(C.r, 2.2);
    C.g = pow(C.g, 2.2);
    C.b = pow(C.b, 2.2);
    return C.rgb;
}


vec3 linearTosRGB(vec3 C)
{
    C.r = pow(C.r, 0.45454545);
    C.g = pow(C.g, 0.45454545);
    C.b = pow(C.b, 0.45454545);
    return C.rgb;
}


float getLinearDepth(vec2 coord)
{
    float zdepth = texture2D(bgl_DepthTexture,coord).x;
    return zfar*znear / (zfar + zdepth * (znear - zfar));
}


vec3 getViewVector(vec2 coord)
{
    vec2 ndc = (coord * 2.0 - 1.0);
    return vec3(ndc.x*thfov, ndc.y*thfov/aspectratio, 1.0);
}


vec3 getViewPosition(vec2 coord)
{
    return getViewVector(coord) * getLinearDepth(coord);
}


vec3 getViewNormal(vec2 coord)
{
    float pW = 1.0/width;
    float pH = 1.0/height;
    
    vec3 p1 = getViewPosition(coord+vec2(pW,0.0)).xyz;
    vec3 p2 = getViewPosition(coord+vec2(0.0,pH)).xyz;
    vec3 p3 = getViewPosition(coord+vec2(-pW,0.0)).xyz;
    vec3 p4 = getViewPosition(coord+vec2(0.0,-pH)).xyz;


    vec3 vP = getViewPosition(coord);
    
    vec3 dx = vP-p1;
    vec3 dy = p2-vP;
    vec3 dx2 = p3-vP;
    vec3 dy2 = vP-p4;
    
    if(length(dx2)<length(dx)&&coord.x-pW>=0||coord.x+pW>1) {
    dx = dx2;
    }
    if(length(dy2)<length(dy)&&coord.y-pH>=0||coord.y+pH>1) {
    dy = dy2;
    }
    
    return normalize(cross( dx , dy ));
}


float rand(vec2 co)
{
    if (noise) {
        return fract(sin(dot(co.xy,vec2(12.9898,78.233)*3.0)) * 43758.5453);
    } else {
        return ((fract(1.0-co.s*(width/2.0))*0.3)+(fract(co.t*(height/2.0))*0.6))*2.0-1.0;
    }
}


vec2 rand2D(vec2 coord) //generating noise/pattern texture for dithering
{
    float noiseX = ((fract(1.0-coord.s*(width/2.0))*0.25)+(fract(coord.t*(height/2.0))*0.75))*2.0-1.0;
    float noiseY = ((fract(1.0-coord.s*(width/2.0))*0.75)+(fract(coord.t*(height/2.0))*0.25))*2.0-1.0;
    
    if (noise)
    {
        noiseX = clamp(fract(sin(dot(coord ,vec2(12.9898,78.233))) * 43758.5453),0.0,1.0)*2.0-1.0;
        noiseY = clamp(fract(sin(dot(coord ,vec2(12.9898,78.233)*2.0)) * 43758.5453),0.0,1.0)*2.0-1.0;
    }
    return vec2(noiseX,noiseY)*noiseamount;
}


mat4 getViewProjectionMatrix()
{
    mat4 result;
    
    float frustumDepth = zfar - znear;
    float oneOverDepth = 1 / frustumDepth;


    result[0][0] = 1 / thfov;
    result[1][1] = aspectratio * result[0][0];
    result[2][2] = zfar * oneOverDepth;
    result[2][3] = 1;
    result[3][2] = (-zfar * znear) * oneOverDepth;
    
    return result;
}


vec2 ComputeFOVProjection(vec3 pos, mat4 VPM)
{
    vec4 offset = vec4(pos, 1.0);
    offset = VPM * offset;
    offset.xy /= offset.w;
    return offset.xy * 0.5 + 0.5;
}


void main()
{
    mat4 VPM = getViewProjectionMatrix();
    mat4 viewProjectionInverseMatrix  = inverse(VPM);
    
    vec3 color = texture2D(bgl_RenderedTexture,texCoord).rgb;
    
    vec3 originVS = getViewPosition(texCoord);
    vec3 normalVS = getViewNormal(texCoord);
    
    float radiusSS = 0.0;
    float radiusWS = 0.0;
    
    radiusSS = sampleRadius;
    vec4 temp0 = viewProjectionInverseMatrix * vec4(0.0, 0.0, -1.0, 1.0);
    vec3 out0 = temp0.xyz;
    vec4 temp1 = viewProjectionInverseMatrix * vec4(radiusSS, 0.0, -1.0, 1.0);
    vec3 out1 = temp1.xyz;
    
    radiusWS = min(tan(radiusSS * fov * 0.0087266462597222) * originVS.z, length(out1 - out0));
    
    /*if (radiusSS < 1.0 / width) {
        gl_FragColor.rgb = color;
        gl_FragColor.a = 1.0;
        return;
    }*/
    
    const float theta = TWO_PI / float(sampleDirections);
    float cosTheta = cos(theta);
    float sinTheta = sin(theta);
    
    mat2 deltaRotationMatrix = mat2(cosTheta, -sinTheta, sinTheta, cosTheta);
    vec2 deltaUV = vec2(1.0, 0.0) * (radiusSS / (float(sampleDirections * sampleSteps) + 1.0));
    
    vec2 sampleNoise = rand2D(texCoord);
    mat2 rotationMatrix = mat2(sampleNoise.x, -sampleNoise.y, 
                               sampleNoise.y,  sampleNoise.x);
    
    deltaUV = rotationMatrix * deltaUV;
    
    float occlusion = 0.0;
    float jitter = rand(texCoord) * jitterAmount;
    
    for(int i = 0; i < sampleDirections; i++) {
        deltaUV = deltaRotationMatrix * deltaUV;
        
        vec2 sampleDirUV = deltaUV;
        float oldAngle = angleBias;
        
        for(int j = 0; j < sampleDirections; j++) {
            vec2 sampleUV = texCoord + (jitter + float(j)) * sampleDirUV;
            vec3 sampleVS = getViewPosition(sampleUV);
            vec3 sampleDirVS = (sampleVS - originVS);
            
            float gamma = 1.570796326 - acos(dot(normalVS, normalize(sampleDirVS)));
            
            if (gamma > oldAngle) {
                float value = sin(gamma) - sin(oldAngle);
                
                if(useAttenuation) {
                    float attenuation = clamp(1.0 - pow(length(sampleDirVS) / radiusWS * attenuationScale, 2.0), 0.0, 1.0);
                    occlusion += attenuation * value;
                } else {
                    occlusion += value;
                }
                
                oldAngle = gamma;
            }
        }
    }
    
    occlusion = 1.0 - occlusion / float(sampleDirections);
    occlusion = clamp(pow(occlusion, 1.0 + intensity), 0.0, 1.0);
    
    vec3 lumcoeff = vec3(0.299,0.587,0.114);
    float lum = dot(color.rgb, lumcoeff) * lumInfluence;
    
    occlusion = lum + ((1.0 - lum) * occlusion);
    
    gl_FragColor.rgb = color * occlusion;
    gl_FragColor.a = occlusion;
    
    if(externalBlur) {
        gl_FragColor.rgb = color;
        gl_FragColor.a = occlusion;
    }
    
    if(onlyAO) {
        gl_FragColor.rgb = vec3(occlusion);
        gl_FragColor.a = 1.0;
    }
    
    //gl_FragColor.rgb = vec3(jitter);
}

1 Like

Looks fantastic, very different compared to SSAO :open_mouth:
Thanks for sharing :slight_smile:

Looks amazing bro thks nice work on that bro.

Fred/K.S

Great work!!!

How is the performance with respect to SSAO?

This filter truely looks a lot better than ssao! I can’t wait to check it out myself :slight_smile:

Be aware that the size of your game window will affect performance heavily.
Nice shader, it looks a bit weird on my PC, I wish I could achive your scene

I do not know, I do not have this cool effect, and the performance is not very good even on the UPBG. What do I do wrong?


In Tesseract Game/Engine SSAO and performance:



Is it possible to create a filter “dirty edges”?
Make a filter that will generate a gradient from … for example, from “edges crease” - This would be a light SSAO effect. Or generation from edges with a certain angle.

In Upbge dont use modifiers. Heres my test file: http://pasteall.org/blend/index.php?id=46798 I got 151 fps

Really cool!
Are you able to implement screen space reflections into the bge ?

@Nick Manchul
I’m getting similar result - blocky/pixeletted AO.
Intel HD4000.

I’m getting similar result - blocky/pixeletted AO.
Intel HD4000.

The Intel HD4000 is probably already too weak a graphics card. But try installing Tesseract Game, and see how much FPS will be with SSAO - Tesseract Game ~ 200Mb

Test result without changing other option:
SSAO Low: 34 fps
SSAO Med: 28 fps
SSAO High: 24 fps

Ок!
So, the shader HBAO is not quite perfect - on your low-performance video card, even playing Tesseract with SSAO is possible…

Same Glitch as Nick, on my Alienware Alpha. Very sad this doesn’t work on all systems. It’s been almost a month, hope to see a reply soon.

1 Like

On my tech demo HBAO shader work amazing :slight_smile:




But no one said that the shader is bad in itself - it’s about performance.
Tech demo is not an indicator, an indicator of a game that can go on weak computers. In this shader, there is no way to enable a low performance mode - that’s why it’s not perfect, but also high-quality.
Cool demo :yes:

I have a thread about that going over on the bge discussion section (link in my signature, too)

edit: nevermind sorry

Does somebody knows how to apply mipmapping to this filter to get rid of the dither?
i think it’s possible now with upgbe(old renderer 0.2.5).

the filter is just great, works nice in my games.

About performace:
‘const float sampleDirections’ set to 2 is enough, ajusting the other values acording to the scene.
but then you get the dithering…

i’ve tried to do this with single pass but my glsl knowledge is limited.
does anybody know how to do something like:

 occlusion = texture2DLod(aoPass, coord, lod).rgb;

…or similar? I don’t know. :persevere:

or how to use the ‘externalBlur’ option?

const bool externalBlur = true; //Store AO in alpha pass for a later blur

the phrase: ‘it does not seems to affect anything’ is also a constant. (bad joke) :joy:

hi, how did you manage this to work? I get a lot of errors. I used 2.79 and upbge 0.2.5.