Visualization Library 2.1.0

A lightweight C++ OpenGL middleware for 2D/3D graphics

VL     Star     Watch     Fork     Issue

[Download] [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
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.

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. From vesion 2010.10.1115 VL implements a new occlusion culling mechanism which is much more general and efficient than the previous one. The user no longer has to install a specific render token sorter, pay attention to render rank and render blocks, evaluate large occluders etc. because VL will simply use the whole z-buffer of the preceding rendering to occlude contents of the next one! The new vl::OcclusionCullRenderer class implement an occlusion culling strategy that filters out the objects rendered by a specific vl::Renderer, the usage of the vl::OcclusionCullRenderer class is shown in the example below.

[From App_OcclusionCulling.cpp]

class App_OcclusionCulling: public BaseDemo
{
public:
void initEvent()
{
vl::Log::notify(appletInfo());
{
vl::Log::error("No support to hardware occlusion culling found!\n");
exit(1);
}
// #######################################################################
// # These 4 lines are the only code needed to enable occlusion culling, #
// # no special sorter or render rank/block setup needed! #
// #######################################################################
// wraps the regular renderer inside the occlusion renderer
vl::Renderer* regular_renderer = rendering()->as<vl::Rendering>()->renderer();
// creates our occlusion renderer
mOcclusionRenderer = new vl::OcclusionCullRenderer;
mOcclusionRenderer->setWrappedRenderer( regular_renderer );
// installs the occlusion renderer in place of the regular one
rendering()->as<vl::Rendering>()->setRenderer( mOcclusionRenderer.get() );
// note: to disable occlusion culling just restore the 'regular_renderer' as we do below in 'keyPressEvent()'
populateScene();
}
/* populates the scene with tree-like actors */
void populateScene()
{
/* the rest of the code simply generates a forest of thousands of trees */
/* setup a simple red effect */
fx_red->shader()->gocLight(0)->setLinearAttenuation(0.0025f);
fx_red->shader()->gocMaterial()->setDiffuse( vl::red );
/* setup a simple green effect */
fx_green->shader()->enable(vl::EN_LIGHTING);
fx_green->shader()->gocLight(0)->setLinearAttenuation(0.0025f);
fx_green->shader()->gocMaterial()->setDiffuse( vl::green );
/* setup a simple gold effect */
fx_gold->shader()->gocLight(0)->setLinearAttenuation(0.0025f);
fx_gold->shader()->gocMaterial()->setDiffuse( vl::gold );
/* the ground under the trees */
float side = 400;
vl::ref<vl::Geometry> ground = vl::makeGrid(vl::vec3(0, -1.0f, 0), side*2.1f, side*2.1f, 100, 100);
ground->computeNormals();
sceneManager()->tree()->addActor(ground.get(), fx_green.get(), NULL);
/* the red wall in front of the camera */
vl::ref<vl::Geometry> wall = vl::makeBox(vl::vec3(0,25,500), 50, 50 ,1);
wall->computeNormals();
sceneManager()->tree()->addActor(wall.get(), fx_red.get(), NULL);
/* the trees */
float trunk_h = 20;
float trunk_w = 4;
/* the tree's branches */
vl::ref<vl::Geometry> branches = vl::makeIcosphere(vl::vec3(0,trunk_h/2.0f,0), 14, 2, false);
branches->computeNormals();
/* the tree's trunk */
vl::ref<vl::Geometry> trunk = vl::makeCylinder(vl::vec3(0,0,0),trunk_w,trunk_h, 50, 50);
trunk->computeNormals();
/* 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;
sceneManager()->tree()->addActor(trunk.get(), fx_gold.get(), tr.get());
sceneManager()->tree()->addActor(branches.get(), fx_green.get(), tr.get());
}
/* text statistics */
mText = new vl::Text;
mText->setText("*** N/A ***");
mText->setFont( vl::defFontManager()->acquireFont("/font/bitstream-vera/VeraMono.ttf", 10) );
mText->setAlignment( vl::AlignLeft | vl::AlignTop );
mText->setViewportAlignment( vl::AlignLeft | vl::AlignTop );
mText->setTextAlignment(vl::TextAlignLeft);
mText->translate(+5,-5,0);
mText->setColor(vl::white);
effect->shader()->enable(vl::EN_BLEND);
vl::Actor* text_actor = sceneManager()->tree()->addActor(mText.get(), effect.get());
text_actor->setOccludee(false);
/* start stats timer */
mTimer.start();
/* occlusion culling enable flag */
mOcclusionCullingOn = true;
}
void updateText()
{
if (mOcclusionRenderer)
{
vl::String msg = vl::Say("Occlusion ratio = %.1n%% (%n/%n)\n")
<< 100.0f * mOcclusionRenderer->statsOccludedObjects() / mOcclusionRenderer->statsTotalObjects()
<< mOcclusionRenderer->statsTotalObjects() - mOcclusionRenderer->statsOccludedObjects()
<< mOcclusionRenderer->statsTotalObjects();
mText->setText( msg );
}
}
void updateScene()
{
/* update text every 0.5 secs */
if( mTimer.elapsed() > 0.5f && mOcclusionCullingOn )
{
updateText();
mTimer.start();
}
}
/* spacebar = toggles occlusion culling */
void keyPressEvent(unsigned short ch, vl::EKey key)
{
BaseDemo::keyPressEvent(ch, key);
if (key == vl::Key_Space)
{
mOcclusionCullingOn = !mOcclusionCullingOn;
if (mOcclusionCullingOn)
{
rendering()->as<vl::Rendering>()->setRenderer( mOcclusionRenderer.get() );
}
else
{
rendering()->as<vl::Rendering>()->setRenderer( mOcclusionRenderer->wrappedRenderer() );
mText->setText("Occlusion Culling Off");
}
}
}
protected:
bool mOcclusionCullingOn;
vl::Time mTimer;
};
// Have fun!