Professional Documents
Culture Documents
and Transformations.
----------------------------------------------------------------------------
Edition 1.2, by Trip V. Reece, June 1992.
e-mail: tvreece@caticsuf.csufresno.edu
cis: Trip V. Reece 70620,2371
Permission is given to copy and distribute without alteration.
Table of Contents
-----------------
I. Mapping 3d onto 2d
A. Skrinkage of X,Y dimensions
B. Vanishing Point
@ C. Aspect Ratio
III. Rotation
A. Rotating a 2d coordinate
B. Rotation around another point
C. Rotating a 3d coordinate
D. The Matrix Solution
V. Z-Buffering
A. Why?
B. How?
VI. Techniques
A. Bresenham's Drawing algorithm
Now, imagine each of these dots to be 3cm from the dot above/below or to
the left or right of it. Imagine now, if you will, another, perfectly
identical sheet of plastic exactly 3cm from the first, parallel to the first
and further away from you than the first. Now, imagining how it would look,
you would notice that the X and Y dimensions are contracted with greater
intensity the further away a sheet of plastic is from you. The marks on the
plastic appear to be "pulled in" to the point directly in front of your
eyeball, contracted >towards< that point. The first concept, that of
diminishing size with distance is given by the relationship:
( Equation 1-1 )
Precondition: Z is greater than or equal to 1.
Value: Shrinkage = ( 1 - scale / Z )
New X = Old_X - Shrinkage * Old_X
New Y = Old_Y - Shrinkage * Old_Y
Now, we have declared Shrinkage to be the value of one minus the value
of scale divided by the distance plus one. Ignore scale for now, the value Z
is the Z coordinate of the three-dimensional point. If you multiplied out
the expanded form of Shrinkage into the value of New X you would get:
New X = Old_X - Old_X * ( 1 - scale/Z )
which is the same as:
New X = Old_X - Old_X + Old_X * scale / Z
which is the same as:
New X = Old_X / Z * scale
( Equation 1-2 )
PreCondition: Z is greater or equal to 1.
Shrinkage = ( 1 - scale / Z )
New X = Old_X - Shrinkage * Old_X
New Y = Old_Y - Shrinkage * ( Old_Y - EyeLevel )
A few assumptions are made here, first: that the origin is located in
the center of the screen, and that EyeLevel is a positive number. EyeLevel
tells us how HIGH UP from the origin the vanishing point should be.
Typically, if the vertical resolution of the screen is MaxY then the maximum
positive coordinate would be MaxY / 2 and if this value were divided by 3
then you would have EyeLevel = MaxY / 6. What the subtraction of MaxY does
to the incoming Y coordinates is to translate everything UP. Yes, it sounds
backwards, but when you subtract inside the parantheses, it becomes addition
outisde of the parantheses, so it really is ADDED, but added BEFORE it is
mapped onto the 2-d plane. Now, why isn't the X coordinate similarly
translated? Because: in a typical picture, the vanishing point is in the
center of the horizontal direction, which is where the origin is in this
implementation. If the origin were anywhere else on the screen, it >would<
have to be translated to the center of the screen.
This should now help to explain how the value of "scale" works. In
practice, a large value of scale tends to make the horizon much closer. In
fact, this is exactly what scale does for us. It narrows the depth of field,
in a sense. If you increase the value of scale from 1 to a large value,
objects that previously stretched back towards the vanishing point now seem
to be much thinner, and hence more realistic. The value of scale is
dependant upon the horizontal resolution of the screen. I have found that a
setting scale equal to the horizontal resolution provides reliable results.
Fiddle around with this value, but after a while, you will want the scale to
remain constant in your program.
One other nagging little problem with all of this is the dreaded aspect
ratio. Depending on the graphics mode you are in, if the maximum X
resolution is not the same as the maximum Y resolution then you must, that is
>MUST< scale either the final X or final Y coordinate by this ratio. You
will want to compute this ratio at the beginning of your program so as to
avoid any unnecessary divides. This is how to do it:
Ratio = MaxYresolution / MaxXresolution
Now, this ratio is usually, 1.333333... for modes such as 640x480, or
800x600, or 1024x768, but may change as video displays improve and older
modes, such as 320x200, are used. To convert your "final" X and Y
coordinates into something that you can plot on the monitor you must multiply
the X coordinate by this ratio before you plot the point. Alternatively, you
can computer the reciprocal of the ratio, and multiply the "final" Y value by
this "inverse ratio" to scale it that way. But always remember to avoid
division wherever possible, multiplication of non-integers is much faster
than division by anything, especially zero. :-)
That's it for mapping! Feel free to use equation 1-2 in any of your
programs!
X = R * Cos (Theta)
Y = R * Sin (Theta)
Tangent ( Y / X ) = Theta
Theta = ArcTangent ( Y / X )
Sine ( Y / R ) = Theta
Theta = ArcSin ( Y / R )
Cosine ( X / R ) = Theta
Theta = ArcCos ( X / R )
Ahh, but wait! This will not work all the time! Why not? Because when
Old_X is negative, the value of Theta will be the value of the reference
angle, NOT the angle from the origin! A simple fix is to use the following
code instead:
Now, perform the rotation with Old_X2 and Old_Y2 in place of Old_X and
Old_Y respectively. Then, after you finish that, translate it back with:
Now, if that isn't enough obfuscation, I shall write out the complete
rotation with all variables in their proper place:
--Matrices
How is this solved with matrices? It is not as easy to rotate about
another point with matrices, but rotating about a single axis is relatively
straightforward. Consider the matrix*matrix problem below:
| 1 0 0 0 | | 1 | | A |
| 0 cT sT 0 | * | x | = | B |
| 0 -sT -cT 0 | | y | | C |
| 0 0 0 1 | | z | | D |
[ cT = Cos(Theta), sT = Sin(Theta) ]
If you go ahead with the matrix multiplication (I'll write it all out for
those who are rusty this first time,) then you get a result of:
A = 1, B= x*cos(Theta)+y*sin(Theta), C= -x*sin(Theta)-y*cos(Theta), D= z
New_X = B
New_Y = C
New_Z = D
This is the rotation of a point (x,y,z) about the X axis, for Theta degrees.
I'd guess this is a correct rotation... But the value of C seems to be
incorrect. However, I will stick to non-matrix calculations to eliminate all
those unnecessary ???*0 + ???*0 + ???*1 operations. [ I consent that these
transforms make no sense to me. ]
| cT 0 -sT 0 | | 1 | | A |
| 0 1 0 0 | * | x | = | B |
| -sT 0 cT 0 | | y | | C |
| 0 0 0 1 | | z | | D |
In this case:
Again, this really looks pretty wrong. Perhaps I'm not multiplying matrices
correctly, or maybe the matrices are set up wrongly. However, if it works-
use it.
In this case:
Now, this may make some sense. Rotating about the z axis should only affect
the x and y coordinates. Hmm, in this case A and B >must< hold the new X and
Y coordinates, since C contains an unchanged value of y. Well, that's all
about rotation with matrices that I'm prepared to be flamed for.
This section is postponed until I can get some accurate information about the
matrices for rotation and other affine transformations.
Okay. translation and rotation may be all fine and dandy, but suppose I
want to display my teapot that I've got encoded in a very nice compact data
structure, but it looks transparent. In fact, it looks like a wireframe,
which it is!
-How to go about Z-Buffering-
First, get the memory. Wherever possible, grab memory. You will need
for this exercise: enough memory for 3 times the video resolution in pixels,
for example: 320x200 = 64000. You will need 320x200x3 = 192000 bytes for
this, unless you choose to perform some in-memory compression, an adivsable
technique! This assumes you are using 8-bit color, i.e. 256 color.
First, reserve one byte for each pixel on the screen as part of a
"virtual" screen. This byte will store the color for that pixel once the 3d
mapping and depth checking is complete. Initialize this array with the
background color of your choice.
Next, reserve two bytes (using the type integer, for example) for each
pixel on the virtual screen in another array (yes this spans two segments of
memory or more.) These two bytes store the distance of the pixel in the
range from 0 .. 65535. This is the Z-Buffer itself. In actuality, it
shouldn't be called a Z buffer in a perspective rendering modeller. It is
properly a Distance-Buffer, but in an orthographic environment, the depth is
the same as the Z coordinate, so the name stuck. Oh well. Initialize this
array of distances with the value of 65535, or whatever your yon/hither
values dictate. It is a good idea to set a maximum distance on the objects
you render, as well as a minimum distance- this prevents unnecessary
calculations that would end up in an object rendered that only appeared as
three small pixels not worthy of being called a polygon, or an impossibly
huge triangle (for example) that covers up everything else on the screen.
Use your judgement.
To draw a single frame now: Scroll through your list of objects that
are in visible sight. Calculate the distance of each vertex from your
eyeball using the pythagorean distance formula:
______________________
Distance = \/ X^2 + Y^2 + Z^2
or:
Distance = ( X^2 + Y^2 + Z^2 ) ^ 0.5
Now, calculate the coordinates of the mapped point on the 2d plane for
this vertex. (This should account for the vanishing point, as well as
perspective.) Now, compare the value of this distance with the value of the
distance contained in the array of distances at the pixel location you just
found the 2d mapped coordinates of. If this vertex is of a less distance
then replace the byte in the array of color with the color of this pixel, and
store the calculated distance into the "Z-Buffer" array of distances at the
location where you found it to lie on the 2d plane. The gist of this all is
to compare the distance of a point with the distance found in the array, if
the distance already in the array is less than the distance of the pixel we
are checking, then throw out that new pixel. A pixel closer to you will
"cover up" a pixel that is farther away.
To Z-Buffer a polygon, the vertices of the polygon must be projected
onto the Z-Buffer plane, and checked for overlapping. Even if the vertices
aren't visible, there may be points on the polygon that >are< visible due to
a "hole" in another polygon that just happens to be blocking the rest of the
polygon we are rendering. Instead of calculating the distance to each pixel
of the polygon (entailing far too many squareds and square root operations to
be feasible on most 80x86's) a form of interpolation may be used provided the
four vertices and their distances; and upon the condition that the polygon is
perfectly flat. Of course Z-Buffering can also apply to non-polygonal
shapes, however, it radically increases rendering time to use non-polygons.
After all the shapes/polygons have been rendered onto the virtual
screen, the screen of visible colors must be copied into the video area.
This can be quickened by storing the color array in video memory to begin
with, and page in the new screens as they are available. Be sure not to have
the video screen showing that you are currently Z-Buffering! That would give
away the secret! :-)
Another improvement to make that is simplified when Z-Buffering is
shading polygons. Simply calculate the angle between the polygon normal (the
perpendicular to the polygon's surface) and the light source (a "global"
value) and take the cosine of this angle to get the intensity of the light
reflected off the polygon. Be sure to multiply the value of the cosine by
the maximum allowable intensity, since cosine returns values ranging from
-1,...+1. Negative values either should be negated to get the positive
value, or it could signify a hidden surface, i.e. a surface that faces away
from the viewer.
The whole trick behind how this works is based on the mathematical fact
that multiplication is really only repeated addition, and division is really
only repeated subtraction. There are two cases for the line drawing
algorithm, 1: a line with a slope >= 1, and 2: a line with a slope < 1.
Since slope is (delta Y)/(delta X) this means that the algorithm is
dependent upon the fact that deltaY or delta X is the larger of the two.
Let's consider case 2, where delta X exceeds delta Y.
Perhaps an table of the values of Cycle, X_plot, & Y_plot as the loop is
executed will better explain what is happening. The values passed to the
algorithm are called x1, y1, x2, y2.
Here the loop ends because delta X = 9 and x1+9 equals delta X. Actually,
this is leaving out an important detail: if y2 is below y1 then instead of
adding a positive 1 for each overflow of Cycle, a 1 must be subtracted. This
can be done easily by setting a variable called Increment equal to positive
one or negative one at the beginning of the routine.
In the case of delta Y being larger than delta X, follow the same procedure
as above, but the loop will follow the line vertically, so replace delta X
with delta Y and vice versa.
It can be shown that the pattern that Cycle takes on needs not be calculated
beyond a certain point. As soon as Cycle regains the value it had to begin
with (delta X divided by two, rounded down) it will follow the same pattern
again and again. I suspect that Cycle will always begin to repeat at or
before delta_X number of positions down the list. It is possible that the
algorithm could beoptimized even further- calculate the first delta_X values
for Cycle, perform a check and identify each position where cycle
>>decreases<< (i.e. it has overflowed and Y_Plot must be incremented)- place
a one at each point where Y_Plot must be incremented, and then follow along
>this< array and increment Y_Plot (or X_Plot as the case may be) where a one
occurs. And if you have the Megs and Megs of memory available, you could
precalculate each of these arrays for every possible combination of delta Y
and delta X... Maybe that's going a bit far now- :-).
Why use the Bresenham line drawing algorithm? It's blazingly fast. Consider
the work needed in dividing half a zillion line slope calculations, in
real-time. This routine only requires addition, subtraction, and a check.
The intel processors have been optimized to all hell for wicked speed at
addition/subtraction/comparisons, whereas division will ALWAYS be slow slow
slow...
-Trip V. Reece
Any comments, suggestions, errata reports please email me.
e-mail: tvreece@caticsuf.csufresno.edu