A screenshot of Impaler's faux ambient occlusion without textured walls

Faking Ambient Occlusion

Impaler is a small game I created with the goal of having it run on integrated graphics (and still look nice). I had to find creative ways to fake expensive graphics techniques to achieve this. Anything that relied on sampling or multiple render passes would be too expensive for low-end GPUs. This meant I had to find a new approach to implement ambient occlusion.


Background on Ambient Occlusion

Ambient occlusion, often abbreviated AO, is a graphics technique for self-shadowing. It estimates how much ambient light is blocked by nearby geometry and adds a soft shadow to those areas. It has a large impact on the visual quality of a scene. I wanted this to give the game world a dark and gloomy feel.

Screenshot of Crysis Crysis (2007) was one of the first games to incorporate AO. It still looks good today!


Common AO Implementations

  • Screen space ambient occlusion (SSAO)
  • Horizon-based ambient occlusion (HBAO)

These approaches perform calculations in screen space and do not slow with complex scenes. They work well for just about any game and produce convincing results. However, they require multiple depth buffer samples per pixel. This is too expensive for the low-end hardware and integrated graphics. A different approach was needed for Impaler.

In the future, screen space AO implementations will disappear as games adopt ray tracing and global illumination models. These are the best-looking solutions. Screen space effects can suffer from under-sampling and artifacts at the edges of the screen.

Screenshot of Quake 2 RTX Quake 2 RTX (2019) Features real-time ray tracing and global illumination. Indirect lighting and shadow are calculated via ray tracing.


Why Did Impaler Need AO?

The game takes place in a dark cathedral-like arena. Without proper light and shadow, it simply does not create the illusion you are there. It is ugly and has a “flat” look remiscient of early 3d games.

Screenshot of Impaler with ambient occlusion forced off

A screenshot from Impaler with AO forced off. Notice the scene is overly bright and the corners are hard to distinguish.

The environment is simple - a large rectangle with 6 columns and game objects scattered throughout. Only a few areas in the world needed ambient occlusion:

  • Corners of the arena
  • Edges between walls and the floor
  • Bases of the stone columns

Approach Used in Impaler

Ray tracing is too heavy for a small indie game. I needed a different solution and was willing to make tradeoffs. The tradeoff was to make the implementation scene-dependent and share actual map geometry with the shader. And instead of sampling, a numeric approximation would be used to calculate the AO for each fragment.

Pseudocode:

  • Pass the fragment shader a list of occluders and their geometry
    • Line segments for boundaries between wall and floor surfaces
    • Positions of the corners
    • Cylinders representing the stone columns
  • For each fragment, evaluate the contribution from each occluder in world space
  • Keep track of the largest occlusion value encountered (max() not sum())
  • Modulate the ambient light intensity based on the largest occlusion value
  • Calculate the final fragment value using an occlusion-modulated ambient light term

GLSL:

The approach above achieves the desired visual effect. It is computationally cheap and doesn’t suffer from visual artifacts related to undersampling. It does a good job leveraging the simple geometry of the environment. However, it would not work for more complex scenes. Nor is it physically correct.


Impact on Visual Quality

Screenshot of Impaler with ambient occlusion enabled

Animation showing comparison of ambient occlusion on and off in Impaler


Closing

Computer graphics is often about balancing performance and visual quality. Finding creative ways to approximate complex lighting models is part of the fun.

If you enjoy these posts and would like to support my work, please consider buying my game on Steam!