|====================================|
| |
| TELEMACHOS proudly presents : |
| |
| Part 3 of the PXD trainers - |
| |
| 3D Vector engine |
| The basics of 3D |
| |
|====================================|
___---__--> The Peroxide Programming Tips <--__---___
<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
Intoduction
-----------
Hiya... I'm Telemachos of Peroxide - a new danish group (yes... we DO
have groups in Denmark too :))) )
Since my last trainer quite a few people has mailed me and asked me to
do trainers on various 3D subjects.
As a result of this I have decided to dedicate my next two trainers
to the realm of 3D graphic.
What we'll be doing in the next two trainers is to build a 3D vector
engine capable of doing most of the effects used in todays demos.
Our engine will have the following features.
Tuturial #3 (this one) :
- Nice 3D object organization - no unnessesary rotations done.
- Depth sorting
- Hidden face removal.
- Solid Fill
- Glenzing
Tuturial #4 (next week or so...) :
- Z-shading
- Flat shading according to moving lightsource(s)
- Gouraud Shading according to moving lightsource(s)
- Texturemapping
- Environment-mapping
- Phong Shading (a fake 'cause )
So when you are done with these two tuturials you should be able to
do some nice 3D vector objects.
It might seem that tuturial 4 will be a hard one to get through -
but believe me : We're doing all the preparations in this trainer.
Once our 3D engine is set up all the different shading/fill types are
surprisingly alike :)
******************* ATTENTION ARTISTS!!! *****************************
ARE YOU AN ARTIST ?
DO YOU DRAW VGA-BITMAPS ?
CAN YOU DO GFX IN ALL RESOLUTIONS - 320x200x256 AND VARIOUS SVGA-MODES ?
DO YOU WANT TO SEE YOUR WORK IN AMAZING PRODUCTIONS ?
CAN YOU DO GAME-GFX AS WELL AS BACKGROUNDS FOR DEMOS ?
IF YOU MEET THE ABOVE TERMS (or some at least :) ),
THEN DROP ME (TELEMACHOS) A MESSAGE OR MAIL THE GROUP AT :
Peroxide@image.dk
*********************************************************************
If you want to get in contact with me, there are several ways of
doing it :
1) Write me via FIDO-net : 2:235/350.22
2) E-mail me : tm@image.dk
3) Snail mail me : Kasper Fauerby
Saloparken 226
8300 Odder
Denmark
4) Call me (Voice ! ) : +45 86 54 07 60
Get this serie from the major demo-related FTP-sites - currently :
GARBO ARCHIVES (forgot the address) : /pc/programming/
ftp.teeri.oulu.fi : /msdos/programming/docs/
Or grap it from my homepage :
Telemachos' Codin' Corner http://www.image.dk/~tm
FIRST THINGS FIRST! - 3D DEFINATIONS AND TRANSLATIONS
------------------------------------------------------
Lets define the 3D space where our object is placed. The origin of
the 3D object space is located at the middle of the screen and the
axis is defined as shown below :
\ |
-----------------------------> X-axis
| \
| \
| \
| \
| \
| \>
| Z-axis (into the screen :) )
|
V
Y-axis
Ok.. so our object is made from a number of 3D points each defined by
(X,Y,Z) But how do we plot such a 3D point to the screen ?? We must
find some way of translating the 3 coordinated in the 2 coordinates
of the screen.
This is done by calculating the intersection between the vector made
from the 3D point and the eye position and the viewplane.
I won't get into the math involved in this operation - if you'r
interrested go grap a book on vector math and read the bit about
intersection between a line and a plane.
I'll just give you some formulas :
X0 := Xofs + Round(X*(Zeye/(Zeye-Z)));
Y0 := Yofs + Round(Y*(Zeye/(Zeye-Z)));
Set Xofs to 160 and Yofs to 100 to center object space at the center
of the screen.
If you'r unhappy with these formulas (they are a bit slow because of
the Round and all the floating point muls and divs) you can use a
approximation of the formulas :
X0 := Xofs + (x div (z-Zoff)) shl 8;
Y0 := Yofs + (y div (z-Zoff)) shl 8;
This will also work allthough not entirely correct.
LETS TALK ABOUT DATA-STRUCTURES
--------------------------------
Now for the keyword of 3D programming - DATA STRUCTURES !!
In 3D it is VERY important that you think about how to structure your
data so that a minimum of calculations has to be done.
First problem is how to store your 3D object.
The easiest way is to store it an array of polygons, each polygon
consisting of 4 3D points. But it is also the way ONE SHOULD NEVER!!
store 3D objects.
Lets have a look at a simple cube. A cube consists of 6 sides right ?
And each side consist of 4 points. That sums up to 24 points to be
rotated using the above mentioned data structure.
But as we all know there is only 8 corners in a cube - ie. there is
only 8 DIFFERENT points in a cube.
So what we do is to store all the different points in one array - and
then introduce another variable - we could call it faces - to store
information about which points appears in which faces.
This way we only rotates ONE THIRD of the points we rotate using the
first methode.
The goal in 3D is never do do unnessesary calculations. Only rotate
each point ONCE, only draw each pixel on the screen ONCE and so on.
Now some words about precision.
As we are using a pretty low resolution (320X200) a fairly big amount
of rounding is performed when rotation / projecting a 3D point.
If you store your 3D object in a single variable and rotates the
object into the very same variable the object will in time be distorted
from the roundings. So my advice is to store the original 3D object
in ONE variable and then rotate this BASEOBJECT into a buffer from
which you then perform all the calculations on the rotated 3D object.
This way each rotation starts out with a "fresh" 3D object and the
distortion will be minimal.
For an idea of a 3D data structure check out the sample program
supplied with this text.
NOTE!!
This is only meant as an idea of a data structure. Modify it to match
your needs.
OK - NOW I WANT TO ROTATE THE POINTS
-------------------------------------
Having an object defined by 3D coords is no fun if it just stands
still on the screen. So we want to rotate it around the different
axes.
For this there is 3 formulas - one for each axis. Please note that
the formulas presented here is the standard rotation formulas given
by vector math books. Numerous optimizations has been made to these
routines making rotation a little faster but I won't get into that
in this text. For now the important thing is to make the points
rotate correctly :)
Before we throw ourselves into the formulas one more important note
has to be made. One has to bear in mind that the sequence in which
the point is rotated around the different axes DOES matter. To rotate
the point 5 degrees around the X axis and then 5 degrees around the
Y axis is NOT the same as rotating it about the Y axis and then the
X axis!! Think about it!
So, it has been decided that one rotates around the axes in the
following order : X, Y and then Z.
Ok... now it's time for the formulas :
rotation around Z-axis : (rotating the point X,Y,Z A degrees)
Xrotated = Cos (A)*X - Sin (A)*Y
Yrotated = Sin (A)*X + Cos (A)*Y
Zrotated = Z
rotation around X-axis :
Xrotated = X
Yrotated = Cos (A)*Y - Sin (A)*Z
Zrotated = Sin (A)*Y + Cos (A)*Z
rotation around Y-axis :
Xrotated = Cos (A)*X - Sin (A)*Z
Yrotated = Y
Zrotated = Sin (A)*X + Cos (A)*Z
OK... these are the formulas - check out the sample program for an
idea of how to combine them in a pretty fast way.
I just grapped the rotation routine from an Asphyxia tuturial
by Denthor.
His routine is a straight implemention of the above mentioned
formulas - in fixed point assembler. Thanx Denthor.
OK.. lets sum things up.
By now we have learned to
- rotate a 3D point around all 3 axis
- Translate a 3D point into 2D screen coordinates
This is allmost enough to get you started coding a 3D object engine.
With this information you could easily code a wirefram engine - as a
matter of fact I suggest you do just that! By coding a simple 3D
engine you really learn the next stuff faster :)
BACK SO SOON ??? - POLYGON DRAWING
-----------------------------------
OK.. I guess you have now coded a wireframe engine (not so hard ehh ?)
and you want more. Lets face it - your demo won't win The Party if
it's only running wireframe 3D graphics.
But we're going to change that - now is the time for solid objects!!
(WEE!!)
To draw a solid face we need to code a good polygon routine.
We'll code it in a way so that all the nice shadings in the next
tuturial will be really easy to implement - I'll tell you how right
now!
What is a polygon ?? In demos most polygons are triangles but today
we'll draw four sided polygons because I'm gonna use a simple square
box for my sample code. Don't worry though - I'll tell you how to do
triangles too.
So we got our four points and we want to fill the shape with some
color.
But how ?? Well... we draw polygons scanline per scanline.
Ie. a polygon is a bunch of horizontal lines.
So the first thing we'll need is a horizontal line drawer :
Procedure HorLine(Xbegin,Xend,Ypos: integer; color: byte; where: word);
Assembler;
asm
mov cx,[Xend]
inc cx
sub cx,[Xbegin] {cx = length of line - used for counter }
{note, I assume that Xbegin < Xend -
the poly routine}
{will take care of that...}
mov ax,[ypos]
shl ax,8
mov di,ax
shr ax,2
add di,ax
add di,[Xbegin] {di = Ypos * 320 + Xbegin - offset for our line}
mov es,[where] {where to draw..}
mov al,[color]
rep stosb {I draw byte by byte - slower than drawing a word}
{at a time but it is because of the changes we are}
{going to make to this routine when
glenzing/gouraud/texturemapping}
end;
OK.. now we just have to find out what the X-coords for each
scanline is.
We define a variable to hold that information for us :
polygon : Array[0..199,1..2] of byte;
This variable can hold the endpoints of a horizontal line for each of
the 200 y-lines on the VGA screen.
Now we want to fill this buffer out with the correct information.
For each of the four sides (three if it were a triangle) we scan
along the edge of the polygon and updates the polygon variable like
this :
Procedure ScanPolySide(X1,Y1,X2,Y2 : integer);
var
DeltaX : integer;
temp : integer;
Xposfixed,Xpos : integer;
counter : integer;
begin
if Y2=Y1 then exit; {exit if side is a horizontal line }
if (Y2<Y1) then {make sure Y1 is top point}
begin
temp := Y1;
Y1 := Y2;
Y2 := temp;
temp := X1;
X1 := X2;
X2 := temp; {switch the points if Y1 is not top..}
end;
DeltaX := ((X2-X1) shl 7) div (Y2-Y1);
{DeltaX in 9.7 fixed point math}
Xposfixed := X1 shl 7; {Xpos in 9.7 fixed point math }
for counter := Y1 to Y2 do
begin
Xpos := XposFixed shr 7;
if (Xpos < polygon[counter,1]) then polygon[counter,1] := Xpos;
if (Xpos > polygon[counter,2]) then polygon[counter,2] := Xpos;
Xposfixed := XposFixed + DeltaX;
end;
end;
Now... run this procedure on each side of the poly, and the variable
'polygon' is set up and ready for our friend Mr. Horline.
So..
Procedure Polygon(X1,Y1,X2,Y2,X3,Y3,X4,Y4: integer;
color : byte; where : word);
var
counter : integer;
Ymin, Ymax : integer;
polygon : Array[0..199,1..2] of integer;
begin
Ymin := Y1;
Ymax := Y1;
if (Y2 < Ymin) then Ymin := Y2;
if (Y2 > Ymax) then Ymax := Y2;
if (Y3 < Ymin) then Ymin := Y3;
if (Y3 > Ymax) then Ymax := Y3;
if (Y4 < Ymin) then Ymin := Y4;
if (Y4 > Ymax) then Ymax := Y4;
{what is Ymin and Ymax in this polygon ?}
for counter := 0 to 199 do
begin
polygon[counter,1] := 32000;
polygon[counter,2] := -32000;
end;
{we have to initialize our variable 'polygon' to some extreme values}
ScanPolySide(X1,Y1,X2,Y2);
ScanPolySide(X2,Y2,X3,Y3);
ScanPolySide(X3,Y3,X4,Y4);
ScanPolySide(X4,Y4,X1,Y1); {all four sides scanned}
for counter := Ymin to Ymax do
Horline(polygon[counter,1],polygon[counter,2],counter,color,where);
end;
Well... that was pretty easy... Now we can do solid sides in our 3D
cube. Try it out before continuing :)
WHAT - SOMETHING IS WRONG ?? - DEPTH SORTING :
------------------------------------------------
YES - I know. You have some problems getting your cube look right when
having different colors for each face. This is because of the random
order we draw the faces in.
We have to find some way of depth sorting them. We sort the faces by
their center point. The Z component of their center point, that is.
So for each frame we have to calculate the center Z-value for each
face in the object.
To find the center Z value we add the Z values from the 4 points
making the face and divide this value by 4 (if we are talking
triangles obviously we'll divide by 3)
Only that we don't divide at all! If we don't divide by the number of
points we'll NOT end up with the center Z value. We'll end up with a
value that is 4 times to big. But as ALL the calculated Z values will
be 4 times to big they will still sort corectly.
We'll store the values in a variable called
centers : Array[1..Num_of_faces] of integer;
But when we start sorting this table we'll mess up which center that
belongs to which face. To solve this problem we'll introduce another
variable called
OrderTable : Array[1..Num_of_faces] of integer;
This table will store the correct sequence for drawing the faces.
Ie. it'll store the face-numbers belonging to a centervalue.
To do the actual sorting we'll use a simple bubble-sort.
This is a pretty simple sorting algoritm which will work for us
as long we are not talking sorting hundreds of faces.
If you are rendering LOTS of faces I suggest you use a quicksort
routine instead.
The idea is to scan through the centers variable and compare the
entries two and two. If the value at position+1 is higher than the
value at position we switch the two values and updates the ordertable
variable.
We then reset the scan and start over. We continue doing this until
we reach the end of 'centers' without having to switch any values.
Procedure Sort_faces;
{Just a simple bubble-sort - not to fast but what the heck :) }
{Faces with the HIGHEST Z-val is placed first in Order[] }
VAR
counter : integer;
position : integer;
tempval : integer;
BEGIN
for counter:=1 to Num_of_faces do BEGIN
OrderTable[counter]:=counter;
END;
{we resets the ordertable so that it matches the
unsorted 'centers' variable}
position := 1;
repeat
if (centers[position] < centers[position+1]) then
BEGIN
tempval := Centers[position+1];
Centers[position+1] := centers[position];
centers[position] := tempval;
tempval := OrderTable[position+1];
OrderTable[position+1] := OrderTable[position];
OrderTable[position] := tempval;
position:=1;
END;
inc(position);
until (position = Num_of_faces);
END;
Now when doing the faces we draw them in the order specified in
OrderTable.
Go ahead... try it out...
HIDDEN FACE REMOVAL
--------------------
Fire up your 3D engine and take a look at the rotating cube.
Looks cool ehh ?? Nice work... YOU did that ;)
But try and count the faces visible at a time. How many faces do you
see at one time ? Only about half of them ????
ARGH!! We are drawing half an object that is never seen ???
This won't do. Right now it does not matter much as drawing a polygon
is fairly fast - but next week when we start texturemapping or
shading the faces it will slow things down.
So - we'll just have to remove the faces we do not see.
This is fortunately VERY!! easy.
To be able to perform this operation it is important that you have
defined your faces in a clockwise direction. Ie. all faces should be
defined as :
P1 P2
|---------------|
| |
| |
| |
| |
| |
|---------------|
P4 P3
Ok... now I'll introduce a new formula to you - the cross product.
The cross product is a formula that returns the normal to a plane :
Xnormal=(P2.Y-P1.Y)(P1.Z-P3.Z)-(P2.Z-P1.Z)(P1.Y-P3.Y)
Ynormal=(P2.Z-P1.Z)(P1.X-P3.X)-(P2.X-P1.X)(P1.Z-P3.Z)
Znormal=(P2.X-P1.X)(P1.Y-P3.Y)-(P2.Y-P1.Y)(P1.X-P3.X)
Well... for now we are only interrested in the Z normal.
If the Z normal is negative it means that the normal points towards
us - ie. we draw the face.
If not we discard it.
One more important note to make is that because the Znormal does not
involve any Z components of the points we can use it on the PROJECTED
2D values.
Now go implement this in your 3D engine - if you are having trouble
take a look at the <a href="pxdtut3.zip">sample program</a>.
Next week we'll see how we use this normal (the hole 3D normal though)
for shading...
ONE LAST THING - GLENZING THE POLYGON
--------------------------------------
OK.. now we have done a 3D object engine that handles 3D objects
correct - showing only what HAS to be shown.
That done I think you are ready for another kind of fill - that
TOTALLY ignores ALL we have learned about sorting and hidden face
removal... SIGH, thats life :)
The effect is called glenzing and the idea is to make the object look
like it is made from colored glass. This effect is done by setting
the correct pallette. I don't think there is any way of calculating
this pallette so you'll have to experiment.
Now when drawing your faces you do not draw the polygon in one color.
For each pixel you grap the background color and add it to the face
color.
This is of course a little slower than doing single colored polygons
but not much. You'll have to change your horizontal line drawer to do
this - check the sample program if you experiment any trouble.
When glenzing you should obviously NOT remove the faces pointing away
from you as they can be seen through the glass sides.
Sorting is useless.... the result will look the same with or without
sorting.
In my opinion glenzing is not a cool effect - but well... it can't
hurt to know how to do it :)
LAST REMARKS
-------------
Well, that's about all for now.
Hope you found this doc useful - and BTW : If you DO make anything
public using these techniques please mention me in your greets or
where ever you se fit.
I DO love to see my name in a greeting :=)
As mentioned earlier my next tuturial will be on different types of
shading - using this 3D object engine as a base...
But after that ??
If you have any good ideas for a subject you wish to see a tuturial
on please mail me. If I like the idea (and know anything about it :)
I'll write a tut on it.
Keep on coding...
Telemachos - May '97.
|