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.
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.
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
|