adding occlusion culling

I am attempting to add a type of occlusion culling to Panda.

So far I have added a new type of node, OccluderNode, which is based on PortalNode (basically just a 4-sided polygon). I also found what I believe is the right spot to do the actual occlusion checks in the is_in_view function of CullTraverserData. The occlusion check is actually the same as the frustum check except the results are opposite.

The idea is to create a GeometricBoundingVolume for each occluder, projecting outwards away from the occluder. Anything that is completely inside this volume should be culled. The volumes would be passed along in the CullTraverserData (I think) and have the same transforms done to them as the view frustum.

I think the proper interface would be to store a list of occluders somewhere and create add_occluder and remove_occluder functions. I am looking for advice on what class makes sense to store the occluder list in. I was thinking maybe the CullTraverser?

Hi,

When I implemented an algorithm to do automatic occlusion culling by asking the graphics pipe to perform the needed calculation (which was a bit of a failed experiment), I did this with a specialized CullTraverser object. This has the advantage that it doesn’t add overhead to the regular traverser when the occlusion code is not enabled.

You might want to examine panda/src/grutil/pipeOcclusionCullTraverser.h and related files to see the approach I took.

David

Hmmm, that would be one way to go about it, but I don’t see why different culling methods couldn’t be used together. For example portal culling and polygon occluder culling.
Does a CullTraverser generally hang around with its DisplayRegion, or is there something that might replace it with a new CullTraverser later on (besides calling set_cull_traverser)?

Sure, you could integrate it into a single traverser class; and this is especially appealing if there’s no overhead when the new code isn’t being used.

It’s just a detail. You can switch out the cull traverser in the middle of a traversal. A PGTop node does this, for instance, to switch the traverser for a PGCullTraverser.

David

My occluder implementation is going well!
I have successfully added an occluder that I set with CullTraverser.setOccluderNode(blah).

The obvious problem with this, is that it allows for only one occluder node.
What is the best way for me to manage a list of node(path)s? Is there a good example of this?

There are lots of examples of lists of PandaNodes and/or NodePaths (or other things) all throughout Panda. Usually we just store them in a pvector.

David

I am able to successfully write out (using Panda code) EGG files with occluders in them, for example:

<Group> occluder4 {
<Scalar> occluder { 1 }
  <VertexPool> occluder4-ORG {
    <Vertex> 0 {
      4 2.96946e-016 -0.84965
    }
    <Vertex> 1 {
      4 7.57015 -0.84965
    }
    <Vertex> 2 {
      4 7.57015 -4
    }
    <Vertex> 3 {
      4 0 -4
    }
  }
  <Polygon> {
    <VertexRef> { 0 1 2 3 <Ref> { occluder4-ORG } }
  }
}

Loading one of these gives the following warning:

Warning in /c/occluders.egg at line 30, column 23:
<Scalar> occluder { 1 }
                      ^
Unknown group scalar occluder

This results in the node being just a regular GeomNode instead of an OccluderNode.

Looks like I’ve got all the spots I needed to write them, but missed something needed to read them in?
I can’t seem to locate the code that is generating this warning, a hint would be appreciated!

This is handled by the yacc grammar. See panda/src/egg/parser.yxx, around line 1358, in the expansion for the group_body definition.

In order to modify this file, you will need to have bison installed, so you can re-process the file to generate parser.cxx after you have modified it.

David

Excellent, all is functional! I am putting together a little demo program.
At this point I think I need someone to review my code and probably catch some things that could be more optimized or better written.
Any volunteers? :slight_smile:

I’d be happy to give it the once-over, though I can’t promise to be timely about it. :slight_smile:

Have you made A/B performance measurements on real-life scenes?

David

I haven’t tested real-life scenes yet, though it does make a phenomenal difference on my occluder-biased test scene. :stuck_out_tongue:
What is the best way for me to package the code? Should I just ZIP the modified files, or is there a method of generating some kind of file diffs that you would prefer?

I think the easiest for me to deal with is a unified patchfile, generated with something like “cvs diff -uN”, or the equivalent using diff if you didn’t check out directly from cvs. But I can also accept a zipfile containing modified files, if you tell me precisely which Panda3D version you started with.

David

Hopefully I made the diff file correctly: http://novuscom.net/~spacebullet/out/occluders.diff
Also, I made a demo program that takes advantage of the occluders: http://novuscom.net/~spacebullet/out/Occluder-Culling.zip

Looks like I missed one function in the diff…
This goes in src/egg2pg/eggLoader.cxx below the EggLoader::set_portal_polygon function (around line 2626).

////////////////////////////////////////////////////////////////////
//     Function: EggLoader::set_occluder_polygon
//       Access: Private
//  Description: Defines the OccluderNode from the first polygon found
//               within this group.
////////////////////////////////////////////////////////////////////
void EggLoader::
set_occluder_polygon(EggGroup *egg_group, OccluderNode *pnode) {
  pnode->clear_vertices();

  PT(EggPolygon) poly = find_first_polygon(egg_group);
  if (poly != (EggPolygon *)NULL) {
    LMatrix4d mat = poly->get_vertex_to_node();

    EggPolygon::const_iterator vi;
    for (vi = poly->begin(); vi != poly->end(); ++vi) {
      Vertexd vert = (*vi)->get_pos3() * mat;
      pnode->add_vertex(LCAST(float, vert));
    }
  }
}

OK, looks pretty nice. Good job! I do have a few suggestions for improvements, which I will go ahead and make directly to the code, then commit it. I’ll post a followup when I’m done with that.

David

I think the biggest thing missing is that the occluders are not tested to see if they are actually in view.
Some other ideas I have are to add a debug draw that would show the occluder border, and a setting equivalent to “fake-view-frustum-cull” that would render the occluded objects in a different colour.

I’ve done some real-world testing now in my game. Adding a few well placed occluders does improve performance. Placing too many results in a performance loss.
Basically this means it needs something to help it scale to work well on larger scenes. I think it would help to evaluate all of the valid occluders (ones on screen) and, through some measurement of distance / scale / screen real-estate, pick the occluders with the best potential up to a limit which can be controlled. 10 or less is probably optimal, but it would vary depending on the scene.
Also, I realized the far plane of the occluder frustum does not need to be tested, so that might give a little boost too.

I’ve committed the code. Nice work! The changes I made were:

  • I changed the interface for creating an OccluderNode. I never liked the addVertex() calls for PortalNode; it doesn’t make sense when there must be always exactly four vertices. I replaced this with a setVertices() call that takes four vertices.

  • I removed CullTraverser.addOccluder() and replacd it with render.setOccluder(). This is both easier to get to, a bit more intuitive, and more consistent with the clip plane interface and other Panda interfaces. It also allows you to set occluders on only a part of the scene if you want to do that.

  • I made it so that an OccluderNode can be shown, like a CollisionNode, to render a quick-and-dirty visualization of the occlusion polygon. I find visualizations like this to be very helpful.

  • I also added the show-occluder-volumes config variable; when enabled, this will draw the frustum volumes of the occluders actually being tested against, to further help in debugging.

  • I reworked the traverser logic for more optimizations. Part of this was moving the list of occluder frustums from CullTraverserData, which gets copied and recopied at every node of the traversal, to CullPlanes, where it only gets copied when it needs to be modified. Also, there it can sit side-by-side with similar logic for clipping planes. In addition, I added logic to prune out most of the occluders that aren’t in view. It’s not perfect.

  • fake-view-frustum-cull works the same for objects behind an occluder as well as objects outside the view frustum.

TODO: I forgot to support OccluderNode::xform(). This should be supported for flatten to work correctly.

A new version of your Occluder-Culling.zip file, that supports the new interface, can be found at http://www.ddrose.com/~drose/Occluder-Culling.zip .

I haven’t done any performance testing at all. I wonder how the new changes perform? Of course, there will always be a limit to the number of occluders you can realistically place before the occlusion overhead outweighs the benefits.

David

Awesome!
I recorded some performance numbers already in my real-world scene, I will test the changes tonight and let you know how it turns out.
I had another idea as well, it might help to do occlusion tests on the occluders themselves. If one occluder is completely behind another, then the occluder in front is guaranteed to occlude anything that the one behind it would have.

Right, I implemented the first half of this: before adding a new occluder to the list of active occluders, it is tested against the current list of active occluders, and if its volume is completely within one of those, it is not added.

The other half would be to make the opposite test: to test each of the active occluders against the new occluder, and remove any of them that are completely within the new volume. That’s just a little bit more work, just because it involves another coordinate system transform. I invite you to investigate if you’re interested in pursuing it. :slight_smile:

David