123 lines
7.5 KiB
Markdown
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.
|