Move source markdown files into src/
This commit is contained in:
parent
9deba4ffa6
commit
3736ff43be
76 changed files with 1 additions and 1 deletions
780
src/chapter-59.md
Normal file
780
src/chapter-59.md
Normal file
|
|
@ -0,0 +1,780 @@
|
|||
---
|
||||
title: Michael Abrash's Graphics Programming Black Book, Special Edition
|
||||
author: Michael Abrash
|
||||
date: '1997-07-01'
|
||||
identifier:
|
||||
- scheme: ISBN
|
||||
text: 1576101746
|
||||
publisher: The Coriolis Group
|
||||
category: 'Web and Software Development: Game Development,Web and Software Development:
|
||||
Graphics and Multimedia Development'
|
||||
chapter: '59'
|
||||
pages: 1095-1114
|
||||
---
|
||||
|
||||
## Chapter 59 -- The Idea of BSP Trees
|
||||
|
||||
### What BSP Trees Are and How to Walk Them
|
||||
|
||||
The answer is: Wendy Tucker.
|
||||
|
||||
The question that goes with that answer isn't particularly interesting
|
||||
to anyone but me—but the manner in which I came up with the answer is.
|
||||
|
||||
I spent many of my childhood summers at Camp Chingacook, on Lake George
|
||||
in New York. It was a great place to have fun and do some growing up,
|
||||
with swimming and sailing and hiking and lots more.
|
||||
|
||||
When I was 14, Camp Chingacook had a mixer with a nearby girls' camp. As
|
||||
best I can recall, I had never had any interest in girls before, but
|
||||
after the older kids had paired up, I noticed a pretty girl looking at
|
||||
me and, with considerable trepidation, I crossed the room to talk to
|
||||
her. To my amazement, we hit it off terrifically. We talked non-stop for
|
||||
the rest of the evening, and I walked back to my cabin floating on air.
|
||||
I had taken a first, tentative step into adulthood, and my world would
|
||||
never be quite the same.
|
||||
|
||||
That was the only time I ever saw her, although I would occasionally
|
||||
remember that warm glow and call up an image of her smiling face. That
|
||||
happened less frequently as the years passed and I had real girlfriends,
|
||||
and by the time I got married, that particular memory was stashed in
|
||||
some back storeroom of my mind. I didn't think of her again for more
|
||||
than a decade.
|
||||
|
||||
A few days ago, for some reason, that mixer popped into my mind as I was
|
||||
trying to fall asleep. And I wondered, for the first time in 20 years,
|
||||
what that girl's name was. The name was there in my mind, somewhere; I
|
||||
could feel the shape of it, in that same back storeroom, if only I could
|
||||
figure out how to retrieve it.
|
||||
|
||||
I poked and worried at that memory, trying to get it to come to the
|
||||
surface. I concentrated on it as hard as I could, and even started going
|
||||
through the alphabet one letter at a time, trying to remember if her
|
||||
name started with each letter. After 15 minutes, I was wide awake and
|
||||
totally frustrated. I was also farther than ever from answering the
|
||||
question; all the focusing on the memory was beginning to blur the
|
||||
original imprint.
|
||||
|
||||
At this point, I consciously relaxed and made myself think about
|
||||
something completely different. Every time my mind returned to the
|
||||
mystery girl, I gently shifted it to something else. After a while, I
|
||||
began to drift off to sleep, and as I did a connection was made, and a
|
||||
name popped, unbidden, into my mind.
|
||||
|
||||
Wendy Tucker.
|
||||
|
||||
There are many problems that are amenable to the straight-ahead, purely
|
||||
conscious sort of approach that I first tried to use to retrieve Wendy's
|
||||
name. Writing code (once it's designed) is often like that, as are some
|
||||
sorts of debugging, technical writing, and balancing your checkbook. I
|
||||
personally find these left-brain activities to be very appealing because
|
||||
they're finite and controllable; when I start one, I know I'll be able
|
||||
to deal with whatever comes up and make good progress, just by plowing
|
||||
along. Inspiration and intuitive leaps are sometimes useful, but not
|
||||
required.
|
||||
|
||||
The problem is, though, that neither you nor I will ever do anything
|
||||
great without inspiration and intuitive leaps, and especially not
|
||||
without stepping away from what's known and venturing into territories
|
||||
beyond. The way to do that is not by trying harder but, paradoxically,
|
||||
by trying less hard, stepping back, and giving your right brain room to
|
||||
work, then listening for and nurturing whatever comes of that. On a
|
||||
small scale, that's how I remembered Wendy's name, and on a larger
|
||||
scale, that's how programmers come up with products that are more than
|
||||
me-too, checklist-oriented software.
|
||||
|
||||
Which, for a couple of reasons, brings us neatly to this chapter's
|
||||
topic, Binary Space Partitioning (BSP) trees. First, games are probably
|
||||
the sort of software in which the right-brain element is most
|
||||
important—blockbuster games are almost always breakthroughs in one way
|
||||
or another—and some very successful games use BSP trees, most notably id
|
||||
Software's megahit DOOM. Second, BSP trees aren't intuitively easy to
|
||||
grasp, and considerable ingenuity and inventiveness is required to get
|
||||
the most from them.
|
||||
|
||||
Before we begin, I'd like to thank John Carmack, the technical wizard
|
||||
behind DOOM, for generously sharing his knowledge of BSP trees with me.
|
||||
|
||||
### BSP Trees
|
||||
|
||||
A BSP tree is, at heart, nothing more than a tree that subdivides space
|
||||
in order to isolate features of interest. Each node of a BSP tree splits
|
||||
an area or a volume (in 2-D or 3-D, respectively) into two parts along a
|
||||
line or a plane; thus the name "Binary Space Partitioning." The
|
||||
subdivision is hierarchical; the root node splits the world into two
|
||||
subspaces, then each of the root's two children splits one of those two
|
||||
subspaces into two more parts. This continues with each subspace being
|
||||
further subdivided, until each component of interest (each line segment
|
||||
or polygon, for example) has been assigned its own unique subspace. This
|
||||
is, admittedly, a pretty abstract description, but the workings of BSP
|
||||
trees will become clearer shortly; it may help to glance ahead to this
|
||||
chapter's figures.
|
||||
|
||||
Building a tree that subdivides space doesn't sound particularly
|
||||
profound, but there's a lot that can be done with such a structure. BSP
|
||||
trees can be used to represent shapes, and operating on those shapes is
|
||||
a simple matter of combining trees as needed; this makes BSP trees a
|
||||
powerful way to implement Constructive Solid Geometry (CSG). BSP trees
|
||||
can also be used for hit testing, line-of-sight determination, and
|
||||
collision detection.
|
||||
|
||||
#### Visibility Determination
|
||||
|
||||
For the time being, I'm going to discuss only one of the many uses of
|
||||
BSP trees: The ability of a BSP tree to allow you to traverse a set of
|
||||
line segments or polygons in back-to-front or front-to-back order as
|
||||
seen from any arbitrary viewpoint. This sort of traversal can be very
|
||||
helpful in determining which parts of each line segment or polygon are
|
||||
visible and which are occluded from the current viewpoint in a 3-D
|
||||
scene. Thus, a BSP tree makes possible an efficient implementation of
|
||||
the painter's algorithm, whereby polygons are drawn in back-to-front
|
||||
order, with closer polygons overwriting more distant ones that overlap,
|
||||
as shown in Figure 59.1. (The line segments in Figure 1(a) and in other
|
||||
figures in this chapter, represent vertical walls, viewed from directly
|
||||
above.) Alternatively, visibility determination can be performed by
|
||||
front-to-back traversal working in conjunction with some method for
|
||||
remembering which pixels have already been drawn. The latter approach is
|
||||
more complex, but has the potential benefit of allowing you to early-out
|
||||
from traversal of the scene database when all the pixels on the screen
|
||||
have been drawn.
|
||||
|
||||
Back-to-front or front-to-back traversal in itself wouldn't be so
|
||||
impressive—there are many ways to do that—were it not for one additional
|
||||
detail: The traversal can always be performed in linear time, as we'll
|
||||
see later on. For instance, you can traverse, a polygon list
|
||||
back-to-front from any viewpoint simply by walking through the
|
||||
corresponding BSP tree once, visiting each node one and only one time,
|
||||
and performing only one relatively inexpensive test at each node.
|
||||
|
||||
It's hard to get cheaper sorting than linear time, and BSP-based
|
||||
rendering stacks up well against alternatives such as z-buffering,
|
||||
octrees, z-scan sorting, and polygon sorting. Better yet, a scene
|
||||
database represented as a BSP tree can be clipped to the view pyramid
|
||||
very efficiently; huge chunks of a BSP tree can be lopped off when
|
||||
clipping to the view pyramid, because if the entire area or volume of a
|
||||
node lies entirely outside the view volume, then *all* nodes and leaves
|
||||
that are children of that node must likewise be outside the view volume,
|
||||
for reasons that will become clear as we delve into the workings of BSP
|
||||
trees.
|
||||
|
||||

|
||||
|
||||
#### Limitations of BSP Trees
|
||||
|
||||
Powerful as they are, BSP trees aren't perfect. By far the greatest
|
||||
limitation of BSP trees is that they're time-consuming to build, enough
|
||||
so that, for all practical purposes, BSP trees must be precalculated,
|
||||
and cannot be built dynamically at runtime. In fact, a BSP-tree compiler
|
||||
that attempts to perform some optimization (limiting the number of
|
||||
surfaces that need to be split, for example) can easily take minutes or
|
||||
even hours to process large world databases.
|
||||
|
||||
A fixed world database is fine for walkthrough or flythrough
|
||||
applications (where the viewpoint moves through a static scene), but not
|
||||
much use for games or virtual reality, where objects constantly move
|
||||
relative to one another. Consequently, various workarounds have been
|
||||
developed to allow moving objects to appear in BSP tree-based scenes.
|
||||
DOOM, for example, uses 2-D sprites mixed into BSP-based 3-D scenes;
|
||||
note, though, that this approach requires maintaining z information so
|
||||
that sprites can be drawn and occluded properly. Alternatively, movable
|
||||
objects could be represented as separate BSP trees and merged anew into
|
||||
the world BSP tree with each move. Dynamic merging may or may not be
|
||||
fast enough, depending on the scene, but merging BSP trees tends to be
|
||||
quicker than building them, because the BSP trees being merged are
|
||||
already spatially sorted.
|
||||
|
||||
Another possibility would be to generate a per-pixel z-buffer for each
|
||||
frame as it's rendered, to allow dynamically changing objects to be
|
||||
drawn into the BSP-based world. In this scheme, the BSP tree would allow
|
||||
fast traversal and clipping of the complex, static world, and the
|
||||
z-buffer would handle the relatively localized visibility determination
|
||||
involving moving objects. The drawback of this is the need for a
|
||||
memory-hungry z-buffer; a typical 640x480 z-buffer requires a fairly
|
||||
appalling 600K, with equally appalling cache-miss implications for
|
||||
performance.
|
||||
|
||||
Yet another possibility would be to build the world so that each dynamic
|
||||
object falls entirely within a single subspace of the static BSP tree,
|
||||
rather than straddling splitting lines or planes. In this case, dynamic
|
||||
objects can be treated as points, which are then just sorted into the
|
||||
BSP tree on the fly as they move.
|
||||
|
||||
The only other drawbacks of BSP trees that I know of are the memory
|
||||
required to store the tree, which amounts to a few pointers per node,
|
||||
and the relative complexity of debugging BSP-tree compilation and usage;
|
||||
debugging a large data set being processed by recursive code (which BSP
|
||||
code tends to be) can be quite a challenge. Tools like the BSP compiler
|
||||
I'll present in the next chapter, which visually depicts the process of
|
||||
spatial subdivision as a BSP tree is constructed, help a great deal with
|
||||
BSP debugging.
|
||||
|
||||
### Building a BSP Tree
|
||||
|
||||
Now that we know a good bit about what a BSP tree is, how it helps in
|
||||
visible surface determination, and what its strengths and weaknesses
|
||||
are, let's take a look at how a BSP tree actually works to provide
|
||||
front-to-back or back-to-front ordering. This chapter's discussion will
|
||||
be at a conceptual level, with plenty of figures; in the next chapter
|
||||
we'll get into mechanisms and implementation details.
|
||||
|
||||
I'm going to discuss only 2-D BSP trees from here on out, because
|
||||
they're much easier to draw and to grasp than their 3-D counterparts.
|
||||
Don't worry, though; the principles of 2-D BSP trees using line segments
|
||||
generalize directly to 3-D BSP trees using polygons. Also, 2-D BSP trees
|
||||
are quite powerful in their own right, as evidenced by DOOM, which is
|
||||
built around 2-D BSP trees.
|
||||
|
||||
First, let's construct a simple BSP tree. Figure 59.2 shows a set of
|
||||
four lines that will constitute our sample world. I'll refer to these as
|
||||
walls, because that's one easily-visualized context in which a 2-D BSP
|
||||
tree would be useful in a game. Think of Figure 59.2 as depicting
|
||||
vertical walls viewed from directly above, so they're lines for the
|
||||
purpose of the BSP tree. Note that each wall has a front side, denoted
|
||||
by a normal (perpendicular) vector, and a back side. To make a BSP tree
|
||||
for this sample set, we need to split the world in two, then each part
|
||||
into two again, and so on, until each wall resides in its own unique
|
||||
subspace. An obvious question, then, is how should we carve up the world
|
||||
of Figure 59.2?
|
||||
|
||||

|
||||
|
||||
There are infinitely valid ways to carve up Figure 59.2, but the
|
||||
simplest is just to carve along the lines of the walls themselves, with
|
||||
each node containing one wall. This is not necessarily optimal, in the
|
||||
sense of producing the smallest tree, but it has the virtue of
|
||||
generating the splitting lines without expensive analysis. It also saves
|
||||
on data storage, because the data for the walls can do double duty in
|
||||
describing the splitting lines as well. (Putting one wall on each
|
||||
splitting line doesn't actually create a unique subspace for each wall,
|
||||
but it does create a unique subspace *boundary* for each wall; as we'll
|
||||
see, that spatial organization provides for the same unambiguous
|
||||
visibility ordering as a unique subspace would.)
|
||||
|
||||
Creating a BSP tree is a recursive process, so we'll perform the first
|
||||
split and go from there. Figure 59.3 shows the world carved along the
|
||||
line of wall C into two parts: walls that are in front of wall C, and
|
||||
walls that are behind. (Any of the walls would have been an equally
|
||||
valid choice for the initial split; we'll return to the issue of
|
||||
choosing splitting walls in the next chapter.) This splitting into front
|
||||
and back is the essential dualism of BSP trees.
|
||||
|
||||

|
||||
|
||||
Next, in Figure 59.4, the front subspace of wall C is split by wall D.
|
||||
This is the only wall in that subspace, so we're done with wall C's
|
||||
front subspace.
|
||||
|
||||
Figure 59.5 shows the back subspace of wall C being split by wall B.
|
||||
There's a difference here, though: Wall A straddles the splitting line
|
||||
generated from wall B. Does wall A belong in the front or back subspace
|
||||
of wall B?
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Both, actually. Wall A gets split into two pieces, which I'll call wall
|
||||
A and wall E; each piece is assigned to the appropriate subspace and
|
||||
treated as a separate wall. As shown in Figure 59.6, each of the split
|
||||
pieces then has a subspace to itself, and each becomes a leaf of the
|
||||
tree. The BSP tree is now complete.
|
||||
|
||||
#### Visibility Ordering
|
||||
|
||||
Now that we've successfully built a BSP tree, you might justifiably be a
|
||||
little puzzled as to how any of this helps with visibility ordering. The
|
||||
answer is that each BSP node can definitively determine which of its
|
||||
child trees is nearer and which is farther from any and all viewpoints;
|
||||
applied throughout the tree, this principle makes it possible to
|
||||
establish visibility ordering for all the line segments or planes in a
|
||||
BSP tree, no matter what the viewing angle.
|
||||
|
||||
Consider the world of Figure 59.2 viewed from an arbitrary angle, as
|
||||
shown in Figure 59.7. The viewpoint is in front of wall C; this tells us
|
||||
that all walls belonging to the front tree that descends from wall C are
|
||||
nearer along every ray from the viewpoint than wall C is (that is, they
|
||||
can't be occluded by wall C). All the walls in wall C's back tree are
|
||||
likewise farther away than wall C along any ray. Thus, for this
|
||||
viewpoint, we know for sure that if we're using the painter's algorithm,
|
||||
we want to draw all the walls in the back tree first, then wall C, and
|
||||
then the walls in the front tree. If the viewpoint had been on the back
|
||||
side of wall C, this order would have been reversed.
|
||||
|
||||
Of course, we need more ordering information than wall C alone can give
|
||||
us, but we get that by traversing the tree recursively, making the same
|
||||
far-near decision at each node. Figure 59.8 shows the painter's
|
||||
algorithm (back-to-front) traversal order of the tree for the viewpoint
|
||||
of Figure 59.7. At each node, we decide whether we're seeing the front
|
||||
or back side of that node's wall, then visit whichever of the wall's
|
||||
children is on the far side from the viewpoint, draw the wall, and then
|
||||
visit the node's nearer child, in that order. Visiting a child is
|
||||
recursive, involving the same far-near visiting order.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The key is that each BSP splitting line separates all the walls in the
|
||||
current subspace into two groups relative to the viewpoint, and every
|
||||
single member of the farther group is guaranteed not to occlude every
|
||||
single member of the nearer. By applying this ordering recursively, the
|
||||
BSP tree can be traversed to provide back-to-front or front-to-back
|
||||
ordering, with each node being visited only once.
|
||||
|
||||

|
||||
|
||||
The type of tree walk used to produce front-to-back or back-to-front BSP
|
||||
traversal is known as an *inorder* walk. More on this very shortly;
|
||||
you're also likely to find a discussion of inorder walking in any good
|
||||
data structures book. The only special aspect of BSP walks is that a
|
||||
decision has to be made at each node about which way the node's wall is
|
||||
facing relative to the viewpoint, so we know which child tree is nearer
|
||||
and which is farther.
|
||||
|
||||
Listing 59.1 shows a function that draws a BSP tree back-to-front. The
|
||||
decision whether a node's wall is facing forward, made by
|
||||
`WallFacingForward()` in Listing 59.1, can, in general, be made by
|
||||
generating a normal to the node's wall in screenspace
|
||||
(perspective-corrected space as seen from the viewpoint) and checking
|
||||
whether the z component of the normal is positive or negative, or by
|
||||
checking the sign of the dot product of a viewspace (non-perspective
|
||||
corrected space as seen from the viewpoint) normal and a ray from the
|
||||
viewpoint to the wall. In 2-D, the decision can be made by enforcing the
|
||||
convention that when a wall is viewed from the front, the start vertex
|
||||
is leftmost; then a simple screenspace comparison of the x coordinates
|
||||
of the left and right vertices indicates which way the wall is facing.
|
||||
|
||||
**Listing 59.1 L59\_1.C**
|
||||
|
||||
```c
|
||||
void WalkBSPTree(NODE *pNode)
|
||||
{
|
||||
if (WallFacingForward(pNode) {
|
||||
if (pNode->BackChild) {
|
||||
WalkBSPTree(pNode->BackChild);
|
||||
}
|
||||
Draw(pNode);
|
||||
if (pNode->FrontChild) {
|
||||
WalkBSPTree(pNode->FrontChild);
|
||||
}
|
||||
} else {
|
||||
if (pNode->FrontChild) {
|
||||
WalkBSPTree(pNode->FrontChild);
|
||||
}
|
||||
Draw(pNode);
|
||||
if (pNode->BackChild) {
|
||||
WalkBSPTree(pNode->BackChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 
|
||||
> Be aware that BSP trees can often be made smaller and more efficient by
|
||||
> detecting collinear surfaces (like aligned wall segments) and generating
|
||||
> only one BSP node for each collinear set, with the collinear surfaces
|
||||
> stored in, say, a linked list attached to that node. Collinear surfaces
|
||||
> partition space identically and can't occlude one another, so it
|
||||
> suffices to generate one splitting node for each collinear set.
|
||||
|
||||
### Inorder Walks of BSP Trees
|
||||
|
||||
It was implementing BSP trees that got me to thinking about inorder tree
|
||||
traversal. In inorder traversal, the left subtree of each node gets
|
||||
visited first, then the node, and then the right subtree. You apply this
|
||||
sequence recursively to each node and its children until the entire tree
|
||||
has been visited, as shown in Figure 59.9. Walking a BSP tree is
|
||||
basically an inorder tree walk; the only difference is that with a BSP
|
||||
tree a decision is made before each descent as to which subtree to visit
|
||||
first, rather than simply visiting whatever's pointed to by the
|
||||
left-subtree pointer. Conceptually, however, an inorder walk is what's
|
||||
used to traverse a BSP tree; from now on I'll discuss normal inorder
|
||||
walking, with the understanding that the same principles apply to BSP
|
||||
trees.
|
||||
|
||||
As I've said again and again in my printed works over the years, you
|
||||
have to dig deep below the surface to *really* understand something if
|
||||
you want to get it right, and inorder walking turns out to be an
|
||||
excellent example of this. In fact, it's such a good example that I
|
||||
routinely use it as an interview question for programmer candidates,
|
||||
and, to my astonishment, not one interviewee has done a good job with
|
||||
this one yet. I ask the question in two stages, and I get remarkably
|
||||
consistent results.
|
||||
|
||||
First, I ask for an implementation of a function `WalkTree()` that
|
||||
visits each node in a passed-in tree in inorder sequence. Each candidate
|
||||
unhesitatingly writes something like the perfectly good code in Listings
|
||||
59.2 and 59.3 shown next.
|
||||
|
||||

|
||||
|
||||
**Listing 59.2 L59\_2.C**
|
||||
|
||||
```c
|
||||
// Function to inorder walk a tree, using code recursion.
|
||||
// Tested with 32-bit Visual C++ 1.10.
|
||||
#include <stdlib.h>
|
||||
#include "tree.h"
|
||||
extern void Visit(NODE *pNode);
|
||||
void WalkTree(NODE *pNode)
|
||||
{
|
||||
// Make sure the tree isn't empty
|
||||
if (pNode != NULL)
|
||||
{
|
||||
// Traverse the left subtree, if there is one
|
||||
if (pNode->pLeftChild != NULL)
|
||||
{
|
||||
WalkTree(pNode->pLeftChild);
|
||||
}
|
||||
// Visit this node
|
||||
Visit(pNode);
|
||||
// Traverse the right subtree, if there is one
|
||||
if (pNode->pRightChild != NULL)
|
||||
{
|
||||
WalkTree(pNode->pRightChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Listing 59.3 L59\_3.H**
|
||||
|
||||
```c
|
||||
// Header file TREE.H for tree-walking code.
|
||||
typedef struct _NODE {
|
||||
struct _NODE *pLeftChild;
|
||||
struct _NODE *pRightChild;
|
||||
} NODE;
|
||||
```
|
||||
|
||||
Then I ask if they have any idea how to make the code faster; some
|
||||
don't, but most point out that function calls are pretty expensive.
|
||||
Either way, I then ask them to rewrite the function without code
|
||||
recursion.
|
||||
|
||||
And then I sit back and squirm for a minimum of 15 minutes.
|
||||
|
||||
I have never had *anyone* write a functional data-recursion inorder walk
|
||||
function in less time than that, and several people have simply never
|
||||
gotten the code to work at all. Even the best of them have fumbled their
|
||||
way through the code, sticking in a push here or a pop there, then
|
||||
working through sample scenarios in their head to see what's broken,
|
||||
programming by trial and error until the errors seem to be gone. No one
|
||||
is ever sure they have it right; instead, when they can't find any more
|
||||
bugs, they look at me hopefully to see if it's thumbs-up or thumbs-down.
|
||||
|
||||
And yet, a data-recursive inorder walk implementation has exactly the
|
||||
same flowchart and *exactly* the same functionality as the
|
||||
code-recursive version they've already written. They already have a
|
||||
fully functional model to follow, with all the problems solved, but they
|
||||
can't make the connection between that model and the code they're trying
|
||||
to implement. Why is this?
|
||||
|
||||
#### Know It *Cold*
|
||||
|
||||
The problem is that these people don't understand inorder walking
|
||||
through and through. They understand the concepts of visiting left and
|
||||
right subtrees, and they have a general picture of how traversal moves
|
||||
about the tree, but they do not understand exactly what the
|
||||
code-recursive version does. If they really comprehended everything that
|
||||
happens in each iteration of `WalkTree()`—how each call saves the
|
||||
state, and what that implies for the order in which operations are
|
||||
performed—they would simply and without fuss implement code like that in
|
||||
Listing 59.4, working with the code-recursive version as a model.
|
||||
|
||||
**Listing 59.4 L59\_4.C**
|
||||
|
||||
```c
|
||||
// Function to inorder walk a tree, using data recursion.
|
||||
// No stack overflow testing is performed.
|
||||
// Tested with 32-bit Visual C++ 1.10.
|
||||
#include <stdlib.h>
|
||||
#include "tree.h"
|
||||
#define MAX_PUSHED_NODES 100
|
||||
extern void Visit(NODE *pNode);
|
||||
void WalkTree(NODE *pNode)
|
||||
{
|
||||
NODE *NodeStack[MAX_PUSHED_NODES];
|
||||
NODE **pNodeStack;
|
||||
// Make sure the tree isn't empty
|
||||
if (pNode != NULL)
|
||||
{
|
||||
NodeStack[0] = NULL; // push "stack empty" value
|
||||
pNodeStack = NodeStack + 1;
|
||||
for (;;)
|
||||
{
|
||||
// If the current node has a left child, push
|
||||
// the current node and descend to the left
|
||||
// child to start traversing the left subtree.
|
||||
// Keep doing this until we come to a node
|
||||
// with no left child; that's the next node to
|
||||
// visit in inorder sequence
|
||||
while (pNode->pLeftChild != NULL)
|
||||
{
|
||||
*pNodeStack++ = pNode;
|
||||
pNode = pNode->pLeftChild;
|
||||
}
|
||||
// We're at a node that has no left child, so
|
||||
// visit the node, then visit the right
|
||||
// subtree if there is one, or the last-
|
||||
// pushed node otherwise; repeat for each
|
||||
// popped node until one with a right
|
||||
// subtree is found or we run out of pushed
|
||||
// nodes (note that the left subtrees of
|
||||
// pushed nodes have already been visited, so
|
||||
// they're equivalent at this point to nodes
|
||||
// with no left children)
|
||||
for (;;)
|
||||
{
|
||||
Visit(pNode);
|
||||
// If the node has a right child, make
|
||||
// the child the current node and start
|
||||
// traversing that subtree; otherwise, pop
|
||||
// back up the tree, visiting nodes we
|
||||
// passed on the way down, until we find a
|
||||
// node with a right subtree to traverse
|
||||
// or run out of pushed nodes and are done
|
||||
if (pNode->pRightChild != NULL)
|
||||
{
|
||||
// Current node has a right child;
|
||||
// traverse the right subtree
|
||||
pNode = pNode->pRightChild;
|
||||
break;
|
||||
}
|
||||
// Pop the next node from the stack so
|
||||
// we can visit it and see if it has a
|
||||
// right subtree to be traversed
|
||||
if ((pNode = *—pNodeStack) == NULL)
|
||||
{
|
||||
// Stack is empty and the current node
|
||||
// has no right child; we're done
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Take a few minutes to look over Listing 59.4 and relate it to Listing
|
||||
59.2. The structure is different, but upon examination it becomes clear
|
||||
that both listings reflect the same underlying model: For each node,
|
||||
visit the left subtree, visit the node, visit the right subtree. And
|
||||
although Listing 59.4 is longer, that's mostly because I commented it
|
||||
heavily to make sure its workings are understood; there are only 13
|
||||
lines that actually do anything in Listing 59.4.
|
||||
|
||||
Let's look at it another way. All the code in Listing 59.2 does is say:
|
||||
"Here I am at a node. First I'll visit the left subtree if there is one,
|
||||
then I'll visit this node, then I'll visit the right subtree if there is
|
||||
one. While I'm visiting the left subtree, I'll just push a marker on a
|
||||
stack that tells me to come back here when the left subtree is done. If,
|
||||
after visiting a node, there are no right children to visit and nothing
|
||||
left on the stack, I'm finished. The code does this at each node—and
|
||||
that's *all* it does. That's all Listing 59.4 does, too, but people tend
|
||||
to get tangled up in pushes and pops and `while` loops when they use
|
||||
data recursion. When the implementation model changes to one with which
|
||||
they are unfamiliar, they abandon the perfectly good model they used
|
||||
before and try to rederive it in the new context by the seat of their
|
||||
pants.
|
||||
|
||||
> 
|
||||
> Here's a secret when you're faced with a situation like this: Step back
|
||||
> and get a clear picture of what your code has to do. Omit no steps. You
|
||||
> should build a model that is so consistent and solid that you can
|
||||
> instantly answer any question about how the code should behave in any
|
||||
> situation. For example, my interviewees often decide, by trial and
|
||||
> error, that there are two distinct types of right children: Right
|
||||
> children visited after popping back to visit a node after the left
|
||||
> subtree has been visited, and right children visited after descending to
|
||||
> a node that has no left child. This makes the traversal code a mass of
|
||||
> special cases, each of which has to be detected by the programmer by
|
||||
> trying out scenarios. Worse, you can never be sure with this approach
|
||||
> that you've caught all the special cases.
|
||||
>
|
||||
> The alternative is to develop and apply a unifying model. There aren't
|
||||
> really two types of right children; the rule is that all right children
|
||||
> are visited after their parents are visited, period. The presence or
|
||||
> absence of a left child is irrelevant. The possibility that a right
|
||||
> child may be reached via different code paths depending on the presence
|
||||
> of a left child does not affect the overall model. While this
|
||||
> distinction may seem trivial it is in fact crucial, because if you have
|
||||
> the model down cold, you can always tell if the implementation is
|
||||
> correct by comparing it with the model.
|
||||
|
||||
#### Measure and Learn
|
||||
|
||||
How much difference does all this fuss make, anyway? Listing 59.5 is a
|
||||
sample program that builds a tree, then calls `WalkTree` () to walk it
|
||||
1,000 times, and times how long this takes. Using 32-bit Visual C++ 1.10
|
||||
running on Windows NT, with default optimization selected, Listing 59.5
|
||||
reports that Listing 59.4 is about 20 percent faster than Listing 59.2
|
||||
on a 486/33, a reasonable return for a little code rearrangement,
|
||||
especially when you consider that the speedup is diluted by calling the
|
||||
`Visit()` function and by the cache miss that happens on virtually
|
||||
every node access. (Listing 59.5 builds a rather unique tree, one in
|
||||
which every node has exactly two children. Different sorts of trees can
|
||||
and do produce different performance results. Always know what you're
|
||||
measuring!)
|
||||
|
||||
**Listing 59.5 L59\_5.C**
|
||||
|
||||
```cpp
|
||||
// Sample program to exercise and time the performance of
|
||||
// implementations of WalkTree().
|
||||
// Tested with 32-bit Visual C++ 1.10 under Windows NT.
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "tree.h"
|
||||
long VisitCount = 0;
|
||||
void main(void);
|
||||
void BuildTree(NODE *pNode, int RemainingDepth);
|
||||
extern void WalkTree(NODE *pRootNode);
|
||||
void main()
|
||||
{
|
||||
NODE RootNode;
|
||||
int i;
|
||||
long StartTime;
|
||||
// Build a sample tree
|
||||
BuildTree(&RootNode, 14);
|
||||
// Walk the tree 1000 times and see how long it takes
|
||||
StartTime = time(NULL);
|
||||
for (i=0; i<1000; i++)
|
||||
{
|
||||
WalkTree(&RootNode);
|
||||
}
|
||||
printf("Seconds elapsed: %ld\n",
|
||||
time(NULL) - StartTime);
|
||||
getch();
|
||||
}
|
||||
//
|
||||
// Function to add right and left subtrees of the
|
||||
// specified depth off the passed-in node.
|
||||
//
|
||||
void BuildTree(NODE *pNode, int RemainingDepth)
|
||||
{
|
||||
if (RemainingDepth == 0)
|
||||
{
|
||||
pNode->pLeftChild = NULL;
|
||||
pNode->pRightChild = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->pLeftChild = malloc(sizeof(NODE));
|
||||
if (pNode->pLeftChild == NULL)
|
||||
{
|
||||
printf("Out of memory\n");
|
||||
exit(1);
|
||||
}
|
||||
pNode->pRightChild = malloc(sizeof(NODE));
|
||||
if (pNode->pRightChild == NULL)
|
||||
{
|
||||
printf("Out of memory\n");
|
||||
exit(1);
|
||||
}
|
||||
BuildTree(pNode->pLeftChild, RemainingDepth - 1);
|
||||
BuildTree(pNode->pRightChild, RemainingDepth - 1);
|
||||
}
|
||||
}
|
||||
//
|
||||
// Node-visiting function so WalkTree() has something to
|
||||
// call.
|
||||
//
|
||||
void Visit(NODE *pNode)
|
||||
{
|
||||
VisitCount++;
|
||||
}
|
||||
```
|
||||
|
||||
Things change when maximum optimization is selected, however: The
|
||||
performance of the two implementations becomes virtually identical! How
|
||||
can this be? Part of the answer is that the compiler does an amazingly
|
||||
good job with Listing 59.2. Most impressively, when compiling Listing
|
||||
59.2, the compiler actually converts all right-subtree descents from
|
||||
code recursion to data recursion, by simply jumping back to the
|
||||
left-subtree handling code instead of recursively calling
|
||||
`WalkTree()`. This means that half the time Listing 59.4 has no
|
||||
advantage over Listing 59.2; in fact, it's at a disadvantage because the
|
||||
code that the compiler generates for handling right-subtree descent in
|
||||
Listing 59.4 is somewhat inefficient, but the right-subtree code in
|
||||
Listing 59.2 is a marvel of code generation, at just 3 instructions.
|
||||
|
||||
What's more, although left-subtree traversal is more efficient with data
|
||||
recursion than with code recursion, the advantage is only four
|
||||
instructions, because only one parameter is passed and because the
|
||||
compiler doesn't bother setting up an EBP-based stack frame, instead it
|
||||
uses ESP to address the stack. (And, in fact, this cost could be reduced
|
||||
still further by eliminating the check for a NULL `pNode` at all but
|
||||
the top level.) There are other interesting aspects to what the compiler
|
||||
does with Listings 59.2 and 59.4 but that's enough to give you the idea.
|
||||
It's worth noting that the compiler might not do as well with code
|
||||
recursion in a more complex function, and that a good assembly language
|
||||
implementation could probably speed up Listing 59.4 enough to make it
|
||||
measurably faster than Listing 59.2, but not even close to being
|
||||
*enough* faster to be worth the effort.
|
||||
|
||||
The moral of this story (apart from it being a good idea to enable
|
||||
compiler optimization) is:
|
||||
|
||||
1. Understand what you're doing, through and through.
|
||||
|
||||
2. Build a complete and consistent model in your head.
|
||||
|
||||
3. Design from the principles that the model provides.
|
||||
|
||||
4. Implement the design.
|
||||
|
||||
5. Measure to learn what you've wrought.
|
||||
|
||||
6. Go back to step 1 and apply what you've just learned.
|
||||
|
||||
With each iteration you'll dig deeper, learn more, and improve your
|
||||
ability to know where and how to focus your design and programming
|
||||
efforts. For example, with the C compilers I used five to 10 years ago,
|
||||
back when I learned about the relative strengths and weaknesses of code
|
||||
and data recursion, and with the processors then in use, Listing 59.4
|
||||
would have blown away Listing 59.2. While doing this chapter, I've
|
||||
learned that given current processors and compiler technology, data
|
||||
recursion isn't going to get me any big wins; and yes, that was news to
|
||||
me. That's *good*; this information saves me from wasted effort in the
|
||||
future and tells me what to concentrate on when I use recursion.
|
||||
|
||||
Assume nothing, keep digging deeper, and never stop learning and
|
||||
growing. The world won't hold still for you, but fortunately you *can*
|
||||
run fast enough to keep up if you just keep at it.
|
||||
|
||||
Depths within depths indeed!
|
||||
|
||||
### Surfing Amidst the Trees
|
||||
|
||||
In the next chapter, we'll build a BSP-tree compiler, and after that,
|
||||
we'll put together a rendering system built around the BSP trees the
|
||||
compiler generates. If the subject of BSP trees really grabs your fancy
|
||||
(as it should if you care at all about performance graphics) there is at
|
||||
this writing (February 1996) a World Wide Web page on BSP trees that you
|
||||
must investigate at
|
||||
[http://www.qualia.com/bspfaq/](http://www.qualia.com/bspfaq/). It's set
|
||||
up in the familiar Internet Frequently Asked Questions (FAQ) style, and
|
||||
is very good stuff.
|
||||
|
||||
#### Related Reading
|
||||
|
||||
Foley, J., A. van Dam, S. Feiner, and J. Hughes, *Computer Graphics:
|
||||
Principles and Practice (Second Edition)*, Addison Wesley, 1990, pp.
|
||||
555-557, 675-680.
|
||||
|
||||
Fuchs, H., Z. Kedem, and B. Naylor, "On Visible Surface Generation by A
|
||||
Priori Tree Structures," *Computer Graphics* Vol. 17(3), June 1980, pp.
|
||||
124-133.
|
||||
|
||||
Gordon, D., and S. Chen, "Front-to-Back Display of BSP Trees," *IEEE
|
||||
Computer Graphics and Applications,* September 1991, pp. 79-85.
|
||||
|
||||
Naylor, B., "Binary Space Partitioning Trees as an Alternative
|
||||
Representation of Polytopes," *Computer Aided Design*, Vol. 22(4), May
|
||||
1990, pp. 250-253.
|
||||
Loading…
Reference in a new issue