11.07.2015 Views

Chapter 54

Chapter 54

Chapter 54

SHOW MORE
SHOW LESS
  • No tags were found...

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

chapter <strong>54</strong>3-d shading


istic Surfaces on Animated 3-D Objectsjust acquired basic hidden-surfaceoved through the use of fixed-pointite a bit more: support for 8088 and. That’s an awful lot to cover in oneapter), so let’s get to it!86, because it uses 32-bit multiply and’t support. I chose 32-bit instructionsfixed-point arithmetic than any apy’re much easier to implement thanany other approach. In short, I was after maximum performance, and I was perhapsjust a little lazy.I should have known better than to try to sneak this one by you. The most commonfeedback I’ve gotten on X-Sharp is that I should make it support the 8088 and 286.Well, I can take a hint as well as the next guy. Listing <strong>54</strong>.1 is an improved version ofFIXED.ASM, containing dual 386/8088 versions of CosSinO, XformVec(), andConcatXforms(), as well as FixedMulO and FixedDivO.Given the new version of FIXED.“, with USE386 set to 0, X-Sharp will now run onany processor. That’s not to say that it will run fast on any processor, or at least not as1007


fast as it used to. The switch to 8088 instructions makes X-Sharp’s fixed-point calculationsabout 2.5 times slower overall. Since a PC is perhaps 40 times slower than a 486/33,we’re talking about a hundred-times spee difference between the low end and mainstream.A 486/33 can animate a 72-sided ball, complete with shading (as discussedlater), at 60 frames per second (fps), with plenty of cycles to spare; an 8-MHz AT cananimate the same ball at about 6 fps. Clearly, the level of animation an applicationuses must be tailored to the available CPU horsepower.The implementation of a 32-bit multiply using 8088 instructions is a simple matter ofadding together four partial products. A 32-bit divide is not so simple, however. Infact, in Listing <strong>54</strong>.1 I’ve chosen not to implement a full 32x32 divide, but rather onlya 3‘2x16 divide. The reason is simple: performance. A 32x16 divide can be implementedon an 8088 with two DIV instructions, but a 32x32 divide takes a great dealmore work, so far as I can see. (If anyone has a fast 32x32 divide, or has a faster wayto handle signed multiplies and divides than the approach taken by Listing <strong>54</strong>.1,please drop me a line care of the publisher.) In X-Sharp, division is used only todivide either X or Y by Z in the process of projecting from view space to screen space,so the cost of using a 32x16 divide is merely some inaccuracy in calculating screencoordinates, especially when objects get very close to the Z = 0 plane. This error isnot cumulative (that is, it doesn’t carry over to later frames), and in my experiencedoesn’t cause noticeable image degradation; therefore, given the already slow performanceof the 8088 and 286, I’ve opted for performance over precision.At any rate, please keep in mind that the non-386 version of FixedDiv() is not ageneral-purpose 32x32 fixed-point division routine. In fact, it will generate a divideby-zeroerror if passed a fixed-point divisor between -1 and 1. As I’ve explained, thenon-386 version of Fixed-Div() is designed to do just what X-Sharp needs, and nomore, as quickly as possible.LISTING <strong>54</strong>.1 FIXED.ASM; Fixed point routines.; Tested with TASMUSE386 equ 1 ;1 for 386-specific opcodes. 0 for; 8088 opcodesMUL-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 ideaDIV-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 necessaryALIGNMENT equ 2.model smal 1.386.code1008 <strong>Chapter</strong> <strong>54</strong>


~ ~~~~; Multiplies two fixed-point values together.; C near-callable as:; Fixedpoint FixedMul(Fixedp0int M1. Fixedpoint HZ);FMparms strucdw 2 dup(?) ;return address & pushed BPM1M2dddd??FMparms endsa1 ign ALIGNMENTpublic Fi xedMul- FixedMul proc nearpush bpmov bp.spif USE386mov eax.[bp+Ml]imul dword ptr [bp+M2]if MUL-ROUNDING-ONadd eax, 8000hadc edx.0endif :MUL-ROUNDING-ONshr eax.16;multiply;round by adding 2^(-17);whole part of result is in;put the fractional part inDXAXelse; !USE386push sipush disub cx.cxmov ax.word ptr [bp+M1+2]mov si.word ptr [bp+Ml]and ax.axjns CheckSecondOperandneg axneg sisbb ax.0inc cxCheckSecondOperand:mov bx.word ptr [bp+M2+2]mov di .word ptr [bp+M2]and bx.bxjns SaveSignStatusneg bxneg disbb bx.0xor cx.1SaveSignStatus:push cxpush axmu1 bxmov cx.ax;do four partial products and; add them together. accumulating; the result in CX:BX;preserve C register variables;figure out signs, so we can use; unsigned multiplies;assume both operands positive;first operand negative?;no;yes. so negate first operand;mark that first operand is negative;second operand negative?;no;yes. so negate second operand;mark that second operandis negative;remember sign of result; 1 if result; negative, 0 if result nonnegative;remember high word of M1;high word M1 times high word M2;accumulate result in CX:BX (BX not used; until next operation, however);assume no overflow into DX3-D Shading 1009


mov ax, simu1 bxmov bx.axadd cx.dxPOP axmu1 diadd bx.axadc cx.dxmov ax, simu1 diif MUL-ROUNDING-ONadd ax.8000hadc bx.dxelse :!MUL-ROUNDING-ONadd bx.dxendif ;MUL-ROUNDING-ONadc cx.0mov dx.cxmov ax.bxPOP cxand cx,cxjz Fi xedMul Doneneg dxneg axsbb dx.0FixedMulDone::low word M1 times high word M2:accumulate result in CX:BX;retrieve high word of M1;high word M1 times low word MZ:accumulate result in CX:BX:low word M1 times low word M2;round by adding 2^(-17):don't round;accumulate result in:is the result negative?;no. we're all set;yes. so negate DX:AXCX:BXpoppopdisi:restore C register variablesendif :USE386POP bPret- FixedMul endp: Divides one fixed-point value by another.: C near-callable as:; Fixedpoint FixedDiv(Fixedp0int Dividend, Fixedpoint Divisor);FDparms strucdw 2 dup(?) :return address & pushed BPDividend dd ?Divisor dd ?FDparms endsa1 i gn ALIGNMENTpublic JixedOivJixedDi v proc nearpush bpmov bP SSPif USE386if DIV-ROUNDING-ONsub cx.cx ;assume positive resultmov eax.[bp+Oividendland eax,eax ;positive dividend?jns FOP1 :yesinc cx :mark it's a negative dividendneg eax :make the dividend positive101 0 <strong>Chapter</strong> <strong>54</strong>


FDPl :FDPZ:sub edx , edx ;make it a 64-bit dividend, then shift: left 16 bits so that result wil be in EAXrol eax,l6 :put fractional part of dividend in: high word of EAXmov dx.ax ;put whole part of dividend in DXsub ax.ax :clear low word EAX ofmov ebx.dword ptr [bp+Divisorlandebx.ebx:positive divisor?jns FOP2:yesdeccx;mark it's a negative divisornegebx:make divisor positivediv ebx:divideshrebx.1;divisor/2. minus 1 if the divisor isadc ebx.O: evendecebxcmp ebx, edx:set Carry if the remainder is at leastadceax.O : half as large as the divisor, then: use that to round up if necessary;should the result be made negative?:no:yes. negate itandcx.cxjz FDP3negeaxFDP3 :else :!DIV-ROUNDING-ONmov edx.[bp+Dividendlsubeax,eaxshrd eax.edx.16:position so that result ends upsaredx.16 : in EAXidiv dword ptr [bp+Divisor]endif :DIV-ROUNDING-ONshld edx.eax.16:whole part of result in DX:: fractional part is already in AXelse: !USE386:NOTE!!! Non-386 division uses a 32-bit dividend but onlv the umer 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 2 coordinate). so there's no cumulative: error, although there wil 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 wil result! For this reason, non-386: projection can't be performed for points closer to the viewpoint than Z-1...sub cx, cxmov ax.word ptr [bp+Dividend+Z]andax.axjns CheckSecondOperandD :nonegaxneg word ptr [bp+Dividend]sbbax.0i nc cxCheckSecondOperandD:mov bx.word ptr [bp+Divisor+Eland bx, bxjnsSaveSignStatusD:figure out signs. so we can use: unsigned divisions:assume both operands positive:first operand negative?:yes. so negate first operand:mark that first;second operand negative?:nooperand is negative3-D Shading 101 1


- CosSin proc nearpush bpmov bp.sp;preserve stack frame:set up local stack frameif USE386movandjnsMa kePos :addjsjmpbx.[bpJ.Anglebx.bxCheckInRangebx.360*10MakePosshort CheckInRange:make sure angle's between 0 and 2*pi:less than 0. so make it positivealign ALIGNMENTMakeInRange:sub bx.360*10CheckInRange:cmp bx.360*10jg MakeInRange;make sure angle isno more than 2*picmp bx. 180*10j a BottomHal fcmp bx.90*10ja Quadrantlshl bx.2mov eax.CosTable[bxlneg bxmov ed~.CosTable[bx+90*10*4Jjmp short CSDonea1 ign ALIGNMENTQuadrantl:neg bxadd bx, 180*10shl bx.2mov eax.CosTable[bxlneg eaxneg bxmov edx.CosTable[bx+90*lO*4]jmp short CSDonea1 i gn ALIGNMENTBottomHal f:neg bxadd bx.360*10cmp bx.90*10ja Quadrant2shl bx.2mov eax.CosTable[bxlneg bxmov edx.CosTable[90*10*4+bxlneg edxjmp short CSDonea1 ign ALIGNMENTQuadrant2:neg bxadd bx.180*10;figure out which quadrant;quadrant 2 or 3:quadrant 0 or 1:quadrant 0:look up sine;sin(Angle) - cos(90-Angle):look up cosine;convert to angle between 0 and 90;look up cosine;negative in this quadrant;sin(Angle) - cos(90-Angle):look up cosine;quadrant 2 or 3;convert to angle between 0 and 180;quadrant 2 or 3:quadrant 3:look up cosine;sin(Angle) - cos(90-Angle);look up sine;negative in this quadrant;convert to angle between 0 and 903-D Shading 101 3


CSDone:shlmovnegnegmovnegmovmovmovmovbx.2eax.CosTable[bxleaxbxedx.CosTable[90*10*4+bxledxbx.[bpl.Cos[bxl .eaxbx.[bpl.Sin[bxl .edx:look up cosine:negative in this quadrant:sin(Angle) - cos(90-Angle):look up sine:negative in this quadrantelse : !USE386mov bx.[bpl.Angleand bx.bxjns CheckInRangeMa kePos :add bx.360*10js MakePosjmp short CheckInRange:make sure angle's between 0 and 2*pi:less than 0, so make it positivea1 i gn ALIGNMENTMakeInRange:sub bx.360*10CheckInRange:cmp bx.360*10jg MakeInRange:make sure angle is nomore than 2*pibx, 180*10 ;figure out which quadrantBottomHal f :quadrant 2 or 3bx, 90*10 :quadrant 0 or 1Quadrantl:quadrant 0bx.2ax.word ptr CosTable[bxl :look up sinedx.word ptr CosTable[bx+2]bx :sin(Angle) - cos(90-Angle)cx.word ptr CosTable[bx+90*10*4+2] :look up cosinebx.word ptr CosTable[bx+90*10*41CSDonea1 ign ALIGNMENTQuadrantl:neg bxadd bx.180*10 :convert to angle between 0 and 90shl bx.2mov ax.word ptr CosTableCbxl :look up cosinemov dx.word ptr CosTableCbx+21neg dx :negative in this quadrantneg axsbb dx.0neg bx :sin(Angle) - cos(90-Angle)mov cx.word ptr CosTable[bx+90*10*4+21 :look up cosinemov bx,word ptr CosTableCbx+90*10*43jmp short CSDonea1 ign ALIGNMENTBottomHal f: :quadrant 2 or 3neg bxadd bx.360*10:convert to angle between 0 and 1801 01 4 <strong>Chapter</strong> <strong>54</strong>


cmp bx.90*10j a Quadrant2shl bx.2mov ax.word ptr CosTableCbx]mov dx.word ptr CosTable[bx+2]neg bxmov cx.word ptr CosTable[90*10*4+bx+2]mov bx.word ptr CosTable[90*10*4+bxlneg cxneg bxsbb cx.0jmp short CSDonea1 ign ALIGNMENTQuadrant2:neg bxadd bx.180*10shl bx.2mov ax.word ptr CosTable[bx]mov dx.word ptr CosTable[bx+P]neg dxneg axsbb dx,Oneg bxmov cx.word ptr CosTable[90*10*4+bx+2]mov bx.word ptr CosTable[90*10*4+bx]neg cxneg bxsbb cx.0CSDone:push bxmov bx, [ bpl .Cosmov [bx] ,axmov [ bx+2] , dxmov bx. [bpl .SinPOP axmov [bx].axmov Cbx+Zl .cx;quadrant 2 or 3:quadrant 3:look up cosine;sin(Angle) - cos(90-Angle);look ;p sine;negative in this:convert to angle:look up cosine:negative in thisquadrantbetween 0 and 90quadrant:sin(Angle) - cos(90-Angle);look UD sine:negative in this quadrantendif ;USE386POPretXosSinbPendp:restore stack frame; Matrix multiplies Xform by SourceVec. and stores the result in; OestVec. Multiplies a 4x4 matrix times a 4x1 matrix; the result; is a 4x1 matrix. Cheats by assuming the W coord is 1 and the; bottom row of the matrix is 0 0 0 1. and doesn't bother to set: the W coordinate of the destination.; C near-callable as:; void XformVec(Xform WorkingXform. Fixedpoint *SourceVec.Fixedpoint *DestVec);; This assembly code is equivalent to this C code:; int i;3-D Shading 101 5


: for (i-0; i


else ;!MUL-ROUNDING-ONadd bx.dxendif :MUL-ROUNDING-ONadc cx.0mov dx,cxmov ax.bxPOP cxand cx.cxjz FixedMulDoneneg dxneg axsbb dx.0FixedMulDone:ENDMa1 ign ALIGNMENTpub1 i c -XformVec-XformVec proc nearpush bpmov bp.sppush sipush di;don’t round:is the result negative?:no. we’re all set;yes. so negate DX:AX:preserve stack frame:set up local stack frame;preserve register variablesif USE366movmovmovsi.[bpl.WorkingXformbx.Cbpl.SourceVecdi ,[bpl .DestVec:SI points to xform matrix:BX points to source vector:DI points to dest vectorsoff-0doff-0REPT 3mov eax.[si+sofflimul dword ptr [bxlif MUL-ROUNDING-ONadd eax.8000hadc edx.Oendif ;MUL-ROUNDING-ONshrd eax.edx.16mov ecx, eaxmov eax.[si+soff+41imul dword ptr [bx+4]if MUL-ROUNDING-ONadd eax.8000hadc edx.0endif ;MUL-ROUNDING-ONshrd eax.edx.16add ecx, eaxmov eax.[si+soff+61imul dword ptr [bx+6]if MUL-ROUNDING-ONadd eax.8000hadc edx.Oendif :MUL-ROUNDING-ONshrd eax.edx.16add ecx.eax:do once each for dest X. Y, and 2:column 0 entry on this row;xform entry times source X entry:round by adding 2”(-17);whole part of result is in:shift the result back to:set running totalDX16.16 form;column 1 entry on this row;xform entry times source Y entry;round by adding 2^(-17);whole part of result is inDX:shift the result back to 16.16 form;running total for this row;column 2 entry on this row:xform entry times source 2 entry;round by adding 2*(-17):whole part of result is inDX;shift the result back to 16.16 form:running total for this rowaddmovecx.[si+soff+l2]Cdi+doffl.ecx:add in translation:save the result in the dest vector3-D Shading 101 7


soff-soff+l6doff-doff+4ENOMelse : !USE386soff-0doff-0REPTpushpushpushpushpushcalladdmovmovmov si.Cbp].WorkingXformmov di.[bp].SourceVecmov bx.[bpl.OestVecpush bppushpushpushpushpushcalladdPOPaddadcpushpushpushpushpushcalladdPOPaddadc3bxword ptr [si+soff+21word ptr [si+sofflword ptr Cdi+21word ptr [dil-FixedMulsp.8cx,ax :set running totalbp.dxcxword ptr [si+soff+4+2]word ptr [si+soff+4]word ptr [di+4+21word ptr [d1+41- FixedMulsp.8cxcx, axbp .dxcxword ptr [si+soff+8+21word ptr [si+soff+81word ptr Cdi+B+ZIword ptr [di+81- FixedMulsp.8cxcx, axbp.dxadd cx.[si+soff+l21adc bp.[si+soff+l2+21POP bxmov [bx+doffl.cxmov [bx+doff+El,bpsoff-soff+l6doff-doff+4ENDM:SI points to xform matrix:DI points to source vector:BX points to dest vector;preserve stack frame pointer;do once each for dest X. Y. and 2:remember dest vector pointer;xform entry times source X entry:clear parameters from stack:preserve low word of running total:xform entry times source Y entry:clear parameters from stack;restore low word of running total;running total for this row:preserve low word of running total:xform entry times source 2 entry;clear parameters from stack;restore low word of running total;running total for this row:add in translation:restore dest vector pointer:save the result in the dest vectorPOPbP:restore stack frame pointerendi f ;USE386popPOPdisi;restore register variables1 01 8 <strong>Chapter</strong> <strong>54</strong>


POP bp:restore stack frameret-XformVec endp. ~""~""""""---y"~"""~Matrix multiplies SourceXforml by SourceXformE and stores theresult in OestXform. Multiplies a 4x4 matrix times a 4x4 matrix:the result is a 4x4 matrix. Cheats by assuming the bottom row ofeach matrix is 0 0 0 1, and doesn't bother to set the bottom rowof the destination.C near-callable as:void ConcatXforms(Xform SourceXforml. Xform SourceXformE.Xform DestXform)This assembly code is equivalent to this C code:int i. j;for (i-0: i


if MUL-ROUNDING-ONaddeax.8000hadc edx , 0endif :MUL-ROUNDING-ONshrd eax.edx.16mov ecx, eax:round by adding 2"(-17):whole part of result is in;shift the result back to:set running totalDX16.16 formmov eax.[si+roff+41imul dword ptr Cbx+coff+l61if MUL-ROUNDING-ONaddeax.8000hadc edx, 0endif :MUL-ROUNDING-ONshrd eax.edx.16addecx.eaxmov eax.[si+roff+81imul dword ptr [bx+coff+321if MUL-ROUNDING-ONaddeax.8000hadc edx, 0endif ;MUL-ROUNDING-ONshrd eax.edx.16addecx.eaxmov [di+coff+roff].ecxcoff-coff+4ENDMmov eax.[si+rofflimul dword ptr [bx+cofflif MUL-ROUNDING-ONaddeax.8000hadc edx, 0endif ;MUL-ROUNDING-ONshrd eax.edx.16mov ecx,eaxmov eax.[si+roff+41imul dword ptr [bx+coff+l61if MUL-ROUNDING-ONadd eax, 8000hadc edx, 0endif ;MUL-ROUNDING-ONshrd eax.edx,l6addecx.eaxmov eax.[si+roff+8]imul dword ptr [bx+coff+321if MUL-ROUNDING-ONaddeax.8000hadcedx.0endif :MUL-ROUNDING-ONshrd eax.edx.16add ecx, eax:column 1 entry on this row:times row 1 entry in col:round by adding 2*(-17):whole part of result is in:shift the result back to:running total;column 2 entry on this row:times row 2 entry in col:round by adding 2"(-17):whole part of result is inDX16.16 formDX:shift the result back to 16.16 form:running total:save the result in dest matrix:point to next col in xform2& dest:now do the fourth column, assuming: 1 as the bottom entry, causing: translation to be performed;column 0 entry on this row:times row 0 entry in column:round by adding 2^(-17);whole part of result is in:shift the result back to:set running total;column 1 entry on this row:times row 1 entry in col:round by adding ZA(-17):whole part of result is inDX16.16 formDX:shift the result back to 16.16 form:running total:column 2 entry on this row:times row 2 entry in col:round by adding 2^(-17);whole part of result is in DX;shift the result back to:running total16.16 formaddecx,Csi+roff+l2]:add in translation1020 <strong>Chapter</strong> <strong>54</strong>


movcoff-coff+4Cdi+coff+roffl.ecx:save the result in dest matrix:point to next col in xform2 & destroff-roff+l6ENDMel se : ! USE386;point to next col inxform2 & destmovmovmovpushdi.[bp].SourceXformZsi.[bpl.SourceXformlbx.[bpl.DestXformbp:DI points to xform2 matrix:SI points to xforml matrix:BX points to dest xform matrix:preserve stack frame pointerroff-0coff-0REPT 3REPT 3pushpushpushpushpushcalladdmovmovbxword ptr Csi+roff+2]word ptr [si+roff]word ptr [di+coff+2]word ptr [di+coff]- FixedMulsp.8cx,ax :set running totalbp.dx;row offset:once for each row;column offset;once for each of the first 3 columns,: assuming 0 as the bottom entry (no: translation):remember dest vector pointer:column 0 entry on this row times row 0: entry in column:clear parameters from stackpushpushpushpushpushcalladdPOPaddadccxword ptr [si+roff+4+Z]word ptr [si+roff+4]word ptr [di+coff+l6+2]word ptr [di+coff+l6]- FixedMulsp.8cxcx.axbp. dx:preserve low word of running total:column 1 entry on this row times row 1; entry in column:clear parameters from stack:restore low word of running total;running total for this rowpushpushpushpushpushcalladdPOPaddadccxword ptr [si+roff+8+21word ptr [si+roff+8]word ptr [di+coff+32+2]word ptr [di+coff+32]JixedMulsp.8cxcx.axbp.dx:preserve low word of running total:column 1 entry on this row times row 1: entry in column:clear parameters from stack;restore low word of running total;running total for this rowPOPmovmovbxCbx+coff+roff].cx[bx+coff+roff+21,bp;restore DestXForm pointer;save the result in dest matrix3-D Shading 1 02 1


coff-coff+4ENDMpushpushpushpushpushcalladdmovmovpushpushpushpushpushcalladdPOPaddadcpushpushpushpushpushcalladdPOPaddadcaddaddbxword ptr [si+roff+2]word ptr [si+rofflword ptr [di+coff+21word ptr [di+coffl- Fi xedMulsp.8cx.ax ;set running totalbp. dxcxword ptr [si+roff+4+21word ptr [si+roff+41word ptr [di+coff+l6+2]word ptr [di+coff+l6]- Fi xedMulsp.8cxcx,axbp , dxcxword ptr [si+roff+8+21word ptr [si+roff+81word ptr [di+coff+32+21word ptr [di+coff+32]- FixedMulsp,8cxcx, axbp.dxcx.Csi+roff+lZlbp.[si+roff+l2+21;point to next col in xform2& dest:now do the fourth column, assuming: 1 as the bottom entry, causing; translation to be performed;remember dest vector pointer;column 0 entry on this row times row 0; entry in column;clear parameters from stack;preserve low wordof running total;column 1 entry on this row times row 1; entry in column;clear parameters from stack;restore low word of running total;running total for this row;preserve low word of running total:column 1 entry on this row times row 1: entry in column:clear parameters from stack;restore low word of running total;running total for this row;add in translationcoff-coff+4POPmovmovbx[bx+coff+roffl,cx[bx+coff+roff+2].bp;restore DestXForm pointer;save the result in dest matrix;point to next col in xform2& destroff-roff+l6ENDM;point to next col in xform2& destPOPbp;restore stack frame pointerendif ;USE386POPPOPPOPret-ConcatXformsenddisibvendp:restore register variables:restore stack frame1022 <strong>Chapter</strong> <strong>54</strong>


ShadingSo far, the polygons out of which our animated objects have been built have hadcolors of fixed intensities. For example, a face of a cube might be blue, or green, orwhite, but whatever color it is, that color never brightens or dims. Fixed colors areeasy to implement, buthey don’t make for very realistic animation. In the real world,the intensity of the color of a surface varies depending on how brightly it is illuminated.The ability to simulate the illumination of a surface, or shading, is the nextfeature we’ll add to X-Sharp.The overall shading of an object is the sum of several types of shading components.Ambient shadingis illumination by what you might think of as background light, lightthat’s coming from all directions; all surfaces are equally illuminated by ambientlight, regardless of their orientation. Directed lighting, producing diffuse shading, isillumination from one or more specific light sources. Directed light has a specificdirection, and the angle at which it strikes a surface determines how brightly it lightsthat surface. Specular reJection is the tendency of a surface to reflect light in a mirrorlikefashion. There are other sorts of shading components, including transparencyand atmospheric effects, but the ambient and diffuse-shading components are allwe’re going to deal with in X-Sharp.Ambient ShadingThe basic model for both ambient and diffuse shading is a simple one. Each surfacehas a reflectivity between 0 and 1, where 0 means all light is absorbed and 1 means alllight is reflected. A certain amount of light energy strikes each surface. The energy(intensity) of the light is expressed such that if light of intensity 1 strikes a surfacewith reflectivity 1, then the brightest possible shading is displayed for that surface.Complicating this somewhat is the need to support color; we do this by separatingreflectance and shading into three components each-red, green, and blue-andcalculating the shading for each color component separately for each surface.Given an ambient-light red intensity of Ured and a surface red reflectance Rred, thedisplayed red ambient shading for that surface, as a fraction of the maximum redintensity, is simply min(IAredx Rred, 1). The green and blue color components arehandled similarly. That’s really all there is to ambient shading, although of course wemust design some way to map displayed color components into the available paletteof colors; I’ll do that in the next chapter. Ambient shading isn’t the whole shadingpicture, though. In fact, scenes tend to look pretty bland without diffuse shading.Diffuse ShadingDiffuse shading is more complicated than ambient shading, because the effectiveintensity of directed light falling on a surface depends on the angle at which it strikesthe surface. According to Lambert’s law, the light energy from a directed light source3-D Shading 1023


striking a surface is proportional to the cosine of the angle at which it strikes thesurface, with the angle measured relative to a vector perpendicular to the polygon (apolygon normal), as shown in Figure <strong>54</strong>.1. If the red intensity of directed light isIDred, the red reflectance of the surface is Rred, and the angle between the incomingdirected light and the surface’s normal is theta, then the displayed red diffuse shadingfor that surface, as a fraction of the largest possible red intensity, is min(1D,edxRre,xcos(8), ’)*That’s easy enough to calculate-but seemingly slow. Determining the cosine of anangle can be sped up with a table lookup, but there’s also the task of figuring out theangle, and, all in all, it doesn’t seem“.”,that diffuse shading is going to be speedy enoughfor our purposes. Consider this, however: According to the properties of the dotproduct (denoted by the operator as shown in Figure <strong>54</strong>.2), cos(B)=(vw) / IvI X IwI ),where v and w are vectors, 8 is the angle between v and w, and Ivl is the length of v.Suppose, now, that v and w are unit vectors; that is, vectors exactly one unit long.Then the above equation reduces to cos(e)=v.w. In other words, we can calculatethe cosine between N, the unit-normal vector (one-unit-long perpendicular vector)of a polygon, and L’, the reverse of a unit vector describing the direction of a lightsource, with just three multiplies and two adds. (I’ll explain why the lightdirectionvector must be reversed later.) Once we have that, we can easily calculate the redLight from directed Polygon normal (perpendicular vector)illumination sourceD, of energy E.Illumination by a directed light source.Figure <strong>54</strong>.1For two vectors v and w, as follows: the dot product vw is:vw = v,w,+ yw,+ v,w,The dot product of two vectors.Figure <strong>54</strong>.21024 <strong>Chapter</strong> <strong>54</strong>


diffuse shading from a directed light source as min(1DredxRredx(L'* N), 1) and likewisefor the green and blue color components.The overall red shading for each polygon can be calculated by summing the ambientshadingred component with the diffuse-shading component from each light source,as in min( (IAredxRred) + (1Drcd,,xRredx(LO' N)) + (IDredlxRredx(L1' N)) +..., 1) whereIDredo and Lo' are the red intensity and the reversed unit-direction vector, respectively,for spotlight 0. Listing <strong>54</strong>.2 shows the X-Sharp module DRAWPOBJ.C, whichperforms ambient and diffuse shading. Toward the end, you will find the code thatperforms shading exactly as described by the above equation, first calculating theambient red, green, and blue shadings, then summing that with the diffuse red,green, and blue shadings generated by each directed light source.LISTING <strong>54</strong>.2 DRAWP0BJ.C/* Draws all visible faces in the specified polygon-based object. The objectmust have previously been transformed and projected, so that al vertexarrays are filled in. Ambient and diffuse shading are supported. */Dincl ude "polygon. h"void DrawPObject(P0bject * ObjectToXform)tint i. j. NumFaces - ObjectToXform->NumFaces. NumVertices:int * VertNumsPtr, Spot;Face * FacePtr - ObjectToXform->FaceList:Point * Screenpoints = ObjectToXform->ScreenVertexList;PointListHeader Polygon:Fixedpoint Diffusion:Model Col or Col orTemp:ModelIntensity IntensityTemp:Point3 UnitNormal, *NormalStartpoint. *NormalEndpoint:long VI. v2, wl. w2:Point VerticesCMAX-POLY-LENGTH];/* Draw each visible face (polygon) of the object in turn */for (i=O: iVertNums:NormalEndpoint - &ObjectToXform->XformedVertexList[*VertNumsPtr++l:NormalStartpoint = &ObjectToXform->XformedVertexListC*VertNumsPtrl:/* 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 */3-D Shading 1025


* Appropriately adjust the extent of the rectangle used toerase this object later */for (j-0: jObjectToXform->EraseRect[NonOisplayedPagel.Right~if (VerticesCj1.X < SCREEN-WIDTH)ObjectToXform->EraseRectCNonDisplayedPagel.Right -VerticesCj1.X;else ObjectToXform->EraseRect[NonDisplayedPagel.Right =SCREEN-WIDTH:if (VerticesCj1.Y >ObjectToXform->EraseRect[NonOisplayedPagel.Bottom~if (VerticesCj1.Y < SCREEN-HEIGHT)ObjectToXform->EraseRect[NonDisplayedPagel.Bottom -Vertices[j].Y:else ObjectToXform->EraseRect[NonDisplayedPagel.Bottom-SCREEN-HEIGHT;if (VerticesCj1.X EraseRect[NonDisplayedPagel.Left)if (VerticesCj1.X > 0)ObjectToXform->EraseRect[NonOisplayedPagel.Left -VerticesCj1.X:else ObjectToXform->EraseRectCNonOisplayedPagel.Left-O:if (VerticesCj1.Y EraseRect[NonDisplayedPagel.Top)if (VerticesCj1.Y > 0)ObjectToXform->EraseRect[NonDisplayedPagel.Top -VerticesCj1.Y:else ObjectToXform->EraseRectCNonDisplayedPagel.Top-O:1/* See if there's any shading *Iif (FacePtr->ShadingType- 0) {I* No shading in effect, so just draw */DRAW_POLYGON(Vertices, NumVertices, FacePtr->ColorIndex. 0, 0):} else {I* Handle shading *I/* 00 ambient shading, if enabled *//* Use the ambient shading component */if (Ambienton && (FacePtr->ShadingType & AMBIENT-SHADING))IntensityTemp - AmbientIntensity:I else {SET-INTENSITY(1ntensityTemp. 0. 0. 0):I* Do diffuse shading, if enabled */if (FacePtr->ShadingType & DIFFUSE-SHADING) {/* Calculate the unit normal for this polygon, for use in dotproducts *IUnitNorma1.X - NormalEndpoint->X - NormalStartpoint->X;UnitNorma1.Y - NormalEndpoint->Y - NormalStartpoint->Y:UnitNormal.2 - NormalEndpoint->2 - NormalStartpoint->Z:/* Calculate the diffuse shading component for each activespotlight */for (Spot-0: Spot


II11if ((Diffusion - DDT~PRODUCT(SpotDirectionView[Spotl,UnitNorma1 1) > 0) I1ntensityTemp.Red +=FixedMul(SpotIntensity[Spotl.Red. Diffusion);1ntensityTemp.Green +-FixedMul(SpotIntensity[Spotl.Green, Diffusion);1ntensityTemp.Blue +-FixedMul(SpotIntensity[Spotl.Blue. Diffusion):I111/* Convert the drawing color to the desired fraction of thebrightest possible color */IntensityAdjustColor(&ColorTemp. &FacePtr->FullColor,&IntensityTemp);I* Draw with the cumulative shading, converting from the generalcolor representation to the best-match color index */DRAWKPOLYGON(Vertices. NumVertices,ModelColorToColorIndex(&ColorTemp). 0. 0):Shading: Implementation DetailsIn order to calculate the cosine of the angle between an incoming light source and apolygon’s unit normal, we must first have the polygon’s unit normal. This could becalculated by generating a cross-product on two polygon edges to generate a normal,then calculating the normal’s length and scaling to produce a unit normal.Unfortunately, that would require taking a square root, so it’s not a desirable courseof action. Instead, I’ve made a change to X-Sharp’s polygon format. Now, the firstvertex in a shaded polygon’s vertex list is the end-point of a unit normal that starts atthe second point in the polygon’s vertex list, as shown in Figure <strong>54</strong>.3. The first pointisn’t one of the polygon’s vertices, but is used only to generate a unit normal. TheVertex 1 must be the startpoint ofa unit normal ending at vertex 0.This point is part of the polygon. PolygonIThe unit normal in the polygon data structure.Figure <strong>54</strong>.3Vertex 0 must be the endpoint of a unitstarting at vertex 1. This pointis not part of the polygon.3-D Shading 1027


iReversed unitvector L’ towardLight from directed illumination source D, directed lightof energy E, with direction expressed bythe unit vector LPolygon unitIPolygon surfaceThe reversed light source vector:Figure <strong>54</strong>.4second point, however, is a polygon vertex. Calculating the difference vector betweenthe first and second points yields the polygon’s unit normal. Adding aunit-normal endpoint to each polygon isn’t free; each of those end-points has to betransformed, along with the rest of the vertices, and that takes time. Still, it’s fasterthan calculating a unit normal for each polygon from scratch.We also need a unit vector for each directed light source. The directed light sourcesI’ve implemented in X-Sharp are spotlights; that is, they’re considered to be pointlight sources that are infinitely far away. This allows the simplifylng assumption thatall light rays from a spotlight are parallel and of equal intensity throughout the displayeduniverse, so each spotlight can be represented with a single unit vector and asingle intensity. The only trick is that in order to calculate the desired cos(theta)between the polygon unit normal and a spotlight’s unit vector, the direction of thespotlight’s unit vector must be reversed, as shown in Figure <strong>54</strong>.4. This is necessarybecause the dot product implicitly places vectors with their start points at the samelocation when it’s used to calculate the cosine of the angle between two vectors. Thelight vector is incoming to the polygon surface, and the unit normal is outbound, soonly by reversing one vector or the other will we get the cosine of the desired angle.Given the two unit vectors, it’s a piece of cake to calculate intensities, as shown inListing <strong>54</strong>.2. The sample program DEMO1, in the X-Sharp archive on the listingsdisk (built by running K1 .BAT), puts the shading code to work displaying a rotatingball with ambient lighting and three spot lighting sources that the user can turn onand off. What you’ll see when you run DEMO1 is that the shading is very good-facecolors change very smoothly indeed-so long as only green lighting sources are on.However, if you combine spotlight two, which is blue, with any other light source,polygon colors will start to shift abruptly and unevenly. As configured in the demo,the palette supports a wide range of shading intensities for a pure version of any oneof the three primary colors, but a very limited number of intensity steps (four, in this1028 <strong>Chapter</strong> <strong>54</strong>


case) for each color component when two or more primary colors are mixed. Whilethis situation can be improved, it is fundamentally a result of the restricted capabilitiesof the 256-color palette, and there is only so much that can be done without alarger color set. In the next chapter, I’ll talk about some ways to improve the qualityof 256-color shading.3-D Shading 1029

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!