Visualization Library

A lightweight C++ OpenGL middleware for 2D/3D graphics
[Home] [Tutorials] [All Classes] [Grouped Classes]

OpenGL-Accelerated Occlusion Culling Tutorial

This tutorial demonstrates how to perform hadware accelerated occlusion culling using the OpenGL extension GL_ARB_occlusion_query.

For more information about the OpenGL extension GL_ARB_occlusion_query see also http://oss.sgi.com/projects/ogl-sample/registry/ARB/occlusion_query.txt

Sample Foreset Model (1681 trees) - Benchmark on NVIDIA GeForce 8600M GT, Intel Core 2 Duo 2.0GHz
pagGuideOcclusionCulling1.jpg
pagGuideOcclusionCulling3.jpg
pagGuideOcclusionCulling4.jpg
pagGuideOcclusionCulling2.jpg
Large occluder in red:
  • Occlusion culling off: 7.9 fps.
  • Occlusion culling on: 46.0 fps.
  • Speedup: 582%
View of the forest from above:
  • Occlusion culling off: 10.2 fps.
  • Occlusion culling on: 24.8 fps.
  • Speedup: 243%
View from under, the ground is a very good occluder:
  • Occlusion culling off: 8.4 fps.
  • Occlusion culling on: 52.0 fps.
  • Speedup: 620%
View of the forest from inside:
  • Occlusion culling off: 8.5 fps.
  • Occlusion culling on: 42.3 fps.
  • Speedup: 497%

When rendering highly detailed and densely populated scenes most of the rendered geometry is actually hidden by the other objects and geometry which are closer to the camera. This means that a consistent part the GPU time and computational power is spent to render objects that will not be visible in the final rendering. Consider for example a dense forest with thousands of trees. The trees that are close to the camera will have a high chance to be visible but the ones that are far away from the camera will probably not be visible due to the fact that the close ones occlude a significant part of the view. The term "occlusion culling" refers to a set of techniques that try to exploit this fact. By detecting in advance which objects will be occluded and thus invisible, we can avoid rendering objects that will not significantly contribute to the scene, potentially obtaining a dramatic rendering boost as seen from the figures above.

Drawbacks
The OpenGL extension GL_ARB_occlusion_query allows a program to request if a given object will contribute or not to the final rendering providing a huge potential in terms of rendering performances increase as we have seen. Unfortunately this technique has also some drawbacks due to the way the GPU and CPU collaborate, the most important being the fact that once you requested this occlusion visibility check, the OpenGL driver cannot reply before having executed all the OpenGL commands that precede such request. For this reason in order to maximize the CPU/GPU concurrency (and with it the rendering performances) Visualization Library waits one rendering frame before checking the result of the occlusion queries. This means that at any given frame the result of such query actually refers to the position that the camera and the objects had relative to each other in the previous frame. As a result of this occasional flickers might occur when an invisible object becomes visible especially when the camera or such object is moving fast compared to the rendering refresh rate. An example of such problem is shown below.

pagGuideOcclusionCulling_Error.jpg

In the image above the camera is quickly moving to the left uncovering a part of the trees that in the preceding frame were covered by the red occluder. Such trees weren't visible in the previous frame and are thus not rendered in the current frame producing a temporary "hole" in the forest. Note that such "hole" lasts for a single rendering frame but potentially new holes might occur while the camera continues its movement.

Artifacts like this might be more or less noticeable based on the kind of scene rendered and based on the refresh rate of the rendering. For example, it is very easy to notice this effect at 10 fps, but is much more difficult if not impossible to notice them at let say 160 fps.

Note that requesting the result of an occlusion query in the same frame in which it is issued is not an option as this would not only kill the GPU/CPU concurrency but would also cause a vicious cpu-stall/gpu-starvation circle that would result in unacceptable performances even for simple scenes.

Enabling Occlusion Culling
Exploiting the huge potential of OpenGL accelerated occlusion culling with Visualization Library is extremely simple provided you pay attention to a few details.

Large Occluders
In order to be sure to maximize the efficiency of the occlusion culling you want to make sure that large occluders are rendered before all the other objects. In our case for example we assign a rendering rank of -1 to the ground vl::Actor. You should always pay particular attention to very large occluders like the ground vl::Actor in our scene. In fact the vl::RenderQueueSorterOcclusion sorts the vl::Actor[s] based on the distance between the camera and their center. This means that a very large occluder might actually be in front of all the other objects but its center might be further away. Consider for example the image below:

pagGuideOcclusionCulling_LargeOccluders.png

The red dot on the left represents the camera, the gray object is a large occluder, the boxes are other objects occluded by the large gray one. As we can see if we simply render the objects in front to back order based on their centers we end up rendering first the red boxes (thus wasting time since they will not be visible!), then the large gray occluder which overdraws the red boxes when rendered on the screen, and then the green boxes (which are actually not rendered thanks to the occlusion culling). With this approach we rendered six boxes (the red ones) which are actually not visible. To optimize this case we would render the gray occluder for first, specifying for it a render rank lower than the boxes' render rank. Remember that the user specified ordering (Actor render rank and render block and Effect render rank) always wins against any eventual automatic sorting performed by a vl::RenderQueueSorter.

Also keep in mind that the concept of "large occluder" is heavily subjective as a skyscraper seen from 1 Km away is not a good occluder while a matchbox seen from very close can become a very good one!

[From App_OcclusionCulling.hpp]

class App_OcclusionCulling: public BaseDemo
{
public:
  void initEvent()
  {
    BaseDemo::initEvent();

    // #######################################################################
    // # These 2 lines are the only code needed to enable occlusion culling! #
    // #######################################################################
    vl::VisualizationLibrary::rendering()->as<vl::Rendering>()->renderer()->setOcclusionCullingEnabled(true);
    vl::VisualizationLibrary::rendering()->as<vl::Rendering>()->setRenderQueueSorter(new vl::RenderQueueSorterOcclusion);

    /* the rest of the code simply generates a forest of thousands of trees */

    /* setup a simple effect to use for all our objects */
    vl::ref<vl::Effect> fx = new vl::Effect;
    fx->shader()->enable(vl::EN_DEPTH_TEST);
    fx->shader()->enable(vl::EN_LIGHTING);
    fx->shader()->gocMaterial()->setColorMaterialEnabled(true);
    fx->shader()->gocLight(0)->setLinearAttenuation(0.0025f);

    /* the ground under the trees */
    float side = 400;
    vl::ref<vl::Geometry> ground = vlut::makeGrid(vl::vec3(0,0,0), side*2.0f,side*2.0f, (int)side, (int)side);
    ground->computeNormals();
    ground->setColorArray(vlut::green);
    vl::Actor* ground_act = sceneManager()->tree()->addActor(ground.get(), fx.get(), NULL);
    /* assign a render rank of -1 (default is 0) to be sure to render the ground before all the trees */
    ground_act->setRenderRank(-1);

    /* the red wall in front of the camera */
    vl::ref<vl::Geometry> wall = vlut::makeBox(vl::vec3(0,25,500), 50,50,1);
    wall->computeNormals();
    wall->setColorArray(vlut::red);
    sceneManager()->tree()->addActor(wall.get(), fx.get(), NULL);

    /* the trees */
    float trunk_h   = 20;
    float trunk_w   = 4;
    /* the tree's branches */
    vl::ref<vl::Geometry> branches = vlut::makeIcosphere(vl::vec3(0,trunk_h/2.0f,0), 7, 1, false);
    branches->computeNormals();
    branches->setColorArray( vlut::green );
    /* the tree's trunk */
    vl::ref<vl::Geometry> trunk = vlut::makeCylinder(vl::vec3(0,0,0),trunk_w,trunk_h, 35, 35);
    trunk->computeNormals();
    trunk->setColorArray( vlut::gold );

    /* fill our forest with trees! */
    int trunk_count = 20;
    for(int i=-trunk_count; i<=trunk_count; ++i)
    for(int j=-trunk_count; j<=trunk_count; ++j)
    {
      float x = (float) i * side / trunk_count;
      float z = (float) j * side / trunk_count;

      vl::ref<vl::Transform> tr = new vl::Transform( vl::mat4::translation(x,trunk_h/2.0f+0.1f,z) );
      tr->computeWorldMatrix();
      sceneManager()->tree()->addActor(trunk.get(), fx.get(), tr.get());
      sceneManager()->tree()->addActor(branches.get(), fx.get(), tr.get());
    }
  }

  /* spacebar = toggles occlusion culling */
  void keyPressEvent(unsigned short ch, vl::EKey key)
  {
    BaseDemo::keyPressEvent(ch, key);
    if (key == vl::Key_Space)
    {
      bool on = vl::VisualizationLibrary::rendering()->as<vl::Rendering>()->renderer()->occlusionCullingEnabled();
      vl::VisualizationLibrary::rendering()->as<vl::Rendering>()->renderer()->setOcclusionCullingEnabled(!on);
      vl::Log::print( vl::Say("Occlusion = %s\n") << (vl::VisualizationLibrary::rendering()->as<vl::Rendering>()->renderer()->occlusionCullingEnabled() ? "On" : "Off") );
    }
  }
};

// Have fun!

Visualization Library v2010.06 Reference Documentation
Copyright 2005-2009 Michele Bosi. All rights reserved.
Updated on Tue Jun 1 00:57:08 2010.
Permission is granted to use this page to write and publish articles regarding Visualization Library.