Fast Algorithms For 3D-Graphics (PDFDrive)
Fast Algorithms For 3D-Graphics (PDFDrive)
Georg Glaeser
With 94 Illustrations
With a diskette
Acknowledgments
A book like this can never be the achievement of one person only. It is not easy
to start with anyone, because many people and institutions have helped to get
the work done.
The idea of writing the book was born several years aga during the time I worked
with Steve M. Slaby at Princeton University. Besides Steve, the man who came
in with the most essential innovations and ideas from the very beginning was
Silvio Levy from the Mathematics Department. It was he who strongly believed
in the project.
At the same time, I received a lot of support from Dave Dobkin from the Com-
puter Science Department and the individuals from the Interactive Computer
Graphics Laboratory.
Preface vii
Back in Europe, Reinhard Thaller, Leonid Dimitrov and Emanuel Wenger from
the Austrian Academy of Science helped me in many ways. During the same
time, I worked together with two of my students, Thomas Grohser and Hein-
rich Pommer, both of whom brought in a lot of ideas to improve the code and
the speed. FUrthermore, I received advice from Hellmuth Stachel and Wolfgang
Rath from the Institute of Geometry at the Technical University of Vienna and
Otto Prem from the Handelsakademie St. Johann im Pongau, who also did the
proofreading together with Silvio Levy.
Suggested Reading
The list of books on 3D-graphics is already very long and it is useless to recom-
mend them all. The books listed below may be useful for a better understanding
of this book:
2 Projections 25
2.1 Central Projections . · ... 26
2.2 The Viewing Pyramid · .... 29
2.3 Coordinate Systems · ........ 32
2.4 Back and Forth Between Coordinate Systems · .... 37
2.5 Clipping Algorithms · ... . . . . . · ...... 43
4 Graphics Output 95
4.1 Graphics Hardware. 95
4.2 System-Dependent Macros and Functions . . . . . . . . . . . . . . . 96
4.3 How to Create Color Palettes and How to Use Them . . . . . . . . . 99
4.4 Wire Frames and Depth Cuing . . . . . . . 105
4.5 Shading...................... 110
4.6 Basic Graphics Output Algorithms 117
References 289
Index 293
1
A Basic Course in Spatial
Analytic Geometry
In this chapter, we deal with the most common problems of analytic geome-
try and present possible solutions for them. We also include an introduction to
pointer arithmetic for programmers with little experience, so that they may be
able to understand more complex things later on.
If we do not start off with two-dimensional geometry, it is because we believe
that, in reaHty, everyone is used to thinking in terms of spatial geometry and
that spealdng of objects in three-space does not necessarily malce things more
complicated. Furthermore, many principles of spatial geometry can also be ap-
plied to two-dimensional geometry without any major changes. As a matter of
fact, most ofthe problems mentioned in 1.1-1.4 have an equivalent in two-space
(such as intersecting lines and measuring angles between lines), which can be
solved by simply ignoring the third coordinate. N evertheless, some specific two-
dimensional problems will be dealt with in other chapters.
1.1 Vectors
Let us consider a three-dimensional Cartesian coordinate system based on the
three pairwise orthogonal axes x, y, z (Figure 1). Each point P in space has
unique coordinates PZ'Pt/'Pz, and we write P(pz'Pt/'Pz). The vector p = oP
from the origin 0(0,0,0) to the point P is called the position vector of P, and
2 Chapter 1. A Basic Course in Spatial Analytic Geometry
we write
(1)
(2)
Now let Q(qx, qy, qz), with position vector if = (qx, qy, qz), be another point in
space. The vectorPQ is then given by
qX - px)
PQ = ( qy - Py ,or more brießy, (3)
qz - pz
• Q - P
-"--_~O
: :
-------;- y
x
FIGURE 1. Cartesian coordinate system.
The vector PQ is called the difference between p and if. An arbitrary point X
with position vector x on the straight line PQ can then be given by the vector
equation
Programming Techniques
Let us now take a short break from theory in order to write a simple C function
that alIows us to calculate a point on the straight line PQ, depending on the
parameter ~.
First of alI, however, we want to say a few words about a programming technique
we will use for our graphics package.
The source code of a !arge program pa.cka.ge should be split up into several
modules maiD.c, file1.c, file2.c, fileS.c, etc. The file maiD.c contains the
mainO function.
The files will share several global variables. In general, we will try to avoid
global variables whenever possible. They malre large programs hard to maintain
and create the potential for confiicts between modules. On the other hand, such
variables may speed up the code.
Global variables will be capitalized throughout this book. Thus, they can easily
be distinguished from the local variables. We will put global variables into an
include file named Globals . h. This file can then be included in the C code by
writing
#include "Global •• h ll
All type definitions that are going to be used should be put into an include
file Types.h. The same is true for alI the macros that will be developed (-
Macros.h). Macro names will be capita1ized, too, so that we can distinguish
them from functions.
Finally, the function dec!arations ("function headers") should be written in the
include file Proto •h. Throughout this book we will use the standard C language
defined by Kemigham/Ritchie ("K&R standard") and not the more modem
ANSIl Standard for the C language.
Now the definitions are at our disposal whenever we need them.
For more convenience, we put alI the include 6.les we need for our graphics
packa.ge into a single include file 3cLstd. h, which may then look like this:
In this file the preprocessor now removes the word Global so that a1l the variables
are declared in the usual manner. In any other module, the variables are declared
extern.
This technique, however, does not allow us to initialize arrays. Thus, we write
#ifdef MAIN
Global 80at Va1'i [3] = {0.5, 1.5, -1 };
#else
Global 80at Va1'i [3];
#endif
SectiOD 1.1. Vectors 5
and write all the pointers to functions at the end of the File Proto . h. A typical
example is:
--- .
After this introduction we can finally write our first C function. We introduce
a new type of variable called Vector, which is meant to be an array of three
real numbers. These numbers may be the space coordinates of either a point or
a vector in our sense. C distinguishes between double precision (double) and
single precision (float). Since single precision should be sufficient for most cases,
we choose float so as not to waste any space.
I I
void point..onJine(result, p, q, lambda) (-+ Proto.h)
Vector result; /* The point to be calculated. */
Vector p, q; /* The vertices. */
float lambda; / * The parameter. */
{ /* begin point..onJineO */
register 2 ftoat one..minusJambda = 1 - lambda;
register short i;
for (i = 0; i < 3; i++)
result[i] = one_minusJambda * pli] + lambda * q[i];
I} /* end point..onJineO */
2 register varlables speed up the code. They are just a suggestion, however,
and whether the program really profits from them depends on the compiler that
is used.
6 Chapter 1. A Basic Course in Spatial Analytic Geometry
This would not be C if the same thing could not be written in quite a different
manner with the use of macros.
The use of macros helps to save time, considering that every call of a function
takes time to copy variables into the stack.
Another advantage is that with a macro the type of the variables is of no conse-
quence, which means that if you prefer to use long or double instead of 8oat,
it will work as weH. Thus, the C macro
#define Point-on_line(result, p, q, lambda)\ (~ Macros.h)
(result[O] = prO] + lambda * (q[O]- p[O]) , \
result[l] = p[l] + lambda * (q[l]- p[I]), \
result[2] = p[2] + lambda * (q[2]- p[2]))
has exactly the same effect as the previous function, with the only difference
being that, in most cases, it will work a little bit faster because the preprocessor
replaces every call of the macro by the corresponding code, without jumping into
a function. It will also be faster because the counting variable i does not exist
any longer. In addition to that, we use Equation 4 instead of Equation 5 because
in every line there is only one multiplication instead of two - a subtraction can
be done a bit faster! When we compared the calculation times, the results were
that the function point_on_lineO runs 30% to 60% more slowly than the macro
Point_on_lineO, depending on the computers and compilers that were used (see
Appendix A.4).
You may think that this obsession with speed is a little bit exaggerated, but
you always have to keep in mind that the programming of 3D-graphics has to
be extremely efficient in order to keep calculation times within limits. If we
take every opportunity to save a little bit of time, the program as a whole will
work much faster, which means that, in the end, we will be able to generate an
animated picture 20 times a second instead of just 12 times a second.
Macros can be quite tricky, though, which means that for the purposes of pointer
arithmetic it is advisable to write (result) instead of result, (P) instead of p and
(q) instead of q in the macro! The reason for this will be explained at the end
of Section 1.4. Furthermore, it is always a good idea to protect the whole macro
with parentheses (we will soon have an example of this).
(6)
Section 1.1. Vecton 7
If Ti is to be normal to another vector b88 well, its components have to fulfill the
additional condition n z bz + n" b" + nz bz = O. The so-called cross produ.ct
(7)
In general, three points P, Q, R determine aplane (Figure 2). Any other point X
on the plane can be expressed as a position vector X, which is a linear combination
of the vectors p, PQ and PR:
(8)
In order to eliminate the parameters ),1 and ),2 we simply multiply the vector
equation by the normal vector ii = PQ x PR,
so that by Equation 6 we get the
pammeter free (or implicit) equation 0/ a plane:
The constant c still depends on the length of the normal vector, which in turn
depends on the points P, Q and R. For every point T (position vector t) in space
r
that is not on the plane PQ R, we have ii # c. Two space points Tl and T 2 lie
on different sides of the plane if
The following nlllction calculates the normal vector and the constant of aplane
that is given by three points:
I I
void plane_CO'nstants(pqr, p, q, r) ( - Proto.h)
Plane *pqr; /* Pointer to the struct Plane. 4 */
Vector p, q, r;5 /* Three points on the plane. */
{ /* begin plane-COnBtantsO */
Vector PQ, pr; /* Düference vectors PQ and PR. */
register Vector *n = (Vector *) pqr->normalj
/* To speed. up the m&ero8. */
Subt_vec(pq, p, q)j Subt_vec(pr, p, r)j
Oross..prod:uct(*n, pq, pr)j
pqr->cnst = Dof-produd(*n, p)j
I} / * end plane...constantsO */
(11)
Now we can nofT1&alize every non-zero vector ii, which means that we scale the
vector so that its length is one:
~ 1 ~ (d..:/It!I) .
do = i f d = dv/I~I (12)
Idl d"Jldl
A C subroutine for the normalization of vectors might look like this:
#define EPS le - 7 (-+ Macros.h)
/ * This very small number is quite usefull */
#deftne Length(a) sqrt{Dot.product(a, a» (-+ Macros.h)
This time it turns out to be more efficient to use a function instead of a macro,
beeause now we ean use one register variable several times. You will have noticed
that within the nmetion we do not need to write (v) instead ofv. For the compiler,
the variable v is in fact apointer to a f1oat. The eomponents of the vector are
ehanged when the program leaves the subroutine (this would not have been the
ease if the variable had not been apointerl).
With the help of normalized veetors, we ean measure distanees on given lines.
For example, if we want to get a point R on the line PQ at a distanee d from P,
this point ean easily be expressed by the equation
(13)
where (PQ)o is obtained by normalizing PQ. Normalized vectors are also the
key to the measuring of angles in space. If we want to measure the angle a of
the two edges PQ and PR in Figure 2, we simply have to normalize the vectors
J = PQ and e = PR and ealeulate their dot product to get the eosine of the
angle 0::
Additionally, we have
though in most eases, we will prefer Formula 14 for reasons of speed. Every
graphies programmer should avoid trigonometrie funetions, like sines, if possible.
U nless you have an efficient mathematics eoproeessor, such functions use up much
more time than arithmetie operations. Fortunately, it is frequently possible to
avoid those funetions. An example is Lambert's eosine law, which says that the
brightness of a polygon (a face of the object we intend to ereate) mainly depends
on the eosine of the angle of incidenee of the light rays. Thus we will do the
following:
Before any drawings are begun, we determine and normalize the normal vectors
of all faces. The eosine of the angle of incidenee of a facet equals the dot produet
of the normalized normal vector and the normalized light ray.6 The angle itself,
however, does not have to be ealculated beeause of Lambert 's law. In this manner,
we are able to avoid the function arccosO for each facet of our scene.
6In fact, this is only true when the light rays are parallel. Otherwise the angle
of incidenee will be different for each point of the face. An "average angle of
incidenee" might be the angle of incidenee of the light ray through the baryeenter
of the polygon. This point ean be precaleulated before any drawings are begun.
Section 1.2. How to Measure Lengths and Angles 11
In order to determine the angle between a plane and a straight Une, we measure
the angle between the line and the normal of the plane and subtract the result
from 'Ir /2. The angle between two planes equals the angle between the normal
vectors of the planes.
I I
void sectJine_and_plane(s, lambda, parallel, a, ab, p) ( - Proto.h)
Vector Sj /* Intersection point. */
register float *lambdaj / * Parameter to intersection point. */
Bool*parallel j
Vector a, abj /* Point on line, direction of line. *f
Plane *Pj
{ /* begin sectJine_and_planeO */
register Vector *n = (Vector *) p->normalj
/ * To speed up the macro. */
float n_abj
n_ab = DoLproduct(*n, ab)j
if (Is..zero(n_ab)) {
/ * Either the line coincides with the plane or it is parallel to the plane.
*/
*parallel = TRUEj
if (Is..zero(Dot...product(*n, a) - p->cnst)) *lambda = Oj
else *lambda = INFINITEj
} else {
*parallel = FALSEj
*lambda = (p->cnst - DoLproduct(*n, a)) / n..abj
} /* end if (I s..zero()) */
Linear _comb( s, a, ab, *lambda) j
I} /* end sectJine..and_planeO */
For less experienced C programmers: if we want to have the variables lambda and
parallel at our disposal outside the function, we have to pass them as pointer
variables, because C "calls by value" (in PASCAL you would have to write the
keyword var before the variable in the argument list). A pointer variable ptr to
a float, for example, is declared by float *ptr. The pointer ptr itself is then the
address of the variable *ptr. The pointer can only be changed within a function,
whereas a change of its contents *ptr will also be effective outside of it!
When calculating lambda, we had to make sure that there was no division by
zero. It will occur when the direction of the line is parallel to the plane. In this
case, A is 00 or it is not defined, if the point on the line coincides with the plane. If
we now let A= INFINITE or A = 0, the "intersection point" will be somewhere
far out on the line or it will be the initial point itself, which is fine. Computer
scientists prefer unconventional solutions like this one, rather than having to
distinguish between several different cases. The Hag parallel is set additionally
in order to let us know when a special case has occurred.
Section 1.3. IntersectioDS of Lines and Planes 13
Next we need to know how to intersect two straight lines within a plane. The
two lines may have the parametrie equations
(17)
Now we intersect the lines by symbolically writing Xl = X2. When eis parallel
to the z-axis (ez = 0 and eil = 0) (and when clis not parallel to the z-axis), we
have
_ { b..;;,.a.. if d z =/; 0;
.x - b,-a,
d
th
0 erwlse.
. (18)
11
When eis not parallel to the z-axis we multiply the vector equation Xl = X2 by
the vector ne = (-eil' ez , 0), which is normal to e, according to Equation 6, and
we get
(19)
I I
void intersectJines(s, lambda, parallel, a, d, b, e, dim) (- Proto.h)
float s[]j
/ * Interseetion point. The reason why we use an array of ftoats instead of
the types Vector or Vector2 is because this function works without
any changes when the vectors a, b, d, e in the argument list are two-
dimensional! (Both Vector and Vector2 are interpreted as pointers to
a float.) The only difference between a 2D-version and a 3D-version is
in the ealculation of the intersection point. */
float *lambdaj
/ * Parameter A of the interseetion point of the lines
x=a+Ad=b+JLe. */
Bool *parallel;
float a[], d[], b[], e[]j
short dim; /* 2D or 3D. */
{ /* begin intersectJinesO */
Vector2 ne, ab;
float d_ne, ab_ne;
if (dirn == 3 && ParalleLz(e)) {
if ((*parallel = Parallel..z(d») {
if (a[X] == b[X] && arYl == b[Y]) *lambda = 0;
else *lambda = INFINITE; / * Lines are parallel. */
} else if (!Is..zero(d[X])) *lambda = (b[X]- a[X])/d[X];
else if (!Is..zero(d[Y])) *lambda = (b[Y]- a[Y])/d[Y];
e
} else {/ * is not parallel to the z-axis. */
/* Because of ne[Z] = 0, it is enough to make some
of the following caleulations only two-dimensionally. */
NormaLvec2(ne, e);
d_ne = DoLproduct2(d, ne);
SubLvec2(ab, a, b);
ab_ne = DoLproduct2(ab, ne)j
if ((*parallel = Is..zero(d_ne))) {
if (Is..zero(ab_ne)) *lambda = 0;
else *lambda = INFINITE;
} else { / * This is the general ease at last. */
*lambda = ab_ne/ d_nej
} /* end if (Is..zero(d_ne)) */
} /* end if (eparallel z)) */
if (dim == 2)
Linear _comb2(s, a, d, *lambda)j
else
Linear _comb(s, a, d, *lambda);
I} /* end intersectJinesO */
Seclion 1.3. Intersections of Lines and Planes 15
For some hidden line determinations (Section 7.2) we need another function
intersecLsegmentsO that is related to the function intersectJinesO. It cal-
culates both the parameters tl and t2 frorn the equation PI + t l {jh - pt} =
ih + t 2 (if2 - i/I) (and not hing else). When the segments are parallel (or identical)
the function returns FALBE, else it returns TRUE.
I I
Bool intersect...segments(tl, t2, pI, ql, p2, q2) (-+ Proto.h)
float *tl, *t2j
float pl[], ql[]j /* The first segment. */
float p2[], q2[]; /* The second segment. */
{ /* begin intersect...segmentsO */
Vector2 dirl, dir2, pIp2j
Vector2 nI, n2j
double detj
The intersection 0/ two planes can be achieved by the intersection of two lines of
the first plane with the second plane. Since we do not usually deal with infinite
16 Chapter 1. A Basic Course in Spatial Analytic Geometry
planes, but rather with polygons that span the plane, we want to solve an addi-
tional problem: given two convex polygons (usually triangles or quadrilaterals )
that intersect each other as in Figure 3, we look for the end points 8 1 and 8 2 of
the intersection, which is a straight line.
Provided that such a line of intersection exists, we will always be able to find
exactly two edges on the first polygon that intersect the plane of the second
one. The two intersection points may be called Tl, T2 • The line joining them is
represented by the parametric equation
(21)
(i = 1,2) (22)
and have the parameter values of the end points 8l, 8 2 of the actual line of
intersection.
Here is the corresponding C code:
8This gives us the chance to explain the different writings we can use when
we pass an array x as a function argument. The compiler will convert your
writing into the pointer *:1: in any case. Thus, it does not make any difference
whether you really pass apointer or the start address of an array. For better
reading, however, it is preferable to write x[ ] or, when the size of the array is
constant, x[size]. The compiler uses the size information only for bounds-checking
(provided that it supports that feature).
Section 1.3. Interseetions of Lines and Planes 17
{ /* begin secLpolysO */
register short i, j j
Plane plane1, plane2j
-
Vector t[2]j /* Tl. T 2 */
Vector tlt2j /* T l T 2 */
ft.oat lambda[2]j /* Parameters to section points. */
Vector p1p2j /* Difference vector. */
short side1, side2j /* Which side of the plane? */
short found = Oj
Bool parallelj
/ * First calculate the intersection points Tl, T 2 of
the first polygon with the plane of the second polygon. */
plane_canstants(&plane2 9, poly2[0] , poly2[1], poly2[2])j
side2 = Which--side(poly1[0], plane2)j
for (i = 0, j = 1j i < nlj i ++, j ++) {
if(j == nl) j =Oj
sidel = side2j
side2 = Which_side(polylf:i], plane2)j
if (sidei != side2) {
SubLvec(plp2, polyl[i], polyl[j])j
sect...line_and_plane(t[f ound], &lambda[found], ¶llel,
polyl[i], plp2, &plane2)j
if (lambda[found] >=0 && lambda [found] <=l)
if (++found == 2)
breakj
} /* end if (sidei) */
} /* end for (i) */
if (found < 2)
return FALSEj /* No line of intersection. */
/* Now we intersect the line T 1T2 with the edges of the second polygon. */
plane_canstants (&planel , polyl[O], poly1[1], polyl [2]) j
found = Oj
SubLvec(tlt2, t[O] , t[l])j
side2 = Which--side(poly2[0], planel)j
for (i =0, j = 1j i < n2j i++, j++) {
if (j = = n2) j = Oj
sidel = side2j
side2 = Which_side(poly2f:i], plane1)j
if (sidel!=side2) {
SubLvec(plp2, poly2[i] , poly2[j])j
intersect...lines(section, &lambda[foundJ, ¶llel,
t[O], tlt2, poly2[i], plp2, 3)j
/* Modify parameter. */
if (lambda(Jound] < 0)
lambda[found] = 0;
else if (lambda[found] > 1)
lambda[found] = 1;
if(++found ==2){
if (lambda[O] > lambda[l]) { /* Sort parameters. */
ftoat temp;10 /* Necessary in the Swap macro. */
Swap( lambda [0] , lambda [1]);
} /* end if (lambda) */
break;
} /* end if (Jound) */
} /* end if (sidei) */
} /* end for (i) */
if (lambda[l] - lambda[O] < EPS) /* Line too short. */
return FALSE;
/ * Determine the actual intersection points. */
Linear _comb( section[O] , t[O] , tlt2, lambda[O]);
Linear-COmb(section[l] , t[O] , tlt2, lambda[I]);
return TRUE;
I} /* end sect...polysO */
1.4 Translations
By adding a translation vector f to the position vector of aspace point we get
the position vector of the translated point. Usually, translations are applied to
hundreds or even thousands of points. Therefore, we want to have a subroutine
that is able to accomplish the task as quickly as possible. With the help of
pointer variables we can keep the coordinates of related points together in one
storage block, which we will call a ''pool.'' Let us have a look at the procedure
in question:
#deflne AdcLvec(result, a, b)\ (- Macros.h)
«(result)[X] = (a)[X] + (b)[X] , \
(result)[Y] = (a)[Yj + (b)[Yj, \
(result)[ZJ = (a)[ZJ + (b)[ZJ)
I • I
vOld translate_pool(n, pool, trans) ( - Proto.h)
short nj / * N umber of points to be translated. */
Vector *poolj /* Coordinate pool of arbitrary size. */
Vector transj /* Translation vector. */
{ /* begin translate_poolO */
register Vector *vec = pool, *hLvec = vec + nj
far ( j vec < hLvecj vec++)
AdlLvec(*vec, *vec, trans)j
I} /* end translate_poolO */
I ~"~I
values
~
(*vecJl21 *Ver/2)
FIGURE 4. The difference between the evaluations of (*vec)[2] and *vec[2), where
vec is apointer to a Veetor.
Additions with such variables are probably among the fastest things a computer
can do. There are no function calls, because Add_vec is a macro. If your computer
is able to work with more than two register variables and if you have huge pools
to transform, it may be worth declaring another variable
register Vector *t = (Vector *) trans;
Now write
for ( ; vec < hLvec; vec++)
AdcLvec(*vec, *vec, *t);
1.5 Matrices
Before we talk about rotations in space we will say a few words about matrix
calculus, which is a very convenient way of abbreviating long and confusing
calculations. In this context we will only speak of square 3 x 3-matrices. Such
a matrix is an array of nine numbers (called elements) in three rows and three
columns. The elements in a row may be interpreted aB a vector ("row vector").
Let A and B be two 3 x 3 matrices:
AB = C = (Cile) with Cile = a.o hole + a.l b11e + 0i2 ~Ie. (24)
(25)
Because of the special properties of a rotation matrix we get its inverse matrix
simply by transposing it:
I I
void vec..multJnatrix(result, v, a) (-+ Proto.h)
Vector resultj
Vector Vj
Rot..matrix aj
{ /* begin vec_mulLmatrixO */
register Vector *res = (Vector *) result, *vec = (Vector *) Vj
1.6 Rotations
A rotation of a point P about the z-axis by the angle (is done by the multipli-
cation of its position vector p by the rotation matrix
cos (
Z«() = ( -s~ ( sin (
008 {
o
°1 .
0)
(27)
In order to rotate P about the oriented x-axis by the angle ~, we multiply its
position vector by the matrix
This time the unit point Ey on the y-axis will coincide with the unit point
Ez(O, 0, 1) on the z-axis if we rotate it by ~ = 'Ir /2.
Finally, the rotation about the oriented y-axis by the angle 'T/ is achieved by a
multiplication of the position vector by
Note the signs of cos and sin in this case! As a result, E z will coincide with Ex
when we rotate it by 7] = 'Ir /2.
Now we want to rotate a point about an arbitrary axis 9 through the origin by
the angle cp. This can be done by a multiplication by a single general rotation
matrix R, the elements of which may be determined by the following geometrie
considerations (Figure 5):
Let the rotation axis 9 be the z-axis of a new Cartesian coordinate system ~g
with the three pairwise orthogonal unit vectors e, and g. f
For the unit vector g, we choose the normalized direction (gx, gy, 9z) of the axis
9. Next we choose the horizontal normal vector (- 9y, 9x, 0) of 9 and normalize
e
it. Thus, we have found a second unit vector = (ex, ey, ez ). (If 9x = 9y = 0, we
24 Chapter 1. A Basic Course in Spatial Analytic Geometry
let e = (1,0,0).) Finally, the unit vector f = (1:,)) 1'11' Iz) is the cross product of
the two vectors: f = g x e.
Consider a rotation matrix M that describes the rotation of .space so that the
axes of E g will coincide with the axes of the original system. By means of this
matrix the rotation about 9 is transformed into a rotation about the z-axis,
which we can already handle. The rotation back to the system E g is described
by the inverse matrix M-l. Thus, we have
(30)
The inverse matrix M- 1 can be determined easily. It is nothing but the rotation
matrix
(31)
To prove this, we simply have to apply the rotation described by M-l to the unit
vectors of the original system. The rotated vectOr8 are indeed e, f and g. Finally
we come to the general case of a rotation about an arbitrary axis. If ä is the
position vector to a point A on the axis, we first translate the points that have to
be rotated along the vector -a = AG; then we apply the rotation (Equation 30)
to the translated points, and finally, we translate the points back by means of
the vector a.
2
Projections
From the physical point of view, one-eyed seeing is nothing but projecting three-
dimensional objects onto a projection surface (projection plane) by means of a
lens (the camera lens or the lens of the eyeball). The brain is then more or less
capable of "reconstructing" the objects in space, Le., of estimating the distance
of the objects with the help of size comparisons or shadows. Misinterpretations
are reduced by two-eyed seeing, because it enables the brain to interseet eorre-
sponding projection rays in space.
Geometrists distinguish between central projections (where the projection center
is not at an infinite distance) and parallel projections. In reality, most projec-
tions are central projections ("perspeetives"). For technical drawings we will
mainly use "orthogonal projections" as a special ease of parallel projeetions.
Among these projections there are the "main views" (the top/bottom view, the
front/back view, the right-hand side/left-hand side view) and axonometrie views.
Such projections permit us to compare or even measure lengths and to determine
whether lines in space are parallel or not.
Since parallel projections can be interpreted as extreme cases of eentral projec-
tions, we will only talk about central projeetions in this book.
In this chapter we williearn how to deal with projeetions and how to submit the
process of illumination to the rules of perspeetive. Furthermore, we will develop
functions and macros that allow us to switch between different eoordinate sys-
tems as quickly as possible. Finally, we will develop C functions for the clipping
of lines and polygons.
26 Chapter 2. Projections
o
y
Thedistance d = IcTl is called the distance 0/ the perspective. This is the general
csse of a central projection. The camera itself may be rotated (''twisted'') about
the main projection ray by an angle T. (The angle T is measured with respect to
the horizontal normal vector to the projection ray.)
In order to make the image plane 7r become the base plane xy, we now transform
an the points that have to be projected so that
(1) the principal point T becomes the origin of the coordinate system,
(2) the projection center C becomes a point on the positive z-axis,
(3) the twist angle T is eliminated.
Figure 2 illustrates what we have to do. In order to fulfill condition (1), we
only have to apply a translation by the vector ro
to an the points and to the
projection center as well. To fulfill condition (2), first we rotate the translated
world system about the z-axis until the translated projection center ct (c x , Cy, c z )
lies on the yz-plane. According to Figure 2a, the rotation angle -'1 is given by
~ + arctan ~
c.,' .. >
if "roX O·,
_ { 2 _
(1)
'1 - ~ + (arctan ~ + 7r), otherwise.
Section 2.1. Central Projections 27
z
The translated (c) rotated about Zo
screen system is the z·axis by -0'.
-0'
-ß
y
FIGURE 2. A transformation of the projection center and the image plane.
28 Chapter 2. Projections
Now the image plane contains the x-axis. Thus, if secondly we rotate about the
x-axis by the angle -ß
the image plane will become the xy-plane (Figure 2b). In order to eliminate
the twist-angle (condition (3)), we finally rotate the transformed points about
the transformed principal ray, which coincides with the z-axis, by the angle - Q
(Figure 2c).
(3)
All three rotations can be performed in one step, which is described by the matrix
-=:j'
P =p
-+R. (5)
We have not yet projected the rotated points P to the image plane (i.e., the
xy-plane). The new projection center, which now lies on the z-axis, has the
coordinates C(O, 0, d). If we intersect the projection ray
-+ - (-+-+)
X=C+A15-C, (6)
d
A=--. (7)
d-pz
c ,- c ,-
Pz = "'Pz' Py = I\p y. (8)
The z-coordinate 15% of Pis also the oriented distance P7r of the original space
point P from the image plane 7r.
The three angles a, ß, l' can be interpreted as Euler angles [FOLE90]. First we
translate our objects so that the target point becomes the origin of the coordinate
system. If we then rotate our objects
Seetion 2.2. The Viewing Pyramid 29
This means that each perspective can be given by the equivalent parametriza-
tions 1
The "neutral" or "vanishing" plane v, which is parallel to the image plane and
which coincides with the center of the projection, divides the space into two
2'
FIGURE 4. The neutral plane and the (near) c1ipping plane. The distance ofthe near
clipping plane may have an infiuence on the appearance of the image polygons.
In some cases, it may also be useful to introduce a "far clipping plane K./'
(Figure 4). Objects that lie behind this plane are not drawn at all. Such aplane
should be used for animations of complex scenes, in which some objects are
temporarily far away from the observer . The computer would spend precious
calculation time on the display of the images of such objects, even though these
images are hardly more than pixel size.
In general, we will not display objects that are outside the "viewing pyramid" or
''viewing frustum," which is formed by the near and far clipping planes and by
those four clipping planes that are spanned by one side of the rectangular field
of view and by the projection center (Figure 3). In Section 2.5, we will develop
routines for the clipping of lines and polygons.
An important question is which rectangle to choose for mapping on the screen.
Figure 5 illustrates that it is a good idea to introduce a so-called "field-of-vision
angle" 00 < <p < 90°.
Section 2.2. The Viewing Pyramid 31
Because many people are used to thinking in terms of focal distances J, we give a
table of corresponding values, 2 valid for miniature cameras with a negative size
of 24mm x 36mm ([HECH74]).
1. The first system is the Cartesian "world system," in which the coordinates
of the vertices of our objects are given.
2. The second coordinate system is the non-Cartesian three-dimensional
"screen system." In this system, the coordinates of aspace point are the
ordinary screen coordinates of the image of the point (Equation 8), plus, as
a third dimension, the oriented distance z from the image plane 'Ir.
3. As a third type of coordinate system, we introduce the non-Cartesian "light
system," which is similar to the screen system. In such a system aspace
point can also be described by the two-dimensional coordinates of its shadow
(=projection) on an arbitrary image plane plus the distance of the point
from this image plane as a third coordinate.
w w h
u=-x
C
+-, v=-y
C
+-h (11)
Uo 2 Vo 2
will fulfill this task. If we are lucky, a polygon on the image plane 'Ir will then
appear undistorted on the screen. On many systems, however, the polygon will
appear distorted andj or be turned upside down. This can be avoided by intro-
ducing a system-dependent variable f2:
w C W (h C h)
u=-x +-2' V=f2 - y +-. (12)
Uo Vo 2
u v
Xo = ~' Yo = f2 h . (13)
Seetion 2.2. The Viewing PyraInid 33
We will need such normalized coordinates for plotter drawings or for the output
on other devices.
According to the field-of-vision angle in Figure 5, the values of the variables Uo
and Vo are:
(14)
The third dimension in the screen system (and in the light system) needs some
more explanation. We will work with two transformations Tl: P -+ P and
T 2 : P -+ P* of space, both of which will have the property that the normal
projection of the transformed scene on the projection plane 'Ir is identical with
the central projection of the scene from the projection center (light center).
The first transformation
is non-linear. It is defined by
is defined by
Pa: P'll pz
D = qa: q'll qz = O. (19)
Ta: T'II Tz
Section 2.3. Coordinate Systems 35
k>'pPz
k >'qqz = k >'p >'q >'r D = o. (20)
k >'rrz
dz*
z=--.
1 +z*
(22)
Regular points, i.e., points that are not in the forbidden halfspace, have z*-values
in the interval -1 < z* < 00. For two points P and Q of the same halfspace, we
have
(23)
CI
II, "\
, '.
P~P"
I I
void translate_warld_system( target) ( - Proto.h)
Vector target;
{ / * begin translate_world_systemO */
Vector tj /* The vector -farget */
short systj
Turn_vec(t, target);
translate_pool(TotaLsystems, Proj_center, t);
translate_pool(TotaLvertices, Coord_pool, t);
I} / * end translate_world_systemO */
38 Chapter 2. Projections
j j
void filLroLmatrix(r, angle, axis) (--t Proto.h)
Rot-Illatrix rj
double anglej
short axisj
{ /* begin filLroLmatrixO */
register ftoat s, Cj / * For reasons of speed. */
Section 2.4. Back and Forth Between Coordinate Systems 39
s = sin(angle); c = cos(angle);
switch(axis) {
case X:
r[O][O] = 1; r[O][l] = 0; r[0][2] = 0;
r[l][O] = 0; r[l][l] = C; r[1][2] = S;
r[2][0] = 0; r[2][1] = -Sj r[2][2] = c;
return;
case Y:
r[O][O] = c; r[O][l] = Oj r[0][2] = -s;
r[l][O] = 0; r[l][l] = 1j r[1][2] = 0;
r[2][0] = s; r[2][1] = Oj r[2][2] = c;
return;
case Z:
r[O][O] = c; r[O][l] = s; r[0][2] = Oj
r[l][O] = -s; r[l][l] = c; r[1][2] = 0;
r[2][0] = Oj r[2][1] = Oj r[2][2] = 1;
return;
}
I} / * end /ilLrot_matrixO */
I I
void calc_roLmatrix(syst) (-- Proto.h)
short systj
{ / * begin calc_roLmatrixO */
Rot-IllBtrix z-ßlpha, x..beta, z_gamma, ZXj
double alpha, beta, gammaj /* The Euler angles a, ß, "(. */
spherical..coords(&Dist[syst],
&Azim[syst], &Elev[syst], Proi_center[syst])j
alpha = Twist * (syst == SCREEN_SYST);
beta = PI/2 - Elev[syst];
gamma = PI/2 + Azim[syst]j
jilLroLmatrix(z-ßlpha, alpha, Z);
jilLroLmatrix(x_beta, beta, X)j
jilLroLmatrix(z_gamma, gamma, Z);
matrix_mult(zx, z..alpha, xJJeta)j
matrix_mult(InvRot[syst], zx, z_gamma)j
inverse_roLmatrix(Rot [syst] , I nvRot[syst]) j
I} / * end calc_rot_matrixO */
40 Chapter 2. Projections
I I
void inverse_roLmatrix(inv, mat) (---+ Proto.h)
Rot..matrix inv, matj
{ /* begin inverse_roLmatrixO */
register short i, j;
for (i = 0; i < 3; H+)
for (j = 0; j < 3; j++)
inv[i] [j] = mat[j][i];
I} /* end inverse_roLmatrixO */
I I
void world..to-Bcreen{screen, worlel} ( - Proto.h)
Vector screenj /* result */
Vector worldj /* The point to be transformed. *j
{ /* begin world_to-8creenO */
register ftoat lambda;
vec..mulLmatrix{screen, world, Rot[SCREEN_SYST])j
lambda = Scale_factorj(Dist[O] - screen[Z])j
screen[X] = X _mid + lambda * screen[X]j
screen[Y] = Y _mid + lambda * PixeLratio * screen[Y];
I} / * end world..to-8creenO *j
H we use a macro instead, we can speed up the process (an application of this
macro will be given in Section 3.1.)
#define Rotate-antLproject(scr,wld)\ (- Macros.h)
(V ec..mulLmatrix{*scr, *wld, *Rot), \
lambda = Scale_factorj{Dist[O] - (*scr)[Z]), \
{*scr)[X] = X_mid + lambda * (*scr)[X] , \
(*scr)[y] = Y..mid + PixeLratio * lambda * (*scr) [Y])
---. ---
The inverse function screen..to_worldO is needed only a few times. Therefore,
we are satisfied with the code
I I
void screen..to_world{world, screen) ( - Proto.h)
Vector world, screenj
{ / * begin screen..to_worldO */
register ftoat lambdaj
Vector rotatedj
lambda = Scale_factorj(Dist[O] - screen[Z])j
j* Undo projection */
rotated[X] = {screen[X] - X_mid)jlambdaj
rotated[Y] = (screen[Y] - Y _mid) / (PixeLratio * lambda);
rotated[Z] = screen[Z];
vec..mulLmatrix{world, rotated, InvRot[SCREEN..8YST])j
I} / * end screen..to_worldO */
42 Chapter 2. Projections
x
FIGURE 8. "World system" and "light system."
Let us now develop the equivalent functions (macros) for the light systems. This
time the projection center is the n-th light SOUTCe (given by Proj_center[n]).
We can identify the origin (Le., the former principal point) of the world system
with a point on the "principallight ray." The plane that contains the origin and
that is normal to the principallight ray is our image plane (Figure 8). The only
difference from the usual perspective is that we do not have to deal with twist
angles, because it does not make any difference whether or not we rotate the
"light bulb" about its ans. Instead of the function warld..to-BcreenO, we write
an equivalent function world.1oJightO, which uses a macro
#deflne Rotate_and_illuminate(shad, wld)\ (- Macros.h)
(V ec_mult-matrix{ *shad, *wld, rot), \
lambda = Dist[n] / (Dist[n] - (*shad)[Zj) , \
{*shad)[X] *= lambda, (*shad)[Y] *= lambda)
I
I void world_toJight{ shadow, world, n) (- Proto.h)
Vector shadow, world;
short n; / * The n-th light system. */
{ /* begin world_toJightO */
register ftoat *rot = (ßoat *) Rot[n];
register Vector
*shad = (Vector *) shadow,
*wld = (Vector *) warld;
ßoat lambda; /* Needed within the macro. */
Rotate_and_illuminate{shad, wld);
I} /* end world_toJightO */
Section 2.5. Clipping Algorithms 43
Sometimes we want to make a direct switch from the screen system to one of the
light systems and vice versa. For the time being, adetour over the world system
will help us to do so.
/1axy
---~=====~--- Hin y
Hin x I1axx
FIGURE 9. The nine regions of Cohen-Sutherland.
I I
Bool clip2d...line(exists, pO, qO, p, q) (-+ Proto.h)
Bool * existsj /* Is the line outside the clipping region? */
Vector pO, qOj /* The vertices of the clipped line. */
Vector p, qj / * The vertices of the line. */
{ /* begin clip2d...lineO */
register long xl = p[X], yl = p[Y] , x2 = q[X] , y2 = q[Y]j
char regl, reg2j
short temp, outside = Oj
Section 2.5. Clipping Algorithms 45
*exists = TRUEj
Region(regl, xl, yl)j
Region (reg2 , x2, y2)j
if (regi) ++outsidej
if (reg2) ++outsidej
if (!outside) return FALBEj
while (regl I reg2) {
if (regl & reg2) {/* Line outside window. */
* exists = FALBEj
return TRUEj
}
if (regl == 0) {
Bwap(regl, reg2)j
Bwap (xl, x2)j Swap(yl, y2)j
}
if (regl & RIGHT) {
yl + = «y2 - yl) * (Min...x - xl)) j(x2 - xl)j
xl = Min..xj
} else if (regl & LEFT) {
yl + = «y2 - yl) * (Max...x - xl)) j(x2 - xl)j
xl = Max.-Xj
} eIse if (regl & ABOVE){
xl + = «x2 - xl) * (Max_y - yl)) j(y2 - yl)j
yl = Max_yj
} else if (regl & BELOW) {
xl + = «x2 - xl) * (Min_y - yl)) j(y2 - yl)j
yl = Min_yj
}
Regian(regl, xl, yl)j
}
pO [X]= xlj pOlY] = ylj qO[X] = x2j qO[Y] = y2j
return TRUEj
I} /* end clip2dJ.ineO */
The clipping region must be initialized. This is done by means of the function
float Clip_vol[6]j (~Globals.h)
I
I void xy_region(xmin, xmax, ymin, ymax) (~ Proto.h)
float xmin, xmax, ymin, ymaXj
{ /* begin xy_regionO */
Max...x = Clip_vol[O] = xmaXj
Min...x = Clip_vol[l] = xminj
Max_y = Clip_vol[2] = ymaXj
Min_y = Clip_vol[3] = yminj
I} /* end xy_regionO *j
46 Chapter 2. Projections
I I
void clipJLnd_ploUine(p, q) (~ Proto.h)
Vector p, q;
{ /* begin clip_and_plotJineO */
Vector pO, qO;
Bool exists;
if (!clipJine(&exists, pO, qO, p, q»
drawJine(p, q);
else if (exists )
drawJine(pO, qO);
} / * end clip....and_plotJineO */
I
I I
void z..region(far, near) (-+ Proto.h)
ftoat Jar, nearj
{ /* begin z..regionO */
Max-z = Clip_vol[4] = nearj
Min-z = Clip_vol[5] = Jarj
I} / * end z..regionO */
Then all the points behind thefar clippingplane 1\:/ : Z = -O.8d/{1-0.8) = -4d
and all the points in front of the near clipping plane l'On : Z = 4d/{1 + 4) = O.8d
are outside the clipping volume (Formula (22». The clipping with 1\:71. has to be
done beJore the clipping with the drawing area: if one vertex of an edge is in
the ''forbidden halfspace," its image point may weIl be inside the drawing area.
After clipping with Kn, however, the vertex is replaced by a point, the image of
which can be outside the drawing area.
The interpolation of the intersection point of the line with the clipping planes is
done by means of the function
48 Chapter 2. Projections
I I
void intpol(r, a, b, i) (-t Proto.h)
register ftoat * rj /* The interpolated result. */
register ftoat * a, *bj /* The two end points. */
register short ij /* Information about the order of x, y, z. */
{ /* begin intpolO */
statie ehar e[6] [3] =
{ X, Y, Z, X, Y, Z, Y, Z, X, Y, Z, X, Z, X, Y, Z, X, Y }j
ehar cl, e2, e3j
ftoat tj
Finally, the code for the three-dimensional clipping looks like this:
#define PoinLregion(reg, p) {\ (- Macros.h)
x = (P)[X]j Y = (P)[Y]j Z = (P)[Z]j \
Region3d(reg, x, y, z)j \
}
#define Copy_vec(r, v)\ (- Macros.h)
(r)[X] = (v)[X], (r)[Y) = (v)[Y], (r)[Z) = (v)[Z])
#define Copy_vec2(r, v)\ (- Macros.h)
(r)[X] = (v)[X], (r)[Y] = (v)[Y])
I I
Bool clip3dJine(exists, pO, qO, p, q) (-t Proto.h)
Bool * existsj /* Is the line outside the clipping volume? */
Veetor pO, qOj /* The vertices ofthe clipped line. */
Vector p, qj / * The vertices of the line. */
{ /* begin clip3d_lineO */
register long x, Yj
register short ij
ftoat Zj
PoinLregion(Reg[O], p)j
PoinLregion(Reg[l], q)j
Section 2.5. Clipping Algorithms 49
I I
Bool clip2d_polygan(nO, polyO, n, poly) (-+ Proto. h)
short * nO; /* Size of final polygon. */
Vector polyO[]; /* Final polygon. */
short n; / * Number of vertices. */
float poly[ ]; /* Array of Vector2s or Vectors. */
{ /* begin clip2d_polyganO */
static float additianal[8 * sizeof(Vector)], *add;
/* A maximum of eight intersection points plus apointer. */
register char * reg = Reg;
char * hLreg = reg + n, code;
register short x, y;
register float * p = poly, *q;
short edge, m, cl, c2;
float **r;
float **ppoly; /* Pointer to Tmpl or Tmp2. */
float **pp, **hLpp; /* Pointers into ppoly[]. */
float tj
short cuLo f f = 0;
Section 2.5. Clipping AlgorithDls 51
cuLoff = 0;
for (; reg < hLreg; reg++, p += Dim) {
x = p[X] , Y = p[Y];
Regian(*reg, x, y);
if (*reg! = 0)
++cuLoff;
} /* end for (reg) */
* reg = Reg[O];
if (cuLoff == 0)
return FALBE;
else if (cuLof f = = n) { / * Pol. might be outside of window. * /
for (edge = 0; edge < 4; edge++) {
code = Clip_reg[edge];
for (reg = Reg; reg < hLreg; reg++)
if (!(*reg & code)) break;
if (reg = = hLreg) { / * Polygon is outside of window. */
*nO = 0;
return TRUE;
} /* end if (reg) */
} /* end for (edge) */
} /* end if (cut-off) */
p = poly; *nO = n;
/ * Assign pointers to the vertices of the polygon. */
hLpp = (pp = ppoly = Tmpl) + n;
for (; pp < hLpp; pp++, p += Dim)
*pp = p;
cl = X; c2 = Y;
add = additional;
for (edge = 0; edge < 4; edge++) {
code = Clip_reg[edge];
if (edge = = 2)
cl = Y, c2 = X;
for (hLreg = (reg = Reg) + *nO; reg < hLreg; reg++)
if (*reg & code) break;
if (reg < hLreg) {
m = Clip_vol[edge];
hLpp = (pp = ppoly) + mO;
* hi_pp = *pp;
r = ppoly = (ppoly == Tmpl ? Tmp2 Tmpl);
reg = Reg;
while (pp < hLpp) {
P = *pp++; q = *pp;
if (*reg++ & code) {
if (!( *reg & code)) {
* r++ = add;
add[cl] = m;
52 Chapter 2. Projections
For example, if we plot the faces of an object that is not to be clipped at all, it
is a waste of time to do the 3d-elipping for each face.
54 Chapter 2. Projections
I I
Bool clip3d_polygon(nO, polyO, n, poly) (~ Proto .h)
short * nO; / * Size of final polygon. */
Veetor polyO[]; /* Final polygon. */
short n; / * N umber of vertices. */
float poly[ ]; /* Array of Veetors. */
{ /* begin clip3d_polygonO */
statie float additional[12 * sizeof(Veetor)], *add;
/* A maximum of 12 intersection points plus apointer. */
register ehar * reg = Reg;
char * hLreg = reg + n, code;
register short x, y;
float Z;
register float * p = poly, *q;
short i;
float **r;
float **ppoly; /* Points to Tmpl or Tmp2. */
float **pp, **hLpp; /* Pointers into ppoly[]. */
short cuLo f f = 0;
for (; reg < hLreg; reg++, p += Dim) {
PoinLregion(*reg, p);
if (*reg! = 0)
++cuLoff;
} /* end for (reg) */
* reg = Reg[O];
if (euLoff == 0)
return FALSE;
else if (cuLo f f = = n) { / * Pol. might be outside of box. */
for (i = 0; i < 6; i++) {
code = Clip_reg[i];
for (reg = Reg; reg < hi_reg; reg++)
if (!(*reg & code)) break;
if (reg = = hLreg) { / * Polygon is outside of box. */
* nO = 0;
return TRUE;
} /* end if (reg) */
} /* end for (i) */
} /* end if (cuLof f) */
p = poly; *nO = n;
/* Assign pointers to the vertices of the polygon. */
hLpp =
(pp = ppoly = Tmpl) + n;
for (; pp < hi_pp; pp++, p+ = Dim)
* pp = p;
add = additional;
for (i = 5; i> = 0; i--) {
code = Clip_reg[i];
Section 2.5. Clipping Algorithms 55
\
/'
FIGURE 11. 3d-clipping can be done in world coordinates (left side) or it can be
applied to the linearily transformed scene (right side). In the latter case the projection
is parallel orthographie and the viewing frust um is a parallelepiped ("box").
56 Chapter 2. Projections
Figure 11 illustrates how 3d-clipping works: on the left side, the viewing frustum
was used to clip the scene, and on theright side, the whole scene was transformed
by means of the linear transformation T 2 ).
3
How to Describe
Three-Dimensional Objects
Thus, it will be shown how data files like the following can be read and inter-
preted.
The first thing we have to think of is how to organize memory. Arrays of pointers
are extremely useful if we want to stay within certain storage limits (they also
accelerate the program). In principle, we can say that we want to store every
piece of information only once and that if we need this information elsewhere,
we just store the address at which we can find it.
I
I void sa/e_exit(message) ( - Proto.h)
char *messagej
{ /* begin safe_exitO */
Print(message)j
if (Window_opened)
G..close_graphicsOj /* This will be explained in Section 4.2. */
exit(OL)j
I} /* end sa/e_exitO */
I
I char * mem_realloc(origAddress, n, size, name) ( - Proto.h)
char *arig....addressj /* The original address of memory. */
long nj /* Number of elements of the array. */
short sizej /* Size of each element in bytes. */
char *namej /* Name of variable (array) we want to allocate. */
{ /* begin mem_reallocO */
char *new....addressj
new..address = (char *) realloc(arig_address, n * size)j
/* System-dependent!!! See memAllocO. */
if (newAddress!= NULL) return new....addressj
else {
Print("Cannot reallocate ")j
sa/e_exit(name)j
} /* end if */
I
} 1* end mem_reallocO */
60 Chapter 3. How to Describe Three-Dimensional Objects
In our object preprocessor, there are a few arrays, the size of which is unknown
before allocation: Coord....poo~ Edge..pool, Face-1JOOl, Object..pool. Before any
data are read, we allocate a reasonable amount of storage to the world coor-
dinates of all points, edges, faces and objects:
#deflne MAX_POINTS4000 , (_ Maeros.h)
1* The number 4000 is just a suggestion. H you have enough RAM, take
a higher number. Since a vector consists of three ftoats (sizeof{ftoat)
= 4 bytes), 4000 vectors need 48 Kbytes of RAM-memory. The pool is
reallocated as soon as we know the precise number of points so that we
do not waste any memory! *1
#deflne MAX.-EDGES 6000 (- Maeros .h)
1* Takes 48 Kbytes, because for each edge we need two pointers to vectors
of the size of 4 bytes each. *1
#deflne MAX_FACES2500 (- Maeros.h)
1* The definition of the structure Face is given in Section 3.2. Because
sizeof(Face) ~ 40,2500 faces need 100 Kbytes. *1
#deflne MAX_UBYTE (IL« (8uizeof{Ubyte») (- Maeros.h)
1* This is 1 « 8 = 255 or 1 « 16 = 65535. 1 *1
typedef unsigned char Ubytej ( - Types.h)
1* 0... MAX_UBYTE. *1
/ * The only reason why we introduce the type Ubyte is because it helps to save
space (we will use more-dimensional arrays consisting of elements of this
type). If you have a computer with enough RAM-memory, you can define
Ubyte as a sbort. Then you will have no restrictions as to the number of
objects (polyhedra) you want to display. */
IThe C preprocessor will replace the constant by the result of the shift oper-
ation every time it appears in the code.
Section 3.1. Data Pools 61
(-+ Globals.h)
/* This is apointer that indicates where we are in Coord_pool while we
read from the data file. It is initialized when we allocate the coordinate
pool. Whenever we store the world coordinates of a point of the scene
(including points like barycenters, etc.), we increase Cur_coord by one.
*/
[oord_pool
fdge..pool
i .... C\f ! e:;
: ~
. ~ ~: ~
a1
FIGURE 1. Edge..pool is an array of pointers into Coord_pool.
2Pointers into pools will often begin with the prefix "cur_" (which stands for
"current" or, ifyou want, for "cursor"). This indicates that they will usually run
through loops. The upper limit of such a loop will frequently also be apointer
of the same type, the name of which will usually have the prefix "hL"
62 Chapter 3. How to Describe Three-Dimensional Objects
After having stored all the information, we make use of the scaling nature of
pointers to ealculate the actual sizes of the pools:
short Totaledges, Total/aces, TotaLobjectsj (-+ Globals.h)
/* TotaLvertices WaB defined in Chapter 2. */
TotaLvertices = Cur.1XJO'T"d - CooTlLpoolj
Total_edges = Cur_egde - Edge...:poolj
TotaL/aces = Cur_/ace - Face...:poolj
TotaLobjects = Cur_object - Object..poolj
Now we ean realloeate the pools:
#deflne Realloc-.array(type, array, n, string) \ (-+ Macros . h)
array = (type *) mem_realloc{array, n, sizeof(type) , string)
#deflne Realloc_ptr_array( type, p-.arr, n, string) \ (-+ Macros . h)
p_arr = (type **) mem_realloc(p-D.rr, n, sizeof(type *), string)
SIf you do not cast the function mem.allocO, you will at least get a warning
message from the compiler.
Section 3.1. Data Pools 63
The n-th point of the scene is given by *( CoonLpool + n) in the world system,
by *(Screen..pool + n) in the screen system and by *(Light..pool[k] + n) in the
k-th light system.
Provided that the n-th vector in the screen pool really contains the screen coor-
dinates of the n-th vector of the coordinate pool, we can easily switch between
the coordinate systems by means of macros:
#deflne ScreerLcoords( warld) \ (--+ Maeros .h)
(Screen..pool + (world - CoorrLpooQ)
#deflne Light_coords(n, world) \ (--+ MaerOB. h)
(Light..pool[n] + (world - Coord_pooQ)
Since we have identified the zeroth light system with the screen system, we can
switch directly between these systems by means of the macro
#define Switch_syst( old....syst, new_syst, v) \ (--+ MaerOB •h)
. (Light-pool[new....syst] + (v - LighLpool[old....syst]))
In order to get the corresponding screen coordinates of a Vector v in the n-th
light system, we now write
v = Switch....syst(n, SCREEN_SYST, v)j
The following function shows how we can "fill the screen pool" very efficiently
in the desired manner:
64 Chapter 3. How to Describe Three-Dimensional Objects
I I
void filLscreen-poolO (--+ Proto. h)
{ /* begin filLscreen..poolO */
register Vector
*world = Coord_pool, /* Pointer into coordinate pool. */
*screen = Screen_pool, /* Pointer into screen pool. */
*hLworld = warld + TotaLvertices;
register float lambda; /* Used within macro. */
drawJine = quickJine;
/* As long as no point is in the "forbidden halfspace,"
we do not have to think about clipping. */
for (; world < hLwarld; world++, screen++) {
Rotate-lLnd-PToject(screen, world);
if «*screen)[Z] >= Dist[SCREEN_SYS'I])
drawJine = clip3dJine;
}
} /* end filLscreen-POOlO */
I
Note that the function does not call any other functions and that it works almost
exclusively with register variables.
The "n-th lightpool" can be filled as quickly as the screen pool:
I I
void filUight-POO1(n) (--+ Proto.h)
short n; / * Index of light system. */
{ /* begin fillJight-POO10 */
register Vector
*world = CoorcLpool, /* Pointer into coordinate pool. */
*shadow = (Vector *) LighLpool[n], /* Ptr into light pool. */
*hLworld = warld + TotaLvertices;
register float *rot = (float *) Rot[n], lambda; /* Used within macro. */
for ( ; world < hLwarld; world++)
Rotate-lLnd_illuminate(shadow, world);
I} /* end filUight-POOlO */
Another example of the scaling nature of pointers may be the following task: you
want to get a list of the names of an the objects.
Polyhedron * obj = Object-POOl, *hLobj = obj + TotaLobjects;
while (obj < hLobj)
fprintf( Output, "%s\n", obj++->name);
Section 3.1. Data Pools 65
Several other pools like the "color pool," the "normal pool," the "mirror pool,"
the "contour pool" and so forth will be declared later on.
The macro can be very tricky if it is written like this. For example, if we write
for (j = Oj j < jmaxj j++)
Alloc.2d_array( ... )j
we will not get a syntax error, but only the first statement of the macro will be
in the loop! Especially when we deal with memory functions, this is an error that
cannot be traced easily and that will cause the program to crash. For this reason,
we rewrite the macro by taking advantage of the fact that, in C, we can write
whole sections of the code as macros, if we only write the code inside braces:
#define Alloc_2d_array(type, array, n, m) {\ (- t Macros. h)
array = (type **) mem_alloc(n, sizeof(type *), "pointers")j\
array[O] = (type *) mem...alloc(n, m * sizeof(type), 12d-arraY ")j\
for (i = 1; i < nj i++)\
array[i] = array[i - 1] + mj\
}
66 Chapter 3. How to Describe Three-Dimensional Objects
4We would prefer to simply write Face **neighbor_faces.- However, this is not
possible, because the compiler does not yet know about the structure Face.
Section 3.2. The 'Polyhedron' and 'Face' Structures 67
5 A "palette" in our sense is achart of different shades of one and the same
color. Therefore, the constant PALETTE_SIZEmeans the number ofthe different
shades of one single color.
68 Chapter 3. How to Describe Three-Dimensional Objects
The structure looks rather complicated by now. In the following chapters, how-
ever, we will still add some more structure members.
12
x 2
FIGURE 2. A simple object with convex outline.
If we want to describe a simple object like the one in Figure 2, we have to give
the computer the following facts:
In this case it is not absolutely necessary to give the computer the edge list.
When each edge is defined as the side of a face, the computer can take them
from the face list.
3. The polyhedron has 8 faces:
(PIP2P3P4P5P6), (P1P2 PS P7), (P2P3 P9 PS ) , (P3P4PlO P9 ),
(P4PS Pll PlO ), (P5P6P12PU), (P6P1P7P12), (PlOPllP12P7)
All this can be achieved by the data file in the introduction of this chapter.
The first Une of the data file contains some keywords that inform the object
preprocessor about the kind of object we deal with, its color and its physical and
geometrical properties.
Of course, it is tedious to count all the vertices, edges and faces. However, if we
do not do that it will be more difficult to write a function that interprets the
last three lines correctly.
Let us first write a function by means of which we can safely open a file:
I I
FILE * safe_open (file_name, mode) (-+ Proto. h)
char *file_name, *mode;
{ /* begin safe_openO */
FILE *fp;
if «(fp = fopen(file_name, mode» = = NULL) {
Print("Cannot open file ");
safe_exit (file-Ilame);
} /* end if (Ifp) */
return fp;
I} /* end safe_openO */
I I
void check.keywCYI'd(keywd, obj) (- Proto.h)
char *keywdj
/* We check whether the word we read is this word. */
Polyhedron *objj /* The object we read. */
{ / * begin check.keywordO */
Fread..str( Gumment);
if (strcmp(Gumment, keywd)) {
jprintj(Output, "Error while reading object Y.s:\n", obj->name)j
jprintj( Output, "keyword '%s' \n", keywd)j
saje-exit("expected")j
} /* end if (strcmp) */
I} / * end check.keywordO */
I I
void read_general..convex_polyhedron(obj) (- Proto.h)
Polyhedron *objj /* The object we want to read now. */
{ /* begin read_general..fxYTw€X_polyhedronO */
register short * indicesj
/ * This large array will be allocated and freed within the function. For
the time being, it stores the indices of the vertices of all the faces of
the polyhedron. */
Fread..str( Gomment) j
check.keywCYI'd("vertices", obj)j
obj ->vertices = Gur...JXXYrdj
/* Read coordinates of vertices until there is no commma left. */
do {
Fread_vec( *Gur_coord)j Gur..coord++j
/* Do not write Fread_vec(*Gur.-eoord++)j (macro!) */
Fread..str( Gomment);
} while (Gomment[O] == ',')j
obj ->no..oj _vertices = Gur_COCYI'd - obj ->verticesj
/* Read edges. */
72 Chapter 3. How to Describe Three-Dimensional Objects
The subroutine calls a function read_pos_intO, which is quite useful for the
detection of errors in the data file:
I I
int read..pos_intO (- Proto.h)
{
int n;
Iscanl(Input,lY.d", &n);
if (n >= 0 . && n < 32000)
return n;
else {
Itoa 6 ((double ) n, Comment, 0, 1);
Print("Positive integer expected instead of ");
sale_exit( Comment);
} /* end if (n) */
I} /* end read_pos_intO */
When the edge list is not given, the computer looks for all the edges that any
two faces have in common:
I I
void geLedges_Irom_Iacelist(obj) (- Proto.h)
Polyhedron * obj;
{ /* begin geLedges_Irom_IacelistO */
Face * 11, */2, *hi_1 = obj->Iaces + obj->no_ol_laces;
Vector * pI, *p2;
short il, i2; /* In this case only dummies. */
obj->edges = Cur_edge; obj->no_ol_edges = 0;
for (/1 = obj -> laces ; 11 < hLI - 1; 11++)
for (f2 = 11 + 1 ; 12 < hL/; 12++)
if (common_edge(&pl, &p2, &il, &i2, 11, 12))
* Cur_edge++ = pI, *Cur_edge++ = p2;
obj->no_ol_edges = (Cur_edge - obj->edges) /2;
I} /* end get_edges_Irom_IacelistO */
The function common_edgeO is useful for several purposes, e.g., for the detection
of contour polygons:
I I
Bool common_edge( a, b, idx_a, idx_b, 11, /2) (---t Proto. h)
Vector **a, **b; /* Vertices of the edge. */
short * idx_a, *idx_b; /* Indices of a, b in the fl-list. */
Face * 11, *12; /* The two faces. */
{ /* begin common_edgeO */
register Vector **curl, **cur2, **hil, **hi2, **lol, **l02;
hil (lol = 1l->vertices) + 1l->no_o/ _vertices;
hi2 = (l02 = /2->vertices) + 12->no_o/ _vertices;
*a = NULL;
for (curl = lol; curl < hili cur1++)
for (cur2 = l02; cur2 < hi2; cur2++)
if (*curl == *cur2) {
if (*a == NULL) {
* a = *cur1; *idx_a = curl -lol; break;
} else {
* b = *curl; *idx_b = curl -lol; return TRUE;
} /* end if (*a) */
} /* end if (*curl) */
return F ALSE;
I} /* end cammon_edgeO */
ean be read and interpreted eorrectly. We will store a face list and an edge list
for the respective convex polyhedron (Figure 3a). In addition to that, we will
also work with data lines like
CYLINDER orange revolution hollow +bottom
meridian 2.5 0 -5 , 2.5 0 5 order 40
1 ! z
....
! ..........,......,.,!"O
l
11 I1
i i l
11
111
11:
11i
I
x
!illi
Ll.!..I.:.u....t...!....U...l.l..l~_ i
li 11 !!11
x .11
I I
Bool is_an_axis(p) (~ Proto.h)
Veetor *p;
{ /* begin is_on_axisO */
return ((*p) [X] == 0 && (*p)[Y] 0) ? TRUE FA LSE;
} / * end is_an_axisO */
I
76 Chapter 3. How to Describe Three-Dimensional Objects
mm.------------ -
ij 1-------------
~::::::::::::- -
111. \\, - - - - - - - - - - - - -
~ \ l\\~ - - - - - - - - - - -
~tt:tj±titi/2!/,f. ______ _
FIGURE 4. How to split a non-convex surface of revolution in order to speed up the
rendering algorithms.
can decide whether this point is on the rotation axis (= z-axis). Another func-
tion corT _vertexO helps to determine the pointer to avertex of the polyhedron,
when the order and the index of the corresponding point on the meridian are
given:
I I
Vector * corr _vertex (index , order) (-+ Proto .h)
short index, order;
{ /* begin corr.vertexO */
register short i;
register Vector * p = Ou.r-Object->vertices;
Now we store the vertex list of the polyhedron. First we read an the points on the
meridian. When the polyhedron is not convex, the computer checks whether we
want to add the top face or the bottom face. Then we ca1culate all the vertices
and store them in CoorrLpool. Finally, we store the edge list and the vertex list.
Section 3.4. Surfaces of Revolution 77
I
I void musLbeJceyword(keywd, obj) (-t Proto .h)
char * keywd;
Polyhedron * obj;
{ /* begin musLbe..keywordO */
if (strcmp(keywd, Comment)) {
fprintf(Output, "Reading obj %8:\n", obj->name);
fprintf(Output, "keyword %s expected instead of %8\n" ,
keywd, Comment);
safe_exit(" ,,);
} /* end if (strcmp) */
I} /* end musLbeJceywordO */
I
I void read_convex_obj_of _rev(obj) (-t Proto.h)
Polyhedron * obj;
{ /* begin read_convex_obj_of_revO */
short order, meridian_size;
short i, j;
ftoat sine, eosine, rot-.angle = 2 * PI;
double angle;
ftoat x, y, z, tmp;
Vector * meridian;
Bool bottom, top;
/* Read meridian. */
Fread.-Str( C omment);
if (!strcmp(Comment, "rot_angle")) {
rot....angle = PI/180 * read_floatO;
Fread-Btr(Comment);
}
top = bottom = Is_convex(obj);
78 Chapter 3. How to Describe Thre&-Dimensional Objects
if (Comment[O] == '+') {
if (lstrcmp(Comment, "+top")) top = TRUE;
else if (lstrcmp(Comment, "+bottOlll")) bottom = TRUEj
else safe-.exit("\n+top or +bottOlll e%pected")j
Fread..str( Comment)j
} /* end if (Comment[O]) */
must..be..keyword("meridian", obj)j
Alloc...array(Vector, meridian, MAX..MERIDIAN, "meridian")j
for (i = Oj i< MAX_MERIDIANj H+) {
Fread_vec(meridian[i])j
Fread..str( Comment) j
if (Comment[O] I = ',') breakj
} /* end far (i) */
if (i = = MAX..MERIDIAN)
safe.-exit("Mer1d1an too luge")j
meridian..size = i + 1j
must..be..keyword("Order", obj)j
order = read..pos_intOj
obj->vertices = Our....courdj
ü (rot...angle < 6.28) angle = rot..angle /(order - l)j
else angle = rot..angle / orderj
sine = sin(angle)j eosine = eos(angle)j
for (i = Oj i < meridian-sizej H+) {
x = meridian[i](X]; y = meridian[i](Y]j z = meridian[i](Zlj
if (x == 0 && y == 0) {/* Point on the axis. */
Store..coords(O, 0, z)j
} else {
for (j = Oj j < orderj j++) {
Store..coords(x, y, z)j
tmp = eosine * x - sine * Yj
Y = sine * x + eosine * Yj
x = tmpj
} /* end for (j) */
} /* end if (x) */
} /* end far (i) */
obj ->no..of _vertices = Cur....courd - obj ->verticesj
Fread-str( Comment)j
trivial..edgeJist( obj, meridian-size, order)j
triviaLfaceJist(obj, meridian-size, order, bottom, top)j
obj-::;>no..of.-edges = (Cur..edge -obj->edges) /2j
Free...array( meridian, "meridian") j
I} /* end read..convex..obj_of _revO */
The edge list of the polyhedron is "trivial," Le., we can determine all the edges
when we know the size of the meridian and the order.
Section 3.4. Surfaces of Revolution 79
I I
void triviaLedge.list(obj, size, order) (----> Proto.h)
Polyhedron * obj;
short size, order;
{ /* begin triviaLedge.listO */
Vector * p, *q;
short i, j, index, other _index;
obj->edges = Cur_edge;
for (index = 1; index <= size; index++) {
p = corr _vertex( index, order);
for (j = 0; j < 2; j++) {
if (j == O){
if (index = = size && is_on_axis(p» return;
other _index -index;
} else {
other _index = index + 1;
if (index = = size) return ;
} /*endif(j==O)*/
if (other _index > 0) {
q = corr _vertex(other _index, order);
for (i = 0; i< order; i++) {
if (is_on_axis(p» * Cur_edge++ = p;
else * Cur_edge++ = p + i;
if (is-.On.-axis(q» * Cur_edge++ = q;
else * Cur_edge++ = q + i;
} /* end for (i) */
} else {
q = corr _vertex( -other _index, order);
for (i = 0; i< order; i++) {
if (is_on.-axis(p» * Cur_edge++ = p;
else * Cur_edge++ = p + i;
if (is_on_axis(q» * Cur_edge++ = q;
else {
if (i< order - 1) * Cur_edge++ = q + i + 1;
else * Cur_edge++ = q;
} /* end if (is_on_axis) */
} /* end for (i) */
} / * end if (othedndex) */
} /* end for (j) */
} /* end for (index) */
} /* end triviaLedge.listO */
I
The same is true for the face list. In general, an approximation to a surface of
revolution has n = (size-l)*order faces with four vertices each plus the bottom
face and the top face with order vertices. (When a point on the meridian lies on
the rotation axis, the quadrilaterals degenerate into triangles.)
80 Chapter 3. How to Describe Three-Dimensional Objects
Now we can write a subroutine that stores the trivial face list.
I I
void triviaLfaceJist(obj, size, order, bottom, top) ( - Proto.h)
Polyhedron * obj;
short size, order;
Bool bottam, top;
{ /* begin triviaLfaceJistO */
short n, j, index;
Vector * corner;
short vI, v2, v3, v4;
statie Veetor **space;9
n = (size - 1) * order + 2;
obj->no_of_faces = n;
obj -> faces = Cur_face;
vI = 0; v2 = 1; v3 = 1 + order; v4 = order;
Alloc_ptr_array(Vector, space, 4 *n+2*order, "TRI");
/* Space for all the quadrilaterals (triangles) on the surface. */
The top face and the bottorn face are stored by rneans of the function
I I
void seL!U, corner, n, arientation, space) (-. Proto.h)
Face * f;
register Vector * corner;
short n;
short orientatian;
/ * Seen frorn the outside of the polyhedron the face has to be oriented
ccw. */
Vector *** spacej
/* Pointer to **space. This is because we want to increase **space. */
{ /* begin seL!O */
register Vector **v;
register short i = 0, j = n - 1;
82 Chapter 3. How to Describe Three-Dimensional Objects
v = f ->vertices = *space;
f ->no_of _vertices = n;
if (orientation > 0)
while (i < n) v[i) corner + i++;
else
while (i < n) v[i++) = corner + j--;
(*space) += n;
I} /* end seLfO */
When we do not want the top face (bottom face) to be drawn, we indicate this
by assigning the color NO_COLOR to the respective face. However, the face is
stored and proceeded like any other face of the polyhedron, firstly, because it is
possible to quickly deterrnine the outline of the object by rneans of "frontfaces"
and "backfaces" (Section 6.1), and secondly, because we can apply a fast hidden-
line rernoval (Section 7.2).
I I
void read..convexJJbj _0 f _transl (obj) (- Proto.h)
Polyhedron * obj j
{ /* begin read_convex-Dbj_of _translO */
short i, sizej
Veetor transj
ftoat x,yj
Veetor * p, *q, *start, *corner, *hLcoordj
statie Veetor **spacej /* Space for all the pointers. */
Bool top, bottamj
/ * Read base points until no comma is left. */
obj ->vertices = Cur_coordj
if (obj->type == TRANSLATION) {
Fread..str(Camment)j
musLbe-keyword( "base_points", obj)j
for (i = Oj i < MAX_BASEj i++) {
Fread_vec( *Cur_coord) j Cur-COOrd++j
Fread_str( C omment) j
if (Camment[O] ! = ',') breakj
} /* end for (i) */
if (i == MAX_BASE) safe_exit("too many base points")j
size = i + 1j
must.be-keyword("trans_vector", obj)j
Fread_vec( trans) j
} else { /* BOX */
size = 4j
trans [X] = trans[Y] = Oj
x = reatLftoatOj Y = reatLftoatOj trans[Z] = reacLftoatOj
Store..coords(O, 0, O)j Store_coords(x, 0, O)j
Store_coords(x, y, O)j Store..coords(O, y, O)j
} /* end if (obj->type) */
/ * Translate base points. */
hLcoord = Cur_coordj
for (p = obj->verticesj p < hi-COOrdj Cur..coord++, p++)
Add_vec(*Cur_coord, *p, trans)j
obj->no_of_vertices = Cur..coord - obj->verticesj
top = bottom = IS..cDnvex(obj)j
Fread..str(Camment)j
if (Camment[O] = = ' +') {
if (!strcmp(Comment, "+top")) top = TRUE;
else if (!strcmp(Comment, "+bottom")) bottom = TRUE;
else safe_exit("\n+top or +bottom eX1)ected")j
Fread..str(Camment)j
} /* end if (Camment[O]) */
/ * Trivial edge list. */
obj ->edges = Cur_edgej
start = obj ->verticesj
Section 3.5. Surfaces of 'Iranslation 85
I I
void normaLvector(n, points) ( - Proto.h)
Vector n, * * points;
{ /* begin normaLvectorO */
register float len;
register Vector * a, *b, *c;
Vector ab, ac;
typedef struct {
Vector * vl, *v2; /* Pointers to the vertices of the edge. */
} Edge; (- Types.h)
I I
void intersecLobjects(obj, obj1, obj2) (- Proto.h)
Polyhedron * obj, /* The intersection. */
* objl, /* Does not have to be convex. */
* obj2; / * Should be convex. */
{ /* begin intersect.nbjectsO */
Face * 11, *hL/1, */2; /* Faces of obj1 and obj2. */
Face 10; /* A temporary face. */
Face * I, *hL/; /* Faces of obj. */
short i, n, m;
Plane * plane_pool, *plane, *hLplane;
static Vector **vtx_ptr _pool;
Polyhedron * tempi
k = Oj
v = f->verticesj
for (i= 0, j = 1j i< nj i++, j++) {
if (j == n) j = Oj
if (Sidelij > 0 && sidefj] > 0) continuej
vI = vi j v2 = v[j]j
if (side i < 0)
tmp..ptr[k++] = v[i]j
if (side[i]! = side[j]) {
SubLvec(dil I, *vl, *v2)j
sectJine-.and.plane(*Cur_add, &t, ¶llel, *vl, dill, plane)j
tmp-ptr[k] = ptr _to_vertexOj
if (k == Oll tmp..ptr[k] ! = tmp-ptr[k - 1]) k++j
} /* end if (side[i]) */
} /* end for (i) */
f ->no..of _vertices = kj
v = f ->verticesj
for (i = Oj i< kj i++)
v[i] = tmp-ptr[i]j
} /* end if (!toJJe_cut) */
I} /* end cuLface_with_planeO */
The function
I I
Vector * ptr _to_vertexO (-+ Proto.h)
{ /* begin ptr _to_vertexO */
register Vector * v = Add_verticesj
checks whether the vertex has been stored before and returns the corresponding
pointer to the vertex. It calls a function
I I
Bool close..1ogether(a, b) (~Proto.h)
register Vector * a, *bj
{ / * begin close_togetherO */
register short ij
which determines whether two points have the same coordinates. (Because of
the limited accuracy of floating point calculations, we have to admit a certain
tolerance. )
The function corr _ptrO picks avertex from the temporary coordinate pool and
copies it to the vertex list of the object:
I I
Vector * corr .ptr(v, obj) (~Proto.h)
Vector *Vj
Polyhedron * objj
{ /* begin corr .ptrO */
register Vector * vO = obj->vertices, *hLvO = obj->hLcoardj
To find out whether a face has an edge on aplane, we use the function
I I
Bool edge_in_plane(edge, face, plane) (~ Proto.h)
Edge *edgej
Face * facej
Plane *planej
Seetion 3.6. The Intersection of Objects 93
{ /* begin edge_in.planeO */
register Vector * *v = face->vertices,
* *hLv = v + face->no..of _verticesj
register Vector * p, *n = (Vector *) plane->normalj
register ftoat c = plane->cnstj
df = Safe-Dpen("intersect.dat", "v"),
fprintJ(Output, "filename = intersect.dat\n")j
fprintJ(dJ, "%su", obj->name);
fprintJ(dJ, "%suuu", "magenta");
fprintf(df, "generalu")j
if (Is...L!011,vex(obj)) fprintf(dJ, "solid\n")j
else Jprintf(df, "hollow\n")j
fprintf(df, "uuuvertices")j
V = obj->verticesj hLv = obj->hLcoordj
for (i = 0; v< hLv; v++) {
if (i > 0) fprintf(df, "U'U")j
if (i++ % 2 == 0) fprintJ(df, "\n uuuuuu")j
P_vec(*v)j
} /* end for (i) */
94 Chapter 3. How to Describe Three-Dimensional Objects
The data file for the two eubes in Figure 6 looks like this:
CUBE1 yellow box solid 6 6 6
translation -3 -3 -3
CUBE2 green box solid 6 6 6
translation -3 -3 -3
rotation x 40 rotation z 20
To ereate the peneil in Figure 6, we used the following file:
eONE gray revolution solid
meridian 5 0 -10, 5 0 0, 0 0 11 order 45
PRISM yellow revolution solid
meridian 4 0 -12, 4 0 12 order 6
4
Graphics Output
In this chapter, we will see how we can write graphics programs that are more
or less independent of the hardware that is used. If we want to adapt these
programs to different kinds of computers, we only have to change a few lines in
one of the include flies and in the system-dependent module.
We will create color palettes and use them for the shading of our facets. For the
shade, we will take into account factors like the angle of incidence of the facet
with the light rays and the distance from the light source.
Furthermore, we williearn how to write a program that allows us to manipulate
a wireframe model of an arbitrary scene.
Graphics Hardware
Before we can make any drawings on the screen, we have to know the graph-
ics commands. These commands are system-dependent. Graphics workstations
provide a lot of functions, most of which are executed by graphics coprocessors.
This helps to speed up the programs considerably. Less sophisticated computers
are only supplied with very few graphics commands so that everything has to be
done by the software.
We will try to write programs that use as few graphics-dependent functions and
macros as possible and that still allow the program to take advantage of the
graphics hardware. You will see that we do not need a lot of these commands.
They should an be written into the include file G...lIlacros. h. When it is neces-
sary to write functions, the macros just call these system-dependent functions
96 Chapter 4. Graphics Output
that are collected by the system-dependent module g-functs . c. The include file
G..macros.h has to be included in the file 3d_std.h (Section 1.1), whereas the
object file g..functs. 0 has to be linked with the program.
In order to reproduce all the features presented in this book, you need a computer
that
• is able to create palette colors by means of RGB values (the more, the better,
with a minimum of 16 for the shading).
• is able to set pixels in these colors on the screen. The compiler should provide
a function for the drawing of a line between two screen points and, if possible,
a function for the filling of convex polygons.
• has an acceptable screen resolution (the absolute minimum is 320 x 200 like
on a Standard-VGA-Card for PCs), ifpossible with two "pages" for "double
buffering" (page flipping).
With the help of these basic functions, we can write an of the other functions
we need for the graphics output. A function for the connection of two points on
the screen, for example, might look like this:
I
Ivoid quickJine(p, q) (-+ Proto .h)
register float * p, *qj
{ /* begin quickJineO */
G_mave(p) , G..draw(q)j
I} /* end quickJineO */
1 Even though the variable poly is apointer, any change of poly is undone
outside the function filLpolyO. We can only change *poly, i.e., the contents of
the vector.
2For the macros G_move_areaO and G_draw_area() , it does not make any
difference whether poly is a two-dimensional Vector2 or a three-dimensional
Vector, because both types are interpreted as pointers to a ftoat and only the
first two elements of the array are taken.
Section 4.3. How to Create Color Palettes and How to U se Them 99
I I
void filLpoly(n, poly, dim) (-+ Proto.h)
short n;
register ftoat *polYj
register short dim; / * Two or three. */
{ /* begin fill_polyO */
register ftoat *hLpoly = poly + dim * nj
G_move..area(poly) j3
for (poly += dim; poly< hLpolYj poly += dim)
G ...draw..area(poly) j
G1!lose..areaO;
I} /* end filLpolyO */
3Now we must not write *poly any more, because this would be a float,
whereas the macro expects a Vector, i.e., apointer to a ftoat.
4This requires at least 8-bit planes for single buffering and 16-bit planes for
double buffering (page ßipping).
100 Chapter 4. Graphics Output
5This requires at least 8-bit planes for single buffering and 16-bit planes for
double buffering (page ßipping).
S8Ction 4.3. How to Create Color Palettes and How to Use Them 101
I I
void make-Bpectrum(pal, starUdx, n) (-+ Proto.h)
Palette *palj /* Pointer to palette. */
short starLidx, nj
/* The palette will be stored in the color lookup-table between the in-
dices starLindex and starLindex + n. */
{ / * begin make_spectrumO */
short color _idx, hLcolaT'-idxj / * Index of the current color. */
Vector
lower _rgb, upper _rgb,
cur_rgb, /* Current RGB in between these vectors. */
delta_rgbj /* "Increase of color." */
short rgb[3], prev_rgb[3]j
/* Current triplet of round RGB values, previous triplet. */
color _idx = starLidxj
/* The following constants MAX_COLORS and RGB..RANGE are defined
in the system-dependent include file G..macros .h:
MAX_COLORS is the number of colors that can be displayed simulta-
neously, RGB-RANGE is the number of values for the red, green and
blue components of a color. Thus, the colors that are to be displayed can
be chosen from of a palette of (RGB-RANGE)3 colors. */
/* Copy extreme color vectors frorn table and fit thern into RGB -RANGE.
*/
Scale_vec(lower _rgb, pal->lower _rgb, RGB-RANGE)j
Scale_vec(upper_rgb, pal->upper_rgb, RGB-RANGE)j
/* Calculate delta_rgb. */
SubLvec( delta_rgb, lower _rgb, upper _rgb) j
Scale_vec( delta_rgb, delta_rgb, 1.0/n) j
/* Initialize previous color with impossible RGB values. */
prev_rgb[O] = prev_rgb[l] = prev_rgb[2] = -lj
Copy_vec(cur _rgb, lower_rgb)j /* We start with lower_rgb. */
hLcolaT'-idx = Minimum(MAX_COLORS, starUdx + n)j
for ( j color _idx < hLcolor _idx6 j color _idx++) {
Round_vec(rgb, cur J'gb)j
/* Take the nearest possible color. Since the RGB values are rounded
off, it may happen that a palette has two identical colors. In order
to avoid that, we compare the current color with the previous one.
H they are identical, we increase one of the three components by
one. */
Now it is easy to create all the necessary palettes according to our predefined
standard colors. In order to have fast access to all the colors, we introduce an
array of pointers *Map_color( ]:
#define MAX_PAL\
((MAX_COLORS - COLOR_OFFSET)/ PAL_SIZEf (-+ Macros .h)
short *Map_color(3 * MAX.:PAL]j (-+ Globals.h)
/* PALSIZEand COLOKOFFSETare defined in G..macros.h. Default for
COLOR_OFFSET is zero. If your computer has enough bit planes, you
can skip the first color entries. These colors are usually predefined by the
system. Otherwise your "Desk Top" alters the colors. The reason why
we allocate space for 3 * MAX_PAL palettes is that from each parent
palette we can extract two "subpalettes." However, they have only half
of the shade range. */
I I
void create..palettesO (-+ Proto. h)
{ / * begin create..palettesO */
register short *pal, *dark, *brightj
short i, pO = COLOR_OFFSET, idxj
for (idx = Oj idx< MAX_PALj idx++) {
make..spectrum(ParenLpalette + idx, pO, PALSIZE)j
/ * Allocate space for three palettes. */
Alloc_array(short , Map_color(idx],3 * PAL_SIZE, "pal")j
pal = Map_color(idx]j
bright = Map_color(idx + MAX_PAL] = pal + PAL_SIZEj
dark = Map_color(idx + 2 * MAX_PAL] = pal + 2 * PALSIZEj
7The C preprocessor will replace the constant MAX_PAL by the result of the
division (MAX_COLORS-COLOR_OFFSET)/ PALSIZEevery time it appears in
the code. The constants PAL_SIZE and COLÖR_OFFSET are system-dependent
and have to be defined in G..macros .h.
Section 4.3. How to Create Color Palettes and How to Use Them 103
Remember that we wrote the color of our object into the datafile itself (Sec-
tion 3.3 and Section 3.6). Here is the listing of a function that interprets the
name of the color palette and that returns the index of the corresponding palette:
I I
Ubyte colar _index( color _name) (~ Proto.h)
char *colar _namej
{ /* begin colar_indexO */
register Palette *pal, *hLpalj
/ * Is colar _name the name of a parent palette? */
hLpal = ParenLpalette + MAX_PALj
104 Cbapter 4. Graphies Output
Ifwe have
idx = color _index {colar -.name) j
in our code, we let
ParenLpalette[idx % MAXYAL].in_use = TRUB;
so that the program knows whether the palette is really going to be used. This
allows us to create larger palettes.
Finally, if we want to set a particular shade of a color palette, we write
G....set-color{Map_color[palette] [shade_value])j
or we use the macro
#define Set_map_color(pal, shade)\ ( - Macros.h)
G-BeLcolor{Map_color[pal] [shade]);
and write
SeLmap_color(palette, shade_value);
As a special case, the color of the background can be defined by
#define BACKGROUND Map_color[O] [0] ( - Macros.h)
4.4. Wire Frames and Depth Cuing 105
I I
void draw_wireframeO (- Proto .h)
{ /* begin draw_wireframeO */
register Vector *vl, *v2j /* Pointers to Coord..:pool. */
register Vector **ptr, **hLptrj /* Pointer to Edge_pool. */
PolyhedroD *obj = Object..:pool, *hLobj = obj + TotaLobjectsj
fill..screen..pool 0 j
for (j obj < hLobjj obj++) {
hi.ptr = (ptr = obj->edges) + 2 * obj->no..of _edgesj
SeLmap..color( obj ->color _index, PAL-BIZE/2)j
while (ptr < hi.ptr) {
vI = Screen..coords(*ptr++)j8
v2 = Screen..coords(*ptr++)j
drawJine( *vl, *v2)j
} /* end while */
} /* end for (obj) */
I} /* end draw_wireframeO */
(After the screen pool is filled, the pointer drawJine points either to quickJineO
or to clip3d.lineO .)
Wireframes are a bit confusing, especially when we deal with unusual views. On
the other hand, they enable UB to quickly move and rotate a scene before we do
the final rendering.
If we apply the so-called "depth cuing" [ROGE87] to the wireframe, the pictures
look much more three-dimensional (Figure 2). The color of the image of a point
(Le., a "pixel") is not only determined by the color of the object the point
belongs to, but it is also infiuenced by the distance from the projection center.
To save calculation time, this distance may be replaced by the distance from the
projection plane (the latter being the third coordinate of the point in the screen
system).
If we want to draw a line from the image of a point P to the image of a point Q,
theoretically, we have to calculate the depths of several points between P and Q,
which takes a lot of time. Therefore, we wil~ only calculate the average depth of
the points between P and Q (i.e., the arithmetic mean of the z-coordinates of P
and Q in the screen system). The result still looks much better than an ordinary
wireframe.
Some graphics workstations are provided with hardware depth cuing. In this
case you should use the hardware according to the user's manual. Usually, it is
enough to set a Boolean variable depthcuing (or similar) TRUE, to store the
Section 4.4. Wire FraIIles and Depth Cuing 107
minimum and the maximum depths in some predefined variables and to indicate
the color palette by its start index and its last index in the color lookup-table.
This may be done by the macros
#define HARDWARKDEPTH_CUING
(void) G_depth_cuing ( on_off); (~ G..macros.h)
/* TRUEorFALSE. */
(void) G_depths_for_depth_cuing (min_depth, max_depth); (~ G..macros.h)
(void) G_colors_for_depth_cuing (min_color, max_color); (~ G..macros.h)
Before we draw the depth-cued wireframe, we allocate and fill a "color pool"
with the shade values of a11 the points:
Ubyte Init(Color _pool, NULL); (- Globals.h)
float Min_depth, Max_depth, TotaLdepth; (- Globals.h)
/* We need these values for the shading of the faces. */
#define T2(w, z, syst)\ ( - Macros.h)
((w) = (z) /(Dist[syst] - (z)))
/* Apply the transformation T 2 : Formula (2.3.9). */
#define UndoT2(z, w, syst)\ (- Macros .h)
((z) = (w) * Dist[syst] /(1 + (w)))
/* Apply the inverse transformation T2'l: Formula (2.3.10). */
I I
void fill_color _pool 0 (~ Proto .h)
#ifdef HARDWAREJJEPTH-CUING
G..depthcuing( TRUE);
G-tlepths_for ßepthcuing(min-z, max-z);
#else
lambda = PALSIZE/(max-z - min-z);
if (draw.Jine == quickJine)
for (z = &Screen..pool[O][Z]; z < hi..z; z += 3)
*cur _colar++ = (*z - min-z) * lambda;
else
for (z = &Screen..pool[O][Z]; z < hLz; z += 3) {
zO = (*z - min-z) * lambda;
*cur _colar++ = (zO < zl) ? zl : ((zO > z2) ? z2 zO);
}
#endif
} /* end filLcolar _pool 0 */
I
Now we can calculate the average depth of a line with the help of pointer arith-
metic (compare the following code with the one in draw_wireframeO at the
beginning of this section):
I I
void draw_depthcued_wireframeO (-+ Proto.h)
{ /* begin draw-tlepthcued_wireframeO */
register Vector *vl, *v2; /* Pointer into Coord..pool. */
register Vector **ptr, **hLptr; /* Pointer into Edge..pool. */
register short shadej
Polyhedron *obj = Object...pool, *hLobj = obj + Total..objects;
short *cur _pali
calc_roLmatrix( SCREEN_SYST);
filLscreen-POO10;
f ill_color _pool 0;
Section 4.4. Wire Frames and Depth Cuing 109
I I
void manipulate-BceneO (-+ Proto.h)
{ /* begin manipulate_sceneO */
Ubyte key;
statie Vector delta = {0.05, 0.05, 0.05};
statie float lambda = 1.03;
while ((key = GJeeY-PTessed()) != ESCAPE) {
if (!key) continuej
switch (key) {
/ * Change azimuth angle, elevation angle, twist angle. */
lOUsually, the shift operator works much faster than an ordinary division. If
you shift an integer value to the right by 1, this is equivalent to dividing it by 2.
110 Chapter 4. Graphics Output
4.5 Shading
Of course, wireframes are not very satisfactory. The next step towards realistic
computer-generated images is to "shade" the facets of our objects. The brightness
of a face (Le., the shade in the corresponding color palette) depends on various
factors. Sophisticated shading models are described in detail in [THAL87] and
[HEAR86]. In Section 8.8, we will extend the formulas derived in this section.
For our purposes, we try to simplify the determination of the shade. First let
us assurne that we only have one light source (Figure 3). The shade value s can
then be calculated in the following three steps:
Section 4.5. Shading 111
t ··,'
\ ...... ' ::
.
.
'.
:1::::::: ::::
a
1. According to Lambert 's eosine law (Section 1.2) the amount of light reaching
the surface depends mainly on the angle of incidence a of the light rays. If the
light rays are not parallel, their angles of incidence will be slightly different
for each vertex of the face. In order to keep calculation time within limits,
we simply take the "average angle of incidence" (i.e., the angle of incidence
with the barycenter of the face),u We have maximum brightness when
the light rays hit the face perpendicularly (a = 0) and minimum brightness
when the light rays coincide with the plane of the face (a = 7r /2). The shade
value s can be calculated by
that this reflected light comes more or less "from the back." Therefore, the
brightness of a dark face also depends on the angle of incidence of the light
rays so that we can again use Formula 2 with a reduced shade value (for
example: s ---+ 0.3 s).
The exponent c has an influence on the appearance of the object (Figure 4).
For "normal material" (neither mat nor shiny) we let c = 1. For metallic
material we may set c ~ 2. 12 For mat materials like wood, we let 0.5 < c < 1.
12The use of the constant c like in Equation 2 represents a very rough sim-
plification of the law of reflection. It will not place highlighted spots exactly
Section 4.5. Shading 113
Remember that cos (a) is nothing but the dot product of the normalized
normal vector of the facet and the normalized vector to the light source
(Section 1.2). In any case, a first approximation of the shade value is
o ~ s ~ reduced_palette. (3)
2. A second criterion for the brightness of the face is its (average) distance
from the light source.
To increase the speed of the program, we replace the distance from the light
source by the distance from the base plane of the light system.
d-z
FIGURE 5. When the light source is elose to the scene, we have to take into account
the distance from the light source.
Let dlight be the z-value of the light source in the light system (i.e., the
distance ofthe light source from the base plane, see Figure 5), let (Zlight)max
be the maximum z-value of the scene in the light system and let Zlight be
the average z-value of the face in the light system. The shade value 2 can
then be modified by
S (dli9ht - (Zlight)max) 2
s--+ (4)
dlight - Zlight
, ... "
::;1
(The brightness of the face decreases with the square of the distance. )
When the light source is far away from the objects, the above-mentioned
modification is negligible.
where they ought to be. For smooth reflecting surfaces, it is better to use a more
sophisticated model, which will be described in Chapter 9 in connection with
mathematical surfaces and spline curves.
114 Chapter 4. Graphics Output
3. A third factor that has an inHuence on the brightness of the face is its
distance from the projection center. We can caU this factor "additional depth
cuing."
To increase the speed of the program, we replace the distance from the
projection center by the distance from the projection plane, the latter being
the z-coordinate in the screen system (see "depth cuing" in Section 4.4).
Let ambient be PAL_SIZE - reduced_palette, let zo be the average distance
ofthe face from the projection plane (Figure 6), let (ZO)min be the minimum
z-coordinate of the scene in the screen system and let totaLdepth be the
depth of the scene in the screen system. Finally, the shade value is
Zo - (ZO)min
s-+ s + am bient total..depth' (5)
~
~l
Now wehave
0::; s ~ PALSIZE, (6)
which means that we use the whole palette.
Now let us introduce n different light sources. Let Jt, 12 , ••• , In be their
intensities13 and let the n shade values of a face, corresponding to each indi-
13In our simplified model, the colors of the light sources are aU white.
Section 4.5. Shading 115
viduallight source, be S}, S2,'" ,Sn' (The additional depth cuing (5) must not
be introduced yet.) Then we have
(7)
The value Stotal should still fulfill Equation 3. This can easily be achieved by the
eondition lk $ I/n, k = 1, ... , n. Such a strong restriction, however, implies that
Stotal is unlikely to ever reach its theoretical maximum value reduced_palette and
that we will lose parts of the color palette. On the other hand, if Itotal is too
large, this may sometimes eause the shade value to exceed its maximum.
Ifwe want to produee an optimum image of the scene, we have to precalculate the
shade values Stotal for all the facets and to determine their maximum value Smaz.
If we now scale all the light intensities by the factor A = reduced_palette/ Smaz
(Ik -+ Alk), we ean be sure that Stotal will indeed fulfill eondition (3). Finally,
we modify Stotal by additional depth cuing (5). Thus, the entire palette size will
be used for the shade values. But be eareful: onee you animate the scene and
recalculate the light intensities for each frame, the movie will flicker. Therefore,
it is better not to change the intensities for each frame and to take the risk that
from time to time a shade value may exeeed the palette (take the maximum
value in that ease).
To put the theory into practiee, let us now have a look at a funetion
calc..shade_of -faceO.
Bool Solidi (-+ Globals .b)
/* This variable indicates whether the object the face belongs to is solid. */
float Intensity[MAX_SYS1lj (-+ Globals .b)
float FUlLwidth[MAX_SYS1l, Half_width[MAX_SYS1lj (-+ Globals .b)
/ * Default values for the intensities of the light sources may be
for (syst = 1j syst < Total..systemsj syst++)
lntensity[syst] = I/sqrt«double) No_of_lights)j
(Other values 0 < lntensity[syst] < 1 can be read from the data file.)
The variables F'ulLwidth, Half_width are initialized by
#deftne AMBIENT (PAL_SIZE/5) (-+ Macros.b)
#deftne REDUCED_PAL (PALSIZE - AMBIENT) (-+ Macros .b)
for (syst = 1j syst < TotaLsystemsj syst++) {
FUlLwidth[syst] = REDUCED_PAL * lntensity[syst]j
Half_width[syst] = 0.5 * FUlLwidth[syst];
}
*/
/ * We add two members to struct Face. */
Ubyte darkface[MAX_SYSl1i
Ubyte shade[MAX_SYS1li
116 Chapter 4. Graphics Output
/* Lambert's law. */
SubLvec(light_ray, *1 ->barycenter, Proj_center[syst])j
eosine = DoLproduct(f->normal,light..ray)/Length(light..ray)j
if (Solid) {
if (eosine > 0) {
s = eosine * FulLwidth[syst);
Backlit(f, syst) = FALSE;
} eIse {
s = -Hall _width[syst] * eosinej
Backlit(f, syst) = TRUE;
} /* end if (eosine > 0) */
} eIse { / * Light source and eye on the same side of the plane? */
if (eosine * (0.5 - Backface(f» > 0) {
if (eosine < p) eosine = -eosinej
s = eosine * FulLwidth[syst]j
Backlit(f, syst) = FALSE;
} eIse {
if (eosine < 0) eosine = -eosinej
s = eosine * Hall_width[syst];
Backlit(f, syst) = TRUE;
} /* end if (eosine ... ) */
} /* end if (Solid) */
return Sj
J /* end shadeO */
Section 4.5. Shading 117
I
Ivoid platJine(p, q) (- Proto.h)
Vector15 p, q;
{ /* begin plotJineO */
register short xl = p[X] , yl = p[Y] , x2 = q[X], y2 = q[Y];
short mod, dx, dy, sgn..dy, tempi
if (xl - x2)
Swap(xI, x2), Swap(yI, y2);
dx = x2 -xl;
if (y2 - yI)
dy = y2 - yI, sgn_dy = 1;
else
dy = yl - y2, sgn..dy = -1;
if (dy <:= dx)
for (mod = -«dx + 1) » 1); ; xI++, mod += dy) {
if (mod >= 0)
yl += sgn..dy, mod -= dx;
G-seLpixel(xI, yI);
if (xl == x2)
return;
} /* end for (mod ... ) */
else
for (mod = -«dy + 1) » 1); ; yl += sgn..dy, mod += dx) {
if (mod >= 0)
xI++, mod -= dy;
G-set_pixel(xI, yI);
if (yl == y2)
return;
} /* end for (mod ... ) */
I} /* end plotJineO */
'Rubber Bands'
A function rubber J1andO haB almost the same code aB the function plot.lineO.
We only have to replace the macro G..set..pixel 0 by the following macro:
#define Set_xor_pixel(x, y)\ ( - Macros.h)
temp = G_get..pixeLcolor(x, y),\
Cur _color = temp, \
A
G_seLpixel(x, y),\
Cur -color A= temp
15With this function it does not malre any difference whether p and q are of
the type Vector2 or of the type Vector. Both types are interpreted 88 pointers
to a float and only the first two elements of the array are used.
Section 4.6. Basic Graphics Output Algorithms 119
The color of a pixel on the line now depends on the color the pixel had before.
The two colors are connected by the XOR-assign, a fast bitwise operation that
is undone if we apply it twice. Thus, if we write
rubber ..band(p, q);
• Not all C compilers provide a function that fills polygons the way we need
it. The so-called "ftoodfill" -command, for example, which needs a point in
the interior of a closed area that is defined by a specific color, is not always
able to fill polygons correctly for our purposes. (We want to erase other
polygons. Therefore, parts of the interior of the polygon will usually have
been filled with several colors before.)
• !magine a face of our scene that is comparatively large (for example, the
base plane of the scene). If we fill the image polygon of the face with one
and the same color, it will look rather unrealistic. In such a case, we can
fill our polygon line by line with slightly different colors (mainly according
to the depth of the points in the screen system) in order to get the impres-
sion of smooth shading. This method is a special case of Gouraud shading
([GOUR71]).
• We will need a fill algorithm for polygons in a slightly modified form in
Section 6.6, where we talk about transparency.
• The algorithm is essential for the so-called "depth buffering" and "shadow
buffering" (Chapter 7).
I
I void poly_draw(a) (-t Proto.h)
Vector aj
{ /* begin poly..drawO */
register short * x = &Cur_vtx->x, *y = x + 1j
16It is also possible to use the type Ubyte for the shade. In this case, however,
sizeof(Vertex) will be 15 instead of 16. The program runs faster when the size
of the structure can be divided by 4.
Section 4.6. Basic Graphics Output Algorithms 121
I
I void polYAoseO ( - Proto.h)
{ /* begin poly_doseO */
register short i, sizej
trapezoid
The actual filling of the polygon is done by the function flush_polyO. This
function splits the polygons into triangles and trapezoids with x-parallel sides.
Figure 7 shows that, in general, each trapezoid will have two new (temporary)
vertices.
I
I void flush_polyO ( - Proto.h)
{ /* begin flush_polyO */
register Vertex * al, *a2, *bl, *b2j
static Vertex reserve[2]j /* The temporary vertices. */
Vertex * r = reservej
short ij
Hoat tj
122 Chapter 4. Graphics Output
al = bl = a2 = b2 = Min_vtxj
for (j a2 ! = Max_vtx 11 b2 ! = Max_vtxj al = a2, bl = b2) {
while (a2! = Max_vtx) { /* Next A 2 with greater y. */
a2 = Vtx + al->prevj
if (al->y - - a2->y)
al = a2j
else breakj
} /* end while */
while (b2! = Max_vtx) { /* Next B 2 with greater y. */
b2 = Vtx + bl->nextj
if (bl->y == b2->y)
bl = b2j
else breakj
} /* end while */
if (a2->y > b2->y) { /* Interpolate new A 2 • */
t = «ftoat) b2->y - al->y)) /(a2->y - al->Y)j
r->xO = al->xO + t * (a2->xO - al->xO)j
r->y = b2->yj r->prev = a2 - VtXj a2 = rj
r++j
} else if (a2-?y < b2->y) { /* Interpolate new B2 • */
t = «ftoat) a2->y - bl->y» /(b2->y - bl->y)j
r->xO = bl->xO + t * (b2->xO - bl->xO)j
r->y = a2->yj r->next = b2 - VtXj b2 = rj
r++j
} /* end if (a2->y) */
filLtrapezoid(al, bl, a2, b2)j
if (r - reserve > 2)
r = reservej
} /* end for (a2 ... ) */
J /* end flush_polyO */
I I
void filLtrapezoid(al, bl, a2, b2) (-t Proto.h)
Vertex * al, *bl, *a2, *b2j
{ /* begin jilLtrapezoidO */
register f10at xl, x2, dxl, dx2j
short y, ymax, dYj
With a computer that is able to display palettes with many different shades, the
objects (especially polygonized surfaces) can be shaded by means of Gouraud
shading [GOUR71]. In the case of curved surfaces, this mayeven save time (in
spite of the fact that the fill algorithm slows down the program ), because we
need much fewer polygons on the approximating polyhedron.
At the end of filLtrapezoidO, we add the lines
if (Smooth..shading) {
float sI, s2, ds1, ds2;
short * p = M ap_color[Cur _palette];
I I
void shade..soonJine(x1, x2, y, sI, s2) (- Proto.h)
register short xl, x2, Yj
float sI, s2j
{ /* begin shade...scanJineO */
float ds = s2 - slj
statie short j, dx, sign...s, kj
Ever sinee the beginnings of computer graphies, many algorithms have been
developed to remove those parts of the image of a seene that are obseured by
other parts, and yet none of these algorithms seems to be entirely satisfactory.
Among the general working algorithms, we have the "sean-line" algorithms, the
"area subdivision" algorithms, "z-buffering," "ray tracing" and many others. If
you want to read more about these algorithms, please refer to the following
sourees: [SUTH74/2], [THAL87], [GLAS90].
In this chapter we will talk about a rendering algorithm that in fact is already
very weIl known: the "painter's algorithm, " also ealled the depth sort or priority
algorithm. It processes polygons in a similar way as a painter might do it. The
images of distant polygons are painted first, to be obseured partly or completely
later on when the images of those polygons are painted that are doser to the
viewer. The problem is to put the polygons into an order according to their
priority.
This algorithm is extremely fast in removing hidden surfaces, but it has three
enormous disadvantages.
• The painter's algorithm only works on the screen and is not suitable for
plotter drawings.
This may be the reason why many books on 3D-graphics mention the painter's
algorithm only briefly and then go on to more general and more sophisticated
rendering methods.
In this chapter, however, we will see that the painter's algorithm can be applied
very effectively to almost any kind of scene (even if it has thousands of faces and
intersecting objects) and to create PostScript images. Of course, this does not
mean that it can replace all the other rendering algorithms, but it can be used
as a powerful tool for
The idea of the painter's algorithm can also be applied to the plotting of cast
shadows. We can even introduce transparent objects and reflections (Chapter 6)
without any major loss of speed.
For really complicated scenes or objects, which can neither be subdivided easily
nor polygonized in the way we need it, we can still use other rendering algo-
rithms (in Chapter 7 we will have a closer look at the above-mentioned "depth
buffering," which will be extended to a "shadow buffering").
2The negation of this statement is: it may be wrong to plot B before A (if
the images of A and B do not overlap, the drawing order is of no importance).
128 Chapter 5. A Fast Hidden-Surface Algorithm
solid) polyhedron with a convex outline can be displayed in two steps: first we
render all the backfaces and then the rest of the faces.
I I
void face_types(obj, syst) (-+ Proto.h)
Polyhedron * obj;
short syst;
{ /* begin face_types{) */
Vector proj _ray;
register Face * f = obj -> faces;
register Vector *Center = (Vector *) Proj_center[syst];
Face * hLf = f + obj->no_of _faces;
I
I void ploLconvex_polyhedron( obj) (-+ Proto.h)
Polyhedron Mbj;
{ /* begin ploLcanvex_polyhedronO */
register Face * I, *hi-I;
I
I void ploLpolyhedran_with_convex_outline(obj) (-+ Proto.h)
Polyhedron *obj;
{ /* begin ploLpolyhedran_with_convex_outlineO */
register Face * f, *hi-I;
facdypes(obj, SCREEN_SYST);
hi_f = obj -> laces + obj ->no_ol _Iaces;
for (f = obj -> laces; f< hi-fi 1++)
if (Backlace(J)) ploLface(J);
for (f = obj -> laces; I< hi-fi 1++)
if (!Backlace(J)) ploLface(J);
I} /* end ploLpolyhedron_with_convex_outlineO */
1. 0"0 does not cross the polyhedron ~. Then the rendering is easy: we can
render one slice after the other, beginning with the slice with the maximum
distance from 0"0 •
2. 0"0 crosses ~. Then it splits ~ into two polyhedra ~l and ~2 (each of which
is completely in one of the two half spaces defined by 0'0) and a "critical
slice" E o , consisting of the faces that intersect 0"0 • Since 0'0 coincides with
the projection center, its image will be a line So = 0"0 , which separates the
images of ~1 and ~2' Therefore, we can plot these images separately like
in case 1. Only the image of the critical slice will obscure some parts of the
images of ~l and ~2, and that is why it has to be drawn after the rest.
J. 111
.. .
er/lleat slice ....
~
11
......
......
/
11
" separat Ing planes
"
~
, '\ ,
The rendering algorithm is extremely efficient. The computer will need only a
short time for the determination of visibility. Thus, we can render and manipulate
even complicated polyhedra.
Section 5.3. Sliced Surfaces 131
. an
What we still need to know is how to render a general slice. First we determine the
types of all the faces (backfaces or frontfaces ) of the slice and combine adjoining
faces of the same type to "ribbons" (Figure 4). It should always be possible to
render the ribbons one after the other in the correct order (from the back to
the front). The reason for this is that none of the ribbons can contain another
ribbon (otherwise it would have been split into two ribbons).
The priorities among the ribbons can be determined as follows. Imagine an axis
s that is perpendicular to the separating planes (J' of the slices and that goes
through the projection center C. It can be interpreted as the axis of a pencil of
planes (/I). Each ribbon is confined by a sector that is defined by two planes of
the pencll. For the determination of the priority between two ribbons ~ and R j ,
we distinguish between two cases:
1. The two corresponding sectors do not overlap. Then the priority between
14 and Rj is of no importance.
2. The sectors overlap. Then both of the rings have a set of vertices inside the
common sector. For these sets we determine the maximum distances from
132 Chapter 5. A Fast Hidden-Surface Algorithm
the projection center. The ring with greater maximum distance has to be
plotted first.
When we check all the priorities between the ribbons, we can put them into an
order. Even though this way of sorting seems to be similar to the algorithms
for the sorting of numbers, it is different. If you sort numbers, the negation of
nj < ni is, of course, nj ;::: ni . In priority lists, the negation of "Rj prior to R i "
can either be "~ prior to R/ or "no priority between ~ and R j . " (This makes
the sorting easier. On the other hand, if we have an array of sorted primitives it is
no longer possible to reconstruct the priorities between them!) This is the reason
why we occasionally get several priority lists, all of which are correct and which
depend on the "starting position of the primitives" in the sorting algorithm.
Now that we know how to plot an individual slice, it is easy to render the whole
surface. The plotting order of the slices is exactly the same as for the rendering
of surfaces of revolution (Section 5.2).
FIGURE 6. A function graph cut into slices that are parallel to the base plane.
1. Uo and 'l/Jo do not intersect the graph r. Then the image of r can be plot ted
as follows:
The surface is cut into slices ~ which are separated by our planes a. Thus,
the drawing order for the slices is eiear: we plot one slice after the other,
starting off with the slice that has maximum distance from the plane ao .
Each slice consists of a number of patches l)i. These patches are separated
by our planes 1/;. Thus, the patches can also be plotted one after the other,
starting off with the one that has the greatest distance from 'l/Jo.
Each patch consists of two triangles, separated by a "diagonal plane" 8,
which is orthogonal to the base plane ß. (It is of no importance which of the
two diagonal planes is chosen. The patch may even consist of more than two
triangles.) The triangle that is not in the same halfspace as the projection
center has to be plotted first.
Since the separating planes u, 'I/J and 8 appear as straight lines in the nor-
mal projection on the base plane ß, the priority determination is only two-
dimensional. Therefore, it can be done very fast.
2. One of the two planes (ao or 'l/Jo) intersects r. Then f is divided into two
parts f 1 and r 2 plus a "critical slice" ~o (Le., the slice that is intersected by
Uo or 'l/Jo). The plane acts as a separating plane between r 1 and r 2 . Because
its image is a straight line, the images of r 1 and r 2 will not intersect each
other and each of the two parts of the graphs can be rendered separately
like in case 1. At the end, we render ~o. Its image will obscure some parts
of r 1 and r 2 .
3. The two main normal planes Uo and 1/;0 intersect the graph r. Now r is
split into four graphs r i (i = 1, ... ,4) plus four critical slices ~i plus a
critical patch Wo. The images of f i will never overlap because ao and 1/;0
are separating planes, the images of which are straight lines. Thus, we can
render the images of r i separately like in case 1. Then we render the critical
Section 5.5. Priority Among Objects 135
slices ~i in an arbitrary order (their images will not overlap either, even
though they will obscure some parts of the graphs f i ). Finally, we plot the
patch Wo, which may obscure some parts of I:i .
-
! !
t t
-
0-0 - - - - - - - -00--
?/Jo
I tPo
FIGURE 7. How to plot a function graph that is sliced perpendicularly to the base
plane.
For reasons of speed, the priority test will be made in several steps:
The rnethods we discussed in Step 2 are very useful for the quick determination
of priority. They have, however, some disadvantages:
- When the outlines of the polyhedra do not really intersect (which may occur
even when the bounding rectangles overlap), the result may be useless for
the priority list. This means that if the images of A and B do not overlap,
and if we assign a priority between A and B in spite of that, this may lead
to a contradiction in the priority list ("Gordian knot" - see Section 5.6).
- Even though the above-mentioned tests are fast in checking the priority,
they can produce longer computation times later on (especially when we
plot shadows). Consider the following (rather common) case: the object A
is completely obscured by B. The priority test in Step 2 only teIls you that
A has to be plotted first. Therefore, the computer will spend some time
plotting A, but in the end, the image of A will be erased!
a) The outlines
are disjoint.
1. The outlines are disjoint (Figure lOa), so that the drawing order of A
and B is of no importance.
2. The outlines intersect (Figure lOb). Any of the intersection points in
the image plane 7r can be interpreted as the image of a point P on
the spatial contour of A or as the image of a point Q on the spatial
contour of B. The distances of the two space points from the image
plane 7r (the z-values in the screen system) may be pz and qz. According
to the considerations in Section 2.3 (Equation 21), we transform these
values into
(1)
Before we list the eode for a C funetion that returns the priority of two objects
A and B, we have to give the definition of a "halfspace" :
#define NOT..FOUND 0
#deflne USRSOAP_B UBBLES 2
typedef struet { / * Halfspace* /
Veetor narmalj /* Normal to defining plane. */
Hoat cnstj
/* Dot product of the normal vector and the position vector of the
point on the plane. */
short infoj
/* This strueture member eontains some additional information: info =
NOT_FOUND: The plane was not to be found.
info = ±1: The first of the two objects that are to be compared lies
on the positive/negative side of the plane.
info = USE_SOAP_BUBBLES: Do not use the halfspace for priority
decisions. The distanee between the objects is big enough for
the "soap bubble test" (Figure 9), which - in this ease - will
also be more reliable.
*/
} Halfspaeej (_ Types.h)
float max_rad;
/ * This variable equals the radiusof the smallest possible sphere that
circumscribes the polyhedron (with the barycenter as its center). */
Vector *bounding_box;
/ * This array of vectors stores the coordinates of the eight vertices of
a rectangular box that circumscribes the polyhedron in the world
system (usuaHy the sides of the box are parallel with the coordinate
planes, but this does not always have to be the case). */
Vector *box_min, *box_max;
/* Every image of an object (polyhedron) has a so-called "bounding
rectangle," which is determined by the minimum screen coordinates
and the maximum screen coordinates. The first two coordinates
of box_min[SCREEN_SYST] and box_max[SCREEN_SYST] are re-
served for these values. The z-values equal the minimum and the
maximum distances from the projection plane in the screen system.
What we said about the "bounding box" in the screen system is
also true for bounding boxes in the various light systems. Thus,
box_min [LIGHT_S YST 1] and box_max[LIGHT_SYST 1] store the
minimum and the maximum values of this box in the first light sys-
tem, and so forth. */
short *size_o! _contour;
/* The polyhedron has a (spatial) contour in the screen system and in
the light systems. The number of the vertices on each contour is
stored in this array. */
Vector ***contour;
/* Relax. It's just three stars, that's al1. *contour[k] is an array of point-
ers to the vertices on the (spatial) contour line of the polyhedron in
the k-th system (SCREEN_SYST, ... ). Thus, **(contour[k] + n) is
the n-th point on the contour line in the same system. * /
Ubyte obscured[M AX..sy ST EM 8];
/* When an object is completely hidden (shadowed) by other objects in
the k-th system, we let obscured[k] = TRUE. */
Now to the desired function: it can be used for priority tests in the screen system
as weH as in the light systems. (Priority in a light system means: does A cast a
shadow on B, or B cast a shadow on A, or do neither of them cast shadows?)
#define NOTYOUND 0 (- Maeros.h)
#define USKSOAP_B UBBLES 2 (_ Maeros .h)
#deflne NONE MAX_UBYTE (_ Maeros.h)
Section 5.5. Priority Among Objects 141
I I
Ubyte which_obj_is_first(objl, obj2, syst, critical) (-+ Proto .h)
register Polyhedron * objl, *Obj2j
short systj /* Screen system or light systems. */
Bool criticalj /* Without or with separating planes. */
{ /* begin which_obj_is_firstO */
register Vector * minl, *maxl, *min2, *max2j
fioat z[2]j
Halfspace * sep_planej
Ubyte first = NON Ej
intersect..outlines :
I I
Booloverlap(minl,maxl,min2,max2) (- Proto.h)
register Vector * minI, *maxl, *min2, *max2;
{ / * begin overlapO */
if ((*minl) [X] > (*max2) [X] 11 (*maxl)[X] < (*min2) [X] 11
(*minl)[Y] > (*max2)[Y] 11 (*maxl)[Y] < (*min2)[Y])
return FALBE;
else return TRUEj
I} / * end overlapO */
I I
Hoat average_dist(obj, syst) ( - Proto.h)
Polyhedron * obj j
short syst;
{ /* begin average_distO */
Vector r;
4Named after the "Mikado" game: several dozens of thin, painted sticks are
thrown in a random pile on a table. The players have to pick up as many sticks
as possible without moving any of the other sticks.
144 Chapter 5. A Fast Hidden-Surface Algorithm
objects, we will again find objects that are not obscured by any other residual
objects. Their indices have to be put at the lowered end of the priority list, etc.
The algorithm is repeated until no more objects are to be sorted.
The mikado algorithm can be accelerated when we apply it twice (from the front
and from the back):
FIGURE 11. When we try to solve the "mikado"-puzzle we first remove the sticks
that have no influence on the scene. Thus, we simplify the scene in a similar manner
as the "mikado algorithm."
We select the k1 front objects that are not obscured by any other objects. As
usual, we place these objects at the end of our priority list. Furthermore, we pick
those k 2 objects that do not obscure any other objects. These "back objects"
come first on the list. Now there are only n = n - k1 - k2 residual objects to
be sorted. The recursion is finished when no more residual objects are to be
sorted. It has to be stopped when no front objects or back objects can be found
among the residual objects. In this case, we get a certain number of objects with
a Gordian knot. (Imagine a scene of 50 objects where sorting is impossible just
because three of the objects form a Gordian knot. The mikado algorithm will
most probably stop when we only have a few residual objects, among which are
the critical ones. Thus, it will be easy to isolate the critical objects and to split
Section 5.6. Final Priority List 145
them. A message like "Cannot sort your 50 objects - try to split the scene!"
would not be very helpful. .. )
A pseudo-code of the algorithm may look like this:
Determine priorities among the n given objects
Residual objects := given objects
Lmin := 0, Lmax := n
repeat
select k 1 front objects and k2 back objects among residual objects
if k 1 > 0 1\ k 2 > 0
store indices of front objects at
order[Lmax - k 2 ] ••• order[Lmax -1]
store indices of back objects at
order[Lmin] . .. order [Lmin + k1 - 1]
i_min := Lmin + k 1
Lmax := Lmax - k 2
residual objects := residual objects minus
front objects minus back objects
else
error message "Gordian knot"
endif
until no more residual objects V Gordian knot
Before we can do the sorting, we have to determine all the priorities among the
objects. The function prioritiesO is written in a flexible manner so that it can
be used for different systems (screen system, light systems) and for arbitrary sets
of objects (e.g., object groups).
I I
void priorities(n, given_obj, syst, critical) (---+ Proto .h)
short nj /* Number of objects. */
Polyhedron *given_obj[]j /* Array of pointers to objects. */
short systj / * Screen system, light systems. */
Bool critical j / * Can we rely on separating planes? */
{ /* begin prioritiesO */
register Polyhedron **obj1, **obj2j
Polyhedron **hLobj = given...obj + nj
register Ubyte *pr1j
register short i1j
staticBool priority_allocated = FALSEj
if (!priority....allocated) {
Polyhedron *Objj
short ij /* Used within macro. */
for (obj = ObjecLpoolj obj < ObjecLpool + nj obj1++)
Alloc...2d_array(Ubyte, obj->priority, TotaLsystems, TotaLobjects)j
priority....allocated = TRUEj
} /* end if (priority_allocated) */
146 Chapter 5. A Fast Hidden-Surface Algorithm
/* Mikado algorithm. */
i-min = Oj i_max = k = nj
while (k) {
if (!mikado_select( is_in_!rant, is_in_back, k, res_obj, prior» {
if (critical) {
Print("Gordian knot while sorting the objects\n")j
for (i = Oj i < kj i++) {
order[i-min + i] = res_obj[i]->indexj
!print!(Output, "y'S ", res-Obj[i]->name)j
} /* end for (i) */
Print("Next time try to split some of these objects")j
} / * end if (critical) */
return FALBEj /* If not critical, we have another try! */
} /* end if (mikado....select) */
k = Oj i1 = Lminj i2 = Lmaxj
for (i = i1, obj = res_objj i < i2j i++, obj++) {
j = (*Obj)->indexj
if (is_in_backfi])
order[Lmin++] = jj
else if (is_in_!rantfi])
order[--Lmax] = jj
else
res_obj[k++] = *objj
} /* end for (i) */
} /* end while (k) */
return TRUEj
I} /* end sorLobjectsO */
i = (*obj)->indexj
is_in_/ront[i] = is_in_back[i] = TRUEj
}
for (obj = res-Objj obj < hi...objj obj ++) {
i = (*obj)->indexj
priorl = prior[i];
for (obj2 = res_objj obj2< hi...objj obj2 ++) {
if (*Obj = = * obj2) eontinuej
j = (*obj2)->indexj
if (prior 1 [j] = = i) {
is_in.back[i] = FALSE;
if (!is_in_/ront[i]) breakj
} else if (priorl[j] = = j) {
is_in_Iront[i] = FALSE;
if (!is_inJxICk[i]) breakj
} /* end if (priorl) */
} /* end for (obj2) */
} /* end for (obj) */
/* Check whether search was successful. */
for (obj = res-Objj obj < hi...objj obj + +) {
i = (*obj)->indexj
if (iB-in_Iront[i] 11 is_inJJack[i])
return TRUE;
} /* end for (obj) */
return FALSEj
I} /* end mikado-BelectO */
For the correct rendering of the scene, we now simply have to eall the funetion
I I
void sort....and_render...objectsO (-+ Proto .h)
{ /* begin sort...o.nd_render...objectsO */
short ij
statie Ubyte *order = NULLj
statie Polybedron **obj_ptrj
/* Array of pointers to objects. */
Bool critical, succeSSj
if (order = = NULL) { /* Initialize before drawings. */
Alloc...array(Ubyte, order, Total...objects, "sort")j
Alloc..ptr...o.rray(Polyhedron, obj_ptr, Total...objects, "ptr")j
for (i = Oj i < Total...objectsj i++)
obj_ptr[i] = &Object..pool[i]j
}
Section 5.7. The Creation of Object Groups 149
The algorithm we described in Section 5.6 only works for "objects," Le., poly-
hedra. Once a scene gets more complicated, the number of objects will of course
increase. The sorting time, however, runs in quadratic time with the number of
150 Chapter 5. A Fast Hidden-Surface Algorithm
The answer to the first question is yes, if we allow a group to consist of only
one object as weH (in the worst case, we declare each object as a new group).
Nevertheless, the creation of object groups will only reduce calculation time when
there is a reasonable proportion between the number of groups and the number
of objects.
The answer to the second quest ion is no. Even if we have arranged the objects
of our scene in groups that can be separated from each other, it may happen
that we have to undo a "Gordian knot." The reason for this is that it is hard to
find out whether the images of two groups are disjoint, even if their bounding
rectangles overlap. The creation of object groups can still be a great advantage in
such a case. Ifthe polyhedra {Ai} are elements of one group and the polyhedra
{Bj } elements of another group, and if we know the priority between these two
groups, all the priorities (Ai, B j ) are determined automatically. In a few cases,
however, this can be a dead end. AB we have said, it is sometimes necessary to
make use of the fact that between two objects there is no priority because their
images are disjoint. (Otherwise we might try to undo a "Gordian knot.") In these
few cases, we have to do the sorting without the use of object groups.
circumscribes the object in the world system. In the most convenient case, the
sides of the box are parallel to the coordinate axes. To keep the volume of the
box as small as possible, it is a good idea to calculate the box immediately after
the object has been defined and before it is manipulated by means of rotations:
#deftne EPSl le-4 (-+ Macros.h)
I
I void baunding_box(box, n, v) (-+ Proto.h)
Vector box[8]j /* Into this array we write the result. */
short nj / * N umber of points. */
register Vector v[]j /* Arbitrary set of points. */
{ /* begin bounding_boxO */
Vector m[2]j /* Space for two Vectors. */
Vector *min = m, *max = m + Ij /* For the argument lists. */
short i;
min_max_vec_of _pool(min, max, n, v)j
/ * Make sure that none of the coordinate differences is too small. This can
only happen when all the points are on aplane that is parallel to a
coordinate plane. The result could be an ambiguity in the priority test
and - in the worst case - a division by zero later on. */
for (i = Oj i < 3j i++)
if (Is..zero«*max)[i]- (*min)[i])) {
(*min)[i] -= EPSlj (*max)[i] += EPSlj
} /* end if */
create_coard_box(box, min, max)j
I} /* end boundingJJOxO */
For the result, we needed two functions that are useful in other parts of the code
as well. First we calculate the minimum and maximum coordinates.
#deftne Min_max(min, max, x)\ (-+ Macros.h)
if (x< min) min = Xj\
else if (x> max) max = Xj
#deftne Min_max_vec(min_v, max_v, v) {\ (-+ Macros.h)
Min_max«min_v)[X] , (max_v) [X] , (v)[X])j\
Min_max«min_v)[Y], (max_v)[Y] , (v)[Y])j\
Min_max«min_v)[Z] , (max_v)[Z], (v)[Z])j\
}
I I
void min_max:..vec_of _pool (min, max, n, v) (-+ Proto.h)
register Vector *min, *max; /* Pointers6 to the result. */
6The first two arguments of the function are pointers to Vectors. We can
also pass Vectors, because the corresponding function only takes the address
of the first element in the array. For superior style, however, we should pass a
Vector vas (Vector *) v in this case.
152 Cbapter 5. A Fast Hidden-Surface Algoritbm
z PlIilX
.. ..:,. '"
..... ':.-,:
........
x
FIGURE 13. How to create a "coordinate box."
Next we create a box, the sides of which are parallel to the coordinate axes. It
is given by the minimum and the maximum coordinates, Le., the coordinates of
two opposite vertices of the box (Figure 13):
I I
void create..coord_box(box, min, max) (- Proto.h)
register Vector box[8)j
register Vector *min, *maXj
{ / * begin create_coard.boxO */
register Vector *hLbox = box + 8j
staticUbytecorner[8)[3) = { 0,0,0, 1,0,0, 1,1,0, 0,1,0, 0,0,1, 1,0,1, 1,1,1,
O,l,l}j
register Ubyte *c = (Ubyte *) carnerj
for ( j box< hLboxj box++) }
(*box)[X) = (*c++ ? (*max) : (*min))[X]j
{*box)[Y] = (*c++ ? (*max) (*min))[y);
(*box)[Z) = (*c++? (*max) : (*min))[Z)j
} /* end for */
I} /* end create_coard.boxO */
Section 5.8. Bounding Boxes and Separating Planes 153
Now let us see how we can use bounding boxes for the determination of a sepa-
rating plane between two objects:
Let 0 1 and O2 be two (convex or not convex) objects with the bounding boxes
B 1 and B 2 • Each box has eight vertices and six faces. The planes of each face will
not intersect the corresponding object and are, therefore, potential separating
planes. Thus, we will do the following:
We take the first face of B 1 and find out on which side of the face the object 0 1
lies. (This is done by checking one vertex of 0 1 that is not on the plane of the
face.) If all the vertices of O2 are on the opposite side of 0 1 , we have found a
separating plane. Otherwise we continue the search with the other faces of B 1 •7
If none of the faces of B 1 is a separating plane, we continue the search with the
faces of B 2 •
If that does not work either and if none of the objects are convex, we give
up. (Separating planes are a good tool to speed up the program, but they are
not necessary for the priority algorithm.) If 0 1 and/or O2 are convex, we may
continue our search for a separating plane. Each plane of a face of a convex object
is a potential separating plane!
I
I void sep_plane_between..obj(s, obj1, obj2) (---+ Proto .h)
Halfspace * Sj
Polyhedron * obj1, *Obj2j
{ /* begin sep_plane_between..objO */
Halfspace * plane, *hLplanej
Vector dist, *normalj
short sidej
Boat cnstj
Polyhedron * tempj /* Used in the Swap macro. */
Face * f, *hL/j
register Vector * Vj
Vector * lo_vec, *hLvj
/* When the soap bubbles that contain the objects do not intersect, it is
neither necessary nor useful to calculate a separating plane. */
7Note that the test also works for objects with only one face (Le., simple
polygons), because the bounding box is a little removed from the polygon's
plane.
154 Chapter 5. A Fast Hidden-Surface Algorithm
/* In many eases, the faces of the bounding boxes are themselves separating
planes. */
Nowadays, people are used to seeing perfect computer-generated images and even
computer-generated movies of incredible realism. The gap between the products
of professional computer graphics studios and the possibilities of less sophis-
ticated graphics computers is enormous. Therefore, it is important to put an
emphasis on the acceleration of programs that produce an acceptably realistic
output.
In Chapter 5, we saw that the painter's algorithm is a powernd tool for the quick
rendering of even complicated scenes. This algorithm must, of course, be used
with our special object preprocessor, which splits objects according to their type.
One of the first to extend the painter's algorithm to the plotting of cast shadows
was Crow [CROW77]. His ideas turned out to be very useful for scenes with
comparatively few faces. In recent times, however, most people have preferred
different algorithms like ray tracing or the radiosity method [GORA84, ROGE90]
for this purpose. These algorithms work for any kind of scene and are also capable
of doing refiections and transparencies (refractions). They have only one great
disadvantage: they take a lot of computation time.
In this chapter, we will see that the painter's algorithm can be used to add
shadows, simple reflections and refractions, without any major loss of time. To
get perfect images of a scene, we can still use more sophisticated algorithms and
shading models. We can call this program a "movie maker."
To fulfill our task in an efficient manner, we have to insert two important sections
about convex hulls.
156 Chapter 6. Advanced Features
I
I void spatiaLhull(obj, syst) ( - Proto.h)
Polyhedron * objj /* Polyhedron with convex outline. */
short systj /* Screen system and light systems. */
{ /* begin spatiaLhullO */
register short ij
register Face * f, */1, *hL/j
static Edge edge_pool[MAX_POLY_SIZE]j
Edge * edge = edge_poolj
Vector * *contour[l]j /* Space for one pointer. */
short n, sizej
facdypes(obj, syst)j
hLI = (J = obj-> laces) + obj->no_of_facesj
for (j I< hi-fj f + +) {
if (J->darkface[syst]) continuej
n = 1-:::>no_of _verticesj
for (i = 0; i < nj i + +) {
/1 = (Face *) I->neighbor_faces[i/ j
if (f1 == NULL 11 f1->darkface syst]) {
edge->v1 = Screen_to.1ight(f -> vertices[i], syst)j
edge->v2 = Screen..:to.1ight(f -> vertices [(i + 1)% n], syst)j
edge++j
} /* end if (/1. .. ) */
} /*Cnd for (i ... ) */
} /* end for (f ... ) */
n = edge - edge_poolj
contour[O] = obj ->contour[syst]j
concaUo_polygons(&n, &size, contour, n, edge_pool, TRUE)j
Obj->size_of_contour[syst/ = n = size - l j
orienLptr_poly(n, contour O])j
I} /* end spatiaLhullO */
Section 6.1. Convex Hulls 157
The function spatial.hullO calls a routine that connects the edges to a (closed)
polygon. More generally, it is also able to connect sets of edges to several poly-
gons. For example, the contour polygon of a polyhedron that is an approximation
to a torus will consist of two (closed) branches.
* no..o!..branches = Ij
if (n == 0) {
branch-Bize[O] = Oj
return FALBEj
} /* end if (n == 0) */
while (n) {
branch[k] = Vj
edge = edge_poolj
* v + + = edge->vlj
* v + + = edge->v2j
edge_pool[O] = edge_pool[--n]j
/* Replace first edge by last one. */
branch-Bize[k] = 2j
hLv = v + 2 *nj
succ = branch[k][l]j
for (side = Oj side < 2j side + +) {
starLpoint = branch[k][O]j
do {
for (i = 0, edge = edge_poolj i< nj i + +, edge + +)
if (edge->vl == succ) {
succ = edge->v2j
breakj /* Quit the loop for (i ... ). */
} else if (edge->v2 == SUcc) {
SUCC = edge->vlj
breakj /* Quit the loop for (i ... ). */
} /* end if (edge->vl ... ) */
if (i == n)
succ = NULLj
158 Chapter 6. Advanced Features
else {
if (v>= hLv) {
Print("branch too long")j
breakj /* Quit the loop while (n). */
} /* end if (v ... ) */
*v++ = SUCCj
edge_pool[i] = edge_pool[--n]j
/* Replace edge that has been found by last one. */
} /*endif(i==n)*/
} while (succ && n > 0 && succ ! = starLpoint)j
branch..size[k] = v - branch[k]j
if (succ == starLpoint) { / * Closed polygon. */
if (is-O._CO'nvex_CO'ntaur)
return TRUEj
else
breakj /* Quit the loop for (side . .. ). */
} else if (iSJLconvex_contcrur) {
Print("Warning: contour is not closed")j
return FALSEj
} /* end if (succ == start-J)Oint) */
if (side == 0) {
Vector * temp, * * bl, * * b2j
For the intersection of two convex hulls, it is necessary that the contour polygon is
oriented counterclockwise. This can be achieved by a routine orienLptr _polyO.
We check whether the area of a triangle that is given by three points of the
polygon (with the indices il < i 2 < i3) is positive or not. If it is negative, we
change the order of the vertices of the polygon.
#deflne Area_of_2d_triangle(a, b, c)\ (_ Macros.h)
a)1Xj * (b)(Yj- (a)[Y] * (b)[X] + \
!b) X * (e) Y - (b)[Y] * (c)[X] + \
c) X * (a) Y - (c)[Y] * (a)[X]
#deflne SMALL_AREA le - 2 (local macro)
Seetion 6.1. Convex Hulls 159
I I
void orienLptr _poly(n, poly) (-+ Proto.h)
short n;
register Vector * *poly; /* Array of pointers. */
{ /* begin orienLptr _poly 0 */
register Vector * *pI = poly + 1, * * p2 = poly + 2;
Vector * temp, * * hLp2 = poly + n - 1;
float area;
short orientation = 0;
#undef SMALLAREA
Of course, the determination of the spatial hull of an object works much faster
than a general routine for the determination of the convex hull of an arbitrary
set of points. Sometimes, however, such a routine ean be useful. For this reason,
we include the listing of a function hullO, whieh takes an array of n given points
(Vectors or Vector2s) and which ealculates the oriented eonvex outline.
In fact, such an algorithm is needed so frequently that many different approaches
have been tried (some of them are described in [PURG89]). The algorithm we use
is based on a recursive algorithm by Bykat [BYKA 78]. Calculation time inereases
only linearly with the number of points, provided that the points are distributed
homogeneously. We will not need any trigonometrie funetions.
Figure 1 illustrates the idea. In a) we determine the leftmost point L and the
rightmost point R of all the given points. The connection LR between these two
points divides the hull into two convex parts. For both these parts, we now start a
160 Chapter 6. Advanced Features
(local macro)
#define MAXP 1000
statie P _ptr * Point_chain[MAXp]j
I I
void hull(size, result, n, given_points, dim) (--t Proto.h)
short * size; /* Number of points on the hull. */
Vec Hesult[]; / * Array of pointers to the vertices of the hull. The hull is
oriented ccw.*/
short n; / * N umber of given points. */
Vec given_points[ ]; /* Vector2s or Vectors. */
short dim; / * 2D or 3D? */
{ /* begin hullO */
register ßoat * gp = (ftoat *) given_points;
register short i;
ftoat xmax, xmin, ymax, ymin;
P _ptr *min, *max;
P _ptr * residual;
if (n > MAXP)
Print("Too many points for convex hull \n"),
n = MAXP;
min = max = residual = PoinLchain;
for (i = 0; i< n; i++, gp += dim)
PoinLchain[i].next = i + 1,
PoinLchain[i].p = (Vec *) gp;
Point_chain[n - 1].next = NO_MORE;
gp = (ftoat *) given_points;
/* Determine min and max. */
xmax = xmin = gp[X]; ymax = ymin = gp[Y];
for (i = 1, gp += dim; i < n; i++, gp += dim) {
if (gp[X] < xmin)
xmin = gp[X] , min = &PoinLchain[i];
if (gp[X] > xmax)
xmax = gp[X], max = &PoinLchain[i];
if (gp[Y] < ymin)
ymin = gp[Y];
if (gp[Y] > ymax)
ymax = gp[Y];
} /* end for (i) */
/* Remove min and max. */
if (Idx(min) == 0)
residual++;
else
(min - 1)->next = (Idx(min + 1) < n?
Idx(min + 1) : (Idx(max) O? 1 0));
if (max == min + 1) {
if (Idx(min) == 0)
162 Chapter 6. Advanced Features
residual++;
else
(max - 2)->next = max->next;
} else if (Idx(max) == 0)
residual++;
else
(max - 1) ->next = max->next;
* size = 0;
divide_points(min, max, &residual, size, result, 2);
I} /* end hullO */
if (!Is_in_chain(lo» {
/* No point on this side. */
min->next = Idx{max)j
result[(*size)++] = max->pj
} else if (lo->next == NOMORE) {
/* Insert the only point lo. */
min->next = Idx{lo)j
lo->next = Idx{max)j
result[(*size)++] = lo->pj
result[(*size)++] = max->pj
} else {
/* We are not ready yet (more than one point). */
if (lo == p_min)
lo = Next(lo)j
eise {
p = 10j
while (Next(p)!= p_min)
p = Next(p)j
p->next = Next(p)->nextj
} /* end if (lo) */
p_min->next = NOMOREj
divide_points(min, p_min, &lo, size, result, l)j
divide..points(p_min, max, &10, size, result, l)j
p = 10j
} /* end if (Is_in_chain) */
if (sides = = 2) {
if (!Is_in-chain(hi» {
/* No points to insert. Concatenate min with max. */
result[( *size)++] = max->pj
} else if (hi->next == NOMORE) {
/ * Only hi found. */
max->next = Idx{hi)j
result[(*size)++] = hi->pj
result[( *size)++] = max->pj
} else {
if (hi == p.max) {
hi = Next(hi)j
} else {
p = hij
while (Next(p)!= p_max)
p = Next(p)j
p->next = Next(p)->nextj
} /* end if (hi) */
p_max->next = NOMOREj
divide_points(max, p_max, &hi, size, result, l)j
164 Chapter 6. Advanced Features
I I
short relative_pos..oj -hulls(z, nl, hulll, n2, hu1l2, BYst) (- Proto.h)
float z[2]; /* z-values of two space points with the same image. */
short nl, n2j /* Number of vertices of the hulls. */
Vector *hull1[], *hu1l2[); /* Arrays of pointers. */
short systj
{ /* begin relative..pos..oj JtullsO */
register Vector **al, **blj
register Vector **a2, **b2j
/* We will try to intersect two sides (albi), (a2b2) of the polygons. An
intersection point will only be taken into account when it belongs to
both sides. */
Vector **hLl = hulll + nl, **hi2 = hu1l2 + n2j
float tl, t2j /* Parameter values of the intersection point. */
float dz, previous.ßzj /* Distance of corresponding points. */
Bool delay..decision = FALSE;
/* If the z-values of the corresponding points differ only slightly (i.e.,
when dz is small), we will not rely on the result. (After all, we are
talking about the priority between two entire polyhedral) In such a
case, we will continue our search for intersection points. */
Vector2 dl, d2j /* Direction vectors of two edges. */
Section 6.2. The Intersection of Convex Hulls 165
If n is not normalized, the number dR will be the distance scaled by the constant
length ofn.
To determine whether a point Q is inside a given convex polygon P or not we
do the following: we draw a line from the first point P of P through Q and
check whether there is a residual intersection point P with the polygon P. Such
a point exists when we can find two vertices R and S of P that lie on different
sides of PQ (sign(dR) =f: sign(ds)). (Otherwise Q is outside the polygon.) The
residual intersection can then be calculated by means of a linear interpolation:
(2)
Now the point Q is inside the polygon P, when it is inside the segment PP
(0< A = !PQ!/!PP! < 1), otherwise it is outside the polygon.
In our case, the given polygon P is the spatial huH of a convex object. Therefore,
our function inside_polyO can also check whether the space point Q is behind that
polygon or not: we just have to compare the z-value of Q with the interpolated
z-value of a point Q that lies on PP, and the image of which coincides with the
image ofQ.
Section 6.2. The Intersection of Convex Hulls 167
FIGURE 2. How to find out whether a point is inside a convex polygon or not.
I
Bool inside_poly(q, n, poly, disLsyst, z)
(-t Proto.h)
float q[]j /* Point Q that is to be checked. */
short nj / * Size of polygon. */
Vec **polYj /* Array of pointers to the vertices of the polygon. */
float disLsystj /* Distance of proj. center (only for dim = 3). */
float z[]j /* z-values of Q and Qo. */
{ /* begin inside_polyO */
float * p = **polYj /* First point P on polygon. */
float * T, *Sj / * Side RS of the polygon. */
float disLr, disLsj /* Distances of these points. */
float pO[3)j /* Residual intersection point Po on PQ. */
float normal [2) , cnstj /* Implicit equation of PQ. */
register short ij
register float tj /* Parameter. */
break;
r = S; disLr = disLs;
} /* end für (i) */
if (i = = n) / * All the points are on the same side. */
return FALBE;
/ * Interpolate intersection point Po. */
t = disLr / (disLr - disLs);
für (i = 0; i< Dirn; i++)
pO[i] = r[i] + t * (s[i] - r[i]);
/* Calculate ratio PQ / PPo. */
/ * Which coordinate is more accurate? */
i = jabs (pO [X] - p[X]) > jabs(pO [Y] - p[Y]) ? X : Y;
t = (q[i] - pli]) /(pO[i] - pli] + EPB); /* No div. by zero! */
if (!Between_zero_and_one(t)) /* Q is outside the interval. */
return FALBE;
if (Dim = = 3) {/* z-values of Q and Qo. */
Boat w1, w2; /* Apply Transformation T 2 • */
T2(z[O], q[Z], syst);
T2(w1, p[Z], syst); T2(w2, pO[Z], syst);
z[l] = (1 - t) * wl + h w2;
}
return TRUE;
J /* end inside_polyO */
The functions we have described so far in this chapter are quite useful for priority
determinations and - as we will see in Chapter 7 - for hidden-line algorithms.
For the clipping of filled polygons, we need another very important function
clip_convexO, which determines the intersection polygon oftwo convex polygons.
This function is one of the most frequent routines in our program. We need it,
for example, every time we plot a shadow polygon or - as we will see in the
following section - when we clip a polygon with a reflecting face. Therefore, it
has to be perfectly optimized.
In Section 2.5, we explained the Hodgman-Sutherland algorithm for the clipping
of an arbitrary polygon with the drawing window. The rectangular drawing area
is just the special case of a convex polygon. The obvious thing to do is to extend
the algorithm to the clipping with general convex polygons.
Figure 3 shows how this can be done: we cut the given polygon P (which may be
convex or not) with each side ofthe clipping polygon C (in an arbitrary order).
A side PQ (position vectors p and if) of C divides the image plane into two
halfplanes. Let Ti be the oriented normal vector of PQ (n points to the outside
of C). Then the implicit equation of PQ Ti x = np = const (see Equation 10)
helps to determine whether a point R (position vector T) is on the same side
Section 6.2. The Intersection of Convex HuBs 169
....... ~
If you take a closer look at the source code of clip_convexO you will see how this
algorithm can be irnplemented by means of pointer arithmetic, which makes the
function efficient and needs less memory.
#define STATIC static (--+ Macros .h)
/* If the value of a local variable is only needed as long as the function is
active, we will not declare the variable statie, since this takes extra RAM.
static variables speed up the code, however, because they do not have to be
allocated at the stack each time the function is called. When your system has
enough RAM left, you can accelerate functions that are needed frequently
by declaring sorne temporary variables statie. If you run out of RAM, write
#define STATIC
*/
170 Chapter 6. Advanced Features
* which = clipping_polYj
/* We assume that clipping..poly is the intersection polygon. */
r = rl, b = clipping_polYj
for Ci = Oj j < sizej j++, b += dirn)
r[j] = bj
for (i = 0; i< nlj i++) {
a = poly + i * dirnj
b = (i + 1 < nl? a + dirn : polY)j
n[X] = b[Y] - a[Y]j n[Y] = a[X] - b[X]j
t = fabs(n[X]) + fabs(n[Y])j
if (t< EPS) continuej
n[X] /= tj n[Y] /= tj
Section 6.2. The Intersection of Convex Hulls 171
6.3 Shadows
Shadows are very important for the realism of computer-generated images. With-
out shadows, the images appear two-dimensional, and depth comparisons can be
misleading. Since shadows are projections from the light source, a scene with
shadows can be interpreted as a multiple projection (in one and the same im-
age). This allows human imagination to reconstruct the scene in space.
A "shadow" is defined as the darkness cast by an illuminated object. In nature,
we are used to seeing images that are illuminated by one single light source that
is at an "infinite distance" (provided that the weather is fine). Artificial light
sources can be multiple and need not necessarily be point sources. In computer
graphics, only few algorithms are known that produce the shadows of all kinds of
objects. Of course, ray tracing [GLAS 90] and the radiosity method [FOLE90] are
among these algorithms. Another one ("shadow bufIering" [THAL87, FOLE90j)
will be discussed in detail in Section 7.4.
In this section, we will see how the shadows of polygons can be generated by
means of a method that is based on convex polygon-clipping. Usually, it takes a
lot of computation time to plot shadows. Under specific circumstances, however,
this is not necessarily the case, as we will see in this section.
Let Ln be the n-th light source and let PI and P2 be two closed planar polygons
(Figure 4) in space. From the geometrical point of view, the light system con-
nected with Ln (Section 2.3) is the same as the screen system connected with
the projection center C. In the screen system we say: when the images (Le.,
Section 6.3. Shadows 173
the projections onto the image plane 11") of the polygons PI and P2 overlap, the
one that is closer to the projection center obscures (parts of) the other polygon.
When we apply the painter's algorithm, the obscured polygon has to be plotted
first.
The analog in the n-th light system is: when the projections on the imaginary
projection plane 7rn of the light system of PI and P2 overlap, the polygon that is
closer to Ln casts shadows on the other polygon. When we apply the painter's
algorithm, we add these shadows immediately after having plotted the shadowed
polygon. Then we do not have to think ab out visibility.
The shadow polygon S can be added by means of 2D-clipping. The (convex)
intersection polygon sn (with the vertices sn) of the projections P~ and P~ of
the polygons has to beprojected onto the plane 'IjJ that carries the shadowed
polygon. For this purpose, we intersect the light ray LnSn with 'IjJ, and we get
the vertices S of the shadow polygon S.
For a more accurate computation, this projection should be done in the world
system. In the light system, avertex sn of the intersection polygon sn, which
lies entirelyon the projection plane 1I"n, may have the position vector 8". In the
world system, we get the coordinates of points on 11"n by transforming their light
coordinates by means of a simple rotation, which is described by the matrix
R;;I == InvRot[n] (s7l.* = 8" R;;I). We intersect the ray Lnsn with the plane 'IjJ
of the face and we get a vertex S of the shadow polygon in space. The projected
point Sc = es n 11" is a vertex of the image of the shadow polygon.
Which color should we choose for the shadow polygon? For sophisticated illu-
mination models, this is a very difficult question because the color may vary for
each pixel, depending on reflections from other objects, the colors of other light
sources, etc. For our purposes, it will be enough to fill small shadow polygons
with one and the same shade of a color, and, when the polygon is large, to vary
the shade slightly, depending on its distance from the observer. Furthermore, the
color of the shadow will only depend on the color of the face and it will not - as
in more advanced models - be influenced by the colors of the light sources or by
those of surrounding objects. This may seem to be a tremendous simplification,
but in most cases, the images produced in this way are sufficiently realistic. In
our simplified model, a shadow polygon is basically filled in the same manner as
the image of the face, only with a darker shade.
How much darker the shade of a shadow has to be is not subject to any strict
rules. Imagine two photographs of one and the same building with the same angle
of incidence of the light rays, but with different light intensities (fog or dust in
the atmosphere, etc.). The contrast between the shadows and the illuminated
faces will be different. In general, pictures look realistic if we use approximately
one-half the value of the variable reduced_palette in Formula (2).
Since in our illumination model the color of the shadow polygon only depends on
the color and the depth of the corresponding face, we can plot several (convex)
174 Chapter 6. Advanced Features
FIGURE 5. Convex polyhedra only cast shadows on other polyhedra and not on
themselves.
Seetion 6.3. Shadows 175
shadow polygons in an arbitrary order onto the face. The result will be a set of
(convex or not convex) shadows (Figure 5).
This way of painting over existing polygons is very efficient, but it has a disad-
vantage that is due to the fill algorithm of polygons. Let pe be the image polygon
of a face and let Sc be the image of a shadow polygon that is not completely
inside pe. Then pe and se will have part of the side pp of pe in common (e.g.,
the line S8). Even though from the mathematical point of view the line S8 lies
on pp, the filling routine for se will not always cover all the pixels that pp and
S 8 seem to have in common. (This has nothing to do with the screen resolution.)
Some light er pixels may be left at the border of Sc. The reason for this is that all
the coordinates of the vertices are rounded off before the images of the polygons
are drawn. One way to avoid such light pixels is to enlarge the shadow polygon
at each vertex by one pixel. In most cases, this will work and the image will be
improved. If your compiler has a routine linewidthO that allows to draw thicker
lines, you can also draw the outline of a shadow polygon with a "linewidth of
2 pixels." If you choose a shade for this outline that is a bit brighter than the
rest of the shadow polygon, you can create an "aura" around it. Artificial light
sources (which are not perfect point sources) create such auras so that this trick
is a simple method of simulating this effect without any complicated calculations.
So far we have only talked about one light source. What happens when we deal
with severallight sources? In this case, we will have shadow polygons of different
"grade" (this idea is mentioned in [ANGE87]).
First, we plot the shadow polygons {Si} for all the light sources Li. These poly-
gons of grade 1 will appear comparatively light, depending on the intensities
of the residual light sources. Then we clip all the shadow polygons of grade 1
that are cast by other light sources. Thus, we get shadow polygons of grade 2:
{Sij} = {Si n Sj (i i= j)}. They have to be plotted in a darker shade because
they are not illuminated by two light sources. If we have only two light sources,
we are finished. Otherwise we have to check whether there are shadow polygons
of grade 3: {Sijk} = {Sij n Sik (j =I k) }, which will be even darker than the
shadows of grade 2. The algorithm has to be continued until the grade of shadows
equals the number of light sources.
What we have described so far is not very exciting. When we really check whether
each polygon of the scene is shadowed by another polygon, complicated scenes
will need a lot of computation time. To speed up the process, we use the same
techniques as for the painter's algorithm:
By means of the object preprocessor, the faces of the scene are connected to
primitives (objects, object groups, slices, etc.). Let a face belong to a primitive W1
and another face belong to a primitive W2. When the bounding rectangles of W1
and W2 do not overlap, we do not have to check anything. When the rectangles
overlap, and when the result of the priority test in the n-th light system is
that Wl is doser to the light source Ln than W2, all the faces of Wl potentially
cast shadows on all the faces of W2' If in such a case Wl is convex, its outline
176 Chapter 6. Advanced Features
in the light system is convex, too. This means that we only have to clip the
outline of '11 1 with each face of '112' This is the reason why scenes that consist
only of convex primitives can be rendered with shadows without any major loss
of speed.
Convex polyhedra have the convenient property that they cannot cast shadows
on themselves (Figure 5). This is why the shadow-test does not have to be ap-
plied to the other faces of the primitive the face belongs to. Non-convex polyhe-
dra, however, may cast shadows on parts of themselves. The faces whose shadows
can be seen must have their illuminated side turned to the viewer. (The viewer
can see the illuminated side of a face when the projection center C and the light
source Ln are on the same side of the plane of the face, otherwise the face is
"dark.")
We have already dealt with important classes of non-convex polyhedra like func-
tion graphs and approximations to surfaces of revolution. All these surfaces are
at least split up into slices. These slices are separated by parallel planes a. The
special separating plane an, which coincides with the light source Ln, plays a
similarly important role as the "critical planes" ao played for the hidden-surface
algorithm in the Sections 5.2-5.4. If <P is a sliced surface and if an intersects <P, the
two partial surfaces <PI and <1>2 on the different sides of an cannot cast shadows
on each other. A slice of a partial surface can only cast shadows on slices that
are further away from an. The "critical slice" that connects <P1 with <P2 may cast
Section 6.4. Reflections 177
6.4 Refiections
Like shadows, reflections help to make spatial objects look more realistic. In com-
bination with shadows, the effect is even more enhanced. However, this works
only with a limited number of reflecting elements in a scene. Pictures with bounc-
ing reflections are nice to look at and they are a test for really good algorithms
(like ray tracing). On the other hand, such complicated images can be confusing.
In this section, we will only talk about simple reflections, i.e., reflections on one or
several reflecting faces that cannot produce bouncing reflections (for example, the
faces of individual convex objects). We will not talk about reflections on curved
surfaces or ab out bouncing reflections, since the calculation of such images takes a
lot of CPU time. Reflections on single "mirrors," however, can be done amazingly
fast, provided that the scene consists of the primitives that are generated by our
object preprocessor. In relation to the computation time, the quality ofthe result
is satisfactory.
Consider a scene of objects, where only one face is reflecting (Figure 7). The
mirror plane IL separates the space into two halfspaces. All the objects ofthe scene
that are completely at the reflecting side of the mirror, or that are intersected
by the plane IL, are potentially visible on the mirror's face.
We might also say that the imaginary object "doubles," which we get when
we reflect the objects at the plane JL, can be seen through a "window" (the
transparent reflecting face R) from the projection center.
Consequently, the painter's algorithm can also be applied to the plotting of
reflections. We plot the images of the faces of our scene in the above-mentioned
order, until we have plotted the image of the reflecting face. Before we plot the
rest of the image, we do the following:
First we reflect all the objects that coincide (completely or partly) with the
halfspace that is determined by the reflecting side of the mirror plane JL. When the
178 Chapter 6. Advanced Features
,,
\,
Ln '\, p-
I\.
""
\
FIGURE 7. When we look into a mirror, we can see the "doubles" of our objects
through the "mirror window."
scene is static (i.e., the objects remain in their relative positions), this calculation
can be done before any drawings. Otherwise the "reflection pool" (where we store
the coordinates of the reflected vertices) has to be recalculated for each frame.
Among the reflected objects, we select those that can be seen through the mirror
window R. We can check this with the help of bounding rectangles. (Do the
bounding rectangles of the reflected objects overlap with the image R C of the
reflecting face R?) With convex objects, we can find out which position the
outlines of the objects have with respect to the outline of the clipping window.
By means of this test, we can find out which of the selected doubles are partly
visible and which are completely visible.
Now we sort the selected objects with the help of our priority test and then we
plot them as usual, one after the other, according to the priority list. The images
of the faces of objects that are only partly visible have to be clipped with the
clipping window R C •
The shade of a reflected face is basically the same as that of the real face. The
reflected face will appear a bit darker, however, because the light rays that are
reflected from the original face make adetour over the mirror. (In all reflections
the light rays are split up. In addition to that, the virtual distance from the
viewer increases.)
This simplified shading model works fast and is satisfactory for many kinds of an-
imations. Things get more complicated when the reflecting face itself illuminates
the scene indirectly, i.e., with reflected light. (This occurs when its reflecting side
is turned towards a light source and when the reflecting face is not in the shadow
Section 6.4. Reflections 179
FIGURE 8. The simplified shading model works very fast, especially for scenes with
only one reflecting face.
We get the coordinates of L~ when we reflect the given light source Ln- Of
course, the intensity of the point source L~ is less than the intensity of Ln
(because reflections reduce the light intensity). Furthermore, the reflected light
can only come through the mirror window R.
180 Chapter 6. Advanced Features
If we go one step furt her and calculate cast shadows, the reflected light may be
additionally obscured by shadow polygons cast from the objects in front of the
reflecting face, which will happen when the light comes from Ln. We get the
same shadow polygons when we cast shadows from the object doubles and when
the light comes from L;'. Consequently, we may say that the refiected light is
additionally obscured by the refiected objects.
These considerations show that difficulties will increase dramatically when we
have several reflecting faces. As long as the faces are in such a position as to not
allow any bouncing reflections, the simplified shading model (Figure 8) works
fine (and comparatively fast). We plot the scene one face after the other. Every
time we plot a reflecting face, we paint the corresponding reflected scene on it.
Once we calculate shadows, however, each reflecting face that is turned towards
an existing light source will produce a new light source. Therefore, we should
leave such complicated scenes to really sophisticated programs like ray tracing.
6.5 Patterns
Consider an object with patterns like the one in either Figure 6 or Figure 9. The
patterns are polygons painted on different faces. Of course, it would not be wise
to declare these polygons as objects in their own right. In this case, the computer
might have trouble with the priority list because the patterns touch the object.
In addition to that, the number of objects would increase, so that the processing
would be slowed down unnecessarily.
For this reason, we combine each pattern with the corresponding face. After the
face has been plotted, its patterns are added. A face can have any number of
patterns. The patterns should be split into convex polygons.
If we want to add shadows, we first plot the shadows on the face. The images
of the shadow polygons of different degree (Section 6.3) are stored temporarily.
Then we plot the patterns in the following way: If the whole face is "dark" (i.e.,
if it is a backface in all the light systems), all its patterns can be plotted in a
darker shade. If the face is lit by at least one light source, we clip the images
of all the patterns with the precalculated shadows of different degree (starting
with degree 1).
This method is very efficient in terms of computation time. In general, we can
say that the number of patterns has much less influence on the CPU time than
the number of faces.
General patterns (polygons) can be described by two-dimensional coordinates in
a data file (Figure 10):
CUBE gray box voll 10 10 10
patterns face 1 polygon LEFT white -1 -1, 1 -1, 1 1, -1 1
face 2 polygon FRONT yellow -2 -2, 2 -2, 2 2, -2 2
face 3 polygon RIGHT black -3 -3, 3 -3, 3 3, -3 3
Section 6.5. Patterns 181
In
I"
~~~~~~--------~~
P2:
~~~~~~~----~~~
In this section, we will show that the painter's algorithm can still produce ac-
ceptable results (at least for computer animations) for transparent plane surfaces
and transparent solid polyhedra. This simplified model can easily be extended
to single reflections and shadows.
Seetion 6.6. Refraction, Transparent Objects 183
FIGURE 12. A swimming pool with constant depth from two different point of views.
asina
f(o:) == (P2 - a) tano: + . - pl = O. (5)
"';c2 - sm 2 0:
When the projection center is outside the water (P2 > a, 7' ~ 0: ~ ~), the only
real solution of this equation can be found by means of NEWTON's iteration
(see Section 8.2).
7
Hidden-Line Removal
For technical drawings and publications, we often have to produce line drawings,
mainly because they are easier to handle and to reproduce. Sometimes it is also
easier to demonstrate specific facts by means of line drawings than by means
of painted images. For this reason, we dedicate this chapter to the removal of
hidden lines. We will distinguish between algorithms that are screen-oriented
and algorithms that can be used for plotter drawings.
resolution is 1280 x 1024), this poses no problems. For lower screen resolutions we
will either use algorithms that can be used for plottet drawingsl or we will create
Postscript images that work in the same manner as the painter's algorithm.
FIGURE 1. The adapted painter's algorithm removes hidden lines very quickly, hut is
dependent on the screen resolution. However, it is perfectly suited to create PostScript
files.
1 With the appropriate software, the drawings can also be done by printers,
especially by laser printers like the illustrations in this book. In this case, the
accuracy of the image is not dependent on the screen resolution!
Section 7.2. Hidden-Line Removal on Objects with Convex Outlines 187
the- convex outlines of the images of the other objects, which may obscure the
object according to the priority list (Figure 2).
In order to find out whether the outline O~ of the k-th object obscures pCQc we
can check whether or not the bounding rectangles of the image line pCQC and
of O~ overlap. If the rectangles overlap, we try to intersect pCQc with O~. When
we get two intersection points Skl and Sk 2 , we store the parameters Ak 1 , Ak2,
which lead to their position vectors Sk 1 and Sk2 (Ski = pe
+Aki(i' - pe».
We
sort the parameters (Ak 1 < Ak2) and modify them by
If Akl = 0 and Ak2 = 1, the edge is completely hidden and we can turn to the
next image line. Otherwise the "zone" [Ak 1, Ak2] on pCQc is hidden.
FIGURE 2. The image of the edge of a convex object can only be hidden by the
outlines of other polyhedra. Note that the primitives do not intersect, even if it looks
as if they did.
Here is the C code for the determination of the hidden zones for convex objects:
I I
void invisible_zones(k, zanes, p, q, obj) (-4 Proto.h)
short * kj /* Number of zones. */
Vector2 zones( ]; /* Has to be allocated outside the function. */
register Vector p, qj /* The vertices of the edge. */
Polyhedron * obj j / * The object the edge belongs to. */
188 Chapter 1. Hidden-Line Removal
I I
Bool segmenLcuts_poly(lambda, p, q, k, poly) (- Proto.h)
float lambda[]j
Vector p,qj
short kj
Vector **polYi
{ /* begin segmenLcuts_polyO */
register Vector **a = poly, **b = a + 1, **hLpoly = a +kj
short n = Oj
float t1, t2j
float tempj
(2)
190 Chapter 7. Hidden-Line Removal
Now we eome to J,L2' We start with J,L2 = 1 and, for all the hidden zones, check
whether J,L2 is inside a hidden zone. If this is the ease, we transfer 1'2 to the
beginning of the respective zone:
(3)
Now we plot the part of pCQc that is given by the interval (Jtb J,L2]' To enhance
the three-dimensional aspect of the image, it is a good idea to shorten the line
"a little bit," 2 if J,Li is not Oor 1 (Figure 4). At the same time, we store the upper
limit of the zone J,Lo = Ak2 of the hidden zone.
For the next visible zone, we let J,Ll = J,Lo. If 1'1 = 1, we are finished, otherwise
we have to reenter the loop.
The eorresponding C code looks like this:
I
I void plot_visible-zanes(p, q, n, pool) (---+ Proto .h)
Vector p, qj /* The line segment (sereen coordinates). */
short nj / * Number of hidden zones. */
Vector2 pool[]j /* n hidden zones. */
{ /* begin ploLvisible-zonesO */
Vector a, b, pq;
Vector2 * lambda = pool, *hiJambda = lambda + n;
register ftoat mI, m2, tJtelp;
Bool go_up, go.1lown;
In which order do we draw the images of the edges? When we plot them according
to the edge list, we first have to eliminate the edges on the backfaces, which is
not always easy. Therefore, it is best to plot the edges as sides of the faces. (We
know the drawing order of the faces.)
We only have to make sure that we do not plot the images of some edges twice
because an edge is always the intersection line of two faces. (The only edges,
the images of which will never be plotted twice, are the contour edges, because
they are the intersection lines of a frontface and a backface, and we ignore the
backfaces. )
One idea to prevent double drawing is the following: let ~ be the object, the image
of which we intend to plot. Now we copy the edge list of ~ into a temporary
table. Before we draw the image PCQc of an edge PQ, we check whether we can
find the edge PQ (or QP) in the table. When we can find it, we plot pCQc and
eliminate the edge PQ from the temporary table.
When we remove the frontface of a convex polyhedron~, we can look into the
polyhedron. In this case, we also have to check the edges on the back of W, to
Section 7.2. Hidden-Line Removal on Objects with Convex Outlines 193
removed frontface
FIGURE 5. How to modify hidden zones when they are overlapped by the image of
a removed frontface.
clip their images with all the outlines of the other objects that obscure 4> and to
intersect the hidden zones Ski Sk~ with the image F C of the removed frontface.
Whenever a hidden zone is overlapped by the polygon F C , it has to be modified
or even to be split into two zones (Figure 5).
When we remove more than one face of 4>, things get more complicated. First,
we have to modify all the hidden zones by means of the images of all the re-
moved frontfaces. Secondly, it may be possible that we can "look through" the
polyhedron. To find out whether this is the case, we do the following (Figure 6):
• Hall the removed faces are backfaces, the object can be treated like a convex
object.
• H all the removed faces are frontfaces, we cannot look through the poly-
hedron. In this case, we can treat the object like a convex object when we
compare it with the other objects.
• H there are frontfaces and backfaces among the removed faces, we clip the
image of each removed backface with the images of all the removed front-
faces. Thus, we get a set {Sj} of (mostly cop,vex) intersection polygons
through which we can look through the polyhedron. H this set is empty,
there is no "hole" in the image of 4>. If it is not empty, we have to store it
as a part of the outline of 4>.
H the image of 4> has holes (given by the set {Sj} of convex polygons), we
have to modify the hidden zones that are produced by the convex outline
of the image of 4> like in Figure 6.
194 Chapter 7. Hidden-Line Removal
FIGURE 6. How to check whether we can look through a polyhedron with convex
outline.
As we can see, the precise hidden-line algorithm for objects with convex outlines
is still reasonably fast, even if the objects are hollow.
P"(x" = x, y" = y, z" = zj(d - z)) of Section 2.3 to all the vertices of the
polygons. If we project the new set of polygons orthogonally from the infinite
point of the positive z" - axis onto the image plane, we will get the genuine
image of our scene again (Figure 7).
Now consider a grid [0, M - 1] x [0, N - 1] that is placed over the screen. We
initialize each field value of a two-dimensional array Zbu![M][N] by the smallest
z" -value. Then we "buffer" each polygon of the scene in the following way: for
each grid point Xe(i, j) at the inside ofthe image polygon pe = p .. e, we have
aspace point X" on the normal to the projection plane 71' on the plane of p •.
This "depth" z" is stored if and only if z" > Zbu![i]li]. By doing this for each
polygon of the scene, we store the visible parts of our polygons .
..... .....
image polygon
transformed
polygon
FIGURE 7. Depth buffering.
At any time of the rendering of the scene, the visibility test is simple: an arbitrary
space point P(x, y, z) is visible (at the moment when the image pixel is drawn)
if and only if the corresponding field value contains the value z" = z j (d - z) (±
a certain tolerance, depending on the width of the grid).
In the simplest case, the grid dimensions M and N are the dimensions of
the screen (measured in pixels). Then a point P(x, y, z) corresponds to the
field value Zbu![raund(x)][round(y)]. If we want to have control over the two-
dimensional field of ftoat variables, we need 4 M N bytes, Le., 3 Megabytes for
a screen with a resolution of 1024 x 768 or 1 Megabyte for a screen with a res-
olution of 640 x 400! Because we have to admit a certain tolerance anyway, it
196 Chapter 7. Hidden-Line Removal
will be enough to store the z* -values as short numbers, where we assign the
smallest z*-value to 0 and the greatest z*-value to the largest unsigned short
number (~65000). This reduces the necessary memory to 50% (2M N bytes).
As long as we have enough RAM memory at our disposal, we prefer this kind
of storage. Otherwise, we divide the screen into several horizontal zones and
display the scene in several steps - at the cost of computation time, because
many polygons will have to be proceeded two or even more times if they overlap
two or more zones. 4
The allocation and the freeing of the two-dimensional grid can be done by a
function
#define Alloc_huge_2fLarray(type, array, n, m) {5 \ (~Macros.h)
array = (type **) mem_alloc(n, sizeof(type *), "pointers");\
for (i = 0; i < n; i++)\
array[i] = (type *) mem-D-lloc(m, sizeof(type), "2D-array");\
}
typedef unsigned short Ushort; (~ Types.h)
Ushort Init(* * Zbuf, NULL),
Init(Max-x_grid, 100), Init(Max_y_grid, 50); (~ Globa1s.h)
Boollnit(Kbujjering, FALSE); (~ Globals.h)
Global I nit_Jptr( move_scan, quickmove); (~ Proto.h)
Global I niLJptr( dmw_scan, quickdraw); (~ Proto.h)
I
I void z_buffer(on) (~ Proto.h)
Boolon;
{ /* begin z_buJ JerO */
register short i;
Z_buffering = on;
if (on) {
move-Bcan = move_buJ Jer;
draw_scan = quick_draw_buffer;
Max-x_grid = Max-x - Min-x + 1;
Max_y_grid = Max_y - Min_y + 1;
if (ZbuJ == NULL)
Alloc_huge_2d_array(Ushort, Zbuf, M ax_y_grid,Max-X_gritf) ;
4In an extreme case, each line of the screen can be considered as a zone.
Usual1y this is done in connection with scan-line algorithms [FOLE90].
5In Chapter 3, we developed a very similar macro Alloc_2d..array( type, array,
n, m). This macro allocates memory allocation al1 the space for the array at
the same time. On computers with 1ess memory, this might cause an "out of
memory error," even though there is enough memory 1eft. Thus, we split the
memory into n sectors. This will have no influence on the access to the variable
ZbuJIil Lil, which is transformed into the expression *HZbuJ + i) + j) by the
compiler.
Section 7.3. Depth Buffering for Visibility 197
} else {
move-Bcan = quick_move, draw_scan = quick-Bcanj
FreeJ!,uge..2d-.array(Zbuf, Max_y_grid)j Zbuf = NULLj
}
J /* end z.lJufferO */
I I
void clear ..z_buf ferO ( - Proto.h)
{ /* begin clear -z_buf ferO */
register Ushort * grid, *hLgrid, ij
When we proceed a polygon, we first clip it with the clip volume (Section 2.5).
The clipped polygon is then proceeded as usual. At the end of the function
poly_moveO (Section 4.6), we add the lines
if (Z_buffering)
seLplane_point(O, a)j
and at the end of the function poly_drawO (Section 4.6), we add the lines
if (Z_buffering && Cur_vtx - Vtx < 3)
seLplane_point(i, a)j
The function seLplane_pointO transforms the z-value of the point a to the inter-
val 0 < Ztrans < 216 = 65536, Le., to the range of an unsigned short number.
When it comes to the third point, we can determine the constants of the plane
of the polygon:
Plane BuJJere<Lplane; (--+ Globals . h)
#define FloaUo_Ushort(u, f)\ (- Macros.h)
u = 1000 + 64000 * (f - Min..z) / (Max..z - Min..z)
198 Chapter 7. Hidden-Line Removal
, ,
void seLplane_point( i, p) (-+ Proto.h)
short ij
Vector pj
{ /* begin seLplane_pointO */
statie Veetor tri[3]j
if (i > 2) return j
Copy_vec2(tri[i], p)j
/* Transform the z-value into the range of aUshort. */
FloaUo_Ushort(tri[i] [Z], p[Z])j
if (i==2)
plane_constants(&Buffered_plane, tri [0] , tri[l], tri[2])j
,} /* end seLplane_pointO */
With the help of the plane constants, we ean determine the z* -values of all points
in the polygon's plane by means of the routine
, ,
float z_value(x, y) (-+ Proto.h)
short x, Yj
{ /* begin z_valueO */
statie float * d = &Buf fered_plane.cnst,
* a = &Buffered-plane.normal[X],
* b = &Buffered-plane.normal[Y] ,
* c = &Buffered_plane.normal[Z]j
return (*d - M * X - *b *y) / (*c + EPS)j
J /* end z_valueO */
I
'void move_buffer(x, y) (-+ Proto .h)
register short x, Yj
{ /* begin move_buf ferO */
XLgrid = Xj
YLgrid = Yj
} /* end move_buf ferO */
I
Section 7.3. Depth Buffering for Visibility 199
I I
void quickJiraw_buffer(x, y) (-+ Proto.h)
register short x, Yj
{ /* begin quick_draw_buJ ferO */
register short xl = XLgridj
ftoat zl, dZj
register Ushort * xg = &ZbuJ[y - Min_y][XLgrid - Min....x]j
zl = z_value(XLgrid, Y)j
dz = (x! = xl) ? ((z_value(x, y) - zl) /(x - xl)) l',
for ( j xl <= Xj xl + +, zl + = dz, xg + +) {
if (*xg < zl)
*xg = zl,
G_set..pixel(xl, Y)j
} /* end for (xl) */
XLgrid = Xj
J /* end quick..draw_bufJerO */
I I
void buf Jer ..scene 0 (-+ Proto.h)
{ /* begin buJfer_sceneO */
short max-zones = 4j
/* Ifyou have enough RAM, let max-zones = lj */
ftoat h = Window_heightj
ftoat yl, y2, yO = (short) (h) /max-zonesj
I I
void draw_buf fered..1ine(p, q) (-t Proto.h)
Vector pO, qO;
{ /* begin draw_buf feredJineO */
Vector p, q, delta;
short i, iO;
float t;
register short x, y;
Ushort *Z;
Vector g;
Bool exists;
if (!clip3d_line(&exists, p, q, pO, qO)) {
Copy_vec(p, pO); Copy_vec( q, qO);
} else if (!exists) return;
FloaUo_ Ushort(p [Z], pO [Z]) ;
FloaUo_ Ushort( q [Z], qO [Z]) ;
iO = Maximum(fabs(q[X] - p[X]), fabs(q[Y] - p[Y]));
if (!iO) {
x = p[X] + 0.5; y = p[Y] + 0.5;
Z = &Z_buJJer.val[y - Min_y] [x - Min..x];
t= Maximum(p[Z], q[Zl);
if (*z <= t) {
*z = t;
G_set_pixel(x, y);
}
return;
}
t = 1.0 / iO;
Subt_vec( delta, p, q);
Scale_vec( delta, delta, t);
Copy_vec(g, p) ;
for (i = 0; i <= iO; i++) {
x= g[X]; y = g[Y];
z = &Z_buJJer. val [y - Min_y] [x - Min..x];
if (*Z <= g[Z]) {
*Z=g[Z];
G_set_pixel(x, y);
}
Add_vec(g, g, delta);
}
,} /* end draw_buf feredJineO */
8
Mathematical Curves and Surfaces
In Chapters 8 and 9, we will talk about mathematical curves and surfaces and
- as an approximation to them - spline curves and spline surfaces. Sometimes it
is better to have line drawings as an output. The drawn images of such curves
further support the human imagination, especially when we talk about the lines
of intersection of such surfaces. For scientific purposes, the line of intersection is
often of much more interest than the rest of the image.
H we want to draw the image of a surface by hand or with the plotter, we do it by
drawing the images of curves on the surface, especially the parameter lines and
the contour lines. These and, of course, many other curves on surfaces as weH
have always been of great interest to geometrists. With the help of computers,
today we are able to do things that in the past would have meant a lot of
work. This is especially true for the calculation of surfaces that are known only
approximatively, which means that only a limited number of points on the surface
are given.
Of course, from the geometrist's point of view, it is interesting and often neces-
sary to approximate surfaces bit by bit with the help of mathematically seizable
surfaces (spline surfaces). This is what is normally done and the results are by no
means unsatisfactory. However, the price that has to be paid for interpolations
of this kind are comparatively long periods of calculation. Furthermore, every
interpolation is only a guess. This means that even though in some cases we get
pictures that look perfect, they do not necessarily correspond to reality. One only
has to think of alandscape, the shape of which can be completely arbitrary. In
areversal of this train of thought, one can approximate surfaces with the help
202 Chapter 8. Mathematical Curves and Surfaces
of polyhedra. In this case, the computer does not have to distinguish between
mathematically defined or empirically seizable surfaces. Certain kinds of prob-
lems, and intersections in particular, can be solved much more easily this way.
Also, many fast algorithms for the rendering of certain types of polyhedra even
permit an animation of the picture of the surface. An approximation by poly-
hedra may pose problems, particularly when one is forced to come back to the
second differential form of the surface. In that case, a reasonable representation
of osculating lines or lines of curvature without the use of mathematical formu-
las is simply impossible. Therefore, we want to discuss both kinds of surfaces,
however, with an emphasis put on approximational polyhedra.
X(U»)
x = x(u) = ( y(u) (1)
z(u)
(where x(u), y(u), z(u) are arbitrary functions in the parameter u), it is quite
easy to plot its image. We calculate a certain number n of space points, which
belong to n different u-values, and submit their coordinates to a perspective or
orthogonal projection. Then the image points are connected to a polygon that
approximates the image of the curve. The number n of points depends on the
application, but the more points we calculate, the better the approximation will
be.
x= (::!:::) ,
pz +uqz
u E R. (2)
P(p"" Py, pz) is a point on the line, (q"" qy, qz) is the direction vector. With
straight lines, we can generate ruled surfaces like the ones shown in Figures
5,6 and 7.
Example 2: A circle in 3D-space with the radius r, the center M(m"" m y, m z )
and the normalized direction ä = (a"" ay, a z ) of its axis can be described
by
Section 8.1. Parametrized Curves 203
or
mx +rcosu)
( my ~sinu U E [0,271"], (4)
8SintcOSU) 2
x= ( 8sintsinu with 8 =
vI + r
e2ucota
, t = arctane ucota , u E R.(5)
8COSt - r
x= (Jr 2~(a~~O::OsU)2)
ssmu
,U E [0,271"], (6)
In practice, however, there are many cases in which curves are not given by a
parametrized equation. As a matter of fact, many curves are seen in connection
with the surfaces they lie on. This is not so amazing when we consider that on
every surface there are 00 2 lines (on a line there are "only" 00 1 points).
Seetion 8.2. Classes of Parametrized Surfaces 205
X(U, V))
i = i(u, v) = ( y(u, v) (9)
z(u, v)
For v = const, we get the so-called u-lines, and for u = const, we get the v-lines
as the simplest curves on the surface.
Let xo(u)(u), Yo(u)(u), zo(u)(u) be the parametric representation of a line 9 in
space (u E [Ul, U2))' This "generating line" can now be submitted to arbitrary
transformations in order to generate a surface. (A parametrized equation i =
i(t) of a general curve on the surface is defined by the functions u = u(t), v =
v(t).)
xo(U) +xo(V))
i(u, v) ( Yo(u) + Yo(v) (10)
zo( u) + zo(v)
When gis planar and when its plane contains the axis a (e.g., for Yo(u) ==
0), 9 is also called the "meridian line."
Example 3: Helical surfaces. When the generating line 9 is not only rotated
about an axis a (the z-axis) but also translated proportionally along a, the
generated surface is a helical sur/ace (Figure 5):
lThe motion converges to the helical motion when b converges to the infinite
line ([GLAE81]).
Section 8.2. Classes of Parametrized Surfaces 209
FIGURE 8. Minimal spiral surfaces are bent into one another without altering the
metric ofthe surfaces (vanishing main curvature!) ([WUND54]). Similar bendings exist
for any type of minimal surfaces, e.g., for the transformation of a helicoid into a catenoid
([GLAE88/2]).
F(x, y, z) == L .. k
aijkx'yJ z = 0 (i, j, k 2: 0, aijk = constant) (15)
i+j+k~n
Seetion 8.3. Surfaces Given by Implicit Equations 211
describes the surfaces of degree 2. Among these "quadrics", we have the spheres
or, more generally, the ellipsoids, the paraboloids and the hyperboloids. Special
cases are the elliptic (parabolic, hyperbolic) cylinders and the elliptic cones.
In simple cases, it will be possible to find the implicit equation of a surface when
its parametrized equation is given and vice versa.
X
FIGURE 9. Parametrization of a torus.
(a - b cos v) cos u, )
i( u, v) = ( (a - b c~s v) sin u, (u E [0,21l'], V E [0, 21l']). (18)
bsmv
212 Chapter 8. Mathematical Curves and Surfaces
(19)
or
(20)
Without proof, the surface is also of degree 4, and it is called "Dupin's cyclid"
([WUND66]).
FIGURE 10. The inverse surface of a torus with respect to a sphere is called "Dupin's
cyclid."
(Section 8.5). Furthermore, it often takes a lot of CPU time to ealculate enough
points on the line for a smooth image polygon.
Fortunately, many important classes of curves on surfaces q} (x(u, v» can be
eharaeterized by a condition J(u, v) = O.
We will now develop a subroutine zero_maniJold_oJ_f_uvO, which can be used
to traee curves that are characterized by an equation f(u, v) = O. The result will
be a eontiguous (sorted) set of parameter pairs (u, v). Considering the impor-
tance of such a routine for geometrists, it is not surprising that a number of dif-
ferent solutions ofthe problem have been described (see, for example, [SEYB77] ,
[PAUK86], [GLAE86]). The routine we will develop does the job eomparatively
fast, provided that the result does not have to be extremely accurate (there is no
restrietion to accuracy, however, if we do not mind longer periods of calculation
time).
The idea is the following: if we interpret w = J(u, v) as the third coordinate
of a Cartesian (u, v, w) system, the solution of J(u, v) = 0 consists of all the
intersection points of the graph r : w = J(u, v) with the plane ß: w = O. The
zero manifold of J(u, v) can, thus, be interpreted as the planar line of interseetion
s = rn ß, which may consist of several branches.
Equation 22 defines a one-to-one correlation between the surface and the base
reet angle in the (u, v)-plane ß. Therefore, we have a one-to-one correlation be-
tween those lines of interseetion of function graphs and curves that fulfill a eon-
dition f(u, v) = 0 on a surface x{u, v). Every point S{u s , vs) on the line of
intersection s then eorresponds to a point P{x, y, z) on the desired space curve
on the surface: x = X(U B, VB)' y = y{u s , vs), z = z{u s , vs).
It is easy to find a large set of intersection points of the graph. The problem, how-
ever, is to connect these points to polygons (this enables us to draw line segments
instead of little dots on the screen). The key to the solution lies in the triangu-
lation of the graph r.
We ealculate a eomparatively small number of points on r that are above a grid
on the horizontal base rectangle (u E [Ul, U2], v E [Vb V2]) and triangulate this
approximating polyhedron. In order to get the best triangulation possible, we
use "average normals" in each point of the polyhedron.
Now we intersect each triangle with the base plane ß. For all the triangles that
have a line of intersection, we store the two pointers to the vertices of the re-
speetive segment of the line of intersection. This enables us to eonnect the line
segments to polygons. Most of the intersection points will be reached twice, be-
cause, in general, two neighboring triangles have one side in common. Thus, we
start the following algorithm:
We identify a first branch of s with an arbitrary line segment {aa, all. If there is
a line segment {al, a2} {or { a2, al}) among the residual segments, we enlarge the
branch to {aa, al, a2}. This procedure is repeated until no further line segments
214 Chapter 8. Mathematical Curves and Surfaces
can be added. The branch has now grown to {aa, al,"" Clm-b Clm}. If ao = a m ,
the branch is a closed line, otherwise we try to continue it from its other end.
Without altering the branch, we swap its points to {am, Clm-l,"" al, ao}. H
there is a line segment {ao, am+1} (or {am+b aa}) among the residual segments
({aa, al} has alrea.dy been concatenated), we add it to the branch and so forth
(=> {aa, Clm+b"" aB})' The current (k-th) branch then consists of s+l points.
We trace new branches until there are no residual segments left.
AC version (concat.:to..polygons()) ofthe aIgorithm is listed in Section 6.1. Since
it works almost exclusively with pointer arithmetic, we also list a pseudo-code
for better understanding:
residual segments rs == allIine segments
n := mlUlber of residual segments
k := 0
repeat
{branch[k][O], branch[k][l]} .- rs[O]
n := n-1
rs[O] := rs[n]
s := 1
closed := FALBE
/irst...chain := TRUE
repeat
repeat
b := branch[k][s]
if exists rs[i] with {b, succ} V {succ, b}
s := s+ 1
branch[k] [s] := 8UCC
n := n-1
rs[i] := rs[n]
if succ = branch[k] [0]
closed := TRUE
until closed V n = 0 V no succ found
size[k] := s + 1
if not closed " Jirst...chain
for i := 0 to s/2
swap(branch[k][i], branch[k][s - iJ)
/irstJ!hain := FALBE
until closed V -,Jirst...chain V n = 0
k := k+ 1
until n = 0
no..o/.manches := k
In this manner, the line of solution can be found very quickly. The result, how-
ever, is not as satisfactory as one might expect it to be. Because we replace the
graph by a polyhedron, the line of intersection will hardly be very smooth, unless
we drastically increase the number of the faces of the polyhedron.
Seetion 8.4. Special Curves on Mathematical Surfaces 215
t
n, (0, W )
FIGURE 11. The parameter lines on the graph r are approximated piecewise by
means of cubic parabolas. Such a parabola is determined by two "line elements."
To make the curve s smooth, we replace the sides of the intersecting triangles by
planar cubic parabolas (Figure lla). The plane 1/1 on which such a parabola lies
is normal to the base plane ß. The parabola itself is determined by the two end
points Pl and P2 and by the "average" normals nl and n2 in P l and P2 • The
tangent ti of the parabola in Pi is normal both to ni and to the normal P of 1/1.
Figure llb illustrates the situation in 1/1. We introduce a local two-dimensional
coordinate system (t, w). The parabola will have the equation
(22)
The two given points may have the coordinates Pl(O, Wl) and P 2(1, W2). Thus,
we have (1) ... Wl = d and (2) .. . W2 = a + b + c + d. The slope of a general
tangent of the parabola is given by the first derivation
w = ~~ = 3a t 2 + 2b t + c. (23)
The slopes ofthe tangents tl and t2 may be k l and k 2 • Thus, we have (3) ... k l = c
and (4) ... k 2 = 3a + 2b + c. The equations (1) ... (4) lead to the coefficients of
the parabola:
Now we have to intersect the parabola with w = 0. This can be done quickly
by NEWTON's iteration ([FOLE90]). The solution point may have the Iocal
coordinates S(A,O). In the (u,v)-coordinates, the point S is then described by
S" = Pl" + A(P2" - Pl,,), Sv = Pl v + A(p2v - Plv)·
216 Chapter 8. Mathematical Curves and Surfaces
Let us now gradually develop the necessary code for the important function
zero_manilold_ol -I _uvO:
/* We need apointer to a function I(x). */
Global IniLlptr{J_x, c:ubic_parabola)j (~ Proto.h)
I I
float c:ubic_parabola(x) (~ Proto .h)
float Xj
{ / * begin cubic_parabolaO */
return (( Coeff[O] * x + Coeff[l]) * x + Coeff[2]) * x + Coeff[3]j
I } / * end cubic...parabolaO */
do {
xO = Xj
Y = 1...x(xO)j
x = xO - y /«(f ....x(xO + dx) - y) /dx)j
} while (fabs(x - xO) > eps && ++i < lO)j
* ok = (i< 1O? TRUB : FALSE)j
return Xj
J /* end zero_ol_I....x_with_newtonO */
/* The iteration by NEWTON works fast, but it does not converge in any
case. Thus, we need another function for the determination of the zero of a
function. */
Section 8.4. Special Curves on Mathematical Surfaces 217
I I
float zero_ol -I ...x_with_bin_search(xl, dx, yl, eps) (-+ Proto .h)
float xl, dx, yl, epsj
{ /* begin zero_ol -I ..x_with_bin-BearchO */
float xO, yO, dXj
if (dx < eps) return xlj
dx / = 2j
xO = xl + dXj
yO = 1...x(xO)j
if (Jabs(yO) < EPS) return xOj
else if (yl * yO < 0)
return zero_ol -I ..x_with_bin-Bearch(xI, dx, yl, eps)j
else
return zero_ol -I ..x_with_bin_search(xO, dx, yO, eps)j
I} /* end zero...ol -I ...x_with_bin-BearchO */
/* The following function checks whether two vectors are (more or less)
identical. */
I
I Boolidentical (a, b) (-+ Proto.h)
Vector a, bj
{ / * begin identicalO */
#define BMALL le - 4
if (Jabs(a[X]- b[X]) > SMALL
11 labs(a[Y]- b[Y]) > SMALL
11 labs(a[Z]- b[Z]) > SMALL) return FALBEj
else return TRUEj
#undef BMALL
} / * end identicalO */
I
218 Chapter 8. Mathematical Curves and Surfaces
/ * The next function takes a given grid of points on any surface and determines
possible normals of the surface in these grid points (Figure 12). */
I I
void average_normals(normal, grid, imax, jmax) (- Proto.h)
Vector **normal; /* Average normals in grid points (2d). */
Vector **grid; /* Grid of basepoints (2-dimensional). */
short imax, jmax;
/* Size of grid: 0 :::; i < imax, 0 ~ j < jmax. */
{ /* begin average_normalsO */
Vector n, diff[4];
short i, j, k, kl;
register Vector * p, *average_normal;
Vector * v[4];
Bool closed[2];
closed[O] = identical(grid[O][O], grid[O]limax - 1]);
closed[l] = identical(grid[imax - 1][0], grid[O][O]);
for (i = 0; i< imax; i++)
for (j = 0; j < jmax; j++) {
p = v [0] = v[l] = v[2] = v [3] = (Vector *) grid[i]li];
if (i> 0) v[O] = (Vector *) grid[i - l]li];
else if (closed[l]) v[O] = (Vector *) grid[imax - 2] li];
Section 8.4. Special Curves on Mathematical Surfaces 219
/* The following function checks whether the k-th side of the triangle tri inter-
sects the base plane w = O. The side is interpolated by a cubic parabola and
the point of intersection is calculated by means of NEWTON's iteration. */
/* The next function checks whether a point has already been stored in a pool
of size n. */
/* The following function "measures" how much the normal vector of a triangle
differs from the desired normal vectors in its vertices: it returns the sum of
all deviation angles. */
I I
float deviation(tri, grid, normal, il, jl, i2, j2, i3, j3) ( - Proto.h)
Triangle tri;
Vector **grid, **normal;
short il, jl, i2, j2, i3, j3;
{ / * begin deviationO */
Vector face_narmal;
tri [0] = (Vector *) grid[il][jl];
tri[l] = (Vector *) grid[i2][j2];
tri [2] = (Vector *) grid[i3] [j3];
normaLvectar(face_normal, tri);
return acos(fabs(DoLproduct(face_narmal , normal[il][jl]))) +
acos(fabs(DoLproduct(face_normal, normal[i2][j2]))) +
acos(fabs(DoLproduct(face_normal, normal [i3] [j3])));
I} / * end deviationO */
I
void triangulate_sur face (totaLtri, triangles,
normal, surf, imax, jmax) (- Proto.h)
short * totaLtri; /* Number of triangles. */
Triangle **triangles; /* The optimized triangulation. */
Vector ***narmal; /* Pointer to all normals. */
Vector **surf; /* Grid ofpoints in the surface. */
short imax, jmax;
/* Size of grid on the surface: 0 :::; i < imax, 0 ~ j < jmax */
{ /* begin triangulate_sur face 0 */
short i, j, k;
static Triangle * trLpool;
register Triangle * tri;
Triangle alt[2];
static Vector **nrm;
222 Chapter 8. Mathematical Curves and Surfaces
/* This is the funct.ion that determines the line of intersection of the function
graph w = I(u, v). This line consists of a number of branches. AB a side ef-
fect, this function determines the optimized tria.ngula.tion plus the optimized
normals of the surface. */
After these general considerations, we will now present three examples in order
to give you an idea of what the function J(u, v) may look like:
224 Chapter 8. Mathematical Curves and Surfaces
8x
Xu = (x u , Yu, zu) = x(u + c, v) - x(u - c, v), (25)
8u
8x
= Xv = (xv, Yv, zv) = x(u, v + c) - x(u, v - c), c small.
8v
ii = Xu x Xv. (26)
The condition for the existence of a contour point is that the dot product
of the normal vector and the projection ray vanishes
I I
Hoat contour _condition( u, v) (---.. Proto .h)
Hoat u, v;
{ / * begin contour _conditionO */
Vector p, n, proj_ray, pul, 'fYU2, pvl, pv2, tu, tv;
float eps = le - 2;
xyz(p, u, v);
xyz ('fYUl , u - eps, v);
xyz('fYU2, u+ eps, v);
xyz(pvl, u, v - eps);
xyz(pv2, u, v + eps);
SubLvec(tu, 'fYUl, pu2); SubLvec(tv, pvl, pv2);
Cross_product( n, tu, tv);
normalize_vec(n);
SubLvec(proj_ray, p, Eye);
normalizevec(proj _ray);
return DoLproduct(proj_ray, n);
} /* end contour_conditionO */
I
226 Chapter 8. Mathematical Curves and Surfaces
2If the light rays were not parallel, we would have to take the distance from
the light source into aceount as weIl.
Seetion 8.4. Special Curves on Mathematical Surfaces 227
FIGURE 16. The intersection of a hyperboloid and a torus. The surfaces are given
by their parametrized equations Xhyp(U, v) and Xtor(U, v). Furthermore, we know the
implicit equation Gtor(x, y, z) ofthe torus. The line ofintersection corresponds to the
zero manifold of the graph z = Gtor(Xhyp, Yhyp, Zhyp) (image on the left).
surface, the incoming light ray will be reßected according to the law of reßection
(Figure 17): the incoming light ray, the reßected light ray and the normal of
the surface lie on the same plane and the angle of incidence equals the angle of
reßection. If the reßected light ray hits the eye point, the point on the surface is
highlighted. Points that are elose to the highlighted spot can also be considered
to be highlighted, provided that the "perfeet reßection ray" does not pass too
far from the eye point. This is due to the consistency of the surface, which will
never be perfectly smooth so that every spot will send out a bundle of non-
parallel reflected light rays.
If ii is the normalized normal vector in the point P of the surface and t;. the
---+
normalized direction vector of the light ray P Ln through the n-th light source,
the reßected ray has the (normalized) direction
(30)
We now modify Equation (2) by means of the formula
(32)
FIGURE 18. Four different shadings of one and the same surface: plastic, metallic,
wooden coatings, normal shading.
3Colored light sources produce highlighted spots of the same color on the
surface. Since we only deal with lookup-tables for RGB colors, we cannot produce
new RGB colors during the rendering. The only thing we can do is to create
palettes of mixed colors. For example, if we deal with a gray surface and if the
light source is red, we can create a small palette consisting of sorne light red-gray
shades that replace the brighter shades of the red palette.
Seetion 8.6. Shadow Buffering for Shadows 231
FIGURE 19. Shadow buffering generally works and the algorithm is comparatively
fast, hut it requires large amounts of RAM to create images of good quality.
We transform the n-th light coordinates of all the vertices of the scene linearly
into a box Mn X N n X 65000. Before we do the depth buffering of our polygons,
we buffer the scene completely in all the light systems.
If we then want to plot a visible pixel, we first transform the corresponding "space
point" (actually it is a tiny box) to all the light systems and check whether the
depth value w~ of the point corresponds approximately to the buffered value in
the system. If w~ is smaller than the buffered value, the point is shadowed in
the corresponding light system. Finally, the pixel is given a shade that depends
on how often it has been shadowed, similar to the calculation of the shades for
shadow polygons.
An estimation of memory requirements shows that again we will need
0.5 MB RAM for each light source, even if we use a comparatively small grid
size of 500 x 500 in the light systems. It is no longer possible to split the grid into
horizontal zones as we did in the ordinary depth buffering because, in general,
the polygons, the n-th light coordinates of which are covered by the grid strip
in the n-th light system, will not be covered by a strip in another system.
If we run out of memory, a compromise is the only solution. We transform the
depth values to the interval [0,255]. This interval can be covered hy the type
Ubyte which needs only one byte of storage space. Such a restriction also means
that a narrow grid does not malm sense any more, so that we reduce our grid
size to about 250 x 250 in the n-th system.
232 Chapter 8. Mathematical Curves and Surfaces
Now the whole buffer in the n-th light system needs less than 64 Kbytes. 4 The
price to be paid for this restriction is a loss of accuracy when we plot shadows
(Figure 19).
FIGURE 20. These images of a nautilus and a snail were created by means of shadow
buffering. In nature, you can find a lot of spiral surfaces (Section 8.2, Example 5),
because they are a result of continuous growth.
(1)
234 Chapter 9. Spline Curves and Spline Surfaces
Note that, in general, the infinite point of the parabola (t = ±oo) will not be
the infinite point of an axis. Furthermore, the curve is really three-dimensional
and not planar any more.
Let (PI, h) and (P2' t2) be two "line elements" of the parabola p. They may
fulfill Equation (1) for t = 0 and t = 1. By means of the first derivation
we get the "coefficient vectors" almost in the same manner as in Section (8.2):
ä t;, + t; - 2 (P2 - PI),
b = 3 (P2 - PI) - 2t;. - t;,
c = t;"
J = Pl. (3)
Any point on p between PI and P2 can be calculated by evaluating Equation (2)
with a t-value between 0 and 1.
Interpolating Splines
Now we have two line elements and are able to calculate an arbitrary number of
points on the cubic span p. For many applications, however, we also have to solve
the following problem: given a polygon QOQI, .. . , Qn, we try to find a smooth
curve that interpolates the polygon that is given by the n + 1 control points. In
principle, we can "make up" n+ 1 arbitrary tangent vectors ~ in Qi. Then the n
cubic spans between Qi and Qi+I are determined by two line elements each, and
we have a set of parabola segments without any sharp bends (Cl continuity). It
turns out, however, that the visual impression of this interpolating spline curve
is heavily dependent on the tangent vectors we choose. For many purposes, it
is a good idea to let the tangent vector t:
in Qi be dependent on two points
before Qi and two points after Qi (Figure 1).
For the determination of the tangent vectors, several methods have been devel-
oped ([PURG85]). Good results can be achieved with the approach proposed by
Akima. Since Q-b Q-2, Qn+1 and Qn+2 are not given, we say
if-l = cJ2 + 3(tfo - t]i)
if-2 ifl + 3(if-I':QO)
ifn+1 = ifn-2 + 3 (ifn - ifn-l)
ifn+2 = ifn-l + 3 (ifn+l - ifn)· (4)
Now we define
(5)
Seetion 9.1. Interpolating Curves and Surfaces 235
and
>'d,il= {~
/.
0.5
if h + l2 > 0;
otherwise.
(6)
A set of points interpolated in this way will form an interpolating spline curve
without any unwanted oscillations, which tend to occur especially at the end
points of the curve. (This would be the case if we tried to force an algebraic
curve of degree n to go through our n + 1 control points.)
Here is the C code for the calculation of an arbitrary set of points on an in-
terpolating spline curve that is given by n + 1 control vertices Qi . For each
span QiQi+b we calculate Si points so that we have m = So + ... + Sn-l points
on the spline curve at our disposal.
#define AKIMA 0 (-4 Macros.h)
I I
void akima_tangents(tangent, n, ctrLpoint) ( - Proto.h)
Vector tangent[ ];
short n;
Vector ctrLpoint[ ];
{ /* begin akima_tangentsO */
short i, imax = 3 * n;
Vector * dill, *d, *v;
float lenl, len2, *result;
register float * dO, *dl, *d2, *d3;
dO = &diff[O][X]j d1 = &diff[1][X]j
d2 = &diff[2][X]j d3 = &diff[3][X]j
result = &tangent[O][X]j
for (i = Oj i< imaxj i + +, dO + +, d1 + +, d2 + +, d3 + +) {
len1 = fabs( *d3 - *d2)j
len2 = fabs( *d1 - *dO)j
if (len1 + len2 < EPS)
len1 = len2 = 1j
Hesult + + = (lenh (*d1) + len2 * (*d2» /(len1 + len2)j
}
Free_mem(diff, "diffll)j
I} /* end akima_tangentsO */
Interpolating splines with Cl continuity do not always look very smooth. Curves
with C 2 continuity (where even the first derivation of the spline has Cl conti-
nuity) look more natural (Figure 2). Such interpolating spline curves are called
"cubic splines." All of the n spans QiQi+1 (i = 0, ... , n - 1) of the spline are
described by
(7)
I
I void akima_tangents(tangent, n, ctrLpoint) (-+ Proto.h)
Vector tangent[ ];
short n;
Vector ctrLpoint[ ];
{ /* begin akima_tangentsO */
short i, imax = 3 * n;
Vector * dif f, *d, *v;
ftoat Zen1, Zen2, *result;
register ftoat * dO, *d1, *d2, *d3;
dO = &difJ[O][X]j d1 = &difJ[l][X]j
d2 = &difJ[2][X]j d3 = &difJ[3][X]j
result = &tangent[O][X]j
for (i= Oj i< imaxj i++, dO++, d1++, d2++, d3++) {
len1 = fabs( *d3 - *d2);
len2 = fabs( *d1 - *dO);
if (len1 + len2 < EPS)
len1 = len2 = 1;
Hesult++ = (lenh (*d1) + len2* (*d2))/(len1 + len2)j
}
Free_mem(dif I, "diff")j
I} /* end akima_tangentsO */
Interpolating splines with Cl continuity do not always look very smooth. Curves
with C 2 continuity (where even the first derivation of the spline has Cl conti-
nuity) look more natural (Figure 2). Such interpolating spline curves are called
"cubic splines." All of the n spans QiQi+l (i = O, ... ,n -1) of the spline are
described by
Interpolating Surfaces
Interpolating spline curves can be used for the interpolation of surfaces that are
given by a grid Qi,j of (nI + 1) (n2 + 1) vertices (Figure 3).
(~ Proto.h)
I I
void ptdo_calc_spline(type) (~ Proto.h)
short type;
Section 9.1. Interpolating Curves and Surfaces 241
{ /* begin ptdo_calc_splineO */
switeh (type) {
ease AKIMA:
calc_spline_curve = akima_spline_curvej breakj
case CUBIC_SPLINE:
calc_splinLcurve cumcspline_curvej breakj
case B_SP LI NE:
calc_spline_curve = b_spline_curvej breakj
}
I} /* end ptr _to_calcsplineO */
I
void calc-spline_surjace(m, result, drLpoint, n, span_size, type, k)
(~ Proto.h)
ptdo_calc_spline ( type) j
/ * Determine size of surface */
m[O] = m[1] = 1j
for (i = Oj i < n [0] j i++)
m[O] += span_size[O] [i]j
for (i = 0 j i < n [ 1] j i++)
m [1] += span_size [1] [i] j
/ * Allocate surface * /
Alloc_2 d_array (Vector, sur/ace, m [ 0], m [ 1]) j
*result = sur/acej
Alloc_array(Vector, aux_ctrl, n [0], "tmp") j
Alloc_2d...array(Vector, aux_sur/, n[O], m[l])j
/ * Calculate arnumrf * /
for (i = Oj i< n[O]j i++)
calc_spline_curve ( &m [ 1], aux_surJ[ i],
n [1], ctrLpoint [i], span_size [1], k [ 1]) j
242 Chapter 9. Spline Curves and Spline Surfaces
Approximating Splines
Several kinds of approximating splines have been developed, among them the
Bezier-splines, the B-splines [FOLE90] and the ß-splines ([BARS88]). For our
Section 9.2. Approximating Curves and Surfaces 243
purposes, we only need the so-called B-splines. They are given by the parametric
equation
n
x(t) = L bi,k(t) iJi, (12)
i=O
where qo, ql, ... , ifn are the n + 1 control points. The index k = 2,3, ... , deter-
mines the number of control points that have an inftuence on the points of the
curve. The curve is then C k - 2 continuous, i.e., we have C 2 continuity for k 2: 4.
0 if i < kj
Ci = { i - k + 1 if k :::; i :::; nj (13)
n - k + 2 otherwise,
which lead to the knots (breakpoints ) Ci of the n - k + 2 spans of the curve. The
coefficients bi,k(t) of an arbitrary curve point belonging to the parameter t are
defined recursively by means of the constants Ci:
We start with
b. (t) =
.,1
{I if Ci < ~ <
0 otherwlse,
CHI j (14)
and then we calculate bi ,2 bi ,3, ... ' bi,k as a weighted linear combination of the
coefficients of lower degree:
(15)
244 Chapter 9. Spline Curves and Spline Surfaces
ci = &c[i1]j
for (b_ij = &b[i1][1]j b_ij < max_bj b_ij + = deltaJJ)
* b_ij = (*ci < t&& t <= * + +ci) ? 1 : Oj
for (j = 1j j < kj j + +) {
i = i1j ci = &c[i1]j cj = ci + jj
for (b_ij = &b[il[j + 1)j b_ij < max_bj b_ij + = delta_b) {
if (t *b_ij = b[i] fim
* b_ij * = {t - *ci) /(*cj - *ci)j
i + +. ci + +j cj + +j
if (b(il fi])
*b_ij + = (*cj -t) /(*cj - *ci) *b[i]fi]j
} /* end for (b_ij) */
} /* end for (i) */
~hlsis the ''readable code":
for (i = i1j i <= nj i + +)
if (c[i] < t&& t < c[i + 1]) b[iJ[1] = 1j
else b[i][l] = 0;
for (j = 2j j <= kj j + +) {
for (i = ilj i <= nj i + +)
b[i]fi] = (t - c[i]) /(c[i + i - 1] - c[il) * b[i]L1 - 1]+
(c[i + j] - t) /(c[i + j] - c[i + 1]) * o[i + 1]fi - 1];
} /* end for (i) */
I} ~ end b..spline-.eoeIIO */
Section 9.2. Approximating Curves and Surfaces 245
/ * B-spline constants */
i = Oj j = 1j
while (i< k)
c[i++) = Oj
while (i< n)
c[i + +) = j + +j
while (i< n + k)
c[i++) = jj
i = k - 1j i1 = i - (k - l)j i2 = i + (k - l)j
for (j i < n + 1j i + +, il + +, i2 + +) {
t = c[i) + EPSj
dt = (c[i + 1) - t) /(*span..size) - EPSj
i1 = Maximum(i1, O)j
i2 = Minimum(i2, n)j
for (j = Oj j < *span_sizej j + +, t+ = dt, curve + +) {
if (t == EPS) {
Copy_vec( *curve, q[O))j
} else {
if (j == Oll dt > EPS)
b_spline..coeff(b, t, n - 1, k, c, il)j
Z ero_vec( *curve) j
for (i3 = il, q1 = q + i1j i3 < i2j i3 + +, q1 + +) {
if ((r = b[i3)[k))! = 0) {
~:~~~:~~:I:::
(*curve) Z
;: [::NI~)/
+ = r * (*q1) Z)j
} /* end if r) */
} /* end for (i3 */
246 Chapter 9. Spline Curves and Spline Surfaces
} /* end if (t) */
} /* end for (j) */
span_size + +;
} /* end for (i) */
Copy_vec(*curve, q[n -1]);
* no_of _vertices = + + curve - curveO;
Approximating Surfaces
x(u, v) = i 1 j (16)
i=O j=O
The vectors i!ij are the position vectors of the (nl + 1) (n2 + 1) control points Qij
(Figure 6). Note that we now have two indices k 1 and k 2 in the u- and v-directions
Section 9.2. Approximating Curves and Surfaces 247
(so that we have two degrees of freedom). Lines of constant u (v-lines) are B-
spline curves with the control vertices
nl
s the
types of faces: those, where the normal vectors are inclined toward
the For solid objects ,
projection center, and the others, where this is not case.
er 5). Two triangl es of a
they are called frontfaces and backfaces (Chapt
different kind have one edge of the contou r polygon in common.
by the
Consequently, we check for each face, whethe r the angle enclose d
the face is smaller or greater
projection ray and the oriented normal of
oring
than ~. Then we compa re the type of each face with that of its neighb
larly useful for plotter drawin gs,
faces. The contou r line of a surface is particu
display too many images of parame ter
especially when we do not want to
lines.
in u and v, for which we have an infinite number of solution curves on the surface
(e.g., the lines of steepest slope). Let us assume that in each point of the surface,
the tangent vector of the corresponding curve is known so that the direction
dv : du of the line of solution is given. We now want the computer to calculate
the integral curve of the vector field. The direction of the curve is indicated by the
tangent vector in the tangent plane, which can be given as a linear combination
of the tangent vectors of the parameter lines through the point (these vectors
are the difference vectors Xu and Xv in Equation 25):
(18)
Thus, a point that is elose to the point with the position vector X = (u, v) has
the position vector x(u + c.du, v + c.dv). The eloser the points lie together, the
more likely it is that the graphically integrated curve really corresponds to the
actuallines of solution öf the vector field. A branch of the curve ends when the
parameter values belonging to the point are outside the rectangular domain of
definition, or when one, while still proceeding in the same direction, comes back
to the starting point (Le., when the curve is elosed in a circle).
If the vectors ~ Xu and Xv are given, the parameters du, dv in Equation (18)
can be evaluated by
du = tr" Yv D
- t y Xv
' dv = t y X u D- t", Yu ( •
wlth D = X u Yv - Yu Xv
)
, (19)
provided that the determinant D does not vanish. (Otherwise we use another
pair of coordinate equations in Equation (19)).
A pseudo-code of the relevant part of the program might look like this:
Starting point Po := x(u, v)
Determine normalized tangent vector fo in Po
P :=Po
f := fo
repeat
determine tangent vectors xu, Xv of parameter lines
determine du, dv from xu , Xv and f (Equation (19)
u := u+c.du,v := v+c.dv
P := x(u, v)
determine normalized tangent vector tin P
until (u, v) outside domain of definition V
(distance(new point, P_O) < e /\ angle(~ fo) small} V tis not real
For accurate results, the factor c has to be very small. On the other hand, it
should not be too small, for that would increase calculation time.
After these general considerations, we will now give two examples of how we can
find the tangent vector t for specific families of curves:
Section 9.4. Spatial Integral Curves 251
(20)
determines the direction of the tangent with the greatest possible angle of
elevation, the so-called tangent of steepest slope. Any other direction of a
tangent is then a linear combination of the two vectors h and f
(21)
n'"2 +n2y
(22)
1 + A2.n~'
A = ±--;===tan====ß=== (23)
. In '"2 + n y2
V - n 2 tan 2ß
z
Let us now extend the method for the determination of the integral curves of di-
rection fields to approximating polyhedra. Of course, there is no more guarantee
for perfect accuracy, but we have to consider that sometimes there is no other
252 Chapter 9. Spline Curves and Spline Surfaces
way to get such curves. Apart from that, the proposed algorithm is comparatively
fast.
We assume that, for each face, we can determine areal or not real direction
vector of the field. In the case of generalized helices, for example, this is - similar
to Example 1 in this section - the tangent vector that has a constant angle of
elevation with respect to the base plane. Let us start from a point A at the inside
of a face (Figure 8).
The straight line that is determined by the point and by the direction vector of
the face may be called the field line through A. (For certain applications, e.g.,
for trajectories, this line has to be oriented.) It intersects the border of the face
in two points Ab A 2 . The edge between these two points is stored as apart of
the "integral polygon." Al and A 2 lie on edges the face has in common with
two neighbor faces. We now continue the polygon in the directions of both these
neighbor faces until we reach a criterion to break off the search.
Naturally, the vertex of our first edge also lies on an edge of the neighbor face
and, together with the direction vector of this neighbor face, it determines a
new field line. We determine the residual section point R of this line with the
neighbor face. If there is another face on the polyhedron that shares the edge
through R with the current face, we can apply the same procedure once more. If
there is no other neighbor face on the polyhedron or if the polygon is closed, we
break off the search for more points. Since all the problems that are described
here can be solved by simple methods of analytic geometry, without the use
of time--consuming mathematical functions like sine or tangent, we may assume
that the algorithm will be done very quickly.
A pseudo-code illustrates the problem even more clearly (compare with the
pseudo-code at the beginning of this section):
Section 9.4. Spatial Integral Curves 253
Example 2: Potential Areas. The author was motivated to write this section
when dealing with the problem of how to find the equipotential lines and
field lines of electrical potential areas. From the geometrist's point of view
this is nothing but the tracing of Iines of intersection and their orthogo-
nal trajectories on function graphs. In the top view, these lines form an
orthogonal net. Figure 9 shows such a surface, given by the equation
Z --I w+- ll
W 1 (z real, w = x + iy camplex) (24)
x2 + y2 + 1 - 2X)
(~z= x2 + y2 + 1 + 2x . (25)
The normal projections of the equipotential lines and field Iines on the
ground plane are, as is weIl known, two orthogonal pencils of circles. The
results of the integrations of the curves can hardly be distinguished from
the accurate solutions of the differential equations.
FIGURE 9. Equipotential lines and field lines on an electrical potential area. Their
normal projections on the ground plane are two orthogonal pencils of circles.
10
Computer-Generated Movies
Our final goal is to be able to transfer the stored movie to any other computer
(even to one that has only a minimum of graphics facilities) and to replay it
there without having to change anything. In this way, the quality of the output
can be improved if we switch to a more sophisticated graphics computer. (Of
course, we can also store our images in other standards like the IFF-standard,
but in our special case - we only store polygons - this is the most economical
way.)
Let us try to store a movie in such a way as to make it fully compatible with other
graphics computers. We will distinguish between 8-bit integers ints (chars),
which are in the interval Is(O :::; ints < 2S ), and l2-bit integers inh2, which are
in the interval 112 (0 ::; int12 < 212 ). Numbers of different type will be stored in
different files so that no disk space is wasted.
First we predefine some numbers:
#define MAX-12_BIT (short ) Oxff
#define NEW_FRAME (MAX.B_BIT - 1)
#deftne END_MOVIE MAX_8_BIT
~ --- Macros . hj
Macros.h
Macros.h
Numbers ofthe type ints are storedin the file "<filename>.vd1", numbers of
the type int12 in the file ., <filename> . vd2". While switching from one output
file to another (depending on the type of the number) we store
1) the characteristic ID-number
(ints) MAX_8_BIT, (int12) MAX-B_BIT
at the head of each file, so that the
computer can recognize the files as a "movie."
2) the Boolean expressions
(ints) Smooth_shading.
(If TR UE, the polygons have to be smooth-shaded.)
3) the active palettes as follows:
intsj palette_index,
~ ints lower RGB values rl, g1, b1 (transformed into I s )
ints upper RGB values r2, g2, b2 (transformed into I s ).
4) one frame after the other, starting with the keyword
(ints) NEWJ'RAME.
Now we store the Bool variable
(ints) New_image,
which indicates whether the current frame differs from the previous one.
(In a movie an image may be "frozen" for a while. It would be
a waste of disk space to store the same image several times. )
If New_image is TRUE, we store all the polygons in the following way:
~
intsj number n of vertices
ints color (palette index),
ints if Smooth_shadinq
(ints) shade of color ttransformed into I s)
else
(ints) n shades of color (transformed into Is)
(intt2) n pairs of normalized device coordinates x, y
(transformed into 112 )
5) the
(ints) END_MOVIE
constant.
Section 10.1. An EconoDlicaI Way of Storing a Movie 257
Numbers of the type ints are efficiently stored as characters by means of the
C macro putcO and re-read by means of the macro getcO.
To store 12-bit integers, we use fast bitwise operators, which pack eight such
numbers into three long variables (with 32 bits each). Figure 1 shows how the
nmction pack_ar _unpack_96_bitsO works.
I
12 bits
xlOI
I
x/11 x/21 xl3/ x/!'l x/51 x/61 x/71
I I I I I I I LI I I I I I
32 bits
96 bits
FIGURE 1. How to pack eight 12-bit integers into three long variables.
I I
void pack_ar _unpack_96_bits(n, x, pack) (-t Proto.h)
Ulong n[3], x[8];
Boolpack;
{ /* begin pack_or _unpack_96_bitsO */
if (pack)
n[O] = (x[O] « 20) I (x[l]« 8) I (x[2] » 4),
n[l] = «x[2] & Oxf)« 28) I (x[3] « 16)
I (x [4] « 4) I (x[5]» 8),
n[2] = «x[5] & Oxff)« 24) I (x[6]« 12) I x[7];
else
x[O] = (n[O] & OxfffOOOOO»> 20,
x[l] = (n[O] & OxOOOfffOO»> 8,
x[2] = «n[O] & OxOOOOOOff)« 4) I «n[l]& OxfOOOOOOO) » 28),
x [3] = (n[l] & OxOfffOOOO»> 16,
x [4] = (n[l] & OxOOOOfffO»> 4,
x [5] = «n[l] & OxOOOOOOOf)« 8) I «n[2]& OxffOOOOOO) » 24),
x[6] = (n[2] & OxOOfffOOO»> 12,
x[7] = (n[2] & OxOOOOOfff);
I} /* end pack_or _unpack_96_bitsO */
258 Chapter 10. Camputer-Generated Movies
if (put)
for (j c< hLcj c++)
putc(*c, Video_liler1])j /* C macro! */
else
far (j c< hLci c++)
* c = getc(Video_lile[1])j /* C macro! */
I} /* end puLor _geL96_bitsO */
An example will show how efficient this kind of storage is. A scene of medium
complexity may consist of 300 polygons (quadrilaterals, for example). To store
a quadrilateral, we need three 8-bit integers and eight 12-bit integers, Le., 120
bits or 15 bytes.
Thus, a frame requires only 4.5 Kbytes! A real time movie of ten seconds and
with 250 frames can be stored in a file that has a size of little more than one
Mbyte. If the polygons are smooth-shaded (e.g., when we store a smooth-shaded
surface of revolution), we need about 20% more space.
Let us start our graphics packa.ge. By means of an additional option, we tell the
program that we want to store the images. This can be done by writing:
progname <datafile> -v <videofile>.
in the command line (see Appendix A.3).
Aflag
Boollnit(Stordmage, FALSE)j ( - Globals.h)
is then set TRUE. When we create the color palettes, we open the video file and
store the information on the palettes that are going to be used. For this purpose,
we add the following lines at the beginning of the function create_palettesO,
which was described in Section 4.3.
Section 10.1. An Economical Way of Storing a Movie 259
#ifdef ANSI
# detlne READ_BIN "rb" (--t Macros.h)
# detlne WRITE_BIN "wb" (--t Macros.h)
#else
# detlne READ_BIN "r" (--t Macros .h)
# detlne WRITE_BIN "w" (--t Macros.h)
#endif
/ * This may be tricky. The standard C language does not distinguish between
binary files and text files. This will lead to problems, if you work in the
DOS environment, where a file is closed by the character < Ctrl > z.
(In a UNIX environment no EOF Hag is set.) You should write a short
test file to figure this out. */
#detlne Send.KbiUnt(n)\ (--t Macros.h)
putc((Ubyte) (n), Video_lile[O])
#detlne Send_12_biUnt(n)\ (--t Macros.h)
send_l2_biLint«(short ) (n))
if (Store_image && !Video_lile[O]) {
Video_lile[O] = sale_open("<filename>. vdl", WRITKBIN)j
Video-file[l] = sale_open("<filename>.vd2", WRITKBIN)j
Send_8.lJiLint( MAX_8_BIT}j
Send_l2_biUnt( MAX_8_BIT}j
Send_8_biUnt( Smooth_shading) j
iniLvideo_buffer() j / * This function will be explained soon. */
} / * end if (Store_image) */
At the end of the function make....spedrumO (Section 4.3), we send the index of
the palette and its characteristic RGB values to the file:
if (Store_image) {
n = pal - ParenLpalettej
Send_8_biUnt( n) j
Scale_vec( rgb, pal-> lower _rgb, MAX_8_BIT}j
for (n = Oj n < 3j n++)
Send_8_bit_int( rgb[n])j
Scale_vec(rgb, pal->upper_rgb, MAX_8_BIT}j
for (n = Oj n < 3j n++)
Send_8_bit_int(rgb[n])j
} /* end if (Store_image) */
A nmction send_l2_biLintO receives a short between 0 and Oxlf f = 2 12 - 1 =
4095 and integrates it into a field that is 96 bits long. As soon as eight shorts
have been buffered, the bit field is sent to Video_lile[l].
260 Chapter 10. Computer-Generated Movies
I I
void send_12_biLint(s) ( - Proto.h)
short si
{ /* begin send_12_biLintO */
statie Ulong buffer[8]i /* Buffered numbers. */
statie Ulong space[3]i /* Reserves 96 bits. */
statie short i = Oi /* How many numbers in buffer. */
if (s ~ 0)
buffer[i++] = Si
else / * Flush buffer. */
while (i< 8)
buffer[i++] = 0;
if (i==8)
pack_or_unpack_96_bits«ehar *) space, buffer, TR UE) ,
put..or _geL96JJits( space, TR UE),
i = 0;
I} /* end send_12_biLintO */
I I
short receive_12_biLintO ( - Proto.h)
{ /* begin receive_12JJiUntO */
statie Ulong buffer[8]; /* Buffered numbers. */
statie Ulong space[3]; /* Reserves 96 bits. */
statie short i = 7;
% 8;
i = ++i
if (i==O)
puLor _geL96_bits( space, FALBE),
pack_or_unpack_96_bits«char *) space, buffer, FALSE);
return (short ) buffer[i];
I} /* end receive_12_biLintO */
Every time a polygon (or a line) is drawn, the coordinates of its vertices are sent
to a buffer. For the respective code, we introduee a new type and some variables,
which are global in the eurrent module.
Section 10.1. An Economical Way of Storlng a Movie 261
typedef struct {
Ubyte nO..Df _verticesj
Vector2 * verticesj
Ubyte color, shadej
Ubyte * smooth-Bhadej
short xmin, xmax, ymin, ymaxj
float areaj
} Poly2dj (-+ Types .h)
Poly2d * AILpolys, *Our..poly2d, *Hi.poly2dj
Vector2 * VbuJJer = NULL, *Cur_v = NULLj
short NO..Df...JJaved_frames = Oj
Ubyte * Shade_pool, *Cur...JJj
The buffer is initialized for the first time when we open the video files. (It will
be reinitialized every time a frame has been stored.)
I I
void init-video_buffer() (-+ Proto. h)
{ /* begin iniLvideo_buJJer() */
ü (! VbuJJer) {
Alloc-O.rray(Poly2d, AILpolys, MAXPOLY, "vbuf")j
Alloc-O.rray (Vector2, VbuJJer, BUFSIZE, "vbuf")j
if (Smooth-Bhading)
AllocArray(Ubyte, Shade_pool, 4 * MAXPOLY, "smooth")j
}
Cur_v = Vbuffer,
Cur..poly2d = AILpolysj
Cur...JJ = Shade_poo~
I} / * end iniLvideo_buJJer() */
When we buffer a polygon (or a line), we have to malre sure that the polygon
(line) has been clipped! The polygon has to be stored in an array of Vector2s.
When the polygons are smooth-shaded, the shades have to be stored in an array
When plain shading is applied, the current palette and the current shade have
to be stored by means of the global variables
(-+ Globals.h)
262 Chapter 10. Computer-Generated Movies
I I
void buffer Jine(p, q) (--+ Proto .h)
Vector2 p, qj
{ /* begin buffer JineO */
register Poly2d * cp = Ou.r.:poly2dj
ep->color = Our...palettej
ep->shade = Our-Bhadej
ep->noJJf _vertices = 2j
ep->vertices = Gur_vj
GOW_vec2(*Ou.r_v, p)j Gur_v++j
GOW_vec2(*Gur_v, q)j Gur_v++j
Gur.:poly2d++j
I} /* end bufferJineO */
I I
void buffer..polygon(n, vertices) (--+ Proto.h)
short nj
Vector2 * verticesj
{ /* begin buffer-polygonO */
register Poly2d * p = Our.:poly2dj
register short x, Yj
register Vector2 * v, *hLv = vertices + n, *v1j
p->noJJf _vertices = nj
p->color = Gur_poly_color;
if (ISmooth..shading)
p->shade = Gur..shadej
eise {
p->smooth-Bhade = Gur..sj
for (x = Oj x < nj x++)
* Gur-B++ = Shade..of_vertex[x];
} /* end if (Smooth..shading) */
p->vertices = Gur_vj
/ * Area of polygon. */
p->area = 0;
for (v = vertices + 1, vI = v + 1; vI < hi_v; v++, vl++)
p->area + = fabs(Area...ol..2d_triangle(*vertices, *v, *vl));
if (p->area > 4) Cur_poly2d++;
J /* end buffer _polygon{) */
When the screens are swapped, we simply flush this buffer. Some of the polygons,
however, may have been completely erased during the drawing process. Thus, it
is worthwhile to check whether a polygon really has to be drawn. Even though
this takes some computation time, it will both save space and - what is even
more - reduce the replay time.
Boollnit(New_image, FALSE); (--+ Globals.h)
I I
void release_bufferO (--t Proto.h)
{ /* begin release_buffer{) */
register Poly2d * p;
register Vector2 * v, *hLv;
short i, total, n;
static float scale....xy = 0, scale_shade;
if (!scale..xy)
scale..xy = 4096/ Maximum( Window_width, Window_height) ,
scale...shade = (float) MAX_8_BIT / PAL_SIZE;
Send_8_biLint(NEW -FRAME);
if (!New_image && No_ol_saved_lrames > 0) {
Send.-8_biLint( TRUE);
N 0_01 _saved_frames++;
return;
} else
Send_8_biLint(FALSE);
Hi_poly2d = Cur_poly2d;
if (Hi_poly2d - AlLpolys) = = 0)
return;
for (p = AlLpolys; p < Hi_poly2d; p++) {
264 Chapter 10. Computer-Generated Movies
n = p->no_of _verticesj
if (n > 2 && !necessary.to_plot(p»
continuej
hLv = (v = p->vertices) + p->no-Dl_verticesj
Sendß_biLint(n)j
Send_8_biLint(p->color)j
if (!Smooth-Bhading 11 n = = 2)
Send_8.lJiLint(scale..shade * p->shade)j
else
for (i = Oj i < nj i++)
Send_8_biLint(scale..shade * p->smooth_shade[i])j
for (j v < hLvj v++) {
Send_I2_biLint«*v)[X] * scale...xY)j
Send_I2_biLint(*v)[Y] * scale...xY)j
} /* end for (v) */
} /* end for (P) */
N 0_01 ..saved_Irames++j
iniLvideo_buffer()j /* Reset the global pointers. */
I} /* end release.1ntfferO */
I I
Bool necessary_to_plot(p) (---+ Proto.h)
register Poly2d * pj
{ /* begin necessary_to_plotO */
register 2Poly2d * q, *qOj
Vector2 * plj
Vector2 * which_polYj
short nI, n2j
static Vector2 * clipped = NU LLj
if (p->color == NO_COLOR)
return FALSEj
if (!clipped)
Alloc_array (Vector2, clipped, MAX_POLY_SIZE, "clip")j
ni = p->no_of _verticesj
pI = p->verticesj
The ProgrammingPackage
on Your Disk
Directory CODE: It contains the entire sour ce code of the graphics package
(i.e., the *. c files and the *. h files) except the system-dependent mod-
ule g..functs. c and the macro file G..macros .h. The code is compressed to
the file source. zp by means of the file zp and has to be uncompressed by
means of the corresponding file uzp.
Directory SYSTDEPS: In this directory you can find the system-dependent mod-
ule g..functs. c and the macro file G..macros.h plus a makefile for the Sil-
icon Graphics environment, using the Silicon Graphics Library functions,
268 Appendix Chapter A. The Programming Paclcage on Your Disk
and for the DOS environment, using a WATCOM C compiler. The package
was developed on Silicon Graphics Workstations: first on the "good old"
Iris 3020, then on the SG-35 and finallyon the Indigo 2 . By means of the
WATCOM C compiler Version 10.0 it was possible to make the programs
run in a DOS environment as weIl. The corresponding files are compressed
to files named sg. zp and pe. zp.
A former version of the package used to run on an Impuls Workstation
(under UNIX) and on the Commodore Amiga (together with the Aztec C
compiler 3.0). Both versions used to work fine ([GLAE90]).
If you want to implement the programming package on other graphics com-
puters, this should be possible if you change the corresponding system-
dependent macros and the system-dependent functions (see Appendix B).
Directory DATA: In this directory, some dozens of sampie data files are stored.
The files are compressed to the single file data. zp.
Directory ANIM: In this directory, a few sampie animation files are stored. The
files are compressed to the file an im • zp.
5. Copy the file data.zp into the directory DATA and the file anim.zp
into the directory ABIM. Then decompress these files in a similar way.
6. Change to the SOURCE-directory and create the executable files try,
replay and speed. To do so, just write
malte all
7. If you work on a Silicon Graphics Workstation, everything should be
okay now. The executable files are stored in your working directory
FAST3D. Change to this directory:
cd ..
If you do not work on such a workstation, you will have to adapt
the system-dependent module g~uncts. c and the system-dependent
macros in G..JI\acros .h .
• You are working on a pe: Switch to the disk-device and write install.
Then follow the instructions of the installation file.
If you have a WATCOM C compiler at your disposal, you can recompile
the files: change to the SOUR-cE directory and write malte all. This calls
the batch file malte. bat that does the job. If you do not want to recompile
the code, unzip the executable files by means of the widely spread program
pkunzip. exe (Version ~ 2.04). If you want to use another C compiler, you
have to adapt the system-dependent module g-functs. c and the system-
dependent macros in G..JI\acros. h.
Note that the executable programs requires a mathematical coprocessor and
at least 4 MB RAM (preferably 8MB and more!).
IThe name try has historical reasons: it took a while until the first data files
worked, thus each attempt was always a try.
270 Appendix Chapter A. The Programming Package on Your Disk
key function
w, h draw mode: wire frame, hidden lines
s, c draw mode: shading, cast shadows
j hidden lines by means of painter's algorithm
f, F freeze / auto rotation
a, A,<, > change azimuth
e, E, " change elevation
t, T change twist
x, X enlarge image (decrease fovy angle)
d, D change distance
+- -1! change target point
1,2,3 top view / front view / right side view
4, 5, 6 bot tom view / back view / left side view
u undo special view (back to former perspective)
key function
g, G geocentric / heliocentric
k, K show color map / undo
b, B show bounding boxes / undo
0,0 show outlines / undo
m,M show axes / undo
I, L show lights (can be changed interactively) / undo
z, Z software z-buffering / undo
p create a PostScript file
v create a video file (default name VIDEO/tmp)
q quit program
2The data file should either be in the current directory, or - more clearly - in
the default directory DATA: the program will automatically look for the file there.
Section A.3. How to Use the Program 271
The command
try pencil -v VIDEO/pencil
will store your whole animation m the two files VIDEO/peneil. vdl and
VIDEO/pencil.vd2 4
When you use the the -a option and the -v option together you can create a
little movie:
try pencil -a ANIM/pencil -v VIDEO/pencil
To replay the movie, type
replay VIDEO/peneil 5
Provided you have a fast graphics computer, you have now created your first real
time animated film.
3In the DOS environment, the slash has to be a backslash!! This is true for
the whole section. Furthermore, the directory ANIK is the default directory for
animation files.
4The directory VIDEO is the default directory for video files.
sSince VIDEO is the default directory for video files, you can also write
replay peneil
272 Appendix Chapter A. The Programming Package on Your Disk
#include <stdio.h>
#include <math.h>
#include <time.h> /* prototype of dock ( ) */
/*
How fast is your computer?
/* Global variables */
const long Factor = 300000L;
/* Increase this number if your computer is very fast. */
long Repetitions, I, J;
Real TimeO, TimeForLoop = 0;
FILE *f; /* The log-file. */
typedef struct { / * A typical structure (30 bytes) */
int a, b, c;
Real d[3];
char *e, *f. *g;
} A..struct;
Real time..since_prog-start( void)
{
return (Real) cloek() / CLOCKS_PERSECj
}
void prt( char *txt)
{
printf( txt) ;
fprintf(f. txt);
}
Section A.5. How Fast Is Your Computer? 273
}
/* Just for C++:
Macros are fastest (sometimes together wi th inline-functions).
Ordinary function calls take a lot of time. The same is true
for operators. Even if some people caU it "macromania":
fast C programs still depend heavily on the use of macros!
*/
#ifdef _LANGUAGKC..PLUS_PLUS
struct V2d { /* A 2d-vector */
Real x, y;
V2d() { };
V2d(Real xO, Real yO) { x = xO; y = yO; }
friend V2d operator + (V2d &u, V2d &v)
{
return V2d( u.x + v.x, u.y + v.y);
}
};
#else
typedef struct {
Real x, y;
} V2d;
#endif
void sum(V2d *W, V2d *u, V2d *v)
{
w->x = u->x + v->x;
276 Appendix Chapter A. The Programming Package on Your Disk
int main()
{
if (! openJogjile () return 1;
prt( "\nPERFORHAHCEuTEST\n") ;
prt(" (Theuvaluesudependuonutheutestunumbers!) \n") ;
bytes() ;
shorts() ;
longs() ;
fioats() ;
function_calls( ) ;
compare_macros_and_functions( ) ;
printf( "Theuresultuhasubeenuwrittenuintou<speed .log> \n") ;
return 0;
}
Finally, we give a table of the results of the program with the following configu-
rations:
BC-3.1 SG-35IHDIG02
operations with bytes:
inc 0.08 0.11 0.03
log. and 0.05 0.09 0.02
log. or 0.03 0.08 0.02
log. xor 0.06 0.08 0.02
shitt 1 0.05 0.08 0.02
div 2 0.23 0.09 0.02
+ 0.03 0.09 0.02
0.08 0.08 0.02
0.05 0.08 0.02
div
* 0.06 0.08 0.02
mod 0.05 0.08 0.02
278 Appendix Chapter A. The Programming Pacltage on Your Disk
void PC_open_graphics ( )
{
_setvideomode (_ VRES256COLOR);
/* Standard VGA resolution with 256 colors at the same time.
May be set to ..xRES256COLOR (1024 x 768) if your video
card supports that resolution. */
}
void PC_close_graphics ( )
{
_setvideomode(_DEFAULTMODE);
}
void PC_clearscreen ( )
{
_rectangle (_GFILLINTERIOR, 0, 0, 640, 480);
/* 640 x 480 is the standard VGA resolution.
Has to be changed for other resolutions. */
}
void PC..swapbutJers ( )
{
_setvisualpage (1 - _getvis'llalpage (»;
_setactivepage (1 - _getactivepage (»;
}
Example 2: The macros G_move() and G_draw() may directly insert the
system-dependent functions _moveto () and _drawto ( ):
#define G_move(v) _moveto«short) (v)[X], (short) (v)[Y])
#define G_draw(v) _drawto«short) (v)[X], (short) (v)[Y])
The macros G_area_move (), G_area_draw () and G_area_close () should in-
sert the functions poly_move (), poly_draw ( ) and poly_close ( ) of Section 4.6.
Example 3: The macros
#define G..seLcolor( a) PC..setcolor«short) a)
#define G_geLcolorO PC_getcolor()
#define G..seLpixel(x, y) PC..setpixel«short) x, (short) y)
#define G_geLpixeLcolor(x, y) PC_getpixel«short) x, (short) y)
insert calls of the system-dependent functions
Section B.1. How to Change the System-Dependent Commands 281
One problem with the writing graphics software for PCs, however, is that there
are so many different video cards on the market. Thus, the above mentioned
functions will only cover a certain number of video cards.
The WATCOM C compiler supports standard VGA mo des and most of the
VESA 256 color modes on Super VGA's (SVGA). The VESA specifications only
standardize the video mode numbers and the "bits per pixel memory organiza-
tion" (8 bits per pixel = 1 byte per pixel for 256 colors). So far, however, there
is no standard for screen pages. Thus, in order to support doublebuffering on
standardized video cards, we have to write our own graphics primitives.
The actually implemented code for PC_open_graphics () first tests whether there
are two screen pages available or not and will use two pages if supported (the
boolean variable TwoScreens is then set TRUE).
When we can only use one screen page (this is true for most of the video cards),
we buffer the whole drawing into a "pseudo screen" in ordinary RAM and not
into video RAM. When is comes to page flipping, we put the image back on the
screen. This is a slow solution, hut ist works fine for most video cards. (On some
video cards you have to load a VESA driver before using the program!)
282 Appendix Chapter B. System Dependencies and Other Programming Languages
The pseudo screen is built up line per line with no additional bytes at the end of
a line (n pixels per line). Since we need one byte for each pixel, a pixel with the
coordinates (x, y) has the linear position number (ny+x) in our pseudo screen.
There are very fiew basic functions (macros) that actually change the
pseudo screen: The function PC_setpixel() (PC-seLxor_pixel()), the function
PC_clearsereen() and the macro PC_HORLINE(xl, x2, y) that "draws" a hor-
izontalline from (Xl, y) to (X2, V).
char CurCol; /* Number of color in color lookup table: 0 ~ CurCol < 256. */
Bool DrawlnBaek;
/* TRUE when TwoSereens = FALSE and not XOR-mode. */
typedef struct {
short xsize, ysize, BitsPerPixel ;
char Data[];
/* Huge array that is able to store a screen of the dimensions xsize x
ysize. Must have the size xsize * ysize. */
} PCJmage;
PCJmage *Pseudo_sereen;
/* Has to be allocated in PC_open_graphies():
Pseudo_screen =
(PCJmage *) malloc(6 + xsize * ysize);
*/
char *Pseudo_line [768];
/* Pseudo_line [k] points to the beginning of the k-th line on the pseudo
screen:
for (k = 0; k< ysize; k++)
Pseudo_line [k] = &Pseudo_screen ->Data[xsize * k];
};
If now a< and bare two Vectors and c is a float variable, we can write very
comfortably
c = a + b;
The C++ preprocessor translates such a line into something like
c = some_function(&a, &b);
Since no macro can be used, the C++ version of the dot product of two Vectors
runs more slowly than the macro version. If you have a C++ compiler at your
disposal, use the program listed in Appendix A.5 to check how much the loss of
speed is for your own configuration. It will most probably be more than 50%.
Thus, if you write a program in C++ and your program runs too slowly, look out
for segments that are visited frequently and replace the operator by a macro or at
least by a so-caUed inline function! (Some people may caU this "macromania.")
Seetion B.2. 'Macromania': C, ANSI C or C++ ? 285
unit pointers;
interface
type
ptr = record
offs, 8egm: word;
end;
procedure pp(var p; 8: integer);
procedure mm(var p; s: integer);
function smaller(var pI, p2): boolean;
implementation
procedure pp(var p; s: integer);
begin inc(ptr(p).offs, s) end;
procedure mm(var p; s: integer);
begin dec(ptr(p).offs, s) end;
function smaller(var pI, p2): boolean;
var sI, s2: word;
begin
sI := seg(p1); 82 := seg(p2);
if sI = 82 then smaller := (of8(p1) < ofs(p2))
else smaller := (sI < s2);
end;
end. (* unit pointers *)
I
286 Appendix Chapter B. System Dependencies and Other Programming Languages
To give you an idea of how this unit can be used, we list a sampie program.
First the listing of the C file:
I
#defineX 0
#define Y 1
#define Z 2
#define NI 30
#define N2 20
typedef float Vec3 [3];
typedef float Vec2 [2];
Vec3 v3[NIHN2], *p3;
Vec2 v2[NIHN2], *p2, *p_array[NI], * * pptr;
float * r, *hLr;
int i, j, k;
void mainO
{
/* Example 1: Initialize array with zeros. */
/* First the ordinary C code. */
for (i = 0; i< N1; i++)
for (j =
0; j < N2; i++)
v3[i][j][X] = v3[i] [jHY] = v3[iHjHZ] 0;
/* Now the same with pointer arithmetic. */
r = (float *) v3;
hLr = r + 3 * NI * N2;
while (r< hLr)
*r++ = 0;
/* Example 2: Copy arrays of different types. */
/* First the 'readable' code. */
for (i =0; i< NI; i++)
for (j = 0; j < N2; i++) {
v2[i] [j][X] = v3[iJ(jJ[X];
v2[i] [j][YJ = v3[iJ[jJ[Y];
}
/* Now the same with pointer arithmetic. */
p2 = (Vec2 *) v2;
p3 = (Vec3 *) v3;
for (i = 0; i < (Nl * N2); i++) {
(*p2)[X] = (*p3)[XJ; (*p2)[Yj (*p3)[Y];
p2++; p3++;
}
/* Example 3: How to use pointers to pointers. */
pptr = p_array;
for (i = 0; i< N1; i++)
* pptr++ = v2[iJ;
pptr = &p_array[NI - IJ;
Section B.3. Pointer Arithmetic in PASCAL 287
v3[i, j] := zero3;1
(* Now the same with pointer arithmetic. *)
r:= @v3;
hü := r; pp(hü, 3 * NI * N2 * sizeof(r'));
while smaller(r', hü') do begin
r' := 0; pp(r, sizeof(r'));
end;
(* Example 2: Copy arrays of different type. *)
(* First the 'readable' code. *)
for i:= 0 to NI - 1 do
for j := 0 to N2-1 do begin
v2[i,j,X] := v3[i,j,X];
v2[i,j, Y] := v3[i,j, Y];
end;
(* Now the same with pointer arithmetic: *)
p2 := @v2;
p3 := @v3;
for i:= 0 to NI * N2 - 1 do begin
p2'[X] := p3'[X]; p2'[Y] := p3'[Y];
pp(p2, sizeof(p2')); pp(p3, sizeof(p3'));
end;
(* Example 3: How to use pointers to pointers. *)
pptr := @p_array[O];
for i:= 0 to NI - 1 do begin
pptr' := @v2[i];
pp(pptr, sizeof(pptr));
end;
pptr := @p_array[NI - 1];
for i:= 0 to NI - 1 do begin
p2 := pptr'; mm(pptr, sizeof(pptr'));
for j := 0 to N2 - 1 do begin
p2'[X] := 0; p2'[Y] 0;
pp(p2, sizeof(p2'));
end;
end;
end.
I
[AMME92] Ammeraal, L.: Programming Priciples in Computer Graphics. John Wiley & Sons,
Chichester.
[ANGE90] Angel, E.: Computer Graphics. Addison-Wesley.
[ANGE87] AngelI, !. O. and Griffith, G.H.: High-resolution Computer Graphics Using
FORTRAN 77. Macmillan Education Ltd.
[ATHE77] Atherton, P.R., Weiler, P., et al.: Polygon Shadow Generation. Computer Graphics
12(3).
[BARS88] Barsky, B. A.: Beta-Splines. Springer, Tokyo.
[BISH86] Bishop G. and Weimer, D.M.: Fast Phong Shading. Proc. SIGGRAPH 86, Computer
Graphics 20(4): pp 103-106.
[BOEH85] Böhm, W., Gose G. and Kahmann J.: Methoden der numerischen Mathematik.
Vieweg.
[BROT84] Brotman, L.S. and Badler, N.!.: Generating Soft Shadows with a Depth Buffer
Algorithm. IEEE Computer Graphics and Applications 4(10):5-12.
[BUIT75] Bui-Tuong, Phong, and Crow, F.C.: Improved rendition oE polygonal models oE
curved surEaees. Proc. 2nd USA-Japan Computer Conf.
[BURG89] Burger, P. and Gillies, D.: Interaetive Computer Graphics. Addison-Wesley.
290 References
[BYKA 78) Bykat, A.: Convex Hull of a Finite Set of Points in Two Dimensions. Inform. Proc.
Lett. 7, 296-298.
[CHIN89) Chin, N. and Feiner, St.: Near real-time shadow generation using BSP trees. Com-
puter Graphics 23(3):99-106.
[CROW77) Crow, F.C.: Shadow algorithms for computer graphics. Proc. SIGGRAPH'77,
Computer Graphics 13(2):242-248.
[DARN88) Darnell, P.A. and Margolis, P.E.: Software Engineering in C. Springer/New York.
[EARNSHAW) Earnshaw, R.A. and Wyvill, B. (Eds.): New advances in computer graphics.
Proceedings of CG International 89. Springer Tokyo.
[ENCA83) Encarn~äo J. and Schlechtendahl E.G.: Computer Aided Design. F'undamenta.ls and
System Architectures. Springer Berlin.
[FELL92) Feliner, W.D.: ComputergraJik. B.1. Reihe Informatik Band 58
[FOLE90) Foley J., van Damm, A., et al.: Computer Graphics: Principles and Praetice.
Addison-Wesley.
[FUCH80) Fuchs, H., Kedem, Z.M. and Naylor, B.F.: On visible surfaee generation by apriori
tree structures. Computer Graphics 14(3), pp.124-133.
[GARC86) Garcia, A.: EfIicient Rendering oE Synthetic Images. PhD Thesis, Massachusetts
Institute of Technology.
[GLAE81) Glaeser, G.: Über die RotoidenwendelBächen. Sitz.ber. Öst. Akad. Wiss. 190,
pp.285-302, Vienna.
[GLAE88/2) Glaeser, G.: Visualization oE minimal surfaces. Proc. ofthe 3rd Int. Conf. on Eng.
Graphics and Descriptive Geometry, Vol. I, pp.180-184, Vienna.
[GLAS90] Glassner, A.S., et al. An Introduction to Ray Traeing. Academic Press, San Diego.
[GORA84] Goral, C., et al.: Modeling the interaction of light between diffuse surfaces. Comput.
Graph. 18, pp. 213-222 (SIGGRAPH 84).
[GOUR71) Gouraud, H.: Continuous shading on curved surEaces. IEEE Transactions on Com-
puters C-20(6):623-629.
[HEAR86) Hearn, D. and Baker., M.P.: Computer Graphics. Prentice Hall International.
References 291
[WUND54] Wunderlich, W.: Beitrag zur Kenntnis der MinimalspiralBächen. Rend. Mat. 13,
pp.1-15, Rome.