_GRAPHIC PROGRAMMING COLUMN_ by Michael Abrash [LISTING ONE] ; Draws all pixels in the list of horizontal lines passed in, in ; mode X, the VGA's undocumented 320x240 256-color mode. Clips to ; the rectangle specified by (ClipMinX,ClipMinY),(ClipMaxX,ClipMaxY). ; Draws to the page specified by CurrentPageBase. ; C near-callable as: ; void DrawHorizontalLineList(struct HLineList * HLineListPtr, ; int Color); ; ; All assembly code tested with TASM 2.0 and MASM 5.0 SCREEN_WIDTH equ 320 SCREEN_SEGMENT equ 0a000h SC_INDEX equ 03c4h ;Sequence Controller Index MAP_MASK equ 2 ;Map Mask register index in SC HLine struc XStart dw ? ;X coordinate of leftmost pixel in line XEnd dw ? ;X coordinate of rightmost pixel in line HLine ends HLineList struc Lngth dw ? ;# of horizontal lines YStart dw ? ;Y coordinate of topmost line HLinePtr dw ? ;pointer to list of horz lines HLineList ends Parms struc dw 2 dup(?) ;return address & pushed BP HLineListPtr dw ? ;pointer to HLineList structure Color dw ? ;color with which to fill Parms ends .model small .data extrn _CurrentPageBase:word,_ClipMinX:word extrn _ClipMinY:word,_ClipMaxX:word,_ClipMaxY:word ; Plane masks for clipping left and right edges of rectangle. LeftClipPlaneMask db 00fh,00eh,00ch,008h RightClipPlaneMask db 001h,003h,007h,00fh .code align 2 ToFillDone: jmp FillDone public _DrawHorizontalLineList align 2 _DrawHorizontalLineList proc push bp ;preserve caller's stack frame mov bp,sp ;point to our stack frame push si ;preserve caller's register variables push di cld ;make string instructions inc pointers mov dx,SC_INDEX mov al,MAP_MASK out dx,al ;point SC Index to the Map Mask mov ax,SCREEN_SEGMENT mov es,ax ;point ES to display memory for REP STOS mov si,[bp+HLineListPtr] ;point to the line list mov bx,[si+HLinePtr] ;point to the XStart/XEnd descriptor ; for the first (top) horizontal line mov cx,[si+YStart] ;first scan line to draw mov si,[si+Lngth] ;# of scan lines to draw cmp si,0 ;are there any lines to draw? jle ToFillDone ;no, so we're done cmp cx,[_ClipMinY] ;clipped at top? jge MinYNotClipped ;no neg cx ;yes, discard however many lines are add cx,[_ClipMinY] ; clipped sub si,cx ;that many fewer lines to draw jle ToFillDone ;no lines left to draw shl cx,1 ;lines to skip*2 shl cx,1 ;lines to skip*4 add bx,cx ;advance through the line list mov cx,[_ClipMinY] ;start at the top clip line MinYNotClipped: mov dx,si add dx,cx ;bottom row to draw + 1 cmp dx,[_ClipMaxY] ;clipped at bottom? jle MaxYNotClipped ;no sub dx,[_ClipMaxY] ;# of lines to clip off the bottom sub si,dx ;# of lines left to draw jle ToFillDone ;all lines are clipped MaxYNotClipped: mov ax,SCREEN_WIDTH/4 ;point to the start of the first mul cx ; scan line on which to draw add ax,[_CurrentPageBase] ;offset of first line mov dx,ax ;ES:DX points to first scan line to ; draw mov ah,byte ptr [bp+Color] ;color with which to fill FillLoop: push bx ;remember line list location push dx ;remember offset of start of line push si ;remember # of lines to draw mov di,[bx+XStart] ;left edge of fill on this line cmp di,[_ClipMinX] ;clipped to left edge? jge MinXNotClipped ;no mov di,[_ClipMinX] ;yes, clip to the left edge MinXNotClipped: mov si,di mov cx,[bx+XEnd] ;right edge of fill cmp cx,[_ClipMaxX] ;clipped to right edge? jl MaxXNotClipped ;no mov cx,[_ClipMaxX] ;yes, clip to the right edge dec cx MaxXNotClipped: cmp cx,di jl LineFillDone ;skip if negative width shr di,1 ;X/4 = offset of first rect pixel in scan shr di,1 ; line add di,dx ;offset of first rect pixel in display mem mov dx,si ;XStart and si,0003h ;look up left edge plane mask mov bh,LeftClipPlaneMask[si] ; to clip & put in BH mov si,cx and si,0003h ;look up right edge plane mov bl,RightClipPlaneMask[si] ; mask to clip & put in BL and dx,not 011b ;calculate # of addresses across rect sub cx,dx shr cx,1 shr cx,1 ;# of addresses across rectangle to fill - 1 jnz MasksSet ;there's more than one byte to draw and bh,bl ;there's only one byte, so combine the left ; and right edge clip masks MasksSet: mov dx,SC_INDEX+1 ;already points to the Map Mask reg FillRowsLoop: mov al,bh ;put left-edge clip mask in AL out dx,al ;set the left-edge plane (clip) mask mov al,ah ;put color in AL stosb ;draw the left edge dec cx ;count off left edge byte js FillLoopBottom ;that's the only byte jz DoRightEdge ;there are only two bytes mov al,00fh ;middle addresses are drawn 4 pixels at a pop out dx,al ;set the middle pixel mask to no clip mov al,ah ;put color in AL rep stosb ;draw the middle addresses four pixels apiece DoRightEdge: mov al,bl ;put right-edge clip mask in AL out dx,al ;set the right-edge plane (clip) mask mov al,ah ;put color in AL stosb ;draw the right edge FillLoopBottom: LineFillDone: pop si ;retrieve # of lines to draw pop dx ;retrieve offset of start of line pop bx ;retrieve line list location add dx,SCREEN_WIDTH/4 ;point to start of next line add bx,size HLine ;point to the next line descriptor dec si ;count down lines jnz FillLoop FillDone: pop di ;restore caller's register variables pop si pop bp ;restore caller's stack frame ret _DrawHorizontalLineList endp end [LISTING TWO] /* Matrix arithmetic functions. Tested with Borland C++ 2.0 in the small model */ /* Matrix multiplies Xform by SourceVec, and stores the result in DestVec. Multiplies a 4x4 matrix times a 4x1 matrix; the result is a 4x1 matrix, as follows: -- -- -- -- -- -- | | | 4 | | 4 | | 4x4 | X | x | = | x | | | | 1 | | 1 | -- -- -- -- -- -- */ void XformVec(double Xform[4][4], double * SourceVec, double * DestVec) { int i,j; for (i=0; i<4; i++) { DestVec[i] = 0; for (j=0; j<4; j++) DestVec[i] += Xform[i][j] * SourceVec[j]; } } /* Matrix multiplies SourceXform1 by SourceXform2 and stores the result in DestXform. Multiplies a 4x4 matrix times a 4x4 matrix; the result is a 4x4 matrix, as follows: -- -- -- -- -- -- | | | | | | | 4x4 | X | 4x4 | = | 4x4 | | | | | | | -- -- -- -- -- -- */ void ConcatXforms(double SourceXform1[4][4], double SourceXform2[4][4], double DestXform[4][4]) { int i,j,k; for (i=0; i<4; i++) { for (j=0; j<4; j++) { DestXform[i][j] = 0; for (k=0; k<4; k++) DestXform[i][j] += SourceXform1[i][k] * SourceXform2[k][j]; } } } [LISTING THREE] /* Transforms convex polygon Poly (which has PolyLength vertices), performing the transformation according to Xform (which generally represents a transformation from object space through world space to view space), then projects the transformed polygon onto the screen and draws it in color Color. Also updates the extent of the rectangle (EraseRect) that's used to erase the screen later. Tested with Borland C++ 2.0 in the small model */ #include "polygon.h" void XformAndProjectPoly(double Xform[4][4], struct Point3 * Poly, int PolyLength, int Color) { int i; struct Point3 XformedPoly[MAX_POLY_LENGTH]; struct Point ProjectedPoly[MAX_POLY_LENGTH]; struct PointListHeader Polygon; /* Transform to view space, then project to the screen */ for (i=0; i EraseRect[NonDisplayedPage].Right) if (ProjectedPoly[i].X < SCREEN_WIDTH) EraseRect[NonDisplayedPage].Right = ProjectedPoly[i].X; else EraseRect[NonDisplayedPage].Right = SCREEN_WIDTH; if (ProjectedPoly[i].Y > EraseRect[NonDisplayedPage].Bottom) if (ProjectedPoly[i].Y < SCREEN_HEIGHT) EraseRect[NonDisplayedPage].Bottom = ProjectedPoly[i].Y; else EraseRect[NonDisplayedPage].Bottom = SCREEN_HEIGHT; if (ProjectedPoly[i].X < EraseRect[NonDisplayedPage].Left) if (ProjectedPoly[i].X > 0) EraseRect[NonDisplayedPage].Left = ProjectedPoly[i].X; else EraseRect[NonDisplayedPage].Left = 0; if (ProjectedPoly[i].Y < EraseRect[NonDisplayedPage].Top) if (ProjectedPoly[i].Y > 0) EraseRect[NonDisplayedPage].Top = ProjectedPoly[i].Y; else EraseRect[NonDisplayedPage].Top = 0; } /* Draw the polygon */ DRAW_POLYGON(ProjectedPoly, PolyLength, Color, 0, 0); } [LISTING FOUR] /* 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 the 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 (used to describe a scan-converted polygon to the 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; }; 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 int DisplayedPage, NonDisplayedPage; extern struct Rect EraseRect[]; [LISTING FIVE] /* Simple 3D drawing program to view a polygon as it rotates in mode X. View space is congruent with world space, with the viewpoint 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. Tested with Borland C++ 2.0 in the small model */ #include #include #include #include #include "polygon.h" void main(void); /* 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} }; /* Transformation from polygon's object space to world space. Initially set up to perform no rotation and to move the polygon into world space -140 units away from the origin down the Z axis. Given the viewing point, -140 down the Z axis means 140 units away straight ahead in the direction of view. The program dynamically changes the rotation and translation */ static double PolyWorldXform[4][4] = { {1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, 1.0, -140.0}, {0.0, 0.0, 0.0, 1.0} }; /* Transformation from world space into view space. In this program, the view point is fixed at the origin of world space, looking down the Z axis in the direction of increasingly negative Z, so view space is identical to world space; 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} }; static unsigned int PageStartOffsets[2] = {PAGE0_START_OFFSET,PAGE1_START_OFFSET}; int DisplayedPage, NonDisplayedPage; void main() { int Done = 0; double WorkingXform[4][4]; static struct Point3 TestPoly[] = {{-30,-15,0,1},{0,15,0,1},{10,-5,0,1}}; #define TEST_POLY_LENGTH (sizeof(TestPoly)/sizeof(struct Point3)) double Rotation = M_PI / 60.0; /* initial rotation = 3 degrees */ union REGS regset; Set320x240Mode(); ShowPage(PageStartOffsets[DisplayedPage = 0]); /* Keep rotating the polygon, drawing it to the undisplayed page, and flipping the page to show it */ do { CurrentPageBase = /* select other page for drawing to */ PageStartOffsets[NonDisplayedPage = DisplayedPage ^ 1]; /* Modify the object space to world space transformation matrix for the current rotation around the Y axis */ PolyWorldXform[0][0] = PolyWorldXform[2][2] = cos(Rotation); PolyWorldXform[2][0] = -(PolyWorldXform[0][2] = sin(Rotation)); /* Concatenate the object-to-world and world-to-view transformations to make a transformation matrix that will convert vertices from object space to view space in a single operation */ ConcatXforms(WorldViewXform, PolyWorldXform, WorkingXform); /* 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; /* Transform the polygon, project it on the screen, draw it */ XformAndProjectPoly(WorkingXform, TestPoly, TEST_POLY_LENGTH,9); /* Flip to display the page into which we just drew */ ShowPage(PageStartOffsets[DisplayedPage = NonDisplayedPage]); /* Rotate 6 degrees farther around the Y axis */ if ((Rotation += (M_PI/30.0)) >= (M_PI*2)) Rotation -= M_PI*2; if (kbhit()) { switch (getch()) { case 0x1B: /* Esc to exit */ Done = 1; break; case 'A': case 'a': /* away (-Z) */ PolyWorldXform[2][3] -= 3.0; break; case 'T': /* towards (+Z). Don't allow to get too */ case 't': /* close, so Z clipping isn't needed */ if (PolyWorldXform[2][3] < -40.0) PolyWorldXform[2][3] += 3.0; break; case 0: /* extended code */ switch (getch()) { case 0x4B: /* left (-X) */ PolyWorldXform[0][3] -= 3.0; break; case 0x4D: /* right (+X) */ PolyWorldXform[0][3] += 3.0; break; case 0x48: /* up (+Y) */ PolyWorldXform[1][3] += 3.0; break; case 0x50: /* down (-Y) */ PolyWorldXform[1][3] -= 3.0; 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); }