+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ [1.1] - 3D Programming (part 2) Ý Written by Christian Garms +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ --------------------------------------- / / | ---------------------------------------- | 3D Graphics in BASIC - Part II.1: Epilog / ---------------------------------------- It sounds strange to begin with an epilog but I have to explain some real important things about the listing PYRAMID.BAS in the last part: 1. You only have to calculate the edges of a polygon. In the case of the pyramid you only have to calculate FOUR (!!!) points. The rest will do the LINEs. This is very good because it reduces the amount of calculations to a minimum and also, of course, the amount of cpu usage. 2. You must have an exact represantation of your 3D object. In our case of the pyramid this is very simple. There are only four points. The definition of the object is located in the DATAs. You need in general a DATA statement for the points and a DATA statement for the connectivity list. The connectivity list will instruct the program to draw the right lines to the right points. You must determine every edge exactly of any given 3d object you want to display. This is very time consuming and you can only create smaller objects with a pencil and a paper sheet. For 'bigger' objects (more points) you need a special editor. ----------------------------------------------- / / | ------------------------------------------------ | 3D Graphics in BASIC - Part II.2: 3D Animations / ------------------------------------------------ In PYRAMID.BAS there is only one single picture of a simple object. Boring, isn't ? The real 3D effect will only show up if the object will be animated like rotating around an axis or moving in real time. So I want you to show how to get this 'pyramid' into action. But tons of theory first ... To make understanding easier for this relative difficult subject because this part is like "Formula Jones and The Raiders of the lost Arc" and you could be easily get lost somewhere in the Amasinus I'll give you an overview of what to do: 1. 3D animations - and of course, animations in general - need the double buffering technique. That's a common used method of displaying and generating pictures simultaneously on different screen (often done by choosing different memory locations of the displaying screen and the drawing screen). If you would display and draw the picture on the same screen (the same memory location), the picture might become flickery. With the double buffering technique - and eventually waiting for the vertical retrace interrupt - the animated graphics looks very smooth. 2. Rotating, Scaling and Moving of 3D points could be done with matrix operations. Because matrix operations aren't a simply matter of fact at all I'll explain it here in this article but limited for our purpose. When you've worked through this stuff (it's a very thick formula thicket - have you got your machet right by your hand ?) you will see the advantages of this mathematical technique. ------------------------------------------------- / / | -------------------------------------------------- | 3D Graphics in BASIC - Part II.3: Double Buffering / -------------------------------------------------- Double buffering is a simple matter of fact. You only need two screens or pages in any location of the memory. The Video ram is mostly preferred because of higher perfomance (you must not copy the pages to the video ram from the memory anymore). A general algorithm for the Double Buffering technique is as follows: 1. Show the "display" page, hide the "draw" page 2. Clear "draw" page 3. Various Drawing operations in the "draw" page 4. Wait for vertical rectrace 5. Switch "display" and "draw" page The listing II-3.1 is in example for a simple demo of double buffering. The compiled program will show a rectangle that has four moving corners with different speed and direction. ************************************************************************** ' Double Buffering Demo ' (C)) 1996 by Ch. Garms ' Type declarations TYPE pixel x AS INTEGER y AS INTEGER END TYPE ' Some Constants %NOPE = 0 %UP = 1 %DOWN = 2 %PORT = 4 %STARBORD = 8 ' Variable declarations DEFINT a-z DIM r(3) AS pixel ' rectangle points DIM d(3) AS pixel ' direction increment / decrement ' Screen dimensions %MAXX = 639 %MAXY = 349 ' Init the random generator with a different value RANDOMIZE TIMER ' Sub: Switch Drawing / Displaying Screen SUB switchscreen STATIC drawing, display IF drawing = display THEN drawing = 0 display = 1 ELSE SWAP drawing, display END IF WAIT &H3DA, 8 ' wait for vertical retrace SCREEN 9, 0, drawing, display END SUB ' Sub: Draw rectangle SUB rectangle SHARED r() LINE ( r(0).x, r(0).y ) - ( r(1).x, r(1).y ), 11 LINE ( r(1).x, r(1).y ) - ( r(2).x, r(2).y ), 11 LINE ( r(2).x, r(2).y ) - ( r(3).x, r(3).y ), 11 LINE ( r(3).x, r(3).y ) - ( r(0).x, r(0).y ), 11 END SUB ' Sub: Calculate new points SUB newpoints SHARED r(), d() LOCAL i, bounds FOR i = 0 TO 3 bounds = boundcheck( r(i), d(i) ) SELECT CASE bounds CASE ( %UP OR %PORT ) r(i).x = 0 r(i).y = 0 d(i).x = -d(i).x d(i).y = -d(i).y CASE ( %UP OR %STARBORD ) r(i).x = %MAXX r(i).y = 0 d(i).x = -d(i).x d(i).y = -d(i).y CASE ( %DOWN OR %PORT ) r(i).x = 0 r(i).y = %MAXY d(i).x = -d(i).x d(i).y = -d(i).y CASE ( %DOWN OR %STARBORD ) r(i).x = %MAXX r(i).y = %MAXY d(i).x = -d(i).x d(i).y = -d(i).y CASE %UP r(i).x = ( -r(i).y * d(i).x ) / d(i).y + r(i).x r(i).y = 0 d(i).y = -d(i).y CASE %DOWN r(i).x = ( ( %maxy - r(i).y ) * d(i).x ) / d(i).y + r(i).x r(i).y = %MAXY d(i).y = -d(i).y CASE %PORT r(i).y = d(i).x / d(i).y * -r(i).x + r(i).y r(i).x = 0 d(i).x = -d(i).x CASE %STARBORD r(i).y = d(i).x / d(i).y * (%maxx - r(i).x) + r(i).y r(i).x = %MAXX d(i).x = -d(i).x CASE %NOPE INCR r(i).x, d(i).x INCR r(i).y, d(i).y END SELECT NEXT i END SUB ' Function boundcheck: ' Check if pixel left the frontiers of the screen FUNCTION boundcheck(pt AS pixel, dir AS pixel) AS INTEGER LOCAL work work = %NOPE SELECT CASE pt.y + dir.y CASE < 0 INCR work, %UP CASE > %MAXY INCR work, %DOWN END SELECT SELECT CASE pt.x + dir.x CASE < 0 INCR work, %PORT CASE > %MAXX INCR work, %STARBORD END SELECT boundcheck = work END FUNCTION ' Initializing the 2D object and the directions increments/decrements ' Just a few random numbers ... FOR i=0 TO 3 r(i).x = %MAXX * RND(1) r(i).y = %MAXY * RND(1) WHILE d(i).x = 0 d(i).x = 4 - 8 * RND(1) WEND WHILE d(i).y = 0 d(i).y = 4 - 8 * RND(1) WEND NEXT i ' Main Program ' Calling the SUBs and waiting for a key WHILE NOT INSTAT switchscreen ' Show screen CLS ' Clear the screen rectangle ' Drawing rectangle newpoints ' Calculate the new points WEND ************************************************************************** Listing II-3.1 If you change the main program to the one described in Listing II-3.2 then you will see why it's necessary to flip pages. The aninamtion of the rectangle will become flickery. So that's why page flipping is important for any type of animation. ************************************************************************** ' modified Main program ' actually it didn't flip the pages anymore ... SCREEN 9 WHILE NOT INSTAT CLS ' Clear the screen rectangle ' Drawing rectangle newpoints ' Calculate the new points WEND ************************************************************************** Listing II-3.2: RECTANGLE.BAS ------------------------------------------------- / / | -------------------------------------------------- | 3D Graphics in BASIC - Part II.4: 3D Object moving / -------------------------------------------------- Moving - or also called: translation - of an object is done by changing the coordinates of the object. Let's start with a simple example: a point in the 3D world. Moving the point could be done by: 1. changing the points coordinates: obj.x = obj.x + t.x obj.y = obj.y + t.y obj.z = obj.z + t.z With (obj.x/obj.y/obj.z) = 3D point and (t.x/t.y/t.z) = translation vector. The translation vector describes how much a point is moved in any direction (x,y,z). or (very important !) 2. changing the viewers coordinates: eye.x = eye.x + t.x eye.y = eye.y + t.y eye.z = eye.z + t.z With (eye.x/eye.y/eye.z) = viewers' point The result will be the same: The point will be moved. That's the same phenomon as if we watched the sunrise. Not the sun is going up but our planet earth is rotating around his polar axis. We know that the earth is moving but it looks like the sun is moving. The listing II-4.1 is the modified example of the last part - PYRAMID.BAS. Now it shows some motion. The pyramid is bouncing (in fact the viewpoint is moving) to the viewer and away from him/her. ************************************************************************** ' --------------------- ' Moving Pyramid ' based on PYRAMID.BAS ' (C) 1996 by Ch. Garms ' --------------------- ' Compiler Instructions $CPU 80386 $OPTIMIZE SPEED $LIB GRAPH ON $ERROR ALL ON $COMPILE MEMORY ' Creating new TYPEs TYPE vector x AS INTEGER y AS INTEGER z AS INTEGER END TYPE TYPE pixel x AS INTEGER y AS INTEGER END TYPE ' Variable declarations %MAXPT = 3 ' max. points %MAXLN = 5 ' max. lines DIM s(%MAXPT) AS pixel ' 2D coordinates of Pyramid DIM eye AS vector ' viewpoint DEFINT a-z ' Initializing screen constants %MAXPOSX = 639 ' max. X-coordinate of screen %MAXPOSY = 349 ' max. Y-coordinate of screen %CENTERX = 320 ' center of screen (X-position) %CENTERY = 175 ' center of screen (Y-position) ' Initializing Viewpoint eye.x = 15 eye.y = 15 eye.z = 0 ' Calculating the eye coordinates & transformation into screen pixels SUB vec2pix( objpt AS vector, scrpix AS pixel ) SHARED eye DECR objpt.x, eye.x DECR objpt.y, eye.y DECR objpt.z, eye.z scrpix.x = (objpt.x / objpt.z) * %MAXPOSX + %CENTERX scrpix.y = (objpt.y / objpt.z) * %MAXPOSY + %CENTERY END SUB ' Switch screens: ' implementation for PB's SCREEN SUB switchscreen STATIC display, drawing IF display = drawing THEN display = 0 drawing = 1 ELSE SWAP display, drawing END IF WAIT &H3DA, 8 ' wait for vertical retrace SCREEN 9, 0, display, drawing CLS END SUB ' IMPORTANT: from here starts the nonrecycable code ' Main program DIM pwork AS vector WHILE NOT INSTAT FOR j = 40 TO 200 STEP 2 switchscreen eye.z = j RESTORE objectdata FOR i = 0 TO %MAXPT READ pwork.x, pwork.y, pwork.z vec2pix pwork, s(i) NEXT i RESTORE connectdata FOR i = 0 TO %MAXLN READ pt1, pt2 LINE (s(pt1).x,s(pt1).y) - (s(pt2).x,s(pt2).y) NEXT i NEXT j FOR j = 200 TO 40 STEP -2 switchscreen eye.z = j RESTORE objectdata FOR i = 0 TO %MAXPT READ pwork.x, pwork.y, pwork.z vec2pix pwork, s(i) NEXT i RESTORE connectdata FOR i = 0 TO %MAXLN READ pt1, pt2 LINE (s(pt1).x,s(pt1).y) - (s(pt2).x,s(pt2).y) NEXT i NEXT j WEND SCREEN 0 ' Object Data & Connectivity list objectdata: DATA 30, 1, 1 DATA 1, 30, 1 DATA 1, 1, 30 DATA -30, -30, -30 connectdata: DATA 0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3 ************************************************************************** Listing II-4.1: MOVINPYR.BAS The listing II-4.1 has some nice features. It contains code that's recycable (you mustn't reinvent the wheel !). For our purposes there are two new SUBs: SUB switschscreen: This subroutine flips between two pages in the video mode 9 (640x375x16 colours). This EGA resolution is more than enough for simple vector graphics. SUB vec2pix: Converts a vector (3D point) to a screen pixel. This SUB is resolution independant. You have to define only the viewpoint (setting eye.x/eye.y/eye.z) and the screen parameters %MAXPOSX, %MAXPOSY,%CENTERX,%CENTERY before your first call. --------------------------------------------------- / / | ---------------------------------------------------- | 3D Graphics in BASIC - Part II.5: 3D Object rotating / ---------------------------------------------------- There isn't much to say about 3D rotating. Only formulas, formulas, formulas. I think our friend Formula Jones won't be unhappy if we come to the point right now: Rotating around the x-axis (Global coordinate system): x' = x*cos(alpha) - y*sin(alpha) y' = x*sin(alpha) + y*cos(alpha) z' = z Rotating around the y-axis (Global coordinate system): x' = x*cos(beta) + z*sin(beta) y' = y z' = -x*sin(beta) + z*cos(beta) Rotating around the z-axis (Global coordinate system): x' = x' y' = y*cos(gamma) - z*sin(gamma) z' = y*sin(gamma) + z*cos(gamma) With: (x/y/z) = old point (x'/y'/z') = new point alpha = angle to rotate around x-axis clockwise beta = angle to rotate around y-axis clockwise gamma = angle to rotate around z-axis clockwise I won't explain the origin of these formulas because that will not fit into this article. If you're interrested you'll find this very complex stuff in any "higher" math book. Listing II-5.1 is an example of use. Our well known pyramid is now rotating around his z- and x-axis. But the basic program can be easily changed. If you want to rotate to any other axis then you have only to change the calls. Just experimentate with this program ! ************************************************************************** ' --------------------- ' Rotating Pyramid ' based on PYRAMID.BAS ' (C) 1996 by Ch. Garms ' --------------------- ' Compiler Instructions $CPU 80386 $OPTIMIZE SPEED $LIB GRAPH ON $ERROR ALL OFF $FLOAT EMULATE ' Creating new TYPEs TYPE vector x AS INTEGER y AS INTEGER z AS INTEGER END TYPE TYPE pixel x AS INTEGER y AS INTEGER END TYPE ' Variable declarations %MAXPT = 3 ' max. points %MAXLN = 5 ' max. lines %FACTOR = 16384 %ANGLE = 3600 ' max. angles for sinus and cosinus DIM s(%MAXPT) AS pixel DIM sinus(%ANGLE) AS SHARED INTEGER ' array for sinus table DIM cosinus(%ANGLE) AS SHARED INTEGER ' array for cosinus table DIM eye AS SHARED vector ' viewpoint DIM pwork AS vector deg2rad! = 1800/3.14152695 DEFINT a-z ' Initializing Sinus table FOR i = 0 TO %ANGLE sinus(i) = CINT( SIN( i/deg2rad!) * %FACTOR ) cosinus(i) = CINT( COS( i/deg2rad!) * %FACTOR ) NEXT i ' Screen constants %MAXPOSX = 639 ' max. X-coordinate of screen %MAXPOSY = 349 ' max. Y-coordinate of screen %CENTERX = 320 ' center of screen (X-position) %CENTERY = 175 ' center of screen (Y-position) ' Clipping constants %LEFT = 1 %RIGHT = 2 %UP = 4 %DOWN = 8 %TRUE = -1 %FALSE = 0 ' Initializing Viewpoint eye.x = 0 eye.y = 0 eye.z = 150 ' Rotating Point around X-Axis ' objpt : vector in world coordinates (!) ' alpha : angle to rotate around X-Axis (1 means 0.1 deg) SUB rotatex( objpt AS vector, alpha AS INTEGER ) SHARED sinus(), cosinus() DIM p AS vector p.x = (objpt.x * cosinus(alpha) - objpt.y * sinus(alpha)) / %FACTOR p.y = (objpt.x * sinus(alpha) + objpt.y * cosinus(alpha)) / %FACTOR objpt.x = p.x objpt.y = p.y END SUB ' Rotating Point around Y-Axis ' objpt : vector in world coordinates (!) ' beta : angle to rotate around Y-Axis (1 means 0.1 deg) SUB rotatey( objpt AS vector, beta AS INTEGER ) SHARED sinus(), cosinus() DIM p AS vector p.x = (objpt.x * cosinus(beta) + objpt.z * sinus(beta)) / %FACTOR p.z = (objpt.x * -sinus(beta) + objpt.z * cosinus(beta)) / %FACTOR objpt.x = p.x objpt.z = p.z END SUB ' Rotating Point around Z-Axis ' objpt : vector in world coordinates (!) ' gamma : angle to rotate around Y-Axis (1 means 0.1 deg) SUB rotatez( objpt AS vector, gamma AS INTEGER ) SHARED sinus(), cosinus() DIM p AS vector p.y = (objpt.y * cosinus(gamma) - objpt.z * sinus(gamma)) / %FACTOR p.z = (objpt.y * sinus(gamma) + objpt.z * cosinus(gamma)) / %FACTOR objpt.y = p.y objpt.z = p.z END SUB ' Calculating the eye coordinates & transformation into screen pixels ' objpt : vector in world coordinates (!) ' scrpix: pixel on screen ' The variable eye (TYPE vector) must be defined before calling this sub. SUB vec2pix( objpt AS vector, scrpix AS pixel ) SHARED eye DECR objpt.x, eye.x DECR objpt.y, eye.y DECR objpt.z, eye.z scrpix.x = (objpt.x / objpt.z) * %MAXPOSX + %CENTERX scrpix.y = (objpt.y / objpt.z) * %MAXPOSY + %CENTERY END SUB ' Switch screens SUB switchscreen STATIC display, drawing IF display = drawing THEN display = 0 drawing = 1 ELSE SWAP display, drawing END IF WAIT &H3DA, 8 ' wait for vertical retrace SCREEN 9, 0, display, drawing CLS END SUB ' IMPORTANT: from here starts the nonrecycable code ' Main program WHILE NOT INSTAT FOR j=0 TO %ANGLE STEP 15 switchscreen RESTORE objectdata FOR i = 0 TO %MAXPT READ pwork.x, pwork.y, pwork.z rotatez pwork, j rotatey pwork, j vec2pix pwork, s(i) NEXT i RESTORE connectdata FOR i = 0 TO %MAXLN READ pt1, pt2 LINE (s(pt1).x,s(pt1).y) - (s(pt2).x,s(pt2).y) NEXT i NEXT j WEND SCREEN 0 ' Object Data & Connectivity list objectdata: DATA 30, 0, 0 DATA 0, 30, 0 DATA 0, 0, 30 DATA -30,-30, -30 connectdata: DATA 0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3 ************************************************************************** Listing II-5.1: ROTPYR.BAS The listing II-5.1 has a very nice trick: The sinus and cosinus values are converted to integers by multiplying with a constant factor (the factor must be less than 32767) and stored in an integer array. That makes the calculation of the 3D rotating faster than with floating point math. It is not a great secret because it is a well used technique for vector graphics since games like Elite on the C-64. Though the calculation aren't very precise the screen resolution is so small that calculation errors won't disturb much. The listing II-5.1 simplifies the rotating. As you can see all formulas rotate around an axis of the global coordinate system. But the pyramid is rotating around a point in the center of the pyramid. The program achieves this by equalising the center of the object and the center of the global coordinate system. If a chosen scenery is more complex (e.g. two objects who rotates differemt) then we come to a new coordinate system which I will now introduce: The Object coordinate system. That means: All points of a given object will be defined relative to the center of the object. To display the object in the global cordinate system (or: world coordinate system) we have only add the translation vector from the center of the object to the center of the global corrdinate system to all points of the object. For example I will take the single point once more for explanation of this complex subject: The point is the center of the object. The relative object coordinate will be (0/0/0) and the translation vector (x/y/z). To display the point into the world coordinate system we simply add the translation vector to the object coordinates so the derived global coordinte point is (x/y/z). In general: world.x = obj.x + transl.x world.y = obj.y + transl.y world.z = obj.z + transl.z with: (world.x/world.y/world.z) = world coordinates of object point (obj.x/obj.y/obj.z) = object coordinates of object point (transl.x/transl.y/transl.z) = translation vector of object That's the same as translating a 3D point in the world coordinate system. Now we've defined our object within the object coordinate system we only have to equalise the object center and the world center in our mind. For the rotations we take the object coordinates not the world coordinates ! Than we can perform the rotations. To display the object we add the translation vector of the object center to all object points and convert the points to screen pixels. ------------------------------------------------------------------- / / | --------------------------------------------------------------------- | 3D Graphics in BASIC - Part II.6: Introductions to Matrix calculations / ---------------------------------------------------------------------- Matrix operations aren't a mystical thing. You have not to be a math genius to understand what matrices are: "A Matrix is a represantion of a linear equation" In other words: A Matrix isn't more than an array of values which contains the suffixes of any linear equation like: a1*x + b1*y + c1*z = d1 a2*x + b2*y + c2*z = d2 a3*x + b3*y + c3*z = d3 The corresponding matrix looks as follows: |a1 b1 c1| |d1| |a2 b2 c2| = |d2| |a3 b3 c3| |d3| For our purposes we didn't need more to know. As you have seen our 3D operations are often performed by linear equations. E.g. translation of a point is performed by adding the translation vector to a point. If we write down this equation in a matrix form it will look like: Matrix1 Matrix2 Matrix3 |x| |1 0 0 t.x| |x + t.x| |y| |0 1 0 t.y| = |y + t.y| |z| |0 0 1 t.z| |z + t.z| |0| |0 0 0 0 | |0 | That means we only multiply the 3D point (Matrix1) with an operator (Matrix2) to translate the point. It looks like I want to complicate all. But the advantage of matrix operations is that you can chain many 3D operations like rotation or translation to only one single matrix for all points of any object. This will reduce the calculations enormous and speed up 3D graphics dramatically for larger objects. OK, guys. Next time I will continue you to explain the calculation with matrix and go further with filled polygon graphics. Hope to see you again here. -------------------------------------------------------- * EDITOR'S NOTE: * This article was originally printed in Peter Cooper's BASIX Fanzine, * Issue #8 from February 1997.