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

123 lines
7.5 KiB
Markdown

The edge list is an atypical technology for John; it's an extra stage in
the engine, it's complex, and it doesn't scale well. A Quake level might
have a maximum of 500 potentially drawable polygons that get placed into
the edge list, and that runs fine, but if you were to try to put 5,000
polygons into the edge list, it would quickly bog down due to edge
sorting, link following, and dataset size. Different data structures
(like using a tree to store the edges rather than a linear linked list)
would help to some degree, but basically the edge list has a relatively
small window of applicability; it was appropriate technology for the
degree of complexity possible in a Pentium-based game (and even then,
only with the reduction in polygons made possible by the PVS), but will
probably be poorly suited to more complex scenes. It served well in the
Quake engine, but remains an inelegant solution, and, in the end, it
feels like there's something better we didn't hit on. However, as John
says, "I'm pragmatic above all else"—and the edge list did the job.
### Rasterization {#Heading6}
Once the visible spans are scanned out of the edge list, they must still
be drawn, with perspective-correct texture mapping and lighting. This
involves hundreds of lines of heavily optimized assembly language, but
is fundamentally pretty simple. In order to draw the spans for a given
surface, the screenspace equations for 1/z, s/z, and t/z (where s and t
are the texture coordinates and z is distance) are calculated for the
surface. Then for each span, these values are calculated for the points
at each end of the span, the reciprocal of 1/z is calculated with a
divide, and s and t are then calculated as (s/z)\*z and (t/z)\*z. If the
span is longer than 16 pixels, s and t are likewise calculated every 16
pixels along the span. Then each stretch of up to 16 pixels is drawn by
linearly interpolating between these correctly calculated points. This
introduces some slight error, but this is almost never visible, and even
then is only a small ripple, well worth the performance improvement
gained by doing the perspective-correct math only once every 16 pixels.
To speed things up a little more, the FDIV to calculate the reciprocal
of 1/z is overlapped with drawing 16 pixels, taking advantage of the
Pentium's ability to perform floating-point in parallel with integer
instructions, so the FDIV effectively takes only one cycle.
#### Lighting {#Heading7}
Lighting is less simple to explain. The traditional way of doing polygon
lighting is to calculate the correct light at the vertices and linearly
interpolate between those points (Gouraud shading), but this has several
disadvantages; in particular, it makes it hard to get detailed lighting
without creating a lot of extra polygons, the lighting isn't perspective
correct, and the lighting varies with viewing angle for polygons other
than triangles. To address these problems, Quake uses surface-based
lighting instead. In this approach, when it's time to draw a surface (a
world polygon), that polygon's texture is tiled into a memory buffer. At
the same time, the texture is lit according to the surface's light map,
as calculated during preprocessing. Lighting values are linearly
interpolated between the light map's 16-texel grid points, so the
lighting effects are smooth, but slightly blurry. Then, the polygon is
drawn to the screen using the perspective-correct texture mapping
described above, with the prelit surface buffer being the source
texture, rather than the original texture tile. No additional lighting
is performed during texture mapping; all lighting is done when the
surface buffer is created.
Certainly it takes longer to build a surface buffer and then texture map
from it than it does to do lighting and texture mapping in a single
pass. However, surface buffers are cached for reuse, so only the texture
mapping stage is usually needed. Quake surfaces tend to be big, so
texture mapping is slowed by cache misses; however, the Quake approach
doesn't need to interpolate lighting on a pixel-by-pixel basis, which
helps speed things up, and it doesn't require additional polygons to
provide sophisticated lighting. On balance, the performance of
surface-based drawing is roughly comparable to tiled, Gouraud-shaded
texture mapping—and it looks much better, being perspective correct,
rotationally invariant, and highly detailed. Surface-based drawing also
has the potential to support some interesting effects, because anything
that can be drawn into the surface buffer can be cached as well, and is
automatically drawn in correct perspective. For instance, paint
splattered on a wall could be handled by drawing the splatter image as a
sprite into the appropriate surface buffer, so that drawing the surface
would draw the splatter as well.
#### Dynamic Lighting {#Heading8}
Here we come to a feature added to Quake after last year's Computer Game
Developer's Conference (CGDC). At that time, Quake did not support
dynamic lighting; that is, explosions and such didn't produce temporary
lighting effects. We hadn't thought dynamic lighting would add enough to
the game to be worth the trouble; however, at CGDC Billy Zelsnack showed
us a demo of his latest 3-D engine, which was far from finished at the
time, but did have impressive dynamic lighting effects. This caused us
to move dynamic lighting up the priority list, and when I got back to
id, I spent several days making the surface-building code as fast as
possible (winding up at 2.25 cycles per texel in the inner loop) in
anticipation of adding dynamic lighting, which would of course cause
dynamically lit surfaces to constantly be rebuilt as the lighting
changed. (A significant drawback of dynamic lighting is that it makes
surface caching worthless for dynamically lit surfaces, but if most of
the surfaces in a scene are not dynamically lit at any one time, it
works out fine.) There things stayed for several weeks, while more
critical work was done, and it was uncertain whether dynamic lighting
would, in fact, make it into Quake.
Then, one Saturday, John suggested that I take a shot at adding the
high-level dynamic lighting code, the code that would take the dynamic
light sources and project their sphere of illumination into the world,
and which would then add the dynamic contributions into the appropriate
light maps and rebuild the affected surfaces. I said I would as soon as
I finished up the stuff I was working on, but it might be a day or two.
A little while later, he said, "I bet I can get dynamic lighting working
in less than an hour," and dove into the code. One hour and nine minutes
later, we had dynamic lighting, and it's now hard to imagine Quake
without it. (It sure is easier to imagine the impact of features and
implement them once you've seen them done by someone else!)
One interesting point about Quake's dynamic lighting is how inaccurate
it is. It is basically a linear projection, accounting properly for
neither surface angle nor lighting falloff with distance—and yet that's
almost impossible to notice unless you specifically look for it, and has
no negative impact on gameplay whatsoever. Motion and fast action can
surely cover for a multitude of graphics sins.
It's well worth pointing out that because Quake's lighting is
perspective correct and independent of vertices, and because the
rasterizer is both subpixel and subtexel correct, Quake worlds are
visually very solid and stable. This was an important design goal from
the start, both as a point of technical pride and because it greatly
improves the player's sense of immersion.