Journal:   Dr. Dobb's Journal  July 1992 v17 n7 p133(7)
-----------------------------------------------------------------------------
Title:     3-D shading. (Graphics Programming)(Column ) (Tutorial)
Author:    Abrash, Michael.
AttFile:    Program:  GP-JUL92.ASC  Source code.

Abstract:  The development of a real-time animation package called X-Sharp is
           discussed, with special attention paid to three-dimensional
           shading algorithms.  X-Sharp's initial implementation runs only on
           80386 and higher processors because it uses 32-bit multiply and
           divide instructions.  A new version that supports the 8088 and
           80286 processors is described; it uses 32 x 16 instead of 32 x 32
           divides.  Three-dimensional shading may be either ambient or
           diffuse; both use the same basic model, with each surface assigned
           a reflectivity between 1 and 0.  Ambient shading expresses
           intensity as the minimum of a function.  Diffuse shading is more
           complex because the effective intensity of directed light depends
           on the angle at which it strikes a surface.  Details of shading
           implementation are discussed, along with graphics debugging.
-----------------------------------------------------------------------------
Descriptors..
Topic:     Graphical User Interface
           Three-Dimensional Graphics
           Tutorial
           Animation
           Programming Instruction
           Real-Time Systems
           Program Development Techniques
           Computer Graphics
           32-Bit.
Feature:   illustration
           chart
           program.

-----------------------------------------------------------------------------
Full Text:

This month, we return to X-Sharp, the real-time animation package that we
started developing in January.  When last we saw X-Sharp, it had just
acquired basic hidden-surface capability, and performance had been vastly
improved through the use of fixed-point arithmetic.  This month, we're going
to add quite a bit more: support for 8088 and 80286 PCs, a general color
model, and shading.  That's an awful lot to cover in one column (actually,
it'll spill over into the next column), so let's get to it!

Sub-386 Support

To date, X-Sharp has run on only the 386 and 486, because it uses 32-bit
multiply and divide instructions that sub-386 processors don't support.  I
chose 32-bit instructions for two reasons: They're much faster for 16.16
fixed-point arithmetic than any approach that works on the 8088 and 286; and
they're much easier to implement than any other approach.  In short, I was
after maximum performance, and I was perhaps just a little lazy.

I should have known better than to try to sneak this one by you.  The most
common feedback I've gotten on X-Sharp is that I should make it support the
8088 and 286.  Well, I can take a hint as well as the next guy.  Listing One
(page 150) contains the latest versions of the FixedMul[unkeyable] and
FixedDiv[unkeyable] functions, in a form that can be conditionally assembled
to use either 386-specific or generic 8088 instructions.  The complete new
version of FIXED.ASM, containing dual 386/8088 versions of CosSin[unkeyable],
Xform Vec[unkeyable], and ConcatX-forms[unkeyable], as well as
FixedMul[unkeyable] and FixedDiv[unkeyable], can be found in the full X-Sharp
archive, the availability of which is described further on.

Given the new version of FIXED.ASM, with USE386 set to O, X-Sharp will run on
any processor.  That's not to say that it will run fast on any processor, or
at least not as fast as it used to.  The switch to 8088 instructions makes
X-Sharp's fixed-point calculations about 2.5 times slower overall.  Since a
PC is perhaps 40 times slower than a 486/33, we're talking about a
hundred-times speed difference from the low end to the high end.  A 486/33
can animate a 72-sided ball, complete with shading (as discussed later), at
60 frames per second (fps), with plenty of cycles to spare; an 8-MHz AT can
animate the same ball at about 6 fps.  Clearly, the level of animation an
application uses must be tailored to the available CPU horsepower.

The implementation of a 32-bit multiply using 8088 instructions is a simple
matter of adding together four partial products.  A 32-bit divide is not so
simple, however.  In fact, in Listing One I've chosen not to implement a full
32x32 divide, but rather only a 32x16 divide.  The reason is simple: 
performance.  A 32x16 divide can be implemented on an 8088 with two DIV
instructions, but a 32x32 divide takes a great deal more work, so far as I
can see.  (If anyone has a fast 32x32 divide, or has a faster way to handle
signed multiplies and divides than the approach taken by Listing One, please
drop me a line.)  In X-Sharp, division is used only to divide either X or Y
by Z in the process of projecting from view space to screen space, so the
cost of using a 32x16 divide is merely some inaccuracy in calculating screen
coordinates, especially when objects get very close to the Z=0 plane.  This
error is not cumulative (doesn't carry over to later frames), and in my
experience doesn't cause noticeable image degradation; therefore, given the
already slow performance of the 8088 and 286, I've opted for performance over
precision.

At any rate, please keep in mind that the non-386 version of
FixedDiv[unkeyable] is not a general-purpose 32x32 fixed-point division
routine.  In fact, it will generate a divide-by-zero error if passed a
fixed-point divisor between -1 and 1.  As I've explained, the non-386 version
of FixedDiv[unkeyable] is designed to do just what X-Sharp needs, and no
more, as quickly as possible.

Shading

So far, the polygons out of which our animated objects have been built have
had colors of fixed intensities.  For example, a face of a cube might be
blue, or green, or white, but whatever color it is, that color never
brightens or dims.  Fixed colors are easy to implement, but they don't make
for very realistic animation.  In the real world, the intensity of the color
of a surface varies depending on how brightly it is illuminated.  The ability
to simulate the illumination of a surface, or shading, is the next feature
we'll add to X-Sharp.

The overall shading of an object is the sum of several types of shading
components.  Ambient shading is illumination by what you might think of as
background light, light that's coming from all directions; all surfaces are
equally illuminated by ambient light, regardless of their orientation.
Directed lighting, producing diffuse shading, is illumination from one or
more specific light sources.  Directed light has a specific direction, and
the angle at which it strikes a surface determines how brightly it lights
that surface.  Specular reflection is the tendency of a surface to reflect
light in a mirrorlike fashion.  There are other sorts of shading components,
including transparency and atmospheric effects, but the ambient- and
diffuse-shading components are all we're going to deal with for a while, with
specular shading not too far in the future.

Ambient Shading

The basic model for both ambient and diffuse shading is a simple one.  Each
surface has a reflectivity between 0 and 1, where 0 means all light is
absorbed and 1 means all light is reflected.  A certain amount of light
energy strikes each surface.  The energy (intensity) of the light is
expressed such that if light of intensity 1 strikes a surface with
reflectivity 1, then the brightest possible shading is displayed for that
surface.  Complicating this somewhat is the need to support color; we do this
by separating reflectance and shading into three components each--red, green,
and blue--and calculating the shading for each color component separately for
each surface.

Given an ambient-light red intensity of [IA.sub.red] and a surface red
reflectance [R.sub.red'], the displayed red ambient shading for that surface,
as a fraction of the maximum red intensity, is simply min([IA.sub.red] x
[R.sub.red'] 1).  The green and blue color components are handled similarly.
That's really all there is to ambient shading, although of course we must
design some way to map displayed color components into the available palette
of colors, which I'll discuss next month.  Ambient shading isn't the whole
shading picture, though.  In fact, scenes tend to look pretty bland without
diffuse shading.

Diffuse Shading

Diffuse shading is more complicated than ambient shading, because the
effective intensity of directed light falling on a surface depends on the
angle at which it strikes the surface.  According to Lambert's law, the light
energy from a directed light source striking a surface is proportional to the
cosine of the angle at which it strikes the surface, with the angle measured
relative to a vector perpendicular to the polygon (a polygon normal), as
shown in Figure 1.  If the red intensity of directed light is [ID.sub.red,]
the red reflectance of the surface is [R.sub.red,] and the angle between the
incoming directed light and the surface's normal is [unkeyable], then the
displayed red diffuse shading for that surface, as a fraction of the largest
possible red intensity, is min ([ID.sub.red] X [R.sub.red] X cos[unkeyable],
1).

That's easy enough to calculate--but seemingly slow.  Determining the cosine
of an angle can be sped up with a table lookup, but there's also the task of
figuring out the angle, and, all in all, it doesn't seem that diffuse shading
is going to be speedy enough for our purposes.  Consider this, however: 
According to the properties of the dot product (denoted by the operator
"[unkeyable]", as shown in Figure 2), cos [(unkeyable)] =
(v[unkeyable]w)/[v]x[w]), where v and w are vectors, [unkeyable] is the angle
between v and w, and [v] is the length of v. Suppose, now, that v and w are
unit vectors; that is, vectors exactly one unit long.  Then the above
equation reduces to cos[unkeyable] =v[unkeyable]w.  In other words, we can
calculate the cosine between N, the unit-normal vector (one-unit-long
perpendicular vector) of a polygon, and L', the reverse of a unit vector
describing the direction of a light source, with just three multiples and two
adds.  (The reason the light-direction vector must be reversed is explained
later.)  Once we have that, we can easily calculate the red diffuse shading
from a directed light source as min([ID.sub.red]) x [R.sub.red] x
(L'[unkeyable]N), 1) and likewise for the green and blue color components.

The overall red shading for each polygon can be calculated by summing the
ambient-shading red component with the diffuse-shading component from each
light source, as in min(([IA.sub.red] X [R.sub.red] + ([ID.sub.red0] x
[R.sub.red] x ([L.sub.0'][unkeyable]N)) + ([ID.sub.red] x
[R.sub.red]x([L.sub.1'][unkeyable]N))+...), 1) where [ID.sub.red0] and
[L.sub.0'] are the red intensity and the reversed unit-direction vector,
respectively, for spotlight 0.  Listing Two, page 152, shows the X-Sharp
module DRAWPOBJ.C, which performs ambient and diffuse shading.  Toward the
bottom, you will find the code that performs shading exactly as described by
the above equation, first calculating the ambient red, green, and blue
shadings, then summing that with the diffuse red, green, and blue shadings
generated by each directed light source.

Shading: Implementation Details

In order to calculate the cosine of the angle between an incoming light
source and a polygon's unit normal, we must first have the polygon's unit
normal.  This could be calculated by generating a cross-product on two
polygon edges to generate a normal, then calculating the normal's length and
scaling to produce a unit normal.  Unfortunately, that would require taking a
square root, so it's not a desirable course of action.  Instead, I've made a
change to X-Sharp's polygon format.  Now, the first vertex in a shaded
polygon's vertex list is the end-point of a unit normal that starts at the
second point in the polygon's vertex list, as shown in Figure 3.  The first
point isn't one of the polygon's vertices, but is used only to generate a
unit normal.  The second point, however, is a polygon vertex.  Calculating
the difference vector between the first and second points yields the
polygon's unit normal.  Adding a unit-normal endpoint to each polygon isn't
free; each of those endpoints has to be transformed, along with the rest of
the vertices, and that takes time.  Still, it's faster than calculating a
unit normal for each polygon from scratch.

We also need a unit vector for each directed light source.  The directed
light sources I've implemented in X-Sharp are spotlights; that is, they're
considered to be point light sources that are infinitely far away.  This
allows the simplifying assumption that all light rays from a spotlight are
parallel and of equal intensity throughout the displayed universe, so each
spotlight can be represented with a single unit vector and a single
intensity.  The only trick is that in order to calculate the desired
cos([unkeyable]) between the polygon unit normal and a spotlight's unit
vector, the direction of the spotlight's unit vector must be reversed, as
shown in Figure 4.  This is necessary because the dot product implicitly
places vectors with their start points at the same location when it's used to
calculate the cosine of the angle between two vectors.  The light vector is
incoming to the polygon surface, and the unit normal is outbound, so only by
reversing one vector or the other will we get the cosine of the desired
angle.

Given the two unit vectors, it's a piece of cake to calculate intensities, as
shown in Listing Two.  The sample program DEMO1, in the X-Sharp archive
(built by running K1.BAT), puts the shading code to work displaying a
rotating ball with ambient lighting and three spot lighting sources that the
user can turn on and off.  What you'll see when you run DEMO1 is that the
shading is very good--face colors change very smoothly indeed--so long as
only green lighting sources are on.  However, if you combine spotlight two,
which is blue, with any other light source, polygon colors will start to
shift abruptly and unevenly.  As configured in the demo, the palette supports
a wide range of shading intensities for a pure version of any one of the
three primary colors, but a very limited number of intensity steps (four, in
this case) for each color component when two or more primary colors are
mixed.  While this situation can be improved, it is fundamentally a result of
the restricted capabilities of the 256-color palette, and there is only so
much that can be done without a larger color set.  Next month, I'll talk
about some ways to improve the quality of 256-color shading.

Shading problems pretty much vanish in 15-bpp or better modes, such as the
32K-color mode supported by the Sierra Hicolor DAC.  I've designed X-Sharp's
color model looking forward to this emerging generation of highly
color-capable adapters, and DEMO1 gives you a taste of how terrific shading
will look on such adapters.

More on colors next month.

Where to get X-Sharp

The full source for X-Sharp is available electronically as XSHARPn.ARC in the
DDJ Forum on CompuServe, on M&T Online (see "Availability," page 3), and in
the graphic.disp conference on Bix (as a ZIP file).  Alternatively, you can
send me a 360K or 720K formatted diskette and an addressed, stamped diskette
mailer, care of DDJ, 411 Borel Ave., San Mateo, CA 94402, and I'll send you
the latest copy of X-Sharp.  There's no charge, but it'd be very much
appreciated if you'd slip in a dollar or so to help out the folks at the
Vermont Association for the Blind and Visually Impaired.  Your response so
far has been great.  I just took a bundle of checks and money over to VABVI
this week, and they were very happy indeed.  Thanks!

I'm available on a daily basis to discuss X-Sharp on M&T Online and Bix (user
name mabrash in both cases.)

Graphics Debugging Update

A while back, I lamented that Turbo Debugger had some annoying quirks when it
came to debugging graphics in a dual-monitor set-up (using the -do switch).
However, most of these quirks can be worked around by combining two of TD's
display-handling modes.  As I reported, TD blocks manual access (performed
via the I/O menu in the CPU window) to some of the VGA's registers, and
sometimes alters the page flipping and other registers when break-points
occur.  As it turns out, however, all that only happens in Smart display
mode.  If you use None display mode, TD doesn't touch any VGA registers at
all.  The drawback to using None mode is that in None mode, for some reason,
program output via DOS functions goes to the debugging screen rather than the
target screen.  However, you can actively switch between Smart and None mode
via the Options menu, so a reasonably complete solution is to start out in
Smart mode, then switch to None mode when you encounter problems in manually
accessing the VGA's registers from TD, or if you think TD is otherwise
interfering with the state of the VGA.

Recommended Reading

Andrew Glassner's Graphics Gems (Academic Press, 1990) is an oddly enjoyable
book.  Odd, because there's no overall coherency to the book; it's a
collection of more than 100 largely unrelated contributions by various
authors on a hodgepodge of graphics subjects.  Enjoyable, because it's that
rarest sort of graphics programming book:  one that you can open at random
and start reading for fun.  A good example of the nature of Graphics Gems is
a chapter on mapping RGB colors into a 4-bit color space; this chapter
features somewhat arcane theory, an interesting perspective on color space,
and a fast technique for RGB mapping in 16-color modes.  On balance the
chapter is a little uneven, but useful, informative, and interesting--a
description that would serve well for Graphics Gems as a whole, as well.
