_GRAPHICS PROGRAMMING COLUMN_ by Michael Abrash [LISTING ONE] /* 3D animation program to view a cube as it rotates in mode X. The viewpoint is fixed at the origin (0,0,0) of world space, looking in the direction of increasingly negative Z. A right-handed coordinate system is used throughout. All C code tested with Borland C++ 2.0 in C compilation mode */ #include #include #include #include "polygon.h" #define ROTATION (M_PI / 30.0) /* rotate by 6 degrees at a time */ /* Base offset of page to which to draw */ unsigned int CurrentPageBase = 0; /* Clip rectangle; clips to the screen */ int ClipMinX=0, ClipMinY=0; int ClipMaxX=SCREEN_WIDTH, ClipMaxY=SCREEN_HEIGHT; /* Rectangle specifying extent to be erased in each page */ struct Rect EraseRect[2] = { {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT} }; static unsigned int PageStartOffsets[2] = {PAGE0_START_OFFSET,PAGE1_START_OFFSET}; int DisplayedPage, NonDisplayedPage; /* Transformation from cube's object space to world space. Initially set up to perform no rotation and to move the cube into world space -100 units away from the origin down the Z axis. Given the viewing point, -100 down the Z axis means 100 units away in the direction of view. The program dynamically changes both the translation and the rotation. */ static double CubeWorldXform[4][4] = { {1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, 1.0, -100.0}, {0.0, 0.0, 0.0, 1.0} }; /* Transformation from world space into view space. Because in this application the view point is fixed at the origin of world space, looking down the Z axis in the direction of increasing Z, view space is identical to world space, and this is the identity matrix */ static double WorldViewXform[4][4] = { {1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, 1.0, 0.0}, {0.0, 0.0, 0.0, 1.0} }; /* All vertices in the cube */ static struct Point3 CubeVerts[] = { {15,15,15,1},{15,15,-15,1},{15,-15,15,1},{15,-15,-15,1}, {-15,15,15,1},{-15,15,-15,1},{-15,-15,15,1},{-15,-15,-15,1}}; /* Vertices after transformation */ static struct Point3 XformedCubeVerts[sizeof(CubeVerts)/sizeof(struct Point3)]; /* Vertices after projection */ static struct Point3 ProjectedCubeVerts[sizeof(CubeVerts)/sizeof(struct Point3)]; /* Vertices in screen coordinates */ static struct Point ScreenCubeVerts[sizeof(CubeVerts)/sizeof(struct Point3)]; /* Vertex indices for individual faces */ static int Face1[] = {1,3,2,0}; static int Face2[] = {5,7,3,1}; static int Face3[] = {4,5,1,0}; static int Face4[] = {3,7,6,2}; static int Face5[] = {5,4,6,7}; static int Face6[] = {0,2,6,4}; /* List of cube faces */ static struct Face CubeFaces[] = {{Face1,4,15},{Face2,4,14}, {Face3,4,12},{Face4,4,11},{Face5,4,10},{Face6,4,9}}; /* Master description for cube */ static struct Object Cube = {sizeof(CubeVerts)/sizeof(struct Point3), CubeVerts, XformedCubeVerts, ProjectedCubeVerts, ScreenCubeVerts, sizeof(CubeFaces)/sizeof(struct Face), CubeFaces}; void main() { int Done = 0, RecalcXform = 1; double WorkingXform[4][4]; union REGS regset; /* Set up the initial transformation */ Set320x240Mode(); /* set the screen to mode X */ ShowPage(PageStartOffsets[DisplayedPage = 0]); /* Keep transforming the cube, drawing it to the undisplayed page, and flipping the page to show it */ do { /* Regenerate the object->view transformation and retransform/project if necessary */ if (RecalcXform) { ConcatXforms(WorldViewXform, CubeWorldXform, WorkingXform); /* Transform and project all the vertices in the cube */ XformAndProjectPoints(WorkingXform, &Cube); RecalcXform = 0; } CurrentPageBase = /* select other page for drawing to */ PageStartOffsets[NonDisplayedPage = DisplayedPage ^ 1]; /* Clear the portion of the non-displayed page that was drawn to last time, then reset the erase extent */ FillRectangleX(EraseRect[NonDisplayedPage].Left, EraseRect[NonDisplayedPage].Top, EraseRect[NonDisplayedPage].Right, EraseRect[NonDisplayedPage].Bottom, CurrentPageBase, 0); EraseRect[NonDisplayedPage].Left = EraseRect[NonDisplayedPage].Top = 0x7FFF; EraseRect[NonDisplayedPage].Right = EraseRect[NonDisplayedPage].Bottom = 0; /* Draw all visible faces of the cube */ DrawVisibleFaces(&Cube); /* Flip to display the page into which we just drew */ ShowPage(PageStartOffsets[DisplayedPage = NonDisplayedPage]); while (kbhit()) { switch (getch()) { case 0x1B: /* Esc to exit */ Done = 1; break; case 'A': case 'a': /* away (-Z) */ CubeWorldXform[2][3] -= 3.0; RecalcXform = 1; break; case 'T': /* towards (+Z). Don't allow to get too */ case 't': /* close, so Z clipping isn't needed */ if (CubeWorldXform[2][3] < -40.0) { CubeWorldXform[2][3] += 3.0; RecalcXform = 1; } break; case '4': /* rotate clockwise around Y */ AppendRotationY(CubeWorldXform, -ROTATION); RecalcXform=1; break; case '6': /* rotate counterclockwise around Y */ AppendRotationY(CubeWorldXform, ROTATION); RecalcXform=1; break; case '8': /* rotate clockwise around X */ AppendRotationX(CubeWorldXform, -ROTATION); RecalcXform=1; break; case '2': /* rotate counterclockwise around X */ AppendRotationX(CubeWorldXform, ROTATION); RecalcXform=1; break; case 0: /* extended code */ switch (getch()) { case 0x3B: /* rotate counterclockwise around Z */ AppendRotationZ(CubeWorldXform, ROTATION); RecalcXform=1; break; case 0x3C: /* rotate clockwise around Z */ AppendRotationZ(CubeWorldXform, -ROTATION); RecalcXform=1; break; case 0x4B: /* left (-X) */ CubeWorldXform[0][3] -= 3.0; RecalcXform=1; break; case 0x4D: /* right (+X) */ CubeWorldXform[0][3] += 3.0; RecalcXform=1; break; case 0x48: /* up (+Y) */ CubeWorldXform[1][3] += 3.0; RecalcXform=1; break; case 0x50: /* down (-Y) */ CubeWorldXform[1][3] -= 3.0; RecalcXform=1; break; default: break; } break; default: /* any other key to pause */ getch(); break; } } } while (!Done); /* Return to text mode and exit */ regset.x.ax = 0x0003; /* AL = 3 selects 80x25 text mode */ int86(0x10, ®set, ®set); } [LISTING TWO] /* Transforms all vertices in the specified object into view space, then perspective projects them to screen space and maps them to screen coordinates, storing the results in the object. */ #include #include "polygon.h" void XformAndProjectPoints(double Xform[4][4], struct Object * ObjectToXform) { int i, NumPoints = ObjectToXform->NumVerts; struct Point3 * Points = ObjectToXform->VertexList; struct Point3 * XformedPoints = ObjectToXform->XformedVertexList; struct Point3 * ProjectedPoints = ObjectToXform->ProjectedVertexList; struct Point * ScreenPoints = ObjectToXform->ScreenVertexList; for (i=0; iX = XformedPoints->X / XformedPoints->Z * PROJECTION_RATIO * (SCREEN_WIDTH / 2.0); ProjectedPoints->Y = XformedPoints->Y / XformedPoints->Z * PROJECTION_RATIO * (SCREEN_WIDTH / 2.0); ProjectedPoints->Z = XformedPoints->Z; /* Convert to screen coordinates. The Y coord is negated to flip from increasing Y being up to increasing Y being down, as expected by the polygon filler. Add in half the screen width and height to center on the screen */ ScreenPoints->X = ((int) floor(ProjectedPoints->X + 0.5)) + SCREEN_WIDTH/2; ScreenPoints->Y = (-((int) floor(ProjectedPoints->Y + 0.5))) + SCREEN_HEIGHT/2; } } [LISTING THREE] /* Draws all visible faces (faces pointing toward the viewer) in the specified object. The object must have previously been transformed and projected, so that the ScreenVertexList array is filled in. */ #include "polygon.h" void DrawVisibleFaces(struct Object * ObjectToXform) { int i, j, NumFaces = ObjectToXform->NumFaces, NumVertices; int * VertNumsPtr; struct Face * FacePtr = ObjectToXform->FaceList; struct Point * ScreenPoints = ObjectToXform->ScreenVertexList; long v1,v2,w1,w2; struct Point Vertices[MAX_POLY_LENGTH]; struct PointListHeader Polygon; /* Draw each visible face (polygon) of the object in turn */ for (i=0; iNumVerts; /* Copy over the face's vertices from the vertex list */ for (j=0, VertNumsPtr=FacePtr->VertNums; j 0) { /* It is facing the screen, so draw */ /* Appropriately adjust the extent of the rectangle used to erase this page later */ for (j=0; j EraseRect[NonDisplayedPage].Right) if (Vertices[j].X < SCREEN_WIDTH) EraseRect[NonDisplayedPage].Right = Vertices[j].X; else EraseRect[NonDisplayedPage].Right = SCREEN_WIDTH; if (Vertices[j].Y > EraseRect[NonDisplayedPage].Bottom) if (Vertices[j].Y < SCREEN_HEIGHT) EraseRect[NonDisplayedPage].Bottom = Vertices[j].Y; else EraseRect[NonDisplayedPage].Bottom=SCREEN_HEIGHT; if (Vertices[j].X < EraseRect[NonDisplayedPage].Left) if (Vertices[j].X > 0) EraseRect[NonDisplayedPage].Left = Vertices[j].X; else EraseRect[NonDisplayedPage].Left = 0; if (Vertices[j].Y < EraseRect[NonDisplayedPage].Top) if (Vertices[j].Y > 0) EraseRect[NonDisplayedPage].Top = Vertices[j].Y; else EraseRect[NonDisplayedPage].Top = 0; } /* Draw the polygon */ DRAW_POLYGON(Vertices, NumVertices, FacePtr->Color, 0, 0); } } } [LISTING FOUR] /* Routines to perform incremental rotations around the three axes */ #include #include "polygon.h" /* Concatenate a rotation by Angle around the X axis to the transformation in XformToChange, placing result back in XformToChange. */ void AppendRotationX(double XformToChange[4][4], double Angle) { double Temp10, Temp11, Temp12, Temp20, Temp21, Temp22; double CosTemp = cos(Angle), SinTemp = sin(Angle); /* Calculate the new values of the four affected matrix entries */ Temp10 = CosTemp*XformToChange[1][0]+ -SinTemp*XformToChange[2][0]; Temp11 = CosTemp*XformToChange[1][1]+ -SinTemp*XformToChange[2][1]; Temp12 = CosTemp*XformToChange[1][2]+ -SinTemp*XformToChange[2][2]; Temp20 = SinTemp*XformToChange[1][0]+ CosTemp*XformToChange[2][0]; Temp21 = SinTemp*XformToChange[1][1]+ CosTemp*XformToChange[2][1]; Temp22 = SinTemp*XformToChange[1][2]+ CosTemp*XformToChange[2][2]; /* Put the results back into XformToChange */ XformToChange[1][0] = Temp10; XformToChange[1][1] = Temp11; XformToChange[1][2] = Temp12; XformToChange[2][0] = Temp20; XformToChange[2][1] = Temp21; XformToChange[2][2] = Temp22; } /* Concatenate a rotation by Angle around the Y axis to the transformation in XformToChange, placing result back in XformToChange. */ void AppendRotationY(double XformToChange[4][4], double Angle) { double Temp00, Temp01, Temp02, Temp20, Temp21, Temp22; double CosTemp = cos(Angle), SinTemp = sin(Angle); /* Calculate the new values of the four affected matrix entries */ Temp00 = CosTemp*XformToChange[0][0]+ SinTemp*XformToChange[2][0]; Temp01 = CosTemp*XformToChange[0][1]+ SinTemp*XformToChange[2][1]; Temp02 = CosTemp*XformToChange[0][2]+ SinTemp*XformToChange[2][2]; Temp20 = -SinTemp*XformToChange[0][0]+ CosTemp*XformToChange[2][0]; Temp21 = -SinTemp*XformToChange[0][1]+ CosTemp*XformToChange[2][1]; Temp22 = -SinTemp*XformToChange[0][2]+ CosTemp*XformToChange[2][2]; /* Put the results back into XformToChange */ XformToChange[0][0] = Temp00; XformToChange[0][1] = Temp01; XformToChange[0][2] = Temp02; XformToChange[2][0] = Temp20; XformToChange[2][1] = Temp21; XformToChange[2][2] = Temp22; } /* Concatenate a rotation by Angle around the Z axis to the transformation in XformToChange, placing result back in XformToChange. */ void AppendRotationZ(double XformToChange[4][4], double Angle) { double Temp00, Temp01, Temp02, Temp10, Temp11, Temp12; double CosTemp = cos(Angle), SinTemp = sin(Angle); /* Calculate the new values of the four affected matrix entries */ Temp00 = CosTemp*XformToChange[0][0]+ -SinTemp*XformToChange[1][0]; Temp01 = CosTemp*XformToChange[0][1]+ -SinTemp*XformToChange[1][1]; Temp02 = CosTemp*XformToChange[0][2]+ -SinTemp*XformToChange[1][2]; Temp10 = SinTemp*XformToChange[0][0]+ CosTemp*XformToChange[1][0]; Temp11 = SinTemp*XformToChange[0][1]+ CosTemp*XformToChange[1][1]; Temp12 = SinTemp*XformToChange[0][2]+ CosTemp*XformToChange[1][2]; /* Put the results back into XformToChange */ XformToChange[0][0] = Temp00; XformToChange[0][1] = Temp01; XformToChange[0][2] = Temp02; XformToChange[1][0] = Temp10; XformToChange[1][1] = Temp11; XformToChange[1][2] = Temp12; } [LISTING FIVE] /* POLYGON.H: Header file for polygon-filling code, also includes a number of useful items for 3D animation. */ #define MAX_POLY_LENGTH 4 /* four vertices is the max per poly */ #define SCREEN_WIDTH 320 #define SCREEN_HEIGHT 240 #define PAGE0_START_OFFSET 0 #define PAGE1_START_OFFSET (((long)SCREEN_HEIGHT*SCREEN_WIDTH)/4) /* Ratio: distance from viewpoint to projection plane / width of projection plane. Defines the width of the field of view. Lower absolute values = wider fields of view; higher values = narrower */ #define PROJECTION_RATIO -2.0 /* negative because visible Z coordinates are negative */ /* Draws the polygon described by the point list PointList in color Color with all vertices offset by (X,Y) */ #define DRAW_POLYGON(PointList,NumPoints,Color,X,Y) \ Polygon.Length = NumPoints; Polygon.PointPtr = PointList; \ FillConvexPolygon(&Polygon, Color, X, Y); /* Describes a single 2D point */ struct Point { int X; /* X coordinate */ int Y; /* Y coordinate */ }; /* Describes a single 3D point in homogeneous coordinates */ struct Point3 { double X; /* X coordinate */ double Y; /* Y coordinate */ double Z; /* Z coordinate */ double W; }; /* Describes a series of points (used to store a list of vertices that describe a polygon; each vertex is assumed to connect to the two adjacent vertices, and the last vertex is assumed to connect to the first) */ struct PointListHeader { int Length; /* # of points */ struct Point * PointPtr; /* pointer to list of points */ }; /* Describes beginning and ending X coordinates of a single horizontal line */ struct HLine { int XStart; /* X coordinate of leftmost pixel in line */ int XEnd; /* X coordinate of rightmost pixel in line */ }; /* Describes a Length-long series of horizontal lines, all assumed to be on contiguous scan lines starting at YStart and proceeding downward (describes a scan-converted polygon to low-level hardware-dependent drawing code) */ struct HLineList { int Length; /* # of horizontal lines */ int YStart; /* Y coordinate of topmost line */ struct HLine * HLinePtr; /* pointer to list of horz lines */ }; struct Rect { int Left, Top, Right, Bottom; }; /* Structure describing one face of an object (one polygon) */ struct Face { int * VertNums; /* pointer to vertex ptrs */ int NumVerts; /* # of vertices */ int Color; /* polygon color */ }; /* Structure describing an object */ struct Object { int NumVerts; struct Point3 * VertexList; struct Point3 * XformedVertexList; struct Point3 * ProjectedVertexList; struct Point * ScreenVertexList; int NumFaces; struct Face * FaceList; }; extern void XformVec(double Xform[4][4], double * SourceVec, double * DestVec); extern void ConcatXforms(double SourceXform1[4][4], double SourceXform2[4][4], double DestXform[4][4]); extern void XformAndProjectPoly(double Xform[4][4], struct Point3 * Poly, int PolyLength, int Color); extern int FillConvexPolygon(struct PointListHeader *, int, int, int); extern void Set320x240Mode(void); extern void ShowPage(unsigned int StartOffset); extern void FillRectangleX(int StartX, int StartY, int EndX, int EndY, unsigned int PageBase, int Color); extern void XformAndProjectPoints(double Xform[4][4], struct Object * ObjectToXform); extern void DrawVisibleFaces(struct Object * ObjectToXform); extern void AppendRotationX(double XformToChange[4][4], double Angle); extern void AppendRotationY(double XformToChange[4][4], double Angle); extern void AppendRotationZ(double XformToChange[4][4], double Angle); extern int DisplayedPage, NonDisplayedPage; extern struct Rect EraseRect[];