abrash-black-book/chapter-67.md
2014-01-06 23:29:31 +11:00

856 lines
39 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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: '67'
pages: 1223-1241
---
## Chapter 67 -- Sorted Spans in Action
### Implementing Independent Span Sorting for Rendering without Overdraw
In Chapter 66, we dove headlong into the intricacies of hidden surface
removal by way of z-sorted (actually, 1/z-sorted) spans. At the end of
that chapter, I noted that we were currently using 1/z-sorted spans in
Quake, but it was unclear whether we'd switch back to BSP order. Well,
some time after that writing, it's become clear: We're back to sorting
spans by BSP order.
In Robert A. Heinlein's wonderful story "The Man Who Sold the Moon," the
chief engineer of the Moon rocket project tries to figure out how to get
a payload of three astronauts to the Moon and back. He starts out with a
four-stage rocket design, but finds that it won't do the job, so he adds
a fifth stage. The fifth stage helps, but not quite enough, "Because,"
he explains, "I've had to add in too much dead weight, that's why." (The
dead weight is the control and safety equipment that goes with the fifth
stage.) He then tries adding yet another stage, only to find that the
sixth stage actually results in a net slowdown. In the end, he has to
give up on the three-person design and build a one-person spacecraft
instead.
1/z-sorted spans in Quake turned out pretty much the same way, as we'll
see in a moment. First, though, I'd like to note up front that this
chapter is very technical and builds heavily on material I covered
earlier in this section of the book; if you haven't already read
Chapters 59 through 66, you really should. Make no mistake about it,
this is commercial-quality stuff; in fact, the code in this chapter uses
the same sorting technique as the test version of Quake, QTEST1.ZIP,
that id Software placed on the Internet in early March 1996. This
material is the Real McCoy, true reports from the leading edge, and I
trust that you'll be patient if careful rereading and some occasional
catch-up reading of earlier chapters are required to absorb everything
contained herein. Besides, the ultimate reference for any design is
working code, which you'll find, in part, in Listing 67.1, and in its
entirety in the file DDJZSORT.ZIP on the CD-ROM.
### Quake and Sorted Spans
As you'll recall from Chapter 66, Quake uses sorted spans to get zero
overdraw while rendering the world, thereby both improving overall
performance and leveling frame rates by speeding up scenes that would
otherwise experience heavy overdraw. Our original design used spans
sorted by BSP order; because we traverse the world BSP tree from
front-to-back relative to the viewpoint, the order in which BSP nodes
are visited is a guaranteed front-to-back sorting order. We simply gave
each node an increasing BSP sequence number as it was visited, set each
polygon's sort key to the BSP sequence number of the node (BSP splitting
plane) it lay on, and used those sort keys when generating spans.
(In a change from earlier designs, polygons now are stored on nodes,
rather than leaves, which are the convex subspaces carved out by the BSP
tree. Visits to potentially visible leaves are used only to mark that
the polygons that touch those leaves are visible and need to be drawn,
and each marked-visible polygon is then drawn after everything in front
of its node has been drawn. This results in less BSP splitting of
polygons, which is A Good Thing, as explained below.)
This worked flawlessly for the world, but had a couple of downsides.
First, it didn't address the issue of sorting small, moving BSP models
such as doors; those models could be clipped into the world BSP tree's
leaves and assigned sort keys corresponding to the leaves into which
they fell, but there was still the question of how to sort multiple BSP
models in the same world leaf against each other. Second, strict BSP
order requires that polygons be split so that every polygon falls
entirely within a single leaf. This can be stretched by putting polygons
on nodes, allowing for larger polygons on average, but even then,
polygons still need to be split so that every polygon falls within the
bounding volume for the node on which it lies. The end result, in either
case, is more and smaller polygons than if BSP order weren't used—and
that, in turn, means lower performance, because more polygons must be
clipped, transformed, and projected, more sorting must be done, and more
spans must be drawn.
We figured that if only we could avoid those BSP splits, Quake would get
a lot faster. Accordingly, we switched from sorting on BSP order to
sorting on 1/z, and left our polygons unsplit. Things did get faster at
first, but not as much as we had expected, for two reasons.
First, as the world BSP tree is descended, we clip each node's bounding
box in turn to see if it's inside or outside each plane of the view
frustum. The clipping results can be remembered, and often allow the
avoidance of some or all clipping for the node's polygons. For example,
all polygons in a node that has a trivially accepted bounding box are
likewise guaranteed to be unclipped and in the frustum, since they all
lie within the node's volume and need no further clipping. This
efficient clipping mechanism vanished as soon as we stepped out of BSP
order, because a polygon was no longer necessarily confined to its
node's volume.
Second, sorting on 1/z isn't as cheap as sorting on BSP order, because
floating-point calculations and comparisons are involved, rather than
integer compares. So Quake got faster but, like Heinlein's fifth rocket
stage, there was clear evidence of diminishing returns.
That wasn't the bad part; after all, even a small speed increase is A
Good Thing. The real problem was that our initial 1/z sorting proved to
be unreliable. We first ran into problems when two forward-facing
polygons started at a common edge, because it was hard to tell which one
was really in front (as discussed below), and we had to do additional
floating-point calculations to resolve these cases. This fixed the
problems for a while, but then odd cases started popping up where just
the right combination of polygon alignments caused new sorting errors.
We tinkered with those too, adding more code and incurring additional
slowdowns in the process. Finally, we had everything working smoothly
again, although by this point Quake was back to pretty much the same
speed it had been with BSP sorting.
And then yet another crop of sorting errors popped up.
We could have fixed those errors too; we'll take a quick look at how to
deal with such cases shortly. However, like the sixth rocket stage, the
fixes would have made Quake *slower* than it had been with BSP sorting.
So we gave up and went back to BSP order, and now the code is simpler
and sorting works reliably. It's too bad our experiment didn't work out,
but it wasn't wasted time because in trying what we did we learned quite
a bit. In particular, we learned that the information provided by a
simple, reliable world ordering mechanism, such as a BSP tree, can do
more good than is immediately apparent, in terms of both performance and
solid code.
Nonetheless, sorting on 1/z can be a valuable tool, used in the right
context; drawing a Quake world just doesn't happen to be such a case. In
fact, sorting on 1/z is how we're now handling the sorting of multiple
BSP models that lie within the same world leaf in Quake. In this case,
we don't have the option of using BSP order (because we're drawing
multiple independent trees), so we've set restrictions on the BSP models
to avoid running into the types of 1/z sorting errors we encountered
drawing the Quake world. Next, we'll look at another application in
which sorting on 1/z is quite useful, one where objects move freely
through space. As is so often the case in 3-D, there is no one "right"
technique, but rather a great many different techniques, each one handy
in the right situations. Often, a combination of techniques is
beneficial; for example, the combination in Quake of BSP sorting for the
world and 1/z sorting for BSP models in the same world leaf.
For the remainder of this chapter, I'm going to look at the three main
types of 1/z span sorting, then discuss a sample 3-D app built around
1/z span sorting.
### Types of 1/z Span Sorting
As a quick refresher: With 1/z span sorting, all the polygons in a scene
are treated as sets of screenspace pixel spans, and 1/z (where z is
distance from the viewpoint in viewspace, as measured along the
viewplane normal) is used to sort the spans so that the nearest span
overlapping each pixel is drawn. As I discussed in Chapter 66, in the
sample program we're actually going to do all our sorting with polygon
edges, which represent spans in an implicit form.
There are three types of 1/z span sorting, each requiring a different
implementation. In order of increasing speed and decreasing complexity,
they are: intersecting, abutting, and independent. (These are names of
my own devising; I haven't come across any standard nomenclature in the
literature.)
#### Intersecting Span Sorting
Intersecting span sorting occurs when polygons can interpenetrate. Thus,
two spans may cross such that part of each span is visible, in which
case the spans have to be split and drawn appropriately, as shown in
Figure 67.1.
![**Figure 67.1**  *Intersecting span sorting.*](images/67-01.jpg)
Intersecting is the slowest and most complicated type of span sorting,
because it is necessary to compare 1/z values at two points in order to
detect interpenetration, and additional work must be done to split the
spans as necessary. Thus, although intersecting span sorting certainly
works, it's not the first choice for performance.
#### Abutting Span Sorting
Abutting span sorting occurs when polygons that are not part of a
continuous surface can butt up against one another, but don't
interpenetrate, as shown in Figure 67.2. This is the sorting used in
Quake, where objects like doors often abut walls and floors, and turns
out to be more complicated than you might think. The problem is that
when an abutting polygon starts on a given scan line, as with polygon B
in Figure 67.2, it starts at exactly the same 1/z value as the polygon
it abuts, in this case, polygon A, so additional sorting is needed when
these ties happen. Of course, the two-point sorting used for
intersecting polygons would work, but we'd like to find something
faster.
As it turns out, the additional sorting for abutting polygons is
actually quite simple; whichever polygon has a greater 1/z gradient with
respect to screen x (that is, whichever polygon is heading fastest
toward the viewer along the scan line) is the front one. The hard part
is identifying *when* ties—that is, abutting polygons—occur; due to
floating-point imprecision, as well as fixed-point edge-stepping
imprecision that can move an edge slightly on the screen, calculations
of 1/z from the combination of screen coordinates and 1/z gradients (as
discussed last time) can be slightly off, so most tie cases will show up
as near matches, not exact matches. This imprecision makes it necessary
to perform two comparisons, one with an adjust-up by a small epsilon and
one with an adjust-down, creating a range in which near-matches are
considered matches. Fine-tuning this epsilon to catch all ties, without
falsely reporting close-but-not-abutting edges as ties, proved to be
troublesome in Quake, and the epsilon calculations and extra comparisons
slowed things down.
![**Figure 67.2**  *Abutting span sorting.*](images/67-02.jpg)
I do think that abutting 1/z span sorting could have been made reliable
enough for production use in Quake, were it not that we share edges
between adjacent polygons in Quake, so that the world is a large polygon
mesh. When a polygon ends and is followed by an adjacent polygon that
shares the edge that just ended, we simply assume that the adjacent
polygon sorts relative to other active polygons in the same place as the
one that ended (because the mesh is continuous and there's no
interpenetration), rather than doing a 1/z sort from scratch. This
speeds things up by saving a lot of sorting, but it means that if there
is a sorting error, a whole string of adjacent polygons can be sorted
incorrectly, pulled in by the one missorted polygon. Missorting is a
very real hazard when a polygon is very nearly perpendicular to the
screen, so that the 1/z calculations push the limits of numeric
precision, especially in single-precision floating point.
Many caching schemes are possible with abutting span sorting, because
any given pair of polygons, being noninterpenetrating, will sort in the
same order throughout a scene. However, in Quake at least, the benefits
of caching sort results were outweighed by the additional overhead of
maintaining the caching information, and every caching variant we tried
actually slowed Quake down.
#### Independent Span Sorting
Finally, we come to independent span sorting, the simplest and fastest
of the three, and the type the sample code in Listing 67.1 uses. Here,
polygons never intersect or touch any other polygons except adjacent
polygons with which they form a continuous mesh. This means that when a
polygon starts on a scan line, a single 1/z comparison between that
polygon and the polygons it overlaps on the screen is guaranteed to
produce correct sorting, with no extra calculations or tricky cases to
worry about.
Independent span sorting is ideal for scenes with lots of moving objects
that never actually touch each other, such as a space battle. Next,
we'll look at an implementation of independent 1/z span sorting.
### 1/z Span Sorting in Action
Listing 67.1 is a portion of a program that demonstrates independent 1/z
span sorting. This program is based on the sample 3-D clipping program
from Chapter 65; however, the earlier program did hidden surface removal
(HSR) by simply z-sorting whole objects and drawing them back-to-front,
while Listing 67.1 draws all polygons by way of a 1/z-sorted edge list.
Consequently, where the earlier program worked only so long as object
centers correctly described sorting order, Listing 67.1 works properly
for all combinations of non-intersecting and non-abutting polygons. In
particular, Listing 67.1 correctly handles concave polyhedra; a new
L-shaped object (the data for which is not included in Listing 67.1) has
been added to the sample program to illustrate this capability. The
ability to handle complex shapes makes Listing 67.1 vastly more useful
for real-world applications than the 3-D clipping demo from Chapter 65.
**Listing 67.1 L67\_1.C**
```c
// Part of Win32 program to demonstrate z-sorted spans. Whitespace
// removed for space reasons. Full source code, with whitespace,
// available from ftp.idsoftware.com/mikeab/ddjzsort.zip.
#define MAX_SPANS 10000
#define MAX_SURFS 1000
#define MAX_EDGES 5000
typedef struct surf_s {
struct surf_s *pnext, *pprev;
int color, visxstart, state;
double zinv00, zinvstepx, zinvstepy;
} surf_t;
typedef struct edge_s {
int x, xstep, leading;
surf_t *psurf;
struct edge_s *pnext, *pprev, *pnextremove;
} edge_t;
// Span, edge, and surface lists
span_t spans[MAX_SPANS];
edge_t edges[MAX_EDGES];
surf_t surfs[MAX_SURFS];
// Bucket list of new edges to add on each scan line
edge_t newedges[MAX_SCREEN_HEIGHT];
// Bucket list of edges to remove on each scan line
edge_t *removeedges[MAX_SCREEN_HEIGHT];
// Head and tail for the active edge list
edge_t edgehead, edgetail;
// Edge used as sentinel of new edge lists
edge_t maxedge = {0x7FFFFFFF};
// Head/tail/sentinel/background surface of active surface stack
surf_t surfstack;
// pointers to next available surface and edge
surf_t *pavailsurf;
edge_t *pavailedge;
// Returns true if polygon faces the viewpoint, assuming a clockwise
// winding of vertices as seen from the front.
int PolyFacesViewer(polygon_t *ppoly, plane_t *pplane)
{
int i;
point_t viewvec;
for (i=0 ; i<3 ; i++)
viewvec.v[i] = ppoly->verts[0].v[i] - currentpos.v[i];
// Use an epsilon here so we don't get polygons tilted so
// sharply that the gradients are unusable or invalid
if (DotProduct (&viewvec, &pplane->normal) < -0.01)
return 1;
return 0;
}
// Add the polygon's edges to the global edge table.
void AddPolygonEdges (plane_t *plane, polygon2D_t *screenpoly)
{
double distinv, deltax, deltay, slope;
int i, nextvert, numverts, temp, topy, bottomy, height;
edge_t *pedge;
numverts = screenpoly->numverts;
// Clamp the polygon's vertices just in case some very near
// points have wandered out of range due to floating-point
// imprecision
for (i=0 ; i<numverts ; i++) {
if (screenpoly->verts[i].x < -0.5)
screenpoly->verts[i].x = -0.5;
if (screenpoly->verts[i].x > ((double)DIBWidth - 0.5))
screenpoly->verts[i].x = (double)DIBWidth - 0.5;
if (screenpoly->verts[i].y < -0.5)
screenpoly->verts[i].y = -0.5;
if (screenpoly->verts[i].y > ((double)DIBHeight - 0.5))
screenpoly->verts[i].y = (double)DIBHeight - 0.5;
}
// Add each edge in turn
for (i=0 ; i<numverts ; i++) {
nextvert = i + 1;
if (nextvert >= numverts)
nextvert = 0;
topy = (int)ceil(screenpoly->verts[i].y);
bottomy = (int)ceil(screenpoly->verts[nextvert].y);
height = bottomy - topy;
if (height == 0)
continue; // doesn't cross any scan lines
if (height < 0) {
// Leading edge
temp = topy;
topy = bottomy;
bottomy = temp;
pavailedge->leading = 1;
deltax = screenpoly->verts[i].x -
screenpoly->verts[nextvert].x;
deltay = screenpoly->verts[i].y -
screenpoly->verts[nextvert].y;
slope = deltax / deltay;
// Edge coordinates are in 16.16 fixed point
pavailedge->xstep = (int)(slope * (float)0x10000);
pavailedge->x = (int)((screenpoly->verts[nextvert].x +
((float)topy - screenpoly->verts[nextvert].y) *
slope) * (float)0x10000);
} else {
// Trailing edge
pavailedge->leading = 0;
deltax = screenpoly->verts[nextvert].x -
screenpoly->verts[i].x;
deltay = screenpoly->verts[nextvert].y -
screenpoly->verts[i].y;
slope = deltax / deltay;
// Edge coordinates are in 16.16 fixed point
pavailedge->xstep = (int)(slope * (float)0x10000);
pavailedge->x = (int)((screenpoly->verts[i].x +
((float)topy - screenpoly->verts[i].y) * slope) *
(float)0x10000);
}
// Put the edge on the list to be added on top scan
pedge = &newedges[topy];
while (pedge->pnext->x < pavailedge->x)
pedge = pedge->pnext;
pavailedge->pnext = pedge->pnext;
pedge->pnext = pavailedge;
// Put the edge on the list to be removed after final scan
pavailedge->pnextremove = removeedges[bottomy - 1];
removeedges[bottomy - 1] = pavailedge;
// Associate the edge with the surface we'll create for
// this polygon
pavailedge->psurf = pavailsurf;
// Make sure we don't overflow the edge array
if (pavailedge < &edges[MAX_EDGES])
pavailedge++;
}
// Create the surface, so we'll know how to sort and draw from
// the edges
pavailsurf->state = 0;
pavailsurf->color = currentcolor;
// Set up the 1/z gradients from the polygon, calculating the
// base value at screen coordinate 0,0 so we can use screen
// coordinates directly when calculating 1/z from the gradients
distinv = 1.0 / plane->distance;
pavailsurf->zinvstepx = plane->normal.v[0] * distinv *
maxscreenscaleinv * (fieldofview / 2.0);
pavailsurf->zinvstepy = -plane->normal.v[1] * distinv *
maxscreenscaleinv * (fieldofview / 2.0);
pavailsurf->zinv00 = plane->normal.v[2] * distinv -
xcenter * pavailsurf->zinvstepx -
ycenter * pavailsurf->zinvstepy;
// Make sure we don't overflow the surface array
if (pavailsurf < &surfs[MAX_SURFS])
pavailsurf++;
}
// Scan all the edges in the global edge table into spans.
void ScanEdges (void)
{
int x, y;
double fx, fy, zinv, zinv2;
edge_t *pedge, *pedge2, *ptemp;
span_t *pspan;
surf_t *psurf, *psurf2;
pspan = spans;
// Set up the active edge list as initially empty, containing
// only the sentinels (which are also the background fill). Most
// of these fields could be set up just once at start-up
edgehead.pnext = &edgetail;
edgehead.pprev = NULL;
edgehead.x = -0xFFFF; // left edge of screen
edgehead.leading = 1;
edgehead.psurf = &surfstack;
edgetail.pnext = NULL; // mark edge of list
edgetail.pprev = &edgehead;
edgetail.x = DIBWidth << 16; // right edge of screen
edgetail.leading = 0;
edgetail.psurf = &surfstack;
// The background surface is the entire stack initially, and
// is infinitely far away, so everything sorts in front of it.
// This could be set just once at start-up
surfstack.pnext = surfstack.pprev = &surfstack;
surfstack.color = 0;
surfstack.zinv00 = -999999.0;
surfstack.zinvstepx = surfstack.zinvstepy = 0.0;
for (y=0 ; y<DIBHeight ; y++) {
fy = (double)y;
// Sort in any edges that start on this scan
pedge = newedges[y].pnext;
pedge2 = &edgehead;
while (pedge != &maxedge) {
while (pedge->x > pedge2->pnext->x)
pedge2 = pedge2->pnext;
ptemp = pedge->pnext;
pedge->pnext = pedge2->pnext;
pedge->pprev = pedge2;
pedge2->pnext->pprev = pedge;
pedge2->pnext = pedge;
pedge2 = pedge;
pedge = ptemp;
}
// Scan out the active edges into spans
// Start out with the left background edge already inserted,
// and the surface stack containing only the background
surfstack.state = 1;
surfstack.visxstart = 0;
for (pedge=edgehead.pnext ; pedge ; pedge=pedge->pnext) {
psurf = pedge->psurf;
if (pedge->leading) {
// It's a leading edge. Figure out where it is
// relative to the current surfaces and insert in
// the surface stack; if it's on top, emit the span
// for the current top.
// First, make sure the edges don't cross
if (++psurf->state == 1) {
fx = (double)pedge->x * (1.0 / (double)0x10000);
// Calculate the surface's 1/z value at this pixel
zinv = psurf->zinv00 + psurf->zinvstepx * fx +
psurf->zinvstepy * fy;
// See if that makes it a new top surface
psurf2 = surfstack.pnext;
zinv2 = psurf2->zinv00 + psurf2->zinvstepx * fx +
psurf2->zinvstepy * fy;
if (zinv >= zinv2) {
// It's a new top surface
// emit the span for the current top
x = (pedge->x + 0xFFFF) >> 16;
pspan->count = x - psurf2->visxstart;
if (pspan->count > 0) {
pspan->y = y;
pspan->x = psurf2->visxstart;
pspan->color = psurf2->color;
// Make sure we don't overflow
// the span array
if (pspan < &spans[MAX_SPANS])
pspan++;
}
psurf->visxstart = x;
// Add the edge to the stack
psurf->pnext = psurf2;
psurf2->pprev = psurf;
surfstack.pnext = psurf;
psurf->pprev = &surfstack;
} else {
// Not a new top; sort into the surface stack.
// Guaranteed to terminate due to sentinel
// background surface
do {
psurf2 = psurf2->pnext;
zinv2 = psurf2->zinv00 +
psurf2->zinvstepx * fx +
psurf2->zinvstepy * fy;
} while (zinv < zinv2);
// Insert the surface into the stack
psurf->pnext = psurf2;
psurf->pprev = psurf2->pprev;
psurf2->pprev->pnext = psurf;
psurf2->pprev = psurf;
}
}
} else {
// It's a trailing edge; if this was the top surface,
// emit the span and remove it.
// First, make sure the edges didn't cross
if (psurf->state == 0) {
if (surfstack.pnext == psurf) {
// It's on top, emit the span
x = ((pedge->x + 0xFFFF) >> 16);
pspan->count = x - psurf->visxstart;
if (pspan->count > 0) {
pspan->y = y;
pspan->x = psurf->visxstart;
pspan->color = psurf->color;
// Make sure we don't overflow
// the span array
if (pspan < &spans[MAX_SPANS])
pspan++;
}
psurf->pnext->visxstart = x;
}
// Remove the surface from the stack
psurf->pnext->pprev = psurf->pprev;
psurf->pprev->pnext = psurf->pnext;
}
}
}
// Remove edges that are done
pedge = removeedges[y];
while (pedge) {
pedge->pprev->pnext = pedge->pnext;
pedge->pnext->pprev = pedge->pprev;
pedge = pedge->pnextremove;
}
// Step the remaining edges one scan line, and re-sort
for (pedge=edgehead.pnext ; pedge != &edgetail ; ) {
ptemp = pedge->pnext;
// Step the edge
pedge->x += pedge->xstep;
// Move the edge back to the proper sorted location,
// if necessary
while (pedge->x < pedge->pprev->x) {
pedge2 = pedge->pprev;
pedge2->pnext = pedge->pnext;
pedge->pnext->pprev = pedge2;
pedge2->pprev->pnext = pedge;
pedge->pprev = pedge2->pprev;
pedge->pnext = pedge2;
pedge2->pprev = pedge;
}
pedge = ptemp;
}
}
pspan->x = -1; // mark the end of the list
}
// Draw all the spans that were scanned out.
void DrawSpans (void)
{
span_t *pspan;
for (pspan=spans ; pspan->x != -1 ; pspan++)
memset (pDIB + (DIBPitch * pspan->y) + pspan->x,
pspan->color,
pspan->count);
}
// Clear the lists of edges to add and remove on each scan line.
void ClearEdgeLists(void)
{
int i;
for (i=0 ; i<DIBHeight ; i++) {
newedges[i].pnext = &maxedge;
removeedges[i] = NULL;
}
}
// Render the current state of the world to the screen.
void UpdateWorld()
{
HPALETTE holdpal;
HDC hdcScreen, hdcDIBSection;
HBITMAP holdbitmap;
polygon2D_t screenpoly;
polygon_t *ppoly, tpoly0, tpoly1, tpoly2;
convexobject_t *pobject;
int i, j, k;
plane_t plane;
point_t tnormal;
UpdateViewPos();
SetUpFrustum();
ClearEdgeLists();
pavailsurf = surfs;
pavailedge = edges;
// Draw all visible faces in all objects
pobject = objecthead.pnext;
while (pobject != &objecthead) {
ppoly = pobject->ppoly;
for (i=0 ; i<pobject->numpolys ; i++) {
// Move the polygon relative to the object center
tpoly0.numverts = ppoly[i].numverts;
for (j=0 ; j<tpoly0.numverts ; j++) {
for (k=0 ; k<3 ; k++)
tpoly0.verts[j].v[k] = ppoly[i].verts[j].v[k] +
pobject->center.v[k];
}
if (PolyFacesViewer(&tpoly0, &ppoly[i].plane)) {
if (ClipToFrustum(&tpoly0, &tpoly1)) {
currentcolor = ppoly[i].color;
TransformPolygon (&tpoly1, &tpoly2);
ProjectPolygon (&tpoly2, &screenpoly);
// Move the polygon's plane into viewspace
// First move it into worldspace (object relative)
tnormal = ppoly[i].plane.normal;
plane.distance = ppoly[i].plane.distance +
DotProduct (&pobject->center, &tnormal);
// Now transform it into viewspace
// Determine the distance from the viewpont
plane.distance -=
DotProduct (&currentpos, &tnormal);
// Rotate the normal into view orientation
plane.normal.v[0] =
DotProduct (&tnormal, &vright);
plane.normal.v[1] =
DotProduct (&tnormal, &vup);
plane.normal.v[2] =
DotProduct (&tnormal, &vpn);
AddPolygonEdges (&plane, &screenpoly);
}
}
}
pobject = pobject->pnext;
}
ScanEdges ();
DrawSpans ();
// We've drawn the frame; copy it to the screen
hdcScreen = GetDC(hwndOutput);
holdpal = SelectPalette(hdcScreen, hpalDIB, FALSE);
RealizePalette(hdcScreen);
hdcDIBSection = CreateCompatibleDC(hdcScreen);
holdbitmap = SelectObject(hdcDIBSection, hDIBSection);
BitBlt(hdcScreen, 0, 0, DIBWidth, DIBHeight, hdcDIBSection,
0, 0, SRCCOPY);
SelectPalette(hdcScreen, holdpal, FALSE);
ReleaseDC(hwndOutput, hdcScreen);
SelectObject(hdcDIBSection, holdbitmap);
DeleteDC(hdcDIBSection);
}
```
By the same token, Listing 67.1 is quite a bit more complicated than the
earlier code. The earlier code's HSR consisted of a z-sort of objects,
followed by the drawing of the objects in back-to-front order, one
polygon at a time. Apart from the simple object sorter, all that was
needed was backface culling and a polygon rasterizer.
Listing 67.1 replaces this simple pipeline with a three-stage HSR
process. After backface culling, the edges of each of the polygons in
the scene are added to the global edge list, by way of
`AddPolygonEdges()`. After all edges have been added, the edges are
turned into spans by `ScanEdges()`, with each pixel on the screen
being covered by one and only one span (that is, there's no overdraw).
Once all the spans have been generated, they're drawn by
`DrawSpans()`, and rasterization is complete.
There's nothing tricky about `AddPolygonEdges()`, and `DrawSpans()`,
as implemented in Listing 67.1, is very straightforward as well. In an
implementation that supported texture mapping, however, all the spans
wouldn't be put on one global span list and drawn at once, as is done in
Listing 67.1, because that would result in drawing spans from all the
surfaces in no particular order. (A surface is a drawing object that's
originally described by a polygon, but in `ScanEdges()` there is no
polygon in the classic sense of a set of vertices bounding an area, but
rather just a set of edges and a surface that describes how to draw the
spans outlined by those edges.) That would mean constantly skipping from
one texture to another, which in turn would hurt processor cache
coherency a great deal, and would also incur considerable overhead in
setting up gradient and perspective calculations each time a surface was
drawn. In Quake, we have a linked list of spans hanging off each
surface, and draw all the spans for one surface before moving on to the
next surface.
The core of Listing 67.1, and the most complex aspect of 1/z-sorted
spans, is `ScanEdges()`, where the global edge list is converted into
a set of spans describing the nearest surface at each pixel. This
process is actually pretty simple, though, if you think of it as
follows:
For each scan line, there is a set of active edges, which are those
edges that intersect the scan line. A good part of `ScanEdges()` is
dedicated to adding any edges that first appear on the current scan line
(scan lines are processed from the top scan line on the screen to the
bottom), removing edges that reach their bottom on the current scan
line, and x-sorting the active edges so that the active edges for the
next scan can be processed from left to right. All this is per-scan-line
maintenance, and is basically just linked list insertion, deletion, and
sorting.
The heart of the action is the loop in `ScanEdges()` that processes
the edges on the current scan line from left to right, generating spans
as needed. The best way to think of this loop is as a surface event
processor, where each edge is an event with an associated surface. Each
leading edge is an event marking the start of its surface on that scan
line; if the surface is nearer than the current nearest surface, then a
span ends for the nearest surface, and a span starts for the new
surface. Each trailing edge is an event marking the end of its surface;
if its surface is currently nearest, then a span ends for that surface,
and a span starts for the next-nearest surface (the surface with the
next-largest 1/z at the coordinate where the edge intersects the scan
line). One handy aspect of this event-oriented processing is that
leading and trailing edges do not need to be explicitly paired, because
they are implicitly paired by pointing to the same surface. This saves
the memory and time that would otherwise be needed to track edge pairs.
One more element is required in order for `ScanEdges()` to work
efficiently. Each time a leading or trailing edge occurs, it must be
determined whether its surface is nearest (at a larger 1/z value than
any currently active surface). In addition, for leading edges, the
currently topmost surface must be known, and for trailing edges, it may
be necessary to know the currently next-to-topmost surface. The easiest
way to accomplish this is with a *surface stack*; that is, a linked list
of all currently active surfaces, starting with the nearest surface and
progressing toward the farthest surface, which, as described below, is
always the background surface. (The operation of this sort of edge
event-based stack was described and illustrated in Chapter 66.) Each
leading edge causes its surface to be 1/z-sorted into the surface stack,
with a span emitted if necessary. Each trailing edge causes its surface
to be removed from the surface stack, again with a span emitted if
necessary. As you can see from Listing 67.1, it takes a fair bit of code
to implement this, but all that's really going on is a surface stack
driven by edge events.
#### Implementation Notes
Finally, a few notes on Listing 67.1. First, you'll notice that although
we clip all polygons to the view frustum in worldspace, we nonetheless
later clamp them to valid screen coordinates before adding them to the
edge list. This catches any cases where arithmetic imprecision results
in clipped polygon vertices that are a bit outside the frustum. I've
only found such imprecision to be significant at very small z distances,
so clamping would probably be unnecessary if there were a near clip
plane, and might not even be needed in Listing 67.1, because of the
slight nudge inward that we give the frustum planes, as described in
Chapter 65. However, my experience has consistently been that relying on
worldspace or viewspace clipping to produce valid screen coordinates 100
percent of the time leads to sporadic and hard-to-debug errors.
There is no separate routine to clear the background in Listing 67.1.
Instead, a special background surface at an effectively infinite
distance is added, so whenever no polygons are active the background
color is drawn. If desired, it's a simple matter to flag the background
surface and draw the background specially. For example, the background
could be drawn as a starfield or a cloudy sky.
The edge-processing code in Listing 67.1 is fully capable of handling
concave polygons as easily as convex polygons, and can handle an
arbitrary number of vertices per polygon, as well. One change is needed
for the latter case: Storage for the maximum number of vertices per
polygon must be allocated in the polygon structures. In a fully polished
implementation, vertices would be linked together or pointed to, and
would be dynamically allocated from a vertex pool, so each polygon
wouldn't have to contain enough space for the maximum possible number of
vertices.
Each surface has a field named `state`, which is incremented when a
leading edge for that surface is encountered, and decremented when a
trailing edge is reached. A surface is activated by a leading edge only
if `state` increments to 1, and is deactivated by a trailing edge only
if `state` decrements to 0. This is another guard against arithmetic
problems, in this case quantization during the conversion of vertex
coordinates from floating point to fixed point. Due to this conversion,
it is possible, although rare, for a polygon that is viewed nearly
edge-on to have a trailing edge that occurs slightly *before* the
corresponding leading edge, and the span-generation code will behave
badly if it tries to emit a span for a surface that hasn't yet started.
It would help performance if this sort of fix-up could be eliminated by
careful arithmetic, but I haven't yet found a way to do so for
1/z-sorted spans.
Lastly, as discussed in Chapter 66, Listing 67.1 uses the gradients for
1/z with respect to changes in screen x and y to calculate 1/z for
active surfaces each time a leading edge needs to be sorted into the
surface stack. The natural origin for gradient calculations is the
center of the screen, which is (x,y) coordinate (0,0) in viewspace.
However, when the gradients are calculated in `AddPolygonEdges()`, the
origin value is calculated at the upper-left corner of the screen. This
is done so that screen x and y coordinates can be used directly to
calculate 1/z, with no need to adjust the coordinates to be relative to
the center of the screen. Also, the screen gradients grow more extreme
as a polygon is viewed closer to edge-on. In order to keep the gradient
calculations from becoming meaningless or generating errors, a small
epsilon is applied to backface culling, so that polygons that are very
nearly edge-on are culled. This calculation would be more accurate if it
were based directly on the viewing angle, rather than on the dot product
of a viewing ray to the polygon with the polygon normal, but that would
require a square root, and in my experience the epsilon used in Listing
67.1 works fine.