_GRAPHICS PROGRAMMING COLUMN_ by Michael Abrash [LISTING ONE] ; Fixed point multiply and divide routines. Tested with TASM 3.0. USE386 equ 1 ;1 for 386-specific opcodes, 0 for 8088 opcodes MUL_ROUNDING_ON equ 1 ;1 for rounding on multiplies, 0 for no ; rounding. Not rounding is faster, rounding is ; more accurate and generally a good idea DIV_ROUNDING_ON equ 0 ;1 for rounding on divides, 0 for no rounding. ; Not rounding is faster, rounding is more ; accurate, but because division is only ; performed to project to the screen, rounding ; quotients generally isn't necessary ALIGNMENT equ 2 .model small .386 .code ;===================================================================== ; Multiplies two fixed-point values together. C near-callable as: ; Fixedpoint FixedMul(Fixedpoint M1, Fixedpoint M2); FMparms struc dw 2 dup(?) ;return address & pushed BP M1 dd ? M2 dd ? FMparms ends align ALIGNMENT public _FixedMul _FixedMul proc near push bp mov bp,sp if USE386 mov eax,[bp+M1] imul dword ptr [bp+M2] ;multiply if MUL_ROUNDING_ON add eax,8000h ;round by adding 2^(-17) adc edx,0 ;whole part of result is in DX endif ;MUL_ROUNDING_ON shr eax,16 ;put the fractional part in AX else ;!USE386 ;do four partial products and add them ; together, accumulating the result in CX:BX push si ;preserve C register variables push di ;figure out signs, so we can use unsigned multiplies sub cx,cx ;assume both operands positive mov ax,word ptr [bp+M1+2] mov si,word ptr [bp+M1] and ax,ax ;first operand negative? jns CheckSecondOperand ;no neg ax ;yes, so negate first operand neg si sbb ax,0 inc cx ;mark that first operand is negative CheckSecondOperand: mov bx,word ptr [bp+M2+2] mov di,word ptr [bp+M2] and bx,bx ;second operand negative? jns SaveSignStatus ;no neg bx ;yes, so negate second operand neg di sbb bx,0 xor cx,1 ;mark that second operand is negative SaveSignStatus: push cx ;remember sign of result; 1 if result ; negative, 0 if result nonnegative push ax ;remember high word of M1 mul bx ;high word M1 times high word M2 mov cx,ax ;accumulate result in CX:BX (BX not used until ; next operation) assume no overflow into DX mov ax,si ;low word M1 times high word M2 mul bx mov bx,ax add cx,dx ;accumulate result in CX:BX pop ax ;retrieve high word of M1 mul di ;high word M1 times low word M2 add bx,ax adc cx,dx ;accumulate result in CX:BX mov ax,si ;low word M1 times low word M2 mul di if MUL_ROUNDING_ON add ax,8000h ;round by adding 2^(-17) adc bx,dx else ;!MUL_ROUNDING_ON add bx,dx ;don't round endif ;MUL_ROUNDING_ON adc cx,0 ;accumulate result in CX:BX mov dx,cx mov ax,bx pop cx and cx,cx ;is the result negative? jz FixedMulDone ;no, we're all set neg dx ;yes, so negate DX:AX neg ax sbb dx,0 FixedMulDone: pop di ;restore C register variables pop si endif ;USE386 pop bp ret _FixedMul endp ;===================================================================== ; Divides one fixed-point value by another. C near-callable as: ; Fixedpoint FixedDiv(Fixedpoint Dividend, Fixedpoint Divisor); FDparms struc dw 2 dup(?) ;return address & pushed BP Dividend dd ? Divisor dd ? FDparms ends align ALIGNMENT public _FixedDiv _FixedDiv proc near push bp mov bp,sp if USE386 if DIV_ROUNDING_ON sub cx,cx ;assume positive result mov eax,[bp+Dividend] and eax,eax ;positive dividend? jns FDP1 ;yes inc cx ;mark it's a negative dividend neg eax ;make the dividend positive FDP1: sub edx,edx ;make it a 64-bit dividend, then shift ; left 16 bits so that result will be in EAX rol eax,16 ;put fractional part of dividend in ; high word of EAX mov dx,ax ;put whole part of dividend in DX sub ax,ax ;clear low word of EAX mov ebx,dword ptr [bp+Divisor] and ebx,ebx ;positive divisor? jns FDP2 ;yes dec cx ;mark it's a negative divisor neg ebx ;make divisor positive FDP2: div ebx ;divide shr ebx,1 ;divisor/2, minus 1 if the divisor is adc ebx,0 ; even dec ebx cmp ebx,edx ;set Carry if the remainder is at least adc eax,0 ; half as large as the divisor, then ; use that to round up if necessary and cx,cx ;should the result be made negative? jz FDP3 ;no neg eax ;yes, negate it FDP3: else ;!DIV_ROUNDING_ON mov edx,[bp+Dividend] sub eax,eax shrd eax,edx,16 ;position so that result ends up sar edx,16 ; in EAX idiv dword ptr [bp+Divisor] endif ;DIV_ROUNDING_ON shld edx,eax,16 ;whole part of result in DX; ; fractional part is already in AX else ;!USE386 ;NOTE!!! Non-386 division uses a 32-bit dividend but only the upper 16 bits ; of the divisor; in other words, only the integer part of the divisor is ; used. This is done so that the division can be accomplished with two fast ; hardware divides instead of a slow software implementation, and is (in my ; opinion) acceptable because division is only used to project points to the ; screen (normally, the divisor is a Z coordinate), so there's no cumulative ; error, although there will be some error in pixel placement (the magnitude ; of the error is less the farther away from the Z=0 plane objects are). This ; is *not* a general-purpose divide, though; if the divisor is less than 1, ; for instance, a divide-by-zero error will result! For this reason, non-386 ; projection can't be performed for points closer to the viewpoint than Z=1. ;figure out signs, so we can use ; unsigned divisions sub cx,cx ;assume both operands positive mov ax,word ptr [bp+Dividend+2] and ax,ax ;first operand negative? jns CheckSecondOperandD ;no neg ax ;yes, so negate first operand neg word ptr [bp+Dividend] sbb ax,0 inc cx ;mark that first operand is negative CheckSecondOperandD: mov bx,word ptr [bp+Divisor+2] and bx,bx ;second operand negative? jns SaveSignStatusD ;no neg bx ;yes, so negate second operand neg word ptr [bp+Divisor] sbb bx,0 xor cx,1 ;mark that second operand is negative SaveSignStatusD: push cx ;remember sign of result; 1 if result ; negative, 0 if result nonnegative sub dx,dx ;put Dividend+2 (integer part) in DX:AX div bx ;first half of 32/16 division, integer part ; divided by integer part mov cx,ax ;set aside integer part of result mov ax,word ptr [bp+Dividend] ;concatenate the fractional part of ; the dividend to the remainder (fractional ; part) of the result from dividing the ; integer part of the dividend div bx ;second half of 32/16 division if DIV_ROUNDING_ON EQ 0 shr bx,1 ;divisor/2, minus 1 if the divisor is adc bx,0 ; even dec bx cmp bx,dx ;set Carry if the remainder is at least adc ax,0 ; half as large as the divisor, then adc cx,0 ; use that to round up if necessary endif ;DIV_ROUNDING_ON mov dx,cx ;absolute value of result in DX:AX pop cx and cx,cx ;is the result negative? jz FixedDivDone ;no, we're all set neg dx ;yes, so negate DX:AX neg ax sbb dx,0 FixedDivDone: endif ;USE386 pop bp ret _FixedDiv endp end [LISTING TWO] /* Draws all visible faces in the specified polygon-based object. The object must have previously been transformed and projected, so that all vertex arrays are filled in. Ambient and diffuse shading are supported. */ #include "polygon.h" void DrawPObject(PObject * ObjectToXform) { int i, j, NumFaces = ObjectToXform->NumFaces, NumVertices; int * VertNumsPtr, Spot; Face * FacePtr = ObjectToXform->FaceList; Point * ScreenPoints = ObjectToXform->ScreenVertexList; PointListHeader Polygon; Fixedpoint Diffusion; ModelColor ColorTemp; ModelIntensity IntensityTemp; Point3 UnitNormal, *NormalStartpoint, *NormalEndpoint; long v1, v2, w1, w2; Point Vertices[MAX_POLY_LENGTH]; /* Draw each visible face (polygon) of the object in turn */ for (i=0; iVertNums; NormalEndpoint = &ObjectToXform->XformedVertexList[*VertNumsPtr++]; NormalStartpoint = &ObjectToXform->XformedVertexList[*VertNumsPtr]; /* Copy over the face's vertices from the vertex list */ NumVertices = FacePtr->NumVerts; for (j=0; j 0) { /* It is facing the screen, so draw */ /* Appropriately adjust the extent of the rectangle used to erase this object later */ for (j=0; j ObjectToXform->EraseRect[NonDisplayedPage].Right) if (Vertices[j].X < SCREEN_WIDTH) ObjectToXform->EraseRect[NonDisplayedPage].Right = Vertices[j].X; else ObjectToXform->EraseRect[NonDisplayedPage].Right = SCREEN_WIDTH; if (Vertices[j].Y > ObjectToXform->EraseRect[NonDisplayedPage].Bottom) if (Vertices[j].Y < SCREEN_HEIGHT) ObjectToXform->EraseRect[NonDisplayedPage].Bottom = Vertices[j].Y; else ObjectToXform->EraseRect[NonDisplayedPage].Bottom= SCREEN_HEIGHT; if (Vertices[j].X < ObjectToXform->EraseRect[NonDisplayedPage].Left) if (Vertices[j].X > 0) ObjectToXform->EraseRect[NonDisplayedPage].Left = Vertices[j].X; else ObjectToXform->EraseRect[NonDisplayedPage].Left=0; if (Vertices[j].Y < ObjectToXform->EraseRect[NonDisplayedPage].Top) if (Vertices[j].Y > 0) ObjectToXform->EraseRect[NonDisplayedPage].Top = Vertices[j].Y; else ObjectToXform->EraseRect[NonDisplayedPage].Top=0; } /* See if there's any shading */ if (FacePtr->ShadingType == 0) { /* No shading in effect, so just draw */ DRAW_POLYGON(Vertices, NumVertices, FacePtr->ColorIndex, 0, 0); } else { /* Handle shading */ /* Do ambient shading, if enabled */ if (AmbientOn && (FacePtr->ShadingType & AMBIENT_SHADING)) { /* Use the ambient shading component */ IntensityTemp = AmbientIntensity; } else { SET_INTENSITY(IntensityTemp, 0, 0, 0); } /* Do diffuse shading, if enabled */ if (FacePtr->ShadingType & DIFFUSE_SHADING) { /* Calculate the unit normal for this polygon, for use in dot products */ UnitNormal.X = NormalEndpoint->X - NormalStartpoint->X; UnitNormal.Y = NormalEndpoint->Y - NormalStartpoint->Y; UnitNormal.Z = NormalEndpoint->Z - NormalStartpoint->Z; /* Calculate the diffuse shading component for each active spotlight */ for (Spot=0; Spot 0) { IntensityTemp.Red += FixedMul(SpotIntensity[Spot].Red, Diffusion); IntensityTemp.Green += FixedMul(SpotIntensity[Spot].Green, Diffusion); IntensityTemp.Blue += FixedMul(SpotIntensity[Spot].Blue, Diffusion); } } } } /* Convert the drawing color to the desired fraction of the brightest possible color */ IntensityAdjustColor(&ColorTemp, &FacePtr->FullColor, &IntensityTemp); /* Draw with the cumulative shading, converting from the general color representation to the best-match color index */ DRAW_POLYGON(Vertices, NumVertices, ModelColorToColorIndex(&ColorTemp), 0, 0); } } } }