Table of Contents

3D
Creating a cutting-edge engine
  01 - Design document
  02 - Overall structure

  03 - Binary Space Partitioning
  04 - Constrctive Solid Geometry
  05 - Portals
  06 - Possible Visible Set
  07 - Radiosity lighting
  08 - Mirrors

Dictionary of 3D terms

OTHER
A Genetic Algorithm
Line drawing
Line clipping
Line antialiasing
Line thickening
Line curving

Resources in print
Resources on the web
Resources to download

06 - Possible Visible Set
Here is where I'll write about Possible Visible Set and how it is calculated. Then I'll go into the optimizations, RLE the PVS list and lastly appologize for not yet understanding how non-occluders are implemented.

Possible Visible Set (PVS) is the set of all areas visible from this area. In BSP terms, it's the set of all interior leaf nodes visible from any other interior leaf node. If you know this list then you can stop trying to draw certain portions of the tree because you know that portion will be drawn over or clipped and thus turn into a huge waste of time. And since BSP models almost never change shape, you can save even more time by precalculating the list. The whole idea is just to save as much as possible when rendering, even if it involves a certain ammount of work ahead of time.

Now, most of you are probably thinking about the problem of how to calculate the list. You might consider casting "rays" outward from each node and seeing if the ray passes uncut into every other node. This has a few big flaws: how can you be sure something small and really far away doesn't fall between rays? Ok, you could test against every vertex in every other node. How do you find a spot in your starting node from which you can see into all the other possible nodes? There are just too many cases to consider. What we need is a way to cover the entire area visible between any two nodes.

Solution: Portals! Consider a set of portals generated from a simple BSP.

A simple set of portals

If you were standing in section 1 you could see into everything except leaf nodes 7 and 8. The same is also true in reverse. Now let's consider the portals and how they play their part.

Penumbra is the shadow cast by something when a light is shone on it. Antipenumbra is the exact opposite, something like shining a light through a piece of paper with an oddly shaped hole cut in it. the antipenumbra of any one portal through any other portal tells us which of all the other portals is visible when looking in that direction.

antipenumbra example

Since all portals are sandwiched between two nodes, if a generator portal is within the antipenumbra of a source and target portal then the nodes touching the source, generator and target can all "see" each other. But what's the fastest way to figure out which portals lie in the antipenumbras of other source/target combinations? What if a node has more than one portal? What about the possibility of one portal seeing into another and another and then another? EEEP!

Once again, recursion comes to our aide. The pseudocode would go a little like this.
for( every node 'gSourceNode' ) {
  gSourceNode.PVSMarkVisible( gSourceNode ).
  for( all portals touching gSourceNode 'sourcePortal' ) {
    for( all nodes touching sourcePortal 'targetNode' ) {
      if( targetNode == gSourceNode ) continue;
	  gSourceNode.PVSMarkVisible( targetNode );
      // Since targetNode is convex (a rule of BSPs)
      // all target portals must be visible from
	  // sourceNode.
      for( all portals touching targetNode
         'targetPortal' ) {
        // Eliminate trivial cases.
        if( targetPortal == sourcePortal ) continue;
        if( targetPortal and sourcePortal are
            coplanar ) continue;
        RecursePVS( targetNode,
          targetPortal,
          sourcePortal );
      }
    }
  }
}


Recurse( targetNode, targetPortal, sourcePortal ) {
  generatorNode = targetPortal.GetNode()  that is not
                  targetNode.
  gSourceNode.PVSMarkVisible( generatorNode );
  for( all portals touching generatorNode 
      'generatorPortal' ) {
    // Eliminate trivial cases.
    if( generatorPortal == targetPortal ) continue;
    if( generatorPortal on the gSourceNode side
        of sourcePortal ) continue;
    if( generatorPortal on the targetPortal side
        of targetPortal ) continue;
    // This is the tricky part.
    newGenPortals += AntiPenumbraClip( generatorPortal,
        targetPortal, sourcePortal );
    newSrcPortals += AntiPenumbraClip( sourcePortal,
        targetPortal, generatorPortal );
    // If anything is still visible, we have to recurse.
    // This is what makes it memory expensive.
    if( !newGenPortals || !newSrcPortals ) continue;
    for( all newSrcPortals 'nsp' ) {
      for( all newGenPortals 'ngp' ) {
        Recurse( ngp, generatorNode, nsp );
      }
    }
    delete all nsp and ngp.
  }
}
Easier to show than explain, I guarantee you that. The next major step is to determine how to calculate the antipenumbra. Suprisingly, this was the easiest part to code and I got it on the first try. That or it's 'cause I'm just so damn good. Now to convince the rest of the world...

Anyhow, the antipenumbra is really simple: take one point from the source portal and two from the target. Build a plane and then make sure that the source portal is COMPLETELY on one side and the target is COMPLETELY on the other. Clip the generator to this new plane and eliminate any portal fragment that ends on the same side as the source portal. This new plane is the limit of what could be seen looking through the opposite-most edges of the source/target portals. Since we're going all the way around the source and the target, it's equivalent to clipping out everything that cannot be seen through the source and target portals.

Next I repeated the process with one point from target and two points from source just to shave off every little piece I could. Then I reversed the whole process and clipped the source against the generator and the target antipenumbra. Why? So that at the next recursive step the source would be that much smaller, the antipenumbra would be more narrow and there would be an even better chance of entirely clipping the next generator.



Now don't be suprised if this whole process dosen't work on the first try. Some things I can reccomend to speed up debugging include:
  • Limit recursion depth by creating a static global counter. Each time Recurse is called, increment the counter. If it hits a certain depth, return immediately. Watch out though, this number should be very low, no more than 8 or 9. Remember, the ammount of recursion has a potential order of (number of portalsnumber of portals), each layer being (number of portals * number of portals) which is nothing short of a lot.
  • Make lots of run-time file output because it can't hurt. When I did this I made a pleasant discovery: the printout of all nodes and their PVS lists forms an n*n matrix that is symmetrical along the diagonal.
  • Use small test models. I used two models to test. The first was three cubes, the middle cube being offset from the others along one axis. It looked a little like a shallow 'c'. The second was a symmetrical map of four large rooms connected to each other by long hallways so that from any room or hallway you could only see two rooms and two hallways. I really liked this one because I could them move the camera "up" until I went through the "ceiling" and, since I was still in the same leaf node, I could see that the other half of the model was not being drawn.



Finally, there are a few things you can do to optimize memory consumption. Every node is either visible or not visible to some other node. This means you only need one bit in computer ram to store the visibility flag. Thus with some clever bit shifting a single unsigned char can store 8 node's worth of PVS data. Congratulations, you are now using 76.5% less ram. Another technique would be to use only PVS lists that are different from one another, because there are bound to be a few cases (such as my "shallow c" model) where several nodes "see" the same thing. Note that run length encoding is another viable option, but I haven't tried it yet so I can't attest to it's pitfalls or benefits.

But if that's true, why not just calculate one PVS list for all of them in the first place and save time building the list?! Ah ha, you're talking about non-occluders. Imagine a very narrow pole running up the middle of a hollow cube. The polygons of the pole will split the world in to no fewer than four interior leaf nodes but they will all have the same PVS list. There must be a way to somehow ignore that pole's effect on the world. Well, you're right.

But I don't know it yet. *sigh*.



So what have we covered? PVS, PVS and more PVS. With PVS on the side and a dash of PVS for seasoning. No, seriously, we've taken our previous knowledge or portals and built upon it to come up with a pretty neat-o way of precalculating the possible visible set and thus reduce the ammount of work done each frame thus increasing framerates. We've covered Antipenumbras, debugging techniques and optimizations such as compacting the PVS list.

If you've made it this far, I have to shake your hand. Not many make it this far, so reach back and pat that slowly forming hump. But not too much, because now that you've pushed framerates way WAY up, you have enough extra time each frame to add ba-ba-ba-BUM LIGHTMAPS!

Back to the top
PREV: Portals
NEXT: Radiosity lighting


All photos copyright of their respective owners. All other content of this website is © 1997-1999 Dan Royer.
Designed for IE 5.0+, 800x600x16 or greater. Some pages use style sheets.
http://members.home.com/droyer/index.html