Screen Space Ambient Occlusion

Tweet blitzmax blitzmax-ng shaders code-archives
(Posted 1 year ago) RonTek

SSAO for OpenB3DMax. A framerate gobbler, but hey it's still S.S.A.O!

OpenB3DMax OpenB3D BlitzMax SSAO


varying vec4 vertColor;

void main() {
    gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
    vertColor = gl_Color;   


uniform sampler2D colortex;
uniform sampler2D depthtex;

vec2 fragCoords = gl_FragCoord.xy;

const float PI = 3.14159265;

uniform float width; // render target width
uniform float height; // render target height

uniform int onlyAO;

uniform float near;
uniform float far;

uniform int samples;
uniform int rings;

vec2 texCoord = gl_TexCoord[0].st;

vec2 rand( in vec2 coord) {
    float noiseX = (fract(sin(dot(coord, vec2(12.9898, 78.233))) * 43758.5453));
    float noiseY = (fract(sin(dot(coord, vec2(12.9898, 78.233) * 2.0)) * 43758.5453));
    return vec2(noiseX, noiseY) * 0.004;

float readDepth( in vec2 coord) {
    return (2.0 * near) / (far + near - texture2D(depthtex, coord).x * (far - near));

float compareDepths( in float depth1, in float depth2) {
    float aoCap = 1.0;
    float aoMultiplier = 100.0;
    float depthTolerance = 0.0000;
    float aorange = 60.0;
    float diff = sqrt(clamp(1.0 - (depth1 - depth2) / (aorange / (far - near)), 0.0, 1.0));
    float ao = min(aoCap, max(0.0, depth1 - depth2 - depthTolerance) * aoMultiplier) * diff;
    return ao;

void main() {
    float depth = readDepth(texCoord);
    float d;

    float aspect = width / height;
    vec2 noise = rand(texCoord);

    float w = (1.0 / width) / clamp(depth, 0.05, 1.0) + (noise.x * (1.0 - noise.x));
    float h = (1.0 / height) / clamp(depth, 0.05, 1.0) + (noise.y * (1.0 - noise.y));

    float pw;
    float ph;

    float ao;
    float s;

    for (int i = -rings; i < rings; i += 1) {
        for (int j = -samples; j < samples; j += 1) {
            float step = PI * 2.0 / float(samples * i);
            pw = (cos(float(j) * step) * float(i));
            ph = (sin(float(j) * step) * float(i)) * aspect;
            d = readDepth(vec2(texCoord.s + pw * w, texCoord.t + ph * h));
            ao += compareDepths(depth, d);
            s += 1.0;

    ao /= s;
    ao = 1.0 - ao;

    vec3 color = texture2D(colortex, texCoord).rgb;

    // Get luminance from colortex
    vec3 lumcoeff = vec3(0.299, 0.587, 0.114);
    float lum = dot(color.rgb, lumcoeff);
    vec3 luminance = vec3(lum, lum, lum);

    vec3 white = vec3(1.0, 1.0, 1.0);
    vec3 black = vec3(0.0, 0.0, 0.0);
    vec3 treshold = vec3(0.2, 0.2, 0.2);

    luminance = clamp(max(black, luminance - treshold) + max(black, luminance - treshold) + max(black, luminance - treshold), 0.0, 1.0);

    if (onlyAO) {
        gl_FragColor = vec4(mix(vec3(ao, ao, ao), white, luminance), 1.0); //ambient occlusion only
    } else {
        gl_FragColor = vec4(color * mix(vec3(ao, ao, ao), white, luminance), 1.0);

Full Example with SSAO Debug

' ssao.bmx
' postprocess effect - Screen Space Ambient Occlusion (SSAO)


Framework Openb3dMax.B3dglgraphics
Import Brl.Random
?Not bmxng
Import Brl.Timer
Import Brl.TimerDefault

'Local width%=DesktopWidth(),height%=DesktopHeight()

Local width%=800,height%=600

Graphics3D width, height,0,2

SeedRnd MilliSecs()
ClearTextureFilters ' remove mipmap flag for postfx texture

Global Camera:TCamera=CreateCamera()
CameraRange Camera,0.5,1000.0 ' near must be closer than screen sprite to prevent clipping
CameraClsColor Camera,150,200,250

Global postfx_cam:TCamera=CreateCamera() ' copy main camera
CameraRange postfx_cam,0.5,1000.0
CameraClsColor postfx_cam,150,200,250
HideEntity postfx_cam

Local Light:TLight=CreateLight()
TurnEntity Light,45,45,0

Local size:Int=256, vsize:Float=30, maxheight:Float=10
Local terrain:TTerrain=LoadTerrain("../media/heightmap_256.BMP") ' path case-sensitive on Linux
ScaleEntity terrain,1,(1*maxheight)/vsize,1 ' set height
terrain.UpdateNormals() ' correct lighting

PositionEntity terrain,-size/2,-10,size/2

' Texture terrain
Local grass_tex:TTexture=LoadTexture( "../media/terrain-1.jpg" )
EntityTexture terrain,grass_tex
ScaleTexture grass_tex,10,10

Local pivot:TPivot=CreatePivot()
PositionEntity pivot,0,0,0
Local anim_time:Float
Local anim_ent:TMesh=LoadAnimMesh("../media/zombie.b3d",pivot)
PositionEntity anim_ent,0,0,12
TurnEntity anim_ent,0,-90,0

Local cube:TMesh=LoadMesh("../media/wcrate1.3ds")
ScaleMesh cube,0.15,0.15,0.15
PositionEntity cube,0,3,0
Local cube_tex:TTexture=LoadTexture("../media/crate.bmp",1)
EntityTexture cube,cube_tex
TurnEntity cube,0,45,0

' Add 4 more
Local c1:TEntity = CopyEntity(cube)
PositionEntity c1,5,0,5
Local c2:TEntity = CopyEntity(cube)
PositionEntity c2,-5,0,-5
Local c3:TEntity = CopyEntity(cube)
PositionEntity c3,5,0,-5
Local c4:TEntity = CopyEntity(cube)
PositionEntity c4,-5,0,5

Global colortex:TTexture=CreateTexture(width,height,1+256)
ScaleTexture colortex,1.0,-1.0

Global depthtex:TTexture=CreateTexture(width,height,1+256)
ScaleTexture depthtex,1.0,-1.0

' in GL 2.0 render textures need attached before other textures (EntityTexture)
CameraToTex colortex,Camera
DepthBufferToTex depthtex,camera
TGlobal.CheckFramebufferStatus(GL_FRAMEBUFFER_EXT) ' check for framebuffer errors

' screen sprite - by BlitzSupport
Global screensprite:TSprite=CreateSprite()
EntityOrder screensprite,-1
ScaleSprite screensprite,1.0,Float( GraphicsHeight() ) / GraphicsWidth() ' 0.75
MoveEntity screensprite,0,0,1.0 ' set z to 0.99 - instead of clamping uvs
EntityParent screensprite,Camera

Global screensprite2:TSprite=CreateSprite()
ScaleSprite screensprite2,1.0,Float( GraphicsHeight() ) / GraphicsWidth() ' 0.75
EntityOrder screensprite2,-1
EntityParent screensprite2,camera
MoveEntity screensprite2,0,0,1.0 ' set z to 1.0
EntityTexture screensprite2,depthtex

PositionEntity Camera,0,7,0 ' move camera now sprite is parented to it
MoveEntity Camera,0,0,-25

Local shader:TShader=LoadShader("","../glsl/ssao.vert.glsl", "../glsl/ssao.frag.glsl")

Local toggle:Int = 0

ShaderTexture(shader,colortex,"colortex",0) ' Our render texture
ShaderTexture(shader,depthtex,"depthtex",1) ' 1 is depth texture
SetFloat(shader,"width", width)
SetFloat(shader,"height", height)
SetFloat(shader,"near", 1.0)
SetFloat(shader,"far", 1000.0)
SetInteger(shader,"samples", 5)
SetInteger(shader,"rings", 3)
UseInteger(shader,"onlyAO", toggle)
ShadeEntity(screensprite, shader)

Global postprocess%=1
Local time#=0, framerate#=60.0, animspeed#=10
Local timer:TTimer=CreateTimer(framerate)
UseFloat(shader,"time",time) ' Time used to scroll the distortion map

' fps code
Local old_ms%=MilliSecs()
Local renders%, fps%

Local update% = 1

While Not KeyHit(KEY_ESCAPE)

    If KeyHit(KEY_TAB) Then toggle = Not toggle

    time=Float((TimerTicks(timer) / framerate) * animspeed)

    If KeyDown(KEY_MINUS) Then anim_time#=anim_time#-0.1
    If KeyDown(KEY_EQUALS) Then anim_time#=anim_time#+0.1

    If anim_time>20 Then anim_time=2
    TurnEntity pivot,0,1,0

    If KeyHit(KEY_SPACE) Then postprocess=Not postprocess

    ' control camera
    If KeyDown( KEY_RIGHT )=True Then TurnEntity Camera,0,-1,0
    If KeyDown( KEY_LEFT )=True Then TurnEntity Camera,0,1,0
    If KeyDown( KEY_DOWN )=True Then MoveEntity Camera,0,0,-0.25
    If KeyDown( KEY_UP )=True Then MoveEntity Camera,0,0,0.25

    PositionEntity postfx_cam,EntityX(Camera),EntityY(Camera),EntityZ(Camera)
    RotateEntity postfx_cam,EntityPitch(Camera),EntityYaw(Camera),EntityRoll(Camera)

    If KeyHit(KEY_Q) Then update=Not update


    While update = 0
        If KeyHit(KEY_TAB) Then toggle = Not toggle
        If KeyHit(KEY_SPACE) Then postprocess=Not postprocess
        If KeyHit(KEY_Q) Then update=Not update


    ' calculate fps
    If MilliSecs()-old_ms>=1000

    Text 0,20,"FPS: "+fps
    Text 0,40,"Space: postprocess = "+postprocess
    Text 0,60,"Tab: Toggle SSAO only = "+toggle
    Text 0,80,"Q: Pause"
    Text 0,100,"anim_time="+anim_time


Function Update1Pass()

    If postprocess=0
        HideEntity postfx_cam
        ShowEntity Camera
        HideEntity screensprite2
        HideEntity screensprite

    ElseIf postprocess=1
        HideEntity camera
        ShowEntity postfx_cam
        HideEntity screensprite2
        HideEntity screensprite

        CameraToTex colortex,postfx_cam

        HideEntity postfx_cam
        ShowEntity camera

        DepthBufferToTex depthtex,camera

        ShowEntity screensprite


End Function
(Posted 1 year ago) markcwm commented:

Awesome, thanks Ron! It's great to not have to do this myself! It looks like it will need a few more objects to show off the subtle light/shadow effect.

I've been busy with real life this summer and had to drop the programming but I hope to pick it up again at some point.

(Posted 1 year ago) RonTek commented:

You got it! I see and that's pretty normal. Ah I forgot that this was on your list and glad to help out with this one. This is a modern implementation based on most WebGL techniques which simplifies and does not have those blur passes. There are some changes though, but the idea and objective are still there. Ah yes, the params are there to play around with (examples uses only set to basic w/ 5 samples) and probably load a Sponza scene next?

Btw, thanks for the patreon support, supposed to be leaning towards bitcoin if ever (curious in trying it out), anyway very much appreciated.

(Posted 1 year ago) markcwm commented:

Hi RonTek,
yes this was on my list as it seemed pretty popular and a nice effect, I didn't realize it's so slow though, I get 2 fps on my old GL2 laptop LOL! I'd say if it had blur passes it would be 0 fps, so it's better as simple as possible. I'm wondering what the samples and rings parameters mean, I'm not sure what a Sponza scene is, is it a Croatian palace?

Glad to help with the Patreon, you're doing a good job here and deserve some support. I think Bitcoin is like way too expensive now, something like $7000 a coin now, the guys who invested early on made the big money, it seems like a big ponzi/pyramid scheme to me but whatever.

(Posted 1 year ago) RonTek commented:

Ah, yes you really have to expect a huge decrease of performance/fps when applying any ambient occlusion shader techniques. I actually tested this and it went down to half from my usual fps with Openb3d (1500 -> 800fps) on a decent gfx card.

Awesome! Thanks a lot, just trying to keep all these blitz stuff alive just the way it was and hopefully some new interesting stuff with all your help. I hope I do justice and can get that "homey" atmosphere and feeling as this is the reason why I got hooked from Blitz (Basic in general). Aside from the reason that this is my first coding language before I moved to C/C++ and etc., somehow the old community forums got my attention and I always keep coming back when it was still active even though I was that lone spectator from a distance, reading older comments and finding tons of helpful and great information.

(Posted 1 year ago) RonTek commented:

oh forgot about the Sponza scene that you were asking, yes it has a bit of long history with testing cgi effects and lighting like GI and ambient occlusion. It has been one of the defacto standard scenes if you want to showcase most advanced lighting and shader stuff. As far as I can remember, Crytek made it popular (that I know of or at least in the gaming dev scene) since they were ahead with the fancy real-time game rendering business during UDK/UE3 and early Unity 4 days.



Unity 5


Some Voxel engine project that I found randomly on the web..

Sponza Atrium/Palace (actual place)

perhaps one of these days we could create a blitz version here as a showcase.

(Posted 1 year ago) markcwm commented:

Oh wow, looks good Ron, cheers! Cryengine looks awesome.

This reminds me that Openb3d really needs a soft-edge shadow solution.

(Posted 1 year ago) RonTek commented:

Nice! speaking of shadows, recently I'm a bit fascinated with soft shadow stencil volumes and just thought this technique could improve visuals with a good framerate. It looks like the modern counterpart and at the same time cheap which is always a good thing. I'm sure this can be done with openb3d and coincidentally just some time ago I just remembered that Andreyman's AShadows supports this feature.

As for soft textured shadows, I have not dug that deep with the source, but looking at the code it seems that you can manage to get a shadow buffer in there. It's still the same projected texture of just another camera/viewport looking from a light source point of view. I see a lot of new nice openb3dmax camera functions that might easily help in putting this up together. With that, you can hook and expose it with shader attributes just like the depth buffer.

You could maybe check out Blabz minib3dplus as he did it only using bmx code (probably did some gl commands with max). As for the shader part, it's almost the same implementation with most old and modern GL techniques. Once a basic shadow system or shadowbuffer is added, I think the advanced stuff like CSM/PSSM will just be an upgrade. I can definitely help if I can find some good matching reference.

I'm also experimenting with some simple shadow method stuff, which I will be pulling from previous blitz acthive and snippets, just for the learning experience and maybe to archive and showcase that it still works.

Btw, for the ssao ring param part, this is part of the sampling loop but I do see that this can be optimized to improve fps. I usually don't go for realtime AO particularly GI for games, as this puts toll to the gfx card, probably why everyone now moves to PBR lighting but it still not cheap. I remember seeing one PC game that I have played which that time did an update to support GI and then within a few weeks they pulled it back out due to bad performance since it is an open world/terrain. XD

(Posted 1 year ago) markcwm commented:

Wow, well I wasn't aware about most of this so cheers. I'll need to research this a lot but it'll have to go on the roadmap for now, I particularly like the idea of soft stencil shadows though.

I've added the postfx functions a few days ago but no working example yet, then I'll update the shader code which will include geometry shaders.

(Posted 1 year ago) RonTek commented:

Ok that would be great. I'll post an update as well when I get something up, cheers.

Edit: found the closest one here!

(Posted 1 year ago) markcwm commented:

Hi RonTek, nice find! Looks like as good a place as any to start for adding textured shadows to Openb3d.

This dude mentions that Percentage Closer Filtering is slow and recommends Variance Shadow Mapping, as it can use the GPU.

(Posted 1 year ago) RonTek commented:

Yes it is! something to get started. You can see the shader part is just the same and most of the work is done on adding the shadowbuffer, probably same commands like the ShadowBufferToTex?

Ah yes, you can see that demo is a decade ago and actually PCF filtering has evolved, now with Hardware PCF (NVidia also has provided a 4 tap free, not sure the full scope but some discussions here) has been preferred for games, at least with other game engines that I have tried. perhaps provided an option for the 3 (simple/pcf/vsm) eventually?

Blabz's Plus is another great reference to build on since it is already laid out with other lighting shaders to mix in and also uses PCF filtering which looks like the 4 tap free from NVidia (not sure though) :-)

some Hardware PCF video/demo I found quickly


Reply To Topic (minimum 10 characters)

Please log in to reply