Realtime AO

August 15, 2008

Ambient Occlusion

Ambient Occlusion

I figured I should write up what I’ve been playing with lately in realtime ambient occlusion, since it could give some ideas to other people playing with realtime AO.

In a 2005 Siggraph paper, Janne Kontkanen and Samuli Laine presented “Ambient Occlusion Fields“, which store the ambient occlusion cast by a given object in a texture wrapped around it. I looked at this, and figured that for a simple object, it would be just as fast to analytically compute the occlusion cast to a given point, instead of looking it up in a texture. After I implemented this approach by hacking around, I was looking around and found out that Iñigo Quilez has done some nice derivations of analytically correct ambient occlusion cast by spheres: http://rgba.scenesp.org/iq/computer/articles/sphereao/sphereao.htm

My math comes out rather different from Iñigo’s, because I’m aiming for decent performance in scenes involving thousands of objects, and I’m cutting a lot of corners. I’m just going for the visual effect given by AO, rather than necessarily using the same mathematical justification. It’s still good to have the analytic result to compare to (and Iñigo’s site is a great place to look around at some amazing Z-buffer AO demos).

The basic idea of my implementation is the same as Iñigo’s analytic AO. Given a caster object with a simple shape, and a receiving point somewhere in the space around it, it should be possible to compute how much the caster occludes that point. Instead of deriving the correct amount of occlusion mathematically, I just played around with what you want visually – the closer you are to the caster, the darker the contact shadow. It turns out that you also need to consider the normal of the receiving point – if the normal points away from the caster, it doesn’t receive any occlusion.

Basic Idea of Analytic AO

Basic Idea of Analytic AO

My objects are sphere capped cylinders. To approximate the AO from them, I find the closest point on the center line of the cylinder (clamped to the bounds of the cylinder) to the receiving point. The vector from the receiving point to this closest point gives me the direction of occlusion, and how far away from the occluder we are. This is equivalent to pretending that instead of dealing with a capped cylinder, we are dealing with a single sphere positioned inside the cylinder where it will cause the most occlusion to the current point. This is not accurate, but it gives reasonable results, and could pretty easily be extended to shapes such as boxes.

The occlusion strength is 0 when the distance is 3 times the radius of the occluder, and increases in a parabola to full strength when the distance is equal to the radius (on the surface of the occluder). This is absolutely non-physical (the correct falloff as derived by Iñigo is an inverse square), but for performance reasons I needed the occlusion to quickly drop to 0, so that each occluder has a limited area of effect. I multiply this strength by the cosine of the angle between the receiver normal and the occlusion vector, mapped between 0 to 1 instead of -1 to 1.

An interesting aside: Iñigo just uses the cosine of the angle, which drops 0 when the occluder is at 90 degrees to the receiver normal. If the occluder is above the horizon, the cosine is correct, but a nearby occluder centered on the horizon should still cast some occlusion. The correct result for occluders near the horizon would probably be in between the raw cosine and my “remap 0 to 1” hack, and I suspect would be pretty messy to derive.

So, I have a simple mathematical approximation of AO given the caster object and the receiving point. The next step is to apply it to each receiving point with each caster object. I do this by first rendering depth and normal passes with all receiving geometry. Each caster is then rendered as a polygonal bounding cylinder. Because the AO falls off to 0 at 3 times the original radius, the bounding mesh just needs to have 3 times the radius of the original. The shader attached to the bounding mesh is passed in the current caster position, and reads in the receiving point out of the depth and normal buffer. The position of the receiving point can then be reconstructed, and the ambient occlusion approximation applied. I just add up the contributions of all the bounding meshes for all the occluders to give a final result, another approximation that looks OK with some tweaking.

This approach has the potential to be extremely expensive – a given receiver pixel could be within many occlusion boundaries, resulting in a lot of overdraw. I’ve been able to get it working on some pretty complicated scenes in Marlin, so it seems to show a fair bit of potential – though I have to render at pretty low resolutions to approach realtime on really complicated scenes. I’ve only begun optimizing this approach – I’m hoping I can do a fair bit with some clever early rejections and some sort of occlusion culling.

This approach works for Marlin because everything is composed of simple objects. It seems possible that by approximating complex shapes by sets of simple shapes, arbitrary objects could be handled to some degree. With the advancements being made in Z-buffer AO, there may not be too many reasons to use this approach on more complicated geometry, but it does have some real advantages: it is view independent, and it allows very large objects to have very large areas of effect.

Another trick I use in Marlin is doing shadows a very similar way – simple shadow bounding volumes that compare receiver pixels to the caster location. This gives some pretty unique looking soft shadows, which are pretty non-physical currently, but at least look differently wrong than the standard approaches to shadowing. If you can’t look realistic, you can at least look unique.

To wrap things up, here’s a standard breakdown of the different lighting components I’m using in Marlin. AO makes it a whole lot easier to make simple objects look good, and is pretty central to the style of Marlin. This simple scene runs at 1680×1050 at 30 fps on an 8600 GT.

Simple Lighting (N dot L)

Simple Lighting (N dot L)

Specular (Environment Map with Fresnel Falloff)

Specular (Environment Map with Fresnel Falloff)

Ambient Occlusion

Ambient Occlusion

Shadow

Shadow

Everything but Specular (replaced with constant contribution)

Everything but Specular (replaced with constant contribution)

Final Combination

Final Combination

Advertisements

Hello world!

August 4, 2008

I suppose if I’m going to put my stuff on a blog, it would be customary to make blog posts.  I should probably write something up on the realtime ambient occlusion stuff I’ve been playing with for Marlin, since it might actually be of interest to someone else.