abrash-black-book/66-01.md
2013-12-30 20:26:41 +11:00

118 lines
6.4 KiB
Markdown

Chapter 66\
Quake's Hidden-Surface Removal {#Heading1}
-------------------------------
### Struggling with Z-Order Solutions to the Hidden Surface Problem {#Heading2}
Okay, I admit it: I'm sick and tired of classic rock. Admittedly, it's
been a while, about 20 years, since I was last excited to hear anything
by the Cars or Boston, and I was never particularly excited in the first
place about Bob Seger or Queen, to say nothing of Elvis, so some things
haven't changed. But I knew something was up when I found myself
changing the station on the Allman Brothers and Steely Dan and Pink
Floyd and, God help me, the Beatles (just stuff like "Hello Goodbye" and
"I'll Cry Instead," though, not "Ticket to Ride" or "A Day in the Life";
I'm not *that* far gone). It didn't take long to figure out what the
problem was; I'd been hearing the same songs for a quarter-century, and
I was bored.
I tell you this by way of explaining why it was that when my daughter
and I drove back from dinner the other night, the radio in my car was
tuned, for the first time ever, to a station whose slogan is "There is
no alternative."
Now, we're talking here about a 10-year-old who worships the Beatles and
has been raised on a steady diet of oldies. She loves melodies, catchy
songs, and good singers, none of which you're likely to find on an
alternative rock station. So it's no surprise that when I turned on the
radio, the first word out of her mouth was "Yuck!"
What did surprise me was that after listening for a while, she said,
"You know, Dad, it's actually kind of interesting."
Apart from giving me a clue as to what sort of music I can expect to
hear blasting through our house when she's a teenager, her quick uptake
on alternative rock (versus my decades-long devotion to the music of my
youth) reminded me of something that it's easy to forget as we become
older and more set in our ways. It reminded me that it's essential to
keep an open mind, and to be willing, better yet, eager, to try new
things. Programmers tend to become attached to familiar approaches, and
are inclined to stick with whatever is currently doing the job
adequately well, but in programming there are always alternatives, and
I've found that they're often worth considering.
Not that I should have needed any reminding, considering the
ever-evolving nature of Quake.
### Creative Flux and Hidden Surfaces {#Heading3}
Back in Chapter 64, I described the creative flux that led to John
Carmack's decision to use a precalculated potentially visible set (PVS)
of polygons for each possible viewpoint in Quake, the game we're
developing here at id Software. The precalculated PVS meant that instead
of having to spend a lot of time searching through the world database to
find out which polygons were visible from the current viewpoint, we
could simply draw all the polygons in the PVS from back-to-front
(getting the ordering courtesy of the world BSP tree) and get the
correct scene drawn with no searching at all; letting the back-to-front
drawing perform the final stage of hidden-surface removal (HSR). This
was a terrific idea, but it was far from the end of the road for Quake's
design.
#### Drawing Moving Objects {#Heading4}
For one thing, there was still the question of how to sort and draw
moving objects properly; in fact, this is the single technical question
I've been asked most often in recent months, so I'll take a moment to
address it here. The primary problem is that a moving model can span
multiple BSP leaves, with the leaves that are touched varying as the
model moves; that, together with the possibility of multiple models in
one leaf, means there's no easy way to use BSP order to draw the models
in correctly sorted order. When I wrote Chapter 64, we were drawing
sprites (such as explosions), moveable BSP models (such as doors), and
polygon models (such as monsters) by clipping each into all the leaves
it touched, then drawing the appropriate parts as each BSP leaf was
reached in back-to-front traversal. However, this didn't solve the issue
of sorting multiple moving models in a single leaf against each other,
and also left some ugly sorting problems with complex polygon models.
John solved the sorting issue for sprites and polygon models in a
startlingly low-tech way: We now z-buffer them. (That is, before we draw
each pixel, we compare its distance, or z, value with the z value of the
pixel currently on the screen, drawing only if the new pixel is nearer
than the current one.) First, we draw the basic world, walls, ceilings,
and the like. No z-buffer *testing* is involved at this point (the world
visible surface determination is done in a different way, as we'll see
soon); however, we do *fill* the z-buffer with the z values (actually,
1/z values, as discussed below) for all the world pixels. Z-filling is a
much faster process than z-buffering the entire world would be, because
no reads or compares are involved, just writes of z values. Once the
drawing and z-filling of the world is done, we can simply draw the
sprites and polygon models with z-buffering and get perfect sorting all
around.
#### Performance Impact {#Heading5}
Whenever a z-buffer is involved, the questions inevitably are: What's
the memory footprint and what's the performance impact? Well, the memory
footprint at 320x200 is 128K, not trivial but not a big deal for a game
that requires 8 MB to run. The performance impact is about 10 percent
for z-filling the world, and roughly 20 percent (with lots of variation)
for drawing sprites and polygon models. In return, we get a perfectly
sorted world, and also the ability to do additional effects, such as
particle explosions and smoke, because the z-buffer lets us flawlessly
sort such effects into the world. All in all, the use of the z-buffer
vastly improved the visual quality and flexibility of the Quake engine,
and also simplified the code quite a bit, at an acceptable memory and
performance cost.
#### Leveling and Improving Performance {#Heading6}
As I said above, in the Quake architecture, the world itself is drawn
first, without z-buffer reads or compares, but filling the z-buffer with
the world polygons' z values, and then the moving objects are drawn atop
the world, using full z-buffering. Thus far, I've discussed how to draw
moving objects. For the rest of this chapter, I'm going to talk about
the other part of the drawing equation; that is, how to draw the world
itself, where the entire world is stored as a single BSP tree and never
moves.