06. Occlusion Query

Occlusion queries count the number of fragments (or samples) that pass the depth test, which is useful to determine visibility of objects.

If an object is drawn but 0 fragments passed the depth test, it is fully occluded by another object. In practice this means that a simplification of an object is drawn using an occlusion query (for example: A bounding box can be the occlusion substitute for a truck) and only if fragments of the simple object pass the depth test, the complex object is drawn. Please read Conditional Render for a convenient solution.

Note that the simplified object does not actually have to become visible, one can set GL.ColorMask and GL.DepthMask to false for the purpose of the occlusion query. The only GL.Enable/Disable state associated with it is the DepthTest. If DepthTest is disabled all fragments will automatically pass it and the occlusion test becomes pointless.

Occlusion Query handles are generated and deleted similar to other OpenGL handles:

uint MyOcclusionQuery;
GL.GenQueries( 1, out MyOcculsionQuery );
GL.DeleteQueries( 1, ref MyOcculsionQuery );

The draw commands which contribute to the count must be enclosed with GL.BeginQuery() and GL.EndQuery().

GL.BeginQuery( QueryTarget.SamplesPassed, MyOcculsionQuery );
// draw...
GL.EndQuery( QueryTarget.SamplesPassed );

It is very important to understand that this process is running asynchronous, by the time the CPU is querying the result of the count the GPU might not be done counting yet. OpenGL provides additional query commands to determine whether the occlusion query result is available, but before it is confirmed to be available any query of the count is not reliable. The following code will get a reliable result.

uint ResultReady=0;
while ( ResultReady == 0 )
{
  GL.GetQueryObject( MyOcculsionQuery, GetQueryObjectParam.QueryResultAvailable, out ResultReady );
}
uint MyOcclusionQueryResult=0;
GL.GetQueryObject( MyOcculsionQuery, GetQueryObjectParam.QueryResult, out MyOcclusionQueryResult );
// MyOcclusionQueryResult is now reliable.

However this is not very efficient to use because the CPU will spin in a loop until the GPU is done counting.

A better approach is to do the occlusion queries in the first frame and do not wait for a result. Instead continue drawing as normal and wait for the next frame, before you check the results of the query. In other words frame n executes the query and frame n + 1 reads back the results.

This approach hides the latency inherent in occlusion queries and improves performance, at the cost of slight visual glitches (an object may become visible one frame later than it should). You can read a very detailed description of this technique on Chapter 29 of GPU Gems 1, which also covers other caveats of occlusion queries.

Conditional Render

The Extension NV_conditional_render adds a major improvement to occlusion queries: it allows a simple if ( SamplesPassed > 0 ) conditional to decide whether an object should be drawn based on the result of an occlusion query.

This is probably best shown by a simple example, in the given scene there are 3 objects:

  • A huge cylinder which acts as occluder. Think of it as a pillar in the center of the "room".
  • A small cube which acts as ocludee. Think of it as a box that is anywhere in the "room" but not intersecting the pillar.
  • A small sphere which sits ontop of the cube. If the cube is fully occluded by the cylinder, drawing the sphere can be skipped.

Here is some pseudo-code how the implementation looks like.

uint MyOcculsionQuery;
 
public void OnLoad()
{
  GL.GenQueries(1, out MyOcculsionQuery);
  // etc...
  GL.Enable( EnableCap.DepthTest ); 
}
 
public void OnUnload()
{
  GL.DeleteQueries(1, ref MyOcculsionQuery);
  // etc...
}
 
public void OnRenderFrame()
{
  // The cylinder is drawn unconditionally and used as occluder for the Cube and Sphere
  MyCylinder.Draw();
 
  // Next, the cube is drawn unconditionally, but the samples which passed the depth test are counted.
  GL.BeginQuery( QueryTarget.SamplesPassed, MyOcculsionQuery );
  MyCube.Draw();
  GL.EndQuery( QueryTarget.SamplesPassed );
 
  // depending on whether any sample passed the depth test, the sphere is drawn.
  GL.NV.BeginConditionalRender( MyOcculsionQuery, NvConditionalRender.QueryWaitNv );
  MySphere.Draw();
  GL.NV.EndConditionalRender();
 
  this.SwapBuffers();
}

Although the running program might only show a single object on screen (the cylinder), the cube is always drawn too. Only drawing of the sphere might be skipped, depending on the outcome of the occlusion query used for the cube.

Please note that this is not the standard case how to use occlusion query. The most common way to use them is drawing a simple bounding volume (of a more complex object) to determine whether samples passed and only draw the complex object itself, if the bounding volume is not occluded. For example: Drawing a character with skeletal animation is usually expensive, to determine whether it should be drawn at all, a cylinder can be drawn using an occlusion query and the character is only drawn if the cylinder is not occluded.