Move source markdown files into src/
This commit is contained in:
parent
9deba4ffa6
commit
3736ff43be
76 changed files with 1 additions and 1 deletions
458
src/chapter-66.md
Normal file
458
src/chapter-66.md
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
---
|
||||
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: '66'
|
||||
pages: 1209-1222
|
||||
---
|
||||
|
||||
## Chapter 66 -- Quake's Hidden-Surface Removal
|
||||
|
||||
### Struggling with Z-Order Solutions to the Hidden Surface Problem
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
As you may recall from Chapter 64, we're concerned with both raw
|
||||
performance and level performance. That is, we want the drawing code to
|
||||
run as fast as possible, but we also want the difference in drawing
|
||||
speed between the average scene and the slowest-drawing scene to be as
|
||||
small as possible.
|
||||
|
||||
> 
|
||||
> It does little good to average 30 frames per second if 10 percent of the
|
||||
> scenes draw at 5 fps, because the jerkiness in those scenes will be
|
||||
> extremely obvious by comparison with the average scene, and highly
|
||||
> objectionable. It would be better to average 15 fps 100 percent of the
|
||||
> time, even though the average drawing speed is only half as much.
|
||||
|
||||
The precalculated PVS was an important step toward both faster and more
|
||||
level performance, because it eliminated the need to identify visible
|
||||
polygons, a relatively slow step that tended to be at its worst in the
|
||||
most complex scenes. Nonetheless, in some spots in real game levels the
|
||||
precalculated PVS contains five times more polygons than are actually
|
||||
visible; together with the back-to-front HSR approach, this created hot
|
||||
spots in which the frame rate bogged down visibly as hundreds of
|
||||
polygons are drawn back-to- front, most of those immediately getting
|
||||
overdrawn by nearer polygons. Raw performance in general was also
|
||||
reduced by the typical 50% overdraw resulting from drawing everything in
|
||||
the PVS. So, although drawing the PVS back-to-front as the final HSR
|
||||
stage worked and was an improvement over previous designs, it was not
|
||||
ideal. Surely, John thought, there's a better way to leverage the PVS
|
||||
than back-to-front drawing.
|
||||
|
||||
And indeed there is.
|
||||
|
||||
### Sorted Spans
|
||||
|
||||
The ideal final HSR stage for Quake would reject all the polygons in the
|
||||
PVS that are actually invisible, and draw only the visible pixels of the
|
||||
remaining polygons, with no overdraw, that is, with every pixel drawn
|
||||
exactly once, all at no performance cost, of course. One way to do that
|
||||
(although certainly not at zero cost) would be to draw the polygons from
|
||||
front-to-back, maintaining a region describing the currently occluded
|
||||
portions of the screen and clipping each polygon to that region before
|
||||
drawing it. That sounds promising, but it is in fact nothing more or
|
||||
less than the beam tree approach I described in Chapter 64, an approach
|
||||
that we found to have considerable overhead and serious leveling
|
||||
problems.
|
||||
|
||||
We can do much better if we move the final HSR stage from the polygon
|
||||
level to the span level and use a sorted-spans approach. In essence,
|
||||
this approach consists of turning each polygon into a set of spans, as
|
||||
shown in Figure 66.1, and then sorting and clipping the spans against
|
||||
each other until only the visible portions of visible spans are left to
|
||||
be drawn, as shown in Figure 66.2. This may sound a lot like z-buffering
|
||||
(which is simply too slow for use in drawing the world, although it's
|
||||
fine for smaller moving objects, as described earlier), but there are
|
||||
crucial differences.
|
||||
|
||||

|
||||
|
||||
By contrast with z-buffering, only visible portions of visible spans are
|
||||
scanned out pixel by pixel (although all polygon edges must still be
|
||||
rasterized). Better yet, the sorting that z-buffering does at each pixel
|
||||
becomes a per-span operation with sorted spans, and because of the
|
||||
coherence implicit in a span list, each edge is sorted only against some
|
||||
of the spans on the same line and is clipped only to the few spans that
|
||||
it overlaps horizontally. Although complex scenes still take longer to
|
||||
process than simple scenes, the worst case isn't as bad as with the beam
|
||||
tree or back-to-front approaches, because there's no overdraw or
|
||||
scanning of hidden pixels, because complexity is limited to pixel
|
||||
resolution and because span coherence tends to limit the worst-case
|
||||
sorting in any one area of the screen. As a bonus, the output of sorted
|
||||
spans is in precisely the form that a low-level rasterizer needs, a set
|
||||
of span descriptors, each consisting of a start coordinate and a length.
|
||||
|
||||
In short, the sorted spans approach meets our original criteria pretty
|
||||
well; although it isn't zero-cost, it's not horribly expensive, it
|
||||
completely eliminates both overdraw and pixel scanning of obscured
|
||||
portions of polygons and it tends to level worst-case performance. We
|
||||
wouldn't want to rely on sorted spans alone as our hidden-surface
|
||||
mechanism, but the precalculated PVS reduces the number of polygons to a
|
||||
level that sorted spans can handle quite nicely.
|
||||
|
||||
So we've found the approach we need; now it's just a matter of writing
|
||||
some code and we're on our way, right? Well, yes and no. Conceptually,
|
||||
the sorted-spans approach is simple, but it's surprisingly difficult to
|
||||
implement, with a couple of major design choices to be made, a subtle
|
||||
mathematical element, and some tricky gotchas that I'll have to defer
|
||||
until Chapter 67. Let's look at the design choices first.
|
||||
|
||||
### Edges versus Spans
|
||||
|
||||
The first design choice is whether to sort spans or edges (both of which
|
||||
fall into the general category of "sorted spans"). Although the results
|
||||
are the same both ways, a list of spans to be drawn, with no overdraw,
|
||||
the implementations and performance implications are quite different,
|
||||
because the sorting and clipping are performed using very different data
|
||||
structures.
|
||||
|
||||
With span-sorting, spans are stored in x-sorted, linked list buckets,
|
||||
typically with one bucket per scan line. Each polygon in turn is
|
||||
rasterized into spans, as shown in Figure 66.1, and each span is sorted
|
||||
and clipped into the bucket for the scan line the span is on, as shown
|
||||
in Figure 66.2, so that at any time each bucket contains the nearest
|
||||
spans encountered thus far, always with no overlap. This approach
|
||||
involves generating all spans for each polygon in turn, with each span
|
||||
immediately being sorted, clipped, and added to the appropriate bucket.
|
||||
|
||||

|
||||
|
||||
With edge-sorting, edges are stored in x-sorted, linked list buckets
|
||||
according to their start scan line. Each polygon in turn is decomposed
|
||||
into edges, cumulatively building a list of all the edges in the scene.
|
||||
Once all edges for all polygons in the view frustum have been added to
|
||||
the edge list, the whole list is scanned out in a single top-to-bottom,
|
||||
left-to-right pass. An active edge list (AEL) is maintained. With each
|
||||
step to a new scan line, edges that end on that scan line are removed
|
||||
from the AEL, active edges are stepped to their new x coordinates, edges
|
||||
starting on the new scan line are added to the AEL, and the edges are
|
||||
sorted by current x coordinate.
|
||||
|
||||
For each scan line, a z-sorted active polygon list (APL) is maintained.
|
||||
The x-sorted AEL is stepped through in order. As each new edge is
|
||||
encountered (that is, as each polygon starts or ends as we move left to
|
||||
right), the associated polygon is activated and sorted into the APL, as
|
||||
shown in Figure 66.3, or deactivated and removed from the APL, as shown
|
||||
in Figure 66.4, for a leading or trailing edge, respectively. If the
|
||||
nearest polygon has changed (that is, if the new polygon is nearest, or
|
||||
if the nearest polygon just ended), a span is emitted for the polygon
|
||||
that just stopped being the nearest, starting at the point where the
|
||||
polygon first because nearest and ending at the x coordinate of the
|
||||
current edge, and the current x coordinate is recorded in the polygon
|
||||
that is now the nearest. This saved coordinate later serves as the start
|
||||
of the span emitted when the new nearest polygon ceases to be in front.
|
||||
|
||||
Don't worry if you didn't follow all of that; the above is just a quick
|
||||
overview of edge-sorting to help make the rest of this chapter a little
|
||||
clearer. My thorough discussion of the topic will be in Chapter 67.
|
||||
|
||||
The spans that are generated with edge-sorting are exactly the same
|
||||
spans that ultimately emerge from span-sorting; the difference lies in
|
||||
the intermediate data structures that are used to sort the spans in the
|
||||
scene. With edge-sorting, the spans are kept implicit in the edges until
|
||||
the final set of visible spans is generated, so the sorting, clipping,
|
||||
and span emission is done as each edge adds or removes a polygon, based
|
||||
on the span state implied by the edge and the set of active polygons.
|
||||
With span-sorting, spans are immediately made explicit when each polygon
|
||||
is rasterized, and those intermediate spans are then sorted and clipped
|
||||
against other the spans on the scan line to generate the final spans, so
|
||||
the states of the spans are explicit at all times, and all work is done
|
||||
directly with spans.
|
||||
|
||||
Both span-sorting and edge-sorting work well, and both have been
|
||||
employed successfully in commercial projects. We've chosen to use
|
||||
edge-sorting in Quake partly because it seems inherently more efficient,
|
||||
with excellent horizontal coherence that makes for minimal time spent
|
||||
sorting, in contrast with the potentially costly sorting into linked
|
||||
lists that span-sorting can involve. A more important reason, though, is
|
||||
that with edge-sorting we're able to share edges between adjacent
|
||||
polygons, and that cuts the work involved in sorting, clipping, and
|
||||
rasterizing edges nearly in half, while also shrinking the world
|
||||
database quite a bit due to the sharing.
|
||||
|
||||

|
||||
|
||||
One final advantage of edge-sorting is that it makes no distinction
|
||||
between convex and concave polygons. That's not an important
|
||||
consideration for most graphics engines, but in Quake, edge clipping,
|
||||
transformation, projection, and sorting have become a major bottleneck,
|
||||
so we're doing everything we can to get the polygon and edge counts
|
||||
down, and concave polygons help a lot in that regard. While it's
|
||||
possible to handle concave polygons with span-sorting, that can involve
|
||||
significant performance penalties.
|
||||
|
||||

|
||||
|
||||
Nonetheless, there's no cut-and-dried answer as to which approach is
|
||||
better. In the end, span-sorting and edge-sorting amount to the same
|
||||
functionality, and the choice between them is a matter of whatever you
|
||||
feel most comfortable with. In Chapter 67, I'll go into considerable
|
||||
detail about edge-sorting, complete with a full implementation. I'm
|
||||
going the spend the rest of this chapter laying the foundation for
|
||||
Chapter 67 by discussing sorting keys and 1/z calculation. In the
|
||||
process, I'm going to have to make a few forward references to aspects
|
||||
of edge-sorting that I haven't yet covered in detail; my apologies, but
|
||||
it's unavoidable, and all should become clear by the end of Chapter 67.
|
||||
|
||||
### Edge-Sorting Keys
|
||||
|
||||
Now that we know we're going to sort edges, using them to emit spans for
|
||||
the polygons nearest the viewer, the question becomes: How can we tell
|
||||
which polygons are nearest? Ideally, we'd just store a sorting key in
|
||||
each polygon, and whenever a new edge came along, we'd compare its
|
||||
surface's key to the keys of other currently active polygons, and could
|
||||
easily tell which polygon was nearest.
|
||||
|
||||
That sounds too good to be true, but it is possible. If, for example,
|
||||
your world database is stored as a BSP tree, with all polygons clipped
|
||||
into the BSP leaves, then BSP walk order is a valid drawing order. So,
|
||||
for example, if you walk the BSP back-to-front, assigning each polygon
|
||||
an incrementally higher key as you reach it, polygons with higher keys
|
||||
are guaranteed to be in front of polygons with lower keys. This is the
|
||||
approach Quake used for a while, although a different approach is now
|
||||
being used, for reasons I'll explain shortly.
|
||||
|
||||
If you don't happen to have a BSP or similar data structure handy, or if
|
||||
you have lots of moving polygons (BSPs don't handle moving polygons very
|
||||
efficiently), another way to accomplish your objectives would be to sort
|
||||
all the polygons against one another before drawing the scene, assigning
|
||||
appropriate keys based on their spatial relationships in viewspace.
|
||||
Unfortunately, this is generally an extremely slow task, because every
|
||||
polygon must be compared to every other polygon. There are techniques to
|
||||
improve the performance of polygon sorts, but I don't know of anyone
|
||||
who's doing general polygon sorts of complex scenes in realtime on a PC.
|
||||
|
||||
An alternative is to sort by z distance from the viewer in screenspace,
|
||||
an approach that dovetails nicely with the excellent spatial coherence
|
||||
of edge-sorting. As each new edge is encountered on a scan line, the
|
||||
corresponding polygon's z distance can be calculated and compared to the
|
||||
other polygons' distances, and the polygon can be sorted into the APL
|
||||
accordingly.
|
||||
|
||||
Getting z distances can be tricky, however. Remember that we need to be
|
||||
able to calculate z at any arbitrary point on a polygon, because an edge
|
||||
may occur and cause its polygon to be sorted into the APL at any point
|
||||
on the screen. We could calculate z directly from the screen x and y
|
||||
coordinates and the polygon's plane equation, but unfortunately this
|
||||
can't be done very quickly, because the z for a plane doesn't vary
|
||||
linearly in screenspace; however, 1/z *does* vary linearly, so we'll use
|
||||
that instead. (See Chris Hecker's 1995 series of columns on texture
|
||||
mapping in *Game Developer* magazine for a discussion of screenspace
|
||||
linearity and gradients for 1/z.) Another advantage of using 1/z is that
|
||||
its resolution increases with decreasing distance, meaning that by using
|
||||
1/z, we'll have better depth resolution for nearby features, where it
|
||||
matters most.
|
||||
|
||||
The obvious way to get a 1/z value at any arbitrary point on a polygon
|
||||
is to calculate 1/z at the vertices, interpolate it down both edges of
|
||||
the polygon, and interpolate between the edges to get the value at the
|
||||
point of interest. Unfortunately, that requires doing a lot of work
|
||||
along each edge, and worse, requires division to calculate the 1/z step
|
||||
per pixel across each span.
|
||||
|
||||
A better solution is to calculate 1/z directly from the plane equation
|
||||
and the screen x and y of the pixel of interest. The equation is
|
||||
|
||||
1/z = (a/d)x' - (b/d)y' + c/d
|
||||
|
||||
where z is the viewspace z coordinate of the point on the plane that
|
||||
projects to screen coordinate (x',y') (the origin for this calculation
|
||||
is the center of projection, the point on the screen straight ahead of
|
||||
the viewpoint), [a b c] is the plane normal in viewspace, and d is the
|
||||
distance from the viewspace origin to the plane along the normal.
|
||||
Division is done only once per plane, because a, b, c, and d are
|
||||
per-plane constants.
|
||||
|
||||
The full 1/z calculation requires two multiplies and two adds, all of
|
||||
which should be floating-point to avoid range errors. That much
|
||||
floating-point math sounds expensive but really isn't, especially on a
|
||||
Pentium, where a plane's 1/z value at any point can be calculated in as
|
||||
little as six cycles in assembly language.
|
||||
|
||||
#### Where That 1/Z Equation Comes From
|
||||
|
||||
For those who are interested, here's a quick derivation of the 1/z
|
||||
equation. The plane equation for a plane is
|
||||
|
||||
ax + by + cz - d = 0
|
||||
|
||||
where x and y are viewspace coordinates, and a, b, c, d, and z are
|
||||
defined above. If we substitute x=x'z and y=-y'z (from the definition of
|
||||
the perspective projection, with y inverted because y increases upward
|
||||
in viewspace but downward in screenspace), and do some rearrangement, we
|
||||
get:
|
||||
|
||||
z = d / (ax' - by' + c)
|
||||
|
||||
Inverting and distributing yields:
|
||||
|
||||
= ax'/d - by'/d + c/d
|
||||
|
||||
We'll see 1/z sorting in action in Chapter 67.
|
||||
|
||||
#### Quake and Z-Sorting
|
||||
|
||||
I mentioned earlier that Quake no longer uses BSP order as the sorting
|
||||
key; in fact, it uses 1/z as the key now. Elegant as the gradients are,
|
||||
calculating 1/z from them is clearly slower than just doing a compare on
|
||||
a BSP-ordered key, so why have we switched Quake to 1/z?
|
||||
|
||||
The primary reason is to reduce the number of polygons. Drawing in BSP
|
||||
order means following certain rules, including the rule that polygons
|
||||
must be split if they cross BSP planes. This splitting increases the
|
||||
numbers of polygons and edges considerably. By sorting on 1/z, we're
|
||||
able to leave polygons unsplit but still get correct drawing order, so
|
||||
we have far fewer edges to process and faster drawing overall, despite
|
||||
the added cost of 1/z sorting.
|
||||
|
||||
Another advantage of 1/z sorting is that it solves the sorting issues I
|
||||
mentioned at the start involving moving models that are themselves small
|
||||
BSP trees. Sorting in world BSP order wouldn't work here, because these
|
||||
models are separate BSPs, and there's no easy way to work them into the
|
||||
world BSP's sequence order. We don't want to use z-buffering for these
|
||||
models because they're often large objects such as doors, and we don't
|
||||
want to lose the overdraw-reduction benefits that closed doors provide
|
||||
when drawn through the edge list. With sorted spans, the edges of moving
|
||||
BSP models are simply placed in the edge list (first clipping polygons
|
||||
so they don't cross any solid world surfaces, to avoid complications
|
||||
associated with interpenetration), along with all the world edges, and
|
||||
1/z sorting takes care of the rest.
|
||||
|
||||
### Decisions Deferred
|
||||
|
||||
There is, without a doubt, an awful lot of information in the preceding
|
||||
pages, and it may not all connect together yet in your mind. The code
|
||||
and accompanying explanation in the next chapter should help; if you
|
||||
want to peek ahead, the code is available on the CD-ROM as DDJZSORT.ZIP
|
||||
in the directory for Chapter 67. You may also want to take a look at
|
||||
Foley and van Dam's *Computer Graphics* or Rogers' *Procedural Elements
|
||||
for Computer Graphics*.
|
||||
|
||||
As I write this, it's unclear whether Quake will end up sorting edges by
|
||||
BSP order or 1/z. Actually, there's no guarantee that sorted spans in
|
||||
any form will be the final design. Sometimes it seems like we change
|
||||
graphics engines as often as they play Elvis on the ‘50s oldies stations
|
||||
(but, one would hope, with more aesthetically pleasing results!) and no
|
||||
doubt we'll be considering the alternatives right up until the day we
|
||||
ship.
|
||||
Loading…
Reference in a new issue