Spell presents: ////==\\\\ || || //======== |\ /| //\\ //======\ || || || || ||\ /|| // \\ // || || || || || \ / || // \\ || || ||======|| ||==== || \/ || || || || || || || || || || ||====|| || ==\\ || || || || || || || || \\ || || || || \\======== || || || || \\====// Issue 11 24-2-97 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- Index: 1. Introduction 1.1. About the magazine 1.2. About the author 1.3. Distribution 1.4. Subscribing 1.5. Contribuitions 1.6. Hellos and greets 2. Mailroom 3. Designing a text adventure - Part IV 3.1. Monsters 3.2. Special Rooms 4. Some order in a 3d world, please ! 4.1. 3d polygons 4.2. Sorting 4.2.1. The Painter's Algorithm 4.2.2. Z-Buffer 4.2.3. BSP Trees 4.3. Vector maths 4.3.1. Basic concepts 4.3.2. Multiplications 4.3.3. Special vectors 4.4. Backface removal 5. Lights, please ! 6. The optimization bible 7. How to do crossfading 7.1. 16 color crossfading 7.2. 256 color crossfading 8. Sprites Part IV - Rotating and scaling 8.1. Rotation 8.2. Scaling 9. Graphics Part X - Assembling it all 10. Hints and tips 11. Points of view 12. The adventures of Spellcaster, part 11 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 1. Introduction 1.1. About the magazine Well, it's time I start doing this issue... I should have released it today, but I just started now, so I think this issue is going to be a bit late... Curse school !! I hate school !!! But I think this is also going to be a big issue (maybe not as big as issue 10, but big anyway). I have lined for you some nice articles... We'll delve into the realms of 3d to bring info on face sorting, backface removal and flatshading. I also have the continuation of the articles on text adventures... This time it is about monsters !! Then, I have an article on optimization (just a few pointers to get you going)... Also an article on crossfading. In the Sprites tutorial, we'll learn how to scale and rotate a bitmap, and in the Graphics section we'll have a look on how to use assembler to optimize the code... Accompanying this issue is a program called HorizScr.Pas, that demonstrates how smooth horizontal scrolling can be achieved in text mode. It was done Brent Hostetler, and it is so well commented that I don't even need to do an explanation... Thanks a lot, Brent... You probably can get it from the same place you got this issue, but if you can't, look in the BBS's listed somewhere in this issue, or look in my HomePage... It includes full source code, so you can change it, and see how it was done... :) This can only be probably seen with SpellView V1.4... Get it... It's better, bugfixed, etc... When you read this magazine, I'll assume some things. First, I assume you have Borland's Turbo Pascal, version 6 and upwards (and TASM for the assembly tutorials). I'll also think you have a 80386 (or 386 for short; a 486 would be even better), a load of patience and a sense of humor. This last is almost essencial, because I don't receive any money for doing this, so I must have fun doing it. I will also take for certain you have the 9th grade (or equivelent). Finally, I will assume that you have the last issues of 'The Mag', and that you have grasped the concepts I tried to transmit. If you don't have the issues, you can get them by mail, writing to one of the adresses shown below (Snail mail and Email). Almost everything I know was learnt from painfull experience. If you re-re-re-read the article, and still can't understand it, just drop a line, by mail, or just plain forget it. Most of the things I try to teach here aren't linked to each other (unless I say so), so if you don't understand something, skip it and go back to it some weeks later. It should be clearer for you then. Likewise, if you see any terms or words you don't understand, follow the same measures as before. Ok, as I'm earing the Net gurus and other god-like creatures talking already, I'm just going to explain why I use Pascal. For starters, Pascal is a very good language, ideal for the beginner, like BASIC (yech!), but it's powerfull enough to make top-notch programms. Also, I'll will be using assembly language in later issues, and Pascal makes it so EASY to use. Finally, if you don't like my choice of language, you can stop whining. The teory behind each article is very simple, and common with any of the main languages (C, C++, Assembly - Yes, that's true... BASIC isn't a decent language). Just one last thing... The final part of the magazine is a little story made up by my distorted mind. It's just a little humor I like to write, and it hasn't got nothing to do with programming (well, it has a little), but, as I said before, I just like to write it. 1.2. About the author Ok, so I'm a little egocentric, but tell me... If you had the trouble of writing hundreds of lines, wouldn't you like someone to know you, even by name ? My name is Diogo de Andrade, alias Spellcaster, and I'm the creator, editor and writer of this magazine. I live in a small town called Setubal, just near Lisbon, the capital of Portugal... If you don't know where it is, get an encyclopedia, and look for Europe. Then, look for Spain. Next to it, there's Portugal, and Setubal is in the middle. I'm 19 years old, and I'm in the second year in the university (if you do want to know, I'm in the Technical Institute of Lisbon, Portugal), so I'm not a God-Like creature, with dozens of years of practice (I only program by eight years now, and I started in a Spectrum, progressing later to an Amiga. I only program in the PC for two years or so), with a mega-computer (I own a 386SX, 16 Mhz), that wear glasses with lens that look like the bottom of a bottle (I use glasses, but only sometimes), that has his head bigger than a pumpkin (I have a normal sized head) and with an IQ of over 220 (mine is actually something like -220 :) ). I can program in C, C++, Pascal, Assembly, Prolog, CaML, and even BASIC (yech!)... And due to school, this list is increasing... :))) So, if I am a normal person, why do I spend time writing this ? Well, because I have the insane urge to write thousands of words every now and then, and while I'm at it, I may do something productive, like teaching someone. Just one more thing, if you ever program anything, please send to me... I would love to see some work you got, maybe I could learn something with it. Also, give me a greet in your program/game/demo... I love seeing my name. 1.3. Distribution I don't really know when can I do another issue, so, there isn't a fixed space of time between two issues. General rule, I will try to do one every month, maybe more, probably less (Eheheheh). This is getting to an issue every two months, so, I'll think I'll change the above text... :) 'The Mag' is available by the following means: - Snail Mail : My address is below, in the Contributions seccion... Just send me a disk and tell me what issues you want, and I will send you them... - E-Mail : If you E-mail me and ask me for some issues, I will Email you back with the relevant issues attached. - Internet : You can access the Spellcaster page and take the issues out of there in: http://alfa.ist.utl.pt/~l42686 Follow the docs link... - Anonymous ftp : I've put this issue of 'The Mag' on the ftp.cdrom.com site, in the pub/demos/incoming/code... It will probably be moved to pub/demos/code or something like that. - BBS's : You can check out the BBS's list that will carry this and all the other issues of 'The Mag' in the file SPELL.DOC. 1.4. Subscribing If you want, I'm starting "The Mag"'s subscription list... To get 'The Mag' by Email every month, you just need to mail me and tell me so... Then, you will receive it every time a new issue is made... 1.5. Contributions I as I stated before, I'm not a God... I do make mistakes, and I don't have (always) the best way of doing things. So, if you think you've spotted an error, or you have thought of a better way of doing things, let me know. I'll be happy to receive anything, even if it is just mail saying 'Keep it up'. As all human beings, I need incentive. Also, if you do like to write, please do... Send in articles, they will be welcome, and you will have the chance to see your names up in lights. They can be about anything, for a review of a book or program that can help a programmer, to a point of view or a moan. If anyone out there has a question or wants to see an article about something in particular, feel free to write... All letters will be answered, provided you give me your address. If you have a BBS and you want it to include this magazine, feel free to write me... I don't have a modem, so I can only send you 'The Mag' by Email. You can also contact me personally, if you study on the IST (if you don't know what the IST is, you don't study there). I'm the freshman with the black hair and dark-brown eyes... Yes, the one doesn't put a foot in a class for ages ! My adress is: Praceta Carlos Manito Torres, n§4/6§C 2900 Set£bal Portugal Email: spellcaster@bigfoot.com spellcaster@cryogen.com dgan@camoes.rnl.ist.utl.pt l42686@alfa.ist.utl.pt And if you want to contact me on the lighter side, get into the Lost Eden talker... To do that telnet to: Alfa.Ist.Utl.Pt : Port 1414 I'm sometimes there... Not always... As you may have guessed already, my handle is Spellcaster (I wonder why...)... 1.6. Hellos and greets I'll say hellos and thanks to all my friend, especially for those who put up with my not-so-constant whining. Special greets go to Scorpio, Denthor from Asphyxia (for the excelent VGA trainers), Draeden from VLA (for assembly tutorials), Dr.Shadow (Delta Team is still up), Joao Neves for sugestions, testing and BBS services, Garfield (for the 'The Mag' logo and general suport) and all the demo groups out there. Oh, and I almost forgot all the people at Infinity Network ! I will also send greets to everybody that responded to my mag... Thank you very much ! -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 2. Mailroom Well, I have two letters here... Both of them report errors... Let's see them: -------------------------------------------------------------------------------- In "Designing a text adventure, Part 2", in issue 8, more precisely in 4.2 (player input & command parser), you have writen: procedure parse(s:string;var parsed:ParsedType); ..... ..... ..... while ((StringIndex <= Length(S)) and (ArrayIndex<11)) do Well, it should read: while ((StringIndex >= Length(S)) and (ArrayIndex<11)) do as it is in the FangLore.Pas program... Scorpio -------------------------------------------------------------------------------- You got me again... :) Now, another goof-up: -------------------------------------------------------------------------------- Hello Spellcaster ! I read your mag, it's very good. But i think i found 1 foult dad you made , it's this -->> in your chart of hex numbers you stated that 30 in dec is = 1F in hex and that 20 in dec is 32 in hex but i got this numbers 1F in hex is = 1 * 16 = 16 + F "15" = 31 20 in hex is = 2 * 16 = 32 + 0 * 1 = 32 correct me if i am wrong , but i just read your mag01 ? Can you tell me what is best and easiest to use in demos and games is it cel,pcx,spr, or raw picture files , and how i implement the code to load the best file , in pascal 7.0 , help me please ? Now i am using arrays but its very difficult to draw with 1's and 0'roes. where on ftp i could fine one so please send me it please ? i have searched four weeks and only find nothing, excpet one rmaster40.zip , but hey , its a shareware so you could only use 16 colors , and i would pay to get 256 but i dont got any money :( i am poor :( . Well i send you a small demo i made , with skills from asphyxias trainer, its not good , but i only programmed in about 3 weeks . If you see anyone with the nick " sbviking " on irc its probably me , so say hello ! Stefan Bergh -------------------------------------------------------------------------------- Well, for the first part, you got me again... :) That's what I get for writing the articles in a rush... As for the best file formats, I usually use PCX and RAW files, but that depends on the type of thing you're making... To load RAW files, just read the data straight to where you want to store it... PCX was explained in other issue... As for a program, I'm writing a small utility to do sprites, that enables you to grab them and draw them from scratch... It will take some time to finish, as I'm very busy in school now, so wait and see... As for your demos, you're one of the first to send me something, so I quite apreciated it... I think they are great, if you code for only 3 weeks. A couple of errors, but fine anyway... :) As for IRC, I can't get in it... :((( Thanks to everyone who wrote... Cya... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 3. Designing a text adventure - Part IV Well, this article is about monsters and special rooms in a text adventure. 3.1. Monsters "What are monsters ?". This may seem a dumb question, but it isn't. In a text adventure a 'monster' is something that harms you... It can be a robot, or anything at all... In Fanglore, let's put three monsters: 1) A psychopatic gardner 2) A worm 3) A vampire The gardner has a special thing in it: it moves around in some predefined pattern, while the worm and the vampire will stay static in the kitchen and in the library, respectivelly. So, let's define a record for the monsters: Type MonsterType=Record Name:String; Pos:Byte; Index:Byte; Stamina:Byte; Strength:Byte; Positions:Array[1..MaxPos] Of Byte; End; Name is the name or type of the monster. Pos is the current position of the monster. Index and Positions are variables that control the movement. More on this next. Stamina is the health points of monster. Let's define that taking in consideration that the sword (the only way to kill monsters in FangLore) take out 20 points of energy for blow (more a random value, the luck... More on this later). Strength is how hard the monster hits. We'll have to define: Var Stamina:Integer; That defines how healthy we are. We start with 70 points, defined in Const StartHealth=70; Don't forget to initialize Stamina with the value of StartHealth, in the Init procedure. As the only monster that moves in the game is the gardner, lets restrict his movements to the garden. So, as there are 14 garden positions, let's define MaxPos: Const MaxPos=14; This is how the movement works. We fill the Positions array with the values of the rooms we want the monster to move in. Then we set Index as 1. Then, for every time-unit passed, we increase the Index in one and load the Pos variable with the value of the room indexed by Index. If the room indexed is equal to 0, then Index is reseted to 1. If the Index is bigger than the array size, the Index is also reseted to 1. We'll define a time-unit as the time for each command entered. We could define that as 1 second, 1 minute, every 5 commands, etc. That option is to be made. Some minor reprogramming is necessary for every kind of 'timer'. The hardest one to make is the real timer, that uses an interrupt or something like that. We just have to define the variables for the monsters: Var Monsters:Array[1..MaxMons] Of MonsterType; where MaxMons is the number of monsters in the game: Const MaxMons=3; Similarly as I have done with the Objects and Rooms, I made a program to input the values of the monster's records. I've created with it a file called Monster.Dat, and I read that file with the ReadMonsterData procedure, that is very similar to the ReadRoomData and ReadObjData procedures: Procedure ReadMonsterData; { Read from the disk the monster data } Var F:Text; A,B:Byte; Flag:Boolean; Begin { Prepares the text file for accessing } Assign(F,'Monster.Dat'); Reset(F); { For every object in the game } For A:=1 To MaxMons Do Begin { Read the name of the objects } ReadLn(F,Monsters[A].Name); { Read the initial position of the objects } ReadLn(F,Monsters[A].Pos); { Clear the object's description } ReadLn(F,Monsters[A].Stamina); { Clear the object's description } ReadLn(F,Monsters[A].Strength); { Clear the object's description } For B:=1 To 5 Do Monsters[A].Positions[B]:=0; { Read the description of the room } Flag:=True; B:=1; While Flag Do Begin ReadLn(F,Monsters[A].Positions[B]); If (B=MaxPos) Or (Monsters[A].Positions[B]=0) Then Flag:=False; Inc(B); End; Monsters[A].Index:=1; End; Close(F); End; And don't forget to call this procedure in the Init procedure. And now, we have to do a procedure that sees the monster. Why don't we just include the data in the look procedure ? Because we'll have to look at the monster lots of time during battle. We'll also use that procedure to make the monster kick our ass !!!! :))) Everytime the computer 'looks' at the monster, he strikes. Here we'll add the luck element. The monster will only hit a percentage of times. Only when the monster hits we'll suffer some damage. So, let's do the rotine: Procedure SeeMonster; { Sees the monsters } Var A,B:Byte; Damage:Byte; Begin For A:=1 To MaxMons Do Begin { Check if the monster is in the room } If Monsters[A].Pos=CurrentRoom Then Begin { Describe monster } TextColor(LightRed); WriteLn; WriteLn('I see a ',Monsters[A].Name,' here !!!!!'); WriteLn('He''s got ',Monsters[A].Stamina,' health points...'); { Makes him hit us } WriteLn('He sees you !'); Write('He tries to hit you...'); { Check if he hits us. Dificulty is a predefined percentage margin. The higher it his, the greater is our 'luck' } B:=Random(100); If B>Dificulty Then Begin { He hits !!! } WriteLn('and succeeds !!!!'); { Assess damages, taking in acount some luck or bad luck } Damage:=Monsters[A].Strength+(Random(8)-5); Stamina:=Stamina-Damage; WriteLn('You only have ',Stamina,' health points !'); { Check if we're still alive } If Stamina<=0 Then Begin { We have died... :( } WriteLn('You have died...'); Write('The Valkries come to claim your body... But '); WriteLn('you aren''t a warrior, so they leave'); Writeln('you to rot... '); Writeln; TextColor(Magenta); Writeln('Press any key to exit FangLore...'); Repeat Until Keypressed; Halt(0); End; End Else Begin { He misses !!! } Writeln('and misses !!!'); WriteLn('You still have ',Stamina,' health points !'); End; End; End; End; We have taken in account luck, because the game is more exciting that way. We just have to define the Dificulty factor: Const Dificulty=40; Just add a call to the SeeMonster procedure in the beggining of the play procedure, to get that time-unit we already discussed. I also added something to the Inventory procedure. Now, the inventory command also informs you of your health. That is done by adding the lines: WriteLn; TextColor(LightGreen); WriteLn('You have ',Stamina,' health points.'); to the Inventory procedure. Now, let's make the monsters move... As I explained already, the movement is achieved by changing the position of the monster every time a command is issued. To achieve that, I add a call to the MoveMonster procedure in the end of the loop in the Play procedure. The MoveMonster procedure is rather simple: Procedure MoveMonster; Var A:Byte; Begin { Move every monster } For A:=1 To MaxMons Do Begin { Reset the position index, if the position he's pointing is equal to 0 } If Monsters[A].Positions[Monsters[A].Index]=0 Then Monsters[A].Index:=1; Monsters[A].Pos:=Monsters[A].Positions[Monsters[A].Index]; Inc(Monsters[A].Index); End; End; This is the basic movement unit. But we can improve it a lot. One possible improvement is this: We don't want the monster to move if he is in the same room as the player. We can do that by simply adding three lines: Procedure MoveMonster; Var A:Byte; Begin { Move every monster } For A:=1 To MaxMons Do Begin { Check if the monster is in the same room as the player } If Monsters[A].Pos<>CurrentRoom Then Begin { Reset the position index, if the position he's pointing is equal to 0 } If Monsters[A].Positions[Monsters[A].Index]=0 Then Monsters[A].Index:=1; Monsters[A].Pos:=Monsters[A].Positions[Monsters[A].Index]; Inc(Monsters[A].Index); End; End; End; Just to add a bit to the realism of the text, let's be nice to the player and when a monster enters the room the player's in, the computer writes a message saying so. Just add some more lines: Procedure MoveMonster; Var A:Byte; Begin { Move every monster } For A:=1 To MaxMons Do Begin { Check if the monster is in the same room as the player } If Monsters[A].Pos<>CurrentRoom Then Begin { Reset the position index, if the position he's pointing is equal to 0 } If Monsters[A].Positions[Monsters[A].Index]=0 Then Monsters[A].Index:=1; Monsters[A].Pos:=Monsters[A].Positions[Monsters[A].Index]; Inc(Monsters[A].Index); { Check if the monster has entered the same room as the player } If Monsters[A].Pos=CurrentRoom Then Begin TextColor(LightCyan); Write('The ',Monsters[A].Name,' has entered '); Writeln('the room !'); End; End; End; End; The procedure got pretty big (comparing to the first version), but it adds a lot to the realism. Now, we have be able to retaliate to the threat of the monsters. We do that by using the sword we find in the main hall of FangLore. We issue the command USE SWORD and our character tries to hit the monster with the sword (if it has it in his possession). Then, depending on luck, he hits or misses. The luck factor is based upon the Difficulty factor we defined for our luck in dodging the monsters blows. So, we need to add the following lines to the Use procedure, in the section regarding the usage of the sword: Flag:=True; Flag2:=False; { Check if there is any monsters in the room } For A:=1 To MaxMons Do If Monsters[A].Pos=CurrentRoom Then Flag2:=True; WriteLn; If Flag2=False Then Begin TextColor(Green); WriteLn('Use it on whom ???'); WriteLn; End; For A:=1 To MaxMons Do Begin { Check if the monster is in the room } If Monsters[A].Pos=CurrentRoom Then Begin { Try to hit monster } Write('You swing the sword at the ', Monsters[A].Name,'...'); { Check if you hits him. Dificulty is a predefined percentage margin. The higher it his, the greater is our 'luck' } B:=Random(100); If B>(Dificulty Div 2) Then Begin { You hits !!! } WriteLn('and you hit !!!!'); { Assess damages, taking in acount some luck or bad luck } Damage:=12+(Random(8)-5); Dec(Monsters[A].Stamina,Damage); WriteLn('The monster has only ', Monsters[A].Stamina, ' health points !'); { Check if he is still alive } If Monsters[A].Stamina<=0 Then Begin { The monster died... :) } WriteLn('You killed the ', Monsters[A].Name,'!'); Writeln; End; End Else Begin { You miss... :( } Writeln('and you miss !!!'); WriteLn('The monster still has ', Monsters[A].Stamina, ' health points !'); End; End; End; End; I've discovered some stuff I forgot to do, while I did the previous parts of this tutorial. In the MonsterType definition, we must make Stamina a ShortInt, instead of a Byte, because a stupid thing that happens to bytes when you subtract... Check out Tricks and Tips in this issue so that you understand the reason why. Another thing I forgot is to check the monster's stamina in the SeeMonster procedure. The problem with it is that the monster still strikes you even if he is dead !!!!!! So, we have to add some three lines to the SeeMonster procedure that checks if the monster is alive before he tries to kick our ass... We have to add a check if the monster is alive before we can kick his ass. And finally, we can add something in the Look procedure that tells us that the corpse of the monster is in the room. Check out the complete listing of FangLore to see the changes. They are quite simple. 3.2. Special Rooms A special room, as the name indicates, is a room that has anything special about it, something different from the other rooms. In FangLore, the only special room is the Gas Room. The Gas Room is a room that kills the player the moment he steps in, except if he is wearing a mask. To implement the special room, we just add some lines to the beggining of the Look procedure: { Check if the player is in the special room, i.e. the gas room } If RoomNumber=9 Then Begin TextColor(Yellow); Write('You have entered the gas room, '); { Check if the player is wearing the Gas Mask } If Mask=True Then WriteLn('but you are unharmed because of the gas mask.') Else Begin WriteLn('and you died, because of the poisoned gas...'); Write('The Valkries come to claim your body... But '); WriteLn('you aren''t a warrior, so they leave'); Writeln('you to rot... '); Writeln; TextColor(Magenta); Writeln('Press any key to exit FangLore...'); Repeat Until Keypressed; Halt(0); End; WriteLn; End; That's it for this issue's section on text adventures. Next issue, we'll have an article on examination and manipulation of the gamescape, and we'll almost finish the text adventure... NOT ! :)))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 4. Some order in a 3d world, please ! This article has lots of info about how to order your objects in 3d world. It will cover face sorting (in several ways, including an overview of Z-Buffer and BSP trees) and backface removal. If you don't know what I'm talking, you'll get a pretty good idea soon... 4.1. 3d polygons How can you draw a 3d polygon ??? Easy enough... To get an intuitive notion, get a disk... Any disk... Use one of those broken down, wasted disks you use to play frisby with your dog (unless your very rich and play with CD's, in which case I recomend you drop programming and hire something to do it for you :)) ). Now, look at it straigth forward... It will look something like this: 1____2 ASCII art was never a strong point of mine... :) | | Now, draw some numbers on the disk (they are usefull | | now and later. Number them as the figure implies). 3____4 Now, do something funny and hard: rotate the disk 45 degrees around the Z axis... You will get something like this: 1 As I said before, my ASCII art sucks. / \ Now, notice that the disk still has the / \ same inherent proprities... There is 4 2 still a line connecting point 1 to 2, \ / and so on... Basically, only the base \ / points change... The rest of the figure 3 remains the same. And this is not only when the rotation is about the Z axis. For example, if you had rotated around the Y axis, you would get something like this: 1 See ? The points rotate, but there is still a line connecting |\ them all. When you draw a 3d poly, it is just the same thing. | \2 You keep a record with the coordinates of the four points that | | define it (it can be more or less than four points. I just like | | working with four... Working with triangles is more flexible, but | /3 I'll let that for you... :) ) |/ So, let's define a record to keep the 3d polys, and the procedure 4 to draw, rotate and translate a 3d poly on the screen: Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4:Real; Color:Byte; End; We use reals for the coordinates, but for speed's sake, we should use longints and use fixed point math... I'll do an article on that on the next issue. Now, to draw a 3dpoly: Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; The later procedure uses a procedure we defined in the last article on 3d: Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; The Draw3dPoly just converts the 3d coordinates to 2d coordinates and draws a poly based on those coordinates. Now, let's do a rotine to rotate the 3dpoly. We'll use the formulas derived in issue 10. The rotation of a poly in this case is around the origin of the world. So, if you want to rotate the poly around the center, don't forget to move the center of the poly to the origin. More on this later... Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); Var Angle:Real; Temp:Real; Begin { Rotate all points of poly around Z axis } { Convertion to radians } Angle:=0.0175*ZAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around X axis } { Convertion to radians } Angle:=0.0175*XAng; Temp:=P.Z1; P.Z1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z2; P.Z2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z3; P.Z3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z4; P.Z4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around Y axis } { Convertion to radians } Angle:=0.0175*YAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Z1*Sin(Angle); P.Z1:=P.Z1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Z2*Sin(Angle); P.Z2:=P.Z2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Z3*Sin(Angle); P.Z3:=P.Z3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Z4*Sin(Angle); P.Z4:=P.Z4*Cos(Angle)+Temp*Sin(Angle); End; This procedure is big, but it is just the aplication of the formulas given in the previous issue to every point of the polygon. Now, returning to the matter of the relativety of the rotation. Normally, we want to rotate the points around the center of the polygon. To achieve that, we will calculate the center of the polygon and store that value. And we'll rewrite the rotation procedure to reflect those changes. So, the new definition of Poly3d will be: Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Color:Byte; End; Here, Xc,Yc and Zc will be center points. Now, let's do a procedure that 'generates' a poly. Given the coordinates of the four points, the procedure will store those points in the respective variables and calculate the center point. The center point will be the average of all the other points. So, we get: Procedure Gen3dPoly(X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte;Var P:Poly3d); Begin P.X1:=X1; P.Y1:=Y1; P.Z1:=Z1; P.X2:=X2; P.Y2:=Y2; P.Z2:=Z2; P.X3:=X3; P.Y3:=Y3; P.Z3:=Z3; P.X4:=X4; P.Y4:=Y4; P.Z4:=Z4; P.Xc:=(X1+X2+X3+X4)/4; P.Yc:=(Y1+Y2+Y3+Y4)/4; P.Zc:=(Z1+Z2+Z3+Z4)/4; P.Color:=C; End; Now, we just have to change the Rotate3dPoly procedure so that all coordinates be expressed in terms of (Xc,Yc,Zc): Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); Var Angle:Real; Temp:Real; Begin { Make the coordinates of the points relative to the center } P.X1:=P.X1-P.Xc; P.Y1:=P.Y1-P.Yc; P.Z1:=P.Z1-P.Zc; P.X2:=P.X2-P.Xc; P.Y2:=P.Y2-P.Yc; P.Z2:=P.Z2-P.Zc; P.X3:=P.X3-P.Xc; P.Y3:=P.Y3-P.Yc; P.Z3:=P.Z3-P.Zc; P.X4:=P.X4-P.Xc; P.Y4:=P.Y4-P.Yc; P.Z4:=P.Z4-P.Zc; { Rotate all points of poly around Z axis } { Convertion to radians } Angle:=0.0175*ZAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around X axis } { Convertion to radians } Angle:=0.0175*XAng; Temp:=P.Z1; P.Z1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z2; P.Z2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z3; P.Z3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z4; P.Z4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around Y axis } { Convertion to radians } Angle:=0.0175*YAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Z1*Sin(Angle); P.Z1:=P.Z1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Z2*Sin(Angle); P.Z2:=P.Z2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Z3*Sin(Angle); P.Z3:=P.Z3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Z4*Sin(Angle); P.Z4:=P.Z4*Cos(Angle)+Temp*Sin(Angle); { Transform the coordinates again } P.X1:=P.X1+P.Xc; P.Y1:=P.Y1+P.Yc; P.Z1:=P.Z1+P.Zc; P.X2:=P.X2+P.Xc; P.Y2:=P.Y2+P.Yc; P.Z2:=P.Z2+P.Zc; P.X3:=P.X3+P.Xc; P.Y3:=P.Y3+P.Yc; P.Z3:=P.Z3+P.Zc; P.X4:=P.X4+P.Xc; P.Y4:=P.Y4+P.Yc; P.Z4:=P.Z4+P.Zc; End; It's simple to see that the center of a polygon isn't changed with the rotation. Now we just have to do translation: Procedure Translate(Var P:Poly3d;Xt,Yt,Zt:Real); Begin P.X1:=P.X1+Xt; P.Y1:=P.Y1+Yt; P.Z1:=P.Z1+Zt; P.X2:=P.X2+Xt; P.Y2:=P.Y2+Yt; P.Z2:=P.Z2+Zt; P.X3:=P.X3+Xt; P.Y3:=P.Y3+Yt; P.Z3:=P.Z3+Zt; P.X4:=P.X4+Xt; P.Y4:=P.Y4+Yt; P.Z4:=P.Z4+Zt; P.Xc:=P.Xc+Xt; P.Yc:=P.Yc+Yt; P.Zc:=P.Zc+Zt; End; It's easy enough to see that the center of a polygon _IS_ changed with the translation. Now, we are ready to do a bit of example code... This example is pretty simple. It just zooms in a poly, and then rotates it around, and around, and around... :)) Program Polygon3d; Uses Crt,Mode13h; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Color:Byte; End; Var Poly:Poly3d; Procedure Gen3dPoly(X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte;Var P:Poly3d); Begin P.X1:=X1; P.Y1:=Y1; P.Z1:=Z1; P.X2:=X2; P.Y2:=Y2; P.Z2:=Z2; P.X3:=X3; P.Y3:=Y3; P.Z3:=Z3; P.X4:=X4; P.Y4:=Y4; P.Z4:=Z4; P.Xc:=(X1+X2+X3+X4)/4; P.Yc:=(Y1+Y2+Y3+Y4)/4; P.Zc:=(Z1+Z2+Z3+Z4)/4; P.Color:=C; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; Procedure Translate(Var P:Poly3d;Xt,Yt,Zt:Real); Begin P.X1:=P.X1+Xt; P.Y1:=P.Y1+Yt; P.Z1:=P.Z1+Zt; P.X2:=P.X2+Xt; P.Y2:=P.Y2+Yt; P.Z2:=P.Z2+Zt; P.X3:=P.X3+Xt; P.Y3:=P.Y3+Yt; P.Z3:=P.Z3+Zt; P.X4:=P.X4+Xt; P.Y4:=P.Y4+Yt; P.Z4:=P.Z4+Zt; P.Xc:=P.Xc+Xt; P.Yc:=P.Yc+Yt; P.Zc:=P.Zc+Zt; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); Var Angle:Real; Temp:Real; Begin { Make the coordinates of the points relative to the center } P.X1:=P.X1-P.Xc; P.Y1:=P.Y1-P.Yc; P.Z1:=P.Z1-P.Zc; P.X2:=P.X2-P.Xc; P.Y2:=P.Y2-P.Yc; P.Z2:=P.Z2-P.Zc; P.X3:=P.X3-P.Xc; P.Y3:=P.Y3-P.Yc; P.Z3:=P.Z3-P.Zc; P.X4:=P.X4-P.Xc; P.Y4:=P.Y4-P.Yc; P.Z4:=P.Z4-P.Zc; { Rotate all points of poly around Z axis } { Convertion to radians } Angle:=0.0175*ZAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around X axis } { Convertion to radians } Angle:=0.0175*XAng; Temp:=P.Z1; P.Z1:=Temp*Cos(Angle)-P.Y1*Sin(Angle); P.Y1:=P.Y1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z2; P.Z2:=Temp*Cos(Angle)-P.Y2*Sin(Angle); P.Y2:=P.Y2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z3; P.Z3:=Temp*Cos(Angle)-P.Y3*Sin(Angle); P.Y3:=P.Y3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.Z4; P.Z4:=Temp*Cos(Angle)-P.Y4*Sin(Angle); P.Y4:=P.Y4*Cos(Angle)+Temp*Sin(Angle); { Rotate all points of poly around Y axis } { Convertion to radians } Angle:=0.0175*YAng; Temp:=P.X1; P.X1:=Temp*Cos(Angle)-P.Z1*Sin(Angle); P.Z1:=P.Z1*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X2; P.X2:=Temp*Cos(Angle)-P.Z2*Sin(Angle); P.Z2:=P.Z2*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X3; P.X3:=Temp*Cos(Angle)-P.Z3*Sin(Angle); P.Z3:=P.Z3*Cos(Angle)+Temp*Sin(Angle); Temp:=P.X4; P.X4:=Temp*Cos(Angle)-P.Z4*Sin(Angle); P.Z4:=P.Z4*Cos(Angle)+Temp*Sin(Angle); { Transform the coordinates again } P.X1:=P.X1+P.Xc; P.Y1:=P.Y1+P.Yc; P.Z1:=P.Z1+P.Zc; P.X2:=P.X2+P.Xc; P.Y2:=P.Y2+P.Yc; P.Z2:=P.Z2+P.Zc; P.X3:=P.X3+P.Xc; P.Y3:=P.Y3+P.Yc; P.Z3:=P.Z3+P.Zc; P.X4:=P.X4+P.Xc; P.Y4:=P.Y4+P.Yc; P.Z4:=P.Z4+P.Zc; End; Begin InitGraph; InitVirt; SetColor(1,63,63,0); Cls(0,Vp[1]); Gen3dPoly(0,-20,1500,20,0,1500,0,20,1500,-20,0,1500,1,Poly); Repeat Cls(0,Vp[1]); Draw3dPoly(Poly,Vp[1]); CopyPage(Vp[1],VGA); Translate(Poly,0,0,-15); Until (Keypressed) Or (Poly.Z1<=200); Repeat Cls(0,Vp[1]); Draw3dPoly(Poly,Vp[1]); CopyPage(Vp[1],Vga); Rotate3dPoly(Poly,3,2,4); Until Keypressed; CloseVirt; CloseGraph; End. We could speed up this very much, by using fixed point maths and a lookup table for the sines and cosines. Let's see the procedure transformed with that purpose: Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-P.Xc; P.Y1:=P.Y1-P.Yc; P.Z1:=P.Z1-P.Zc; P.X2:=P.X2-P.Xc; P.Y2:=P.Y2-P.Yc; P.Z2:=P.Z2-P.Zc; P.X3:=P.X3-P.Xc; P.Y3:=P.Y3-P.Yc; P.Z3:=P.Z3-P.Zc; P.X4:=P.X4-P.Xc; P.Y4:=P.Y4-P.Yc; P.Z4:=P.Z4-P.Zc; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+P.Xc; P.Y1:=P.Y1+P.Yc; P.Z1:=P.Z1+P.Zc; P.X2:=P.X2+P.Xc; P.Y2:=P.Y2+P.Yc; P.Z2:=P.Z2+P.Zc; P.X3:=P.X3+P.Xc; P.Y3:=P.Y3+P.Yc; P.Z3:=P.Z3+P.Zc; P.X4:=P.X4+P.Xc; P.Y4:=P.Y4+P.Yc; P.Z4:=P.Z4+P.Zc; End; This is much faster than the original one... But don't take my word for it... Time it... In fact, I'm gonna do that now: The procedure without the lookup tables achieved 6.1 FPS (Frames per Second) in my 386/16 Mhz (a good machine, in the age of the Industrial Revolution, in end of the 1800s). The procedure with the lookup tables achieved 7.8 FPS... That's what I call improvement. You can see the alterations I did to the program in order to time it in the file 3d4.Pas. Now, let's move on to 3d solids... 3d solids are nothing more than a collection of 3d polygons. Usually it is associated with a 3d solid a notion of fullness, that is, that it is... well, solid ! :)) Let's define a structure for the 3d solid: Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yx,Zc:Real; End; Cx, Cy and Cz are the coordinates of the center of the solid. This is important so that you can rotate the solid around it's center. Usually, the center is the average of the coordinates of the solid. Polys is an array that refers to the index of the following structure: Var Polygons:Array[1..MaxPolys] Of Poly3d; This array stores all the polygons. It will become clear to you why we do this later. So, if the Polys array has the values [1 2 3 4], it means that the solid uses polygons 1, 2, 3 and 4. The NumPolys variable is the number of polygons the solid has. So, now let's construct something. Most of the 3d tutorials use a 3d cube for the examples. So, who am I to go against the flow ? The code's equal for every type of solid. It is just harder to define... But I bet you'll soon do a program to create the 3d solids (or convert them from a comercial program like 3d Studio). So, let's do an example... First, let's define the types and variables: Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Now, let's define MaxPolys. That's simultaneously the maximum number of polygons per solid and the maximum number of polygons that can be in our 3d world. You could use diferent constants for each one of them in order to save memory. Const MaxPolys=6; I've defined as 6, because that's all we'll need for one cube. Now, let's define the diamond: Var Cube:Solid3d; And put the values in it: Procedure InitCube; Var A:Byte; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,-30,-30,286,30,-30,286,30,-30,226,-30,-30,226,5); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,6); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; In this procedure, I used the altered version of Gen3dPoly Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; End; One thing you should be carefull with is to number your points clockwise (or anticlockwise... Just number them using the same standart... You'll understand why later). I'll explain how to do this now... Let's visualize the cube... Check out the picture Cube.Pcx, that has an explanation on how to the numbering. General rule (this is how I do it), number the points in clockwise manner if the polygon is facing you, and number it anti-clockwise if it faces the other way. Now, let's see the complete example (with an initialization, and a DrawSolid procedure, and a rotation procedure, that doesn't require explanation... It is just a rotation procedure that enables you to choose the center of rotation (in this case, the center of the cube): Program TestCube; Uses Mode13h,Crt; Const MaxPolys=6; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Cube:Solid3d; RotCentX,RotCentY,RotCentZ:Real; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); { This rotates around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-RotCentX; P.Y1:=P.Y1-RotCentY; P.Z1:=P.Z1-RotCentZ; P.X2:=P.X2-RotCentX; P.Y2:=P.Y2-RotCentY; P.Z2:=P.Z2-RotCentZ; P.X3:=P.X3-RotCentX; P.Y3:=P.Y3-RotCentY; P.Z3:=P.Z3-RotCentZ; P.X4:=P.X4-RotCentX; P.Y4:=P.Y4-RotCentY; P.Z4:=P.Z4-RotCentZ; P.Xc:=P.Xc-RotCentX; P.Yc:=P.Yc-RotCentY; P.Zc:=P.Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zc; P.Zc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Zc*Sines^[Angle]; P.Zc:=P.Zc*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+RotCentX; P.Y1:=P.Y1+RotCentY; P.Z1:=P.Z1+RotCentZ; P.X2:=P.X2+RotCentX; P.Y2:=P.Y2+RotCentY; P.Z2:=P.Z2+RotCentZ; P.X3:=P.X3+RotCentX; P.Y3:=P.Y3+RotCentY; P.Z3:=P.Z3+RotCentZ; P.X4:=P.X4+RotCentX; P.Y4:=P.Y4+RotCentY; P.Z4:=P.Z4+RotCentZ; P.Xc:=P.Xc+RotCentX; P.Yc:=P.Yc+RotCentY; P.Zc:=P.Zc+RotCentZ; End; Procedure InitCube; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,-30,-30,286,30,-30,286,30,-30,226,-30,-30,226,5); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,6); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; Procedure DrawSolid(S:Solid3d;Where:Word); Var A:Word; Begin For A:=1 To S.NumPolys Do Draw3dPoly(Polygons[S.Polys[A]],Where); End; Procedure RotateSolid(S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; For A:=1 To S.NumPolys Do Rotate3dPoly(Polygons[S.Polys[A]],XAng,YAng,ZAng); End; Begin InitGraph; InitVirt; InitTables; SetColor(0,0,0,0); SetColor(1,63,0,0); SetColor(2,63,63,0); SetColor(3,0,63,0); SetColor(4,0,63,63); SetColor(5,0,0,63); SetColor(6,63,32,0); Cls(0,VGA); InitCube; Repeat Cls(0,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); RotateSolid(Cube,3,2,5); Until Keypressed; ClearTables; CloseGraph; End. After you run this program, and before you run to the definition of the cube to see what values have I made wrong, stop and think a bit... The error isn't in the definition... It is in the order by which the polygons are drawn on the screen... Confused ?!... Read on... 4.2. Sorting 4.2.1. The Painter's Algorithm Check out the following diagram... It represents the 3d universe seen from the top, with the cube in the middle: ^z | Now, we know (by the way we've defined the cube), ------- that the polygon we seen in the lower part of the | | | universe is polygon number 1. And that the polygon ---|--O--|---> in the upper part is polygon 3. By the way we've | | | x defined the DrawSolid procedure, we first draw poly ------- 1 and then we draw poly 3. It is obvious that poly | 3 will obscure poly 1, but it should happen the exact opposite !!!! But don't go running to the viewpoint ^ code changing the order, because if you rotate the | solid 180 degrees, you won't get this correct that way !!! What we need it to sort the polygons by their Z's, so that the polygons that are further away are drawn first. This is known as the 'Painter's Algorythm', because that's the way painters paint... They draw first the things that are further away, and then the things that are nearer, and so forth. In the example program, I'll use a quicksort algorythm to do the sorting by the Z coordinates of the polygons. Check issue 10 of 'The Mag' to see what the hell I'm talking of. We aren't going to change the polygons from their normal order. We are just going to update an array that tells the order by which the polygons must be drawn (example, if the array Order has the values [1 2 6 3 2], the program will draw first poly 1, then 2, then 6, and so forth). We'll no longer use the DrawSolid procedure, exchanging it with the DrawPolys procedure, that draws all polygons, using the order suplied by the Order array: Program TestCube; Uses Mode13h,Crt; Const MaxPolys=6; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Cube:Solid3d; RotCentX,RotCentY,RotCentZ:Real; Order:Array[1..MaxPolys] Of Word; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); { This rotates around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-RotCentX; P.Y1:=P.Y1-RotCentY; P.Z1:=P.Z1-RotCentZ; P.X2:=P.X2-RotCentX; P.Y2:=P.Y2-RotCentY; P.Z2:=P.Z2-RotCentZ; P.X3:=P.X3-RotCentX; P.Y3:=P.Y3-RotCentY; P.Z3:=P.Z3-RotCentZ; P.X4:=P.X4-RotCentX; P.Y4:=P.Y4-RotCentY; P.Z4:=P.Z4-RotCentZ; P.Xc:=P.Xc-RotCentX; P.Yc:=P.Yc-RotCentY; P.Zc:=P.Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zc; P.Zc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Zc*Sines^[Angle]; P.Zc:=P.Zc*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+RotCentX; P.Y1:=P.Y1+RotCentY; P.Z1:=P.Z1+RotCentZ; P.X2:=P.X2+RotCentX; P.Y2:=P.Y2+RotCentY; P.Z2:=P.Z2+RotCentZ; P.X3:=P.X3+RotCentX; P.Y3:=P.Y3+RotCentY; P.Z3:=P.Z3+RotCentZ; P.X4:=P.X4+RotCentX; P.Y4:=P.Y4+RotCentY; P.Z4:=P.Z4+RotCentZ; P.Xc:=P.Xc+RotCentX; P.Yc:=P.Yc+RotCentY; P.Zc:=P.Zc+RotCentZ; End; Procedure InitCube; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,6); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,5); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; Procedure DrawPolys(Where:Word); Var A:Word; Begin { We draw the polys in reversed order because the sorting is from the smallest to the biggest, and we are interested in the reverse } For A:=MaxPolys DownTo 1 Do Draw3dPoly(Polygons[Order[A]],Where); End; Procedure RotateSolid(S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; For A:=1 To S.NumPolys Do Rotate3dPoly(Polygons[S.Polys[A]],XAng,YAng,ZAng); End; Procedure SortPolys; Var Flag:Boolean; I,J:Integer; X:Integer; N:Real; T:Real; Procedure SortSubArray(Left,Right:Byte); Begin { Partition } I:=Left; J:=Right; N:=Polygons[Order[(Left+Right) Div 2]].Zc; Repeat { Find first number from the left to be < N } While Polygons[Order[I]].Zc N } While Polygons[Order[J]].Zc>N Do Dec(J); { Exchange } If I<=J Then Begin X:=Order[J]; Order[J]:=Order[I]; Order[I]:=X; Inc(I); Dec(J); End; Until J(1/3)). There is a variant of this method, made to work with moving cameras. I think it is called the D-Buffer (or Distance-Buffer). Instead of storing the Z value, it stores the distance from the viewer. This method isn't very good, because calculting the distance implies getting a square root, a 3 squares, plus two addictions... So, the speed edge of Z-buffer get's lost... It is sometimes used with voxels (I'll do an article on them soon). 4.2.3. BSP Trees BSP stands for Binary Space Partition. The BSP trees algorithm was conceived for several reasons: 1) To provide a good algorithm for any viewpoint 2) To solve problems that no sorting method could, like polygon overlapping/instersection. The ideia behind BSP Trees is to build a tree with the polygons, in which a relationship of order is implied from branch to branch. Confused ?! You should be, because I think that BSP Tree is one of the hardest sorting methods. To understand it fully, you must have a good knowledge of analitic geometry... I won't do here a complete guide on the workings of it, because I'm not suficiently at ease with the subject... Maybe one day I'll make an article on it... The disadvantages of BSP trees are: 1) Lots of memory needed 2) SLOW !!!!! 3) Only really usefull for static scenes (most raytracers use BSP trees). BSP trees with moving objects are a nightmare to manage. So, BSP trees suck for demos and games... I just included it here to give you an ideia of what it is. Now, I'll delve into the world of backface removal. But before that, I'll delve into the world of... 4.3. Vector maths 4.3.1. Basic concepts Vectors are very necessary when it comes to 3d... I tried to steer away from them in the beggining, so that you could addapt to the basics of 3d without worring with the math underlying it. Before you start puking, just because I said 'maths', think of this: 'You only dislike what you don't need nor understand'... I hope you'll understand the concepts I'll try to transmit and you'll see the usefullness of them (specially when it comes to lighting). So, let's start this quick crash course on vectors. First of all, what is a vector. Well, we can define vector as a line joining the origin of the 3d universe and a point. This isn't very correct, mathematically speaking, because of this (oh good, more ASCII !!): | | Y Vector A is defined as (3,3), that is the coordinate 4 | P / of point P. But vector A isn't diference of vector 3 | / /B B, that is defined by points X and Y, because if you 2 | /A / translate B to the origin (if X overlaps O), you will 1 |/ X get vector A. So, a vector can be defined as the O____________ diference between two points. A size, a direction 1 3 5 7 9 and an orientation caracterize a vector, but we'll 2 4 6 8 get to that in just some instances. Notice that vector B can be defined as Y-X=(9-6,4-1)=(3,3)=A !! Now, let's get to some more basic concepts. Let's define vector orientation and vector direction. The best way to explain this is by using an example: | B | A / | / / | K / / | / W V | / / / | / C --U--D |___________________________________ Imagine four vectors, K, W, V and U, that point respectively to A, B, C and D. First, let's go to orientation. K, W and V have the same orientation, and U has a diferent orientation. Mathematically speaking, we can think of orientation has having a direct relationship with the angle the vector does with the axis (remember that the true position of the vector in space doesn't matter, it can be translated). Now, let's speak direction. K and W have the same direction, while V doesn't. You can empirically think of direction as 'where the vector points'. Just think of it like this. If you translate K and W to the origin shrink W in order for him to become of the same size as K, the vectors would point to the same point. Now, you couldn't do that with V... The direction of V is the oposite to the direction of K and W. Another thing to keep in mind is that direction and orientation are size independant. Now, let's get to size. The size of the vector is defined by an operation known as the module: |X| = Sqrt(X^2+Y^2+Z^2) This definition is for 3d vectors. If the vector is defined by two points: X=P1-P2 => |X|= Sqrt((P1.X-P2.X)^2+(P1.Y-P2.Y)^2+(P1.Z-P2.Z)^2) Now, let's define some basic vector operations: | ^.... | | /. Vector W is the sum vector of U and V. | U W . In a 2d case, imagine that U=(a,b) and V=(c,d). Then, | |/ . W=U+V=(a+c,b+d). | |-V-> In a 3d case, imagine that U=(a,b,c) and V=(d,e,f). |______________ In that case, W=(a+d,b+e,c+f). Subtraction follows the same line of thinking. Now, let's get to some more complex vector maths: 4.3.2. Multiplications There are three types of multiplications with vectors: the scalar product, the dot product (also known as external product) and the cross product (also known as internal product, or vector product). Their definitions are quite simple. The scalar product is the product between a vector and a scalar number (a scalar number is a normal number): a*X=a*(X1,X2,X3)=(aX1,aX2,aX3) Example: 2.5*(10,5,2.5)=(25,12.5,6.25) Notice that division can be thought in the same way: (10,8,6) -------- = (5,4,3) 2 The dot product returns a value: A.B=(a,b,c).(d,e,f)= ad+be+cf The cross product returns a vector: AxB=(a,b,c)x(d,e,f)=(bf-ec,af-cd,ae-bd) Weird, isn't it ? Notice that AxB isn't equal to BxA: BxA=(d,e,f)x(a,b,c)=(ec-bf,cd-af,bd-ae) Actually, the two resulting vectors have the same orientation and size, but oposite direction, so: AxB= -BxA Well, they are both usefull, as you'll see later. One interesting propretie of the cross product is that the resulting vector is perpendicular with the other two, that is, it makes 90 degrees with them both !! This is going to be very usefull. Another interesting thing is that the module can be expressed with the aid of the dot product: |X|= Sqrt(A.A) The last (and maybe the more important) property is that you can find the angle between two vectors using the dot product. The relation is this: A.B Cos (Angle) = ------- |A|*|B| If the vectors are unitary (see below), the relation is even simpler: Cos (Angle) = A.B which implies: Angle = ArcCos(A.B) Now, let's move onto... 4.3.3. Special vectors There are some special vectors that re known for several properties. There is the null vector, defined as: _ 0 = (0,0,0) Properties include the fact that any of the products is 0 if one of the multiplicands is the null vector. Also, the null vector doesn't affect addition or subtraction. Other special kind of vectors are unitary vectors. A unitary vector is a vector whose size is equal to 1, that is: |X|=1 The process of transforming a vector of any size into a vector of unitary size is the normalization. You can normalize vector W like this: W W'= ----- |W| W' will be the normalized vector. Vectors in this category are many, but there are some that are very special. 1) The canonic vectors: These vectors define a space. That means that any vector in that space can be expressed in terms of those two vectors. For example, the normal 2d space canonic base is vectors X and Y, that are equal to (1,0) and (0,1), respectively. For example, we want to define vector W in that space, W=(5,3). So, W= 5X+3Y... In 3d, the canonic vectors are (1,0,0);(0,1,0);(0,0,1). 2) The normal vector: A normal vector (or normal for short) is a vector perpendicular to a plane (or a polygon, for usually it can be thought as a plane). It is very usefull for backface removal, and almost all kinds of shading and bumpmapping. Well, I think this quite covers it all, in respect to vector maths. If you didn't understood shit, mail me, or get a good 9th grade maths book (I think this is standart 9th grade stuff). Now, finally, on to: 4.4. Backface removal Notice that in a cube, only 3 sides of it are visible at once ? Wouldn't it be great if you could only draw the 3 visibles ? Yes, it would... And it is easier to do than you thought. Notice that the sides that shouldn't be drawn are facing the away from you... If we could find out which ones are facing away and the ones who are facing towards us, we could draw just the ones that matter to us, and discard the others. This works not only with cubes, but also with any kind of solid that closes around himself !!! So, how we'll do this ? Using the NORMAL !!! If we calculate the normal vector of the polygon, we can know where the polygon's facing !! Z^ U and V are the normals to sides 1 and 2, respectively. | ^ Now it is becoming to get clear why we've defined the | |V points of the polygons in a clockwise manner (anti- | | clockwise to those who face away from us) ? | --2-- No ?! | | | Then I'll explain... To determine the normal to the | --1-- polygon, we need 2 vectors belonging to the polygon. | | We'll choose the first three points of the four points | |U of the polygons, calling them a, b and c. So, if we | | call A and B to (a-b) and (a-c) respectively, and we |___________> calculate the cross product AxB, we'll get a vector X perpendicular (or normal) to the polygon, and facing away from us if the points are anticlockwise, or facing toward us if the points were defined clockwise. Now, we just find out the angle between the normal of the polygon and the view vector (in our case, (0,0,1)), and if the angle is between 90 and 270 degrees, we display the polygon, otherwise, we do not. In practice, following the ideia that our camera is fixed and it is staring down the Z axis, we can just check if the normal's Z element is positive or not !! This gives a great speed up to the program, since some sides don't require to be drawn... You don't even have to calc the normals every frame !! It can be done in the beggining of the program, and then the normal is rotated with the rest of the points ! Check out the following example... It was done based on the first example I showed (before the sorting example). I use only backface removal to render the cube correctly. Notice that the Poly3d structure was altered to include the normal, as well as the rotation procedures, and the initialization of the polygons (the Gen3dPoly procedure)... Program TestCube; Uses Mode13h,Crt; Const MaxPolys=6; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Xn,Yn,Zn:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Cube:Solid3d; RotCentX,RotCentY,RotCentZ:Real; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Var Module:Real; Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; { Calc the normals } Polygons[N].Xn:=(Z2-Z1)*(Y3-Y1)-(Y2-Y1)*(Z3-Z1); Polygons[N].Yn:=(Z2-Z1)*(X3-X1)-(X2-X1)*(Z3-Z1); Polygons[N].Zn:=(Y2-Y1)*(X3-X1)-(X2-X1)*(Y3-Y1); { Normalize the normals } Module:=Sqrt(Sqr(Polygons[N].Xn)+Sqr(Polygons[N].Yn)+ Sqr(Polygons[N].Zn)); Polygons[N].Xn:=Polygons[N].Xn/Module; Polygons[N].Yn:=Polygons[N].Yn/Module; Polygons[N].Zn:=Polygons[N].Zn/Module; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); { This rotates around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point, and the normal of the polygon. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-RotCentX; P.Y1:=P.Y1-RotCentY; P.Z1:=P.Z1-RotCentZ; P.X2:=P.X2-RotCentX; P.Y2:=P.Y2-RotCentY; P.Z2:=P.Z2-RotCentZ; P.X3:=P.X3-RotCentX; P.Y3:=P.Y3-RotCentY; P.Z3:=P.Z3-RotCentZ; P.X4:=P.X4-RotCentX; P.Y4:=P.Y4-RotCentY; P.Z4:=P.Z4-RotCentZ; P.Xc:=P.Xc-RotCentX; P.Yc:=P.Yc-RotCentY; P.Zc:=P.Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zc; P.Zc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zn; P.Zn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Zc*Sines^[Angle]; P.Zc:=P.Zc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Zn*Sines^[Angle]; P.Zn:=P.Zn*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+RotCentX; P.Y1:=P.Y1+RotCentY; P.Z1:=P.Z1+RotCentZ; P.X2:=P.X2+RotCentX; P.Y2:=P.Y2+RotCentY; P.Z2:=P.Z2+RotCentZ; P.X3:=P.X3+RotCentX; P.Y3:=P.Y3+RotCentY; P.Z3:=P.Z3+RotCentZ; P.X4:=P.X4+RotCentX; P.Y4:=P.Y4+RotCentY; P.Z4:=P.Z4+RotCentZ; P.Xc:=P.Xc+RotCentX; P.Yc:=P.Yc+RotCentY; P.Zc:=P.Zc+RotCentZ; End; Procedure InitCube; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,6); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,5); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; Procedure DrawSolid(S:Solid3d;Where:Word); Var A:Word; Begin For A:=1 To S.NumPolys Do { Draw if the normal is negative } If (Polygons[S.Polys[A]].Zn<0) Then Draw3dPoly(Polygons[S.Polys[A]],Where); End; Procedure RotateSolid(S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; For A:=1 To S.NumPolys Do Rotate3dPoly(Polygons[S.Polys[A]],XAng,YAng,ZAng); End; Begin InitGraph; InitVirt; InitTables; SetColor(0,0,0,0); SetColor(1,63,0,0); SetColor(2,63,63,0); SetColor(3,0,63,0); SetColor(4,0,63,63); SetColor(5,0,0,63); SetColor(6,63,32,0); Cls(0,VGA); InitCube; Repeat Cls(0,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); RotateSolid(Cube,5,5,5); Until Keypressed; ClearTables; CloseGraph; End. There are lots of gliches in the display, but that's due to errors in the calculation of the rotation of the normals. The way to mend this is fairly simple (altough it requires more memory). You define two Solid3d structures for every object in the world. In one of them, you have the object without any translations or rotations, while you affect the the other structure with this. I didn't explain myself too well... You use the accurate original values to calculate the rotations, and, instead of overwriting the accurate values, you transfer those values to another structure, and display that one. Then, in the next frame, instead of using the inacurate rotated structure, you use the original accurate one... The next example uses this theory. The theory behind the next example is to have several objects in the 3d world (three cubes, actually). And they rotate around themselfs, and one of them rotates around other cube. This example is fairly complex, but it is not hard to understand. It uses both backface removal and facesorting. But, before I code this one, I'm going to sleep a bit... It's 0:30 and I need my beauty rest... I hope I finish this tomorrow, and the lighting article, not to mention the crossfading one... This article is taking almost 9 hours to get writen!!! Am I slow or I spend this time doing something good ?! You tell me ! :))) Here I am again, at 11:30, preparing to code another masterpiece ! :))) When I was coding this masterpiece, I came up with an interesting problem. When the object move a lot around the screen, specially when it comes to edges, you should be able to see the polygons that previously faced away from you. But the program won't let you see them, because your normal is still negative. For this type of programs, you should (instead of just checking if it is positive or negative) check the angle between the view vector and the normal. If it is between 90 and 270, you display the poly, otherwise you discard it. Notice that the view vector for an object that is near an edge isn't (0,0,1) as we were putting it... It is the vector that points to that polygon. That would require the program to calculate distances and other stuff like that... And that's too heavy maths for an optimization, so we're gonna loose tha backface removal in this example... Too bad... :(( But never forget that the backface removal is very good when the object you want to render is in front of you !!! We'll keep the normals and that stuff, because they are needed when I adapt this program to include lightsourcing. Program TestCube; Uses Mode13h,Crt; Const MaxPolys=18; { For 3 cubes } Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Xn,Yn,Zn:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; RotX,RotY,RotZ:Integer; { The orientation of the solid } End; Var Polygons:Array[1..MaxPolys] Of Poly3d; { The accurate polys } TPolys:Array[1..MaxPolys] Of Poly3d; { The inaccurate ones } RotCentX,RotCentY,RotCentZ:Real; Order:Array[1..MaxPolys] Of Word; Var Cube1,Cube2,Cube3:Solid3d; XInc:Integer; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Var Module:Real; Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; { Calc the normals } Polygons[N].Xn:=(Z2-Z1)*(Y3-Y1)-(Y2-Y1)*(Z3-Z1); Polygons[N].Yn:=(Z2-Z1)*(X3-X1)-(X2-X1)*(Z3-Z1); Polygons[N].Zn:=(Y2-Y1)*(X3-X1)-(X2-X1)*(Y3-Y1); { Normalize the normals } Module:=Sqrt(Sqr(Polygons[N].Xn)+Sqr(Polygons[N].Yn)+ Sqr(Polygons[N].Zn)); Polygons[N].Xn:=Polygons[N].Xn/Module; Polygons[N].Yn:=Polygons[N].Yn/Module; Polygons[N].Zn:=Polygons[N].Zn/Module; End; Procedure Rotate3dPoly(N:Word;XAng,YAng,ZAng:Integer); { This rotates polygon N around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point, and the normal of the polygon. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } TPolys[N].X1:=TPolys[N].X1-RotCentX; TPolys[N].Y1:=TPolys[N].Y1-RotCentY; TPolys[N].Z1:=TPolys[N].Z1-RotCentZ; TPolys[N].X2:=TPolys[N].X2-RotCentX; TPolys[N].Y2:=TPolys[N].Y2-RotCentY; TPolys[N].Z2:=TPolys[N].Z2-RotCentZ; TPolys[N].X3:=TPolys[N].X3-RotCentX; TPolys[N].Y3:=TPolys[N].Y3-RotCentY; TPolys[N].Z3:=TPolys[N].Z3-RotCentZ; TPolys[N].X4:=TPolys[N].X4-RotCentX; TPolys[N].Y4:=TPolys[N].Y4-RotCentY; TPolys[N].Z4:=TPolys[N].Z4-RotCentZ; TPolys[N].Xc:=TPolys[N].Xc-RotCentX; TPolys[N].Yc:=TPolys[N].Yc-RotCentY; TPolys[N].Zc:=TPolys[N].Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=TPolys[N].X1; TPolys[N].X1:=Temp*Cosines^[Angle]-TPolys[N].Y1*Sines^[Angle]; TPolys[N].Y1:=TPolys[N].Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X2; TPolys[N].X2:=Temp*Cosines^[Angle]-TPolys[N].Y2*Sines^[Angle]; TPolys[N].Y2:=TPolys[N].Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X3; TPolys[N].X3:=Temp*Cosines^[Angle]-TPolys[N].Y3*Sines^[Angle]; TPolys[N].Y3:=TPolys[N].Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X4; TPolys[N].X4:=Temp*Cosines^[Angle]-TPolys[N].Y4*Sines^[Angle]; TPolys[N].Y4:=TPolys[N].Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xc; TPolys[N].Xc:=Temp*Cosines^[Angle]-TPolys[N].Yc*Sines^[Angle]; TPolys[N].Yc:=TPolys[N].Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xn; TPolys[N].Xn:=Temp*Cosines^[Angle]-TPolys[N].Yn*Sines^[Angle]; TPolys[N].Yn:=TPolys[N].Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=TPolys[N].Z1; TPolys[N].Z1:=Temp*Cosines^[Angle]-TPolys[N].Y1*Sines^[Angle]; TPolys[N].Y1:=TPolys[N].Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z2; TPolys[N].Z2:=Temp*Cosines^[Angle]-TPolys[N].Y2*Sines^[Angle]; TPolys[N].Y2:=TPolys[N].Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z3; TPolys[N].Z3:=Temp*Cosines^[Angle]-TPolys[N].Y3*Sines^[Angle]; TPolys[N].Y3:=TPolys[N].Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z4; TPolys[N].Z4:=Temp*Cosines^[Angle]-TPolys[N].Y4*Sines^[Angle]; TPolys[N].Y4:=TPolys[N].Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Zc; TPolys[N].Zc:=Temp*Cosines^[Angle]-TPolys[N].Yc*Sines^[Angle]; TPolys[N].Yc:=TPolys[N].Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Zn; TPolys[N].Zn:=Temp*Cosines^[Angle]-TPolys[N].Yn*Sines^[Angle]; TPolys[N].Yn:=TPolys[N].Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=TPolys[N].X1; TPolys[N].X1:=Temp*Cosines^[Angle]-TPolys[N].Z1*Sines^[Angle]; TPolys[N].Z1:=TPolys[N].Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X2; TPolys[N].X2:=Temp*Cosines^[Angle]-TPolys[N].Z2*Sines^[Angle]; TPolys[N].Z2:=TPolys[N].Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X3; TPolys[N].X3:=Temp*Cosines^[Angle]-TPolys[N].Z3*Sines^[Angle]; TPolys[N].Z3:=TPolys[N].Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X4; TPolys[N].X4:=Temp*Cosines^[Angle]-TPolys[N].Z4*Sines^[Angle]; TPolys[N].Z4:=TPolys[N].Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xc; TPolys[N].Xc:=Temp*Cosines^[Angle]-TPolys[N].Zc*Sines^[Angle]; TPolys[N].Zc:=TPolys[N].Zc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xn; TPolys[N].Xn:=Temp*Cosines^[Angle]-TPolys[N].Zn*Sines^[Angle]; TPolys[N].Zn:=TPolys[N].Zn*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } TPolys[N].X1:=TPolys[N].X1+RotCentX; TPolys[N].Y1:=TPolys[N].Y1+RotCentY; TPolys[N].Z1:=TPolys[N].Z1+RotCentZ; TPolys[N].X2:=TPolys[N].X2+RotCentX; TPolys[N].Y2:=TPolys[N].Y2+RotCentY; TPolys[N].Z2:=TPolys[N].Z2+RotCentZ; TPolys[N].X3:=TPolys[N].X3+RotCentX; TPolys[N].Y3:=TPolys[N].Y3+RotCentY; TPolys[N].Z3:=TPolys[N].Z3+RotCentZ; TPolys[N].X4:=TPolys[N].X4+RotCentX; TPolys[N].Y4:=TPolys[N].Y4+RotCentY; TPolys[N].Z4:=TPolys[N].Z4+RotCentZ; TPolys[N].Xc:=TPolys[N].Xc+RotCentX; TPolys[N].Yc:=TPolys[N].Yc+RotCentY; TPolys[N].Zc:=TPolys[N].Zc+RotCentZ; End; Procedure CopySolid(Origin:Solid3d;Var Dest:Solid3d;StartPoly:Word); { This procedure copys the Origin solid to the Dest solid, copying the polygons belonging Origin to the polygons array (as every polygon must be stored there), from StartPoly } Var A:Byte; Begin Move(Origin,Dest,SizeOf(Origin)); For A:=StartPoly To (StartPoly+Origin.NumPolys-1) Do Begin Move(Polygons[A-StartPoly+1],Polygons[A],SizeOf(Poly3d)); Dest.Polys[A-StartPoly+1]:=A; End; End; Procedure TranslatePoly(Var P:Poly3d;Xt,Yt,Zt:Real); { This translates a polygon } Begin P.X1:=P.X1+Xt; P.Y1:=P.Y1+Yt; P.Z1:=P.Z1+Zt; P.X2:=P.X2+Xt; P.Y2:=P.Y2+Yt; P.Z2:=P.Z2+Zt; P.X3:=P.X3+Xt; P.Y3:=P.Y3+Yt; P.Z3:=P.Z3+Zt; P.X4:=P.X4+Xt; P.Y4:=P.Y4+Yt; P.Z4:=P.Z4+Zt; P.Xc:=P.Xc+Xt; P.Yc:=P.Yc+Yt; P.Zc:=P.Zc+Zt; End; Procedure TranslateSolid(Var S:Solid3d;Xt,Yt,Zt:Real); { This translates the solid } Var A:Byte; Begin For A:=1 To S.NumPolys Do TranslatePoly(Polygons[S.Polys[A]],Xt,Yt,Zt); S.Xc:=S.Xc+Xt; S.Yc:=S.Yc+Yt; S.Zc:=S.Zc+Zt; End; Procedure ScaleSolid(Var S:Solid3d;Xt,Yt,Zt:Real); { This scales a solid } Var A:Byte; Xs,Ys,Zs:Real; Begin Xs:=S.Xc; Ys:=S.Yc; Zs:=S.Zc; For A:=1 To S.NumPolys Do Begin With Polygons[S.Polys[A]] Do Begin X1:=((X1-Xs)*Xt)+Xs; Y1:=((Y1-Ys)*Yt)+Ys; Z1:=((Z1-Zs)*Zt)+Zs; X2:=((X2-Xs)*Xt)+Xs; Y2:=((Y2-Ys)*Yt)+Ys; Z2:=((Z2-Zs)*Zt)+Zs; X3:=((X3-Xs)*Xt)+Xs; Y3:=((Y3-Ys)*Yt)+Ys; Z3:=((Z3-Zs)*Zt)+Zs; X4:=((X4-Xs)*Xt)+Xs; Y4:=((Y4-Ys)*Yt)+Ys; Z4:=((Z4-Zs)*Zt)+Zs; Xc:=((Xc-Xs)*Xt)+Xs; Yc:=((Yc-Ys)*Yt)+Ys; Zc:=((Zc-Zs)*Zt)+Zs; End; End; End; Procedure InitCubes; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the first cube } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,6); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,5); { Define the main cube solid } Cube1.NumPolys:=6; For A:=1 To 6 Do Cube1.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube1.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube1.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube1.Zc:=Average/6; Cube1.RotX:=0; Cube1.RotY:=0; Cube1.RotZ:=0; { Create the other two cubes } CopySolid(Cube1,Cube2,7); TranslateSolid(Cube2,100,0,0); ScaleSolid(Cube2,0.3,0.3,0.3); CopySolid(Cube1,Cube3,13); TranslateSolid(Cube3,-200,0,300); XInc:=10; { Initialize the rotation array } Move(Polygons,TPolys,SizeOf(Polygons)); End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,P.Color,Where); End; Procedure DrawPolys(Where:Word); Var A:Word; Begin For A:=MaxPolys DownTo 1 Do Draw3dPoly(TPolys[Order[A]],Where); End; Procedure RotateSolid(Var S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; S.RotX:=S.RotX+XAng; If S.RotX>360 Then S.RotX:=S.RotX-360; If S.RotX<-360 Then S.RotX:=S.RotX+360; S.RotY:=S.RotY+YAng; If S.RotY>360 Then S.RotY:=S.RotY-360; If S.RotY<-360 Then S.RotY:=S.RotY+360; S.RotZ:=S.RotZ+ZAng; If S.RotZ>360 Then S.RotZ:=S.RotZ-360; If S.RotZ<-360 Then S.RotZ:=S.RotZ+360; { Put accurate values in the rotation array } For A:=1 To S.NumPolys Do Begin Move(Polygons[S.Polys[A]],TPolys[S.Polys[A]],SizeOf(Poly3d)); Rotate3dPoly(S.Polys[A],S.RotX,S.RotY,S.RotZ); End; End; Procedure RotateSolidP(Var S:Solid3d; X,Y,Z:Real; XAng,YAng,ZAng:Integer); { This is similar to the RotateSolid procedure. The only diferences is that this one allows you to specify the rotation center, and the rotation is not stored in the rotation variables. } Var A:Word; Begin RotCentX:=X; RotCentY:=Y; RotCentZ:=Z; S.RotX:=S.RotX+XAng; If S.RotX>360 Then S.RotX:=S.RotX-360; If S.RotX<-360 Then S.RotX:=S.RotX+360; S.RotY:=S.RotY+YAng; If S.RotY>360 Then S.RotY:=S.RotY-360; If S.RotY<-360 Then S.RotY:=S.RotY+360; S.RotZ:=S.RotZ+ZAng; If S.RotZ>360 Then S.RotZ:=S.RotZ-360; If S.RotZ<-360 Then S.RotZ:=S.RotZ+360; { Put accurate values in the rotation array } For A:=1 To S.NumPolys Do Begin Move(Polygons[S.Polys[A]],TPolys[S.Polys[A]],SizeOf(Poly3d)); Rotate3dPoly(S.Polys[A],S.RotX,S.RotY,S.RotZ); End; End; Procedure SortPolys; Var Flag:Boolean; I,J:Integer; X:Integer; N:Real; T:Real; Procedure SortSubArray(Left,Right:Byte); Begin { Partition } I:=Left; J:=Right; N:=TPolys[Order[(Left+Right) Div 2]].Zc; Repeat { Find first number from the left to be < N } While Tpolys[Order[I]].Zc N } While Tpolys[Order[J]].Zc>N Do Dec(J); { Exchange } If I<=J Then Begin X:=Order[J]; Order[J]:=Order[I]; Order[I]:=X; Inc(I); Dec(J); End; Until J200) Or (Cube3.Xc<-200) Then XInc:=-Xinc; Until Keypressed; ClearTables; CloseGraph; End. You probably noted that the procedures were changed (some of them were changed a lot), but the changes are easy to understand, since they were made to provide an easier manipulation of the solids. This example uses only face sorting... This is a bit slow, and that is due to the usage of real numbers, instead of integers. In next issue, I'll teach you how to convert this code to fast code, without even using assembler ! Well, I think I've finally finished this article !! Thank God... In the overall, it took 10:30 hours to be finished... Hope it is worth it... At least you should be able to start thinkering with a 3d system... Next issue, I'll probably delve into texture-mapping... Don't miss it ! :) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 5. Lights, please ! This article is about adding some lights to a 3d scene... We'll discuss plain light sourcing (also known as Lambert shading). You should read article 4 in this issue, the part of vector maths to understand this better. In this type of shading, a polygon only has one color (not too real, but this is just an introduction 3d shading). Picture a piece of metal... When you rotated around one axis, it becomes brigther when he's facing the Sun, or a lamp, and darker otherwise. I'll do an ASCII to explain you better what I'm heading you for: | | B / | / | ------- <----- Direction of light and viewpoint | A | | | C| |____________________|______ It is obvious to see that C will be brigther than B, that will be brigther than A (that doesn't even get displayed !). Now, check out the following graphic... It's the same display, but the normals to the polygons are also displayed: | | | B / | | /\ | ------- \ <----- Direction of light and viewpoint | A | | | C|-- |____________________|______ The angles between the normals to polygons A, B and C, and the direction of light are respectivelly 90, 225 and 180 degrees. From all that was said before, the intensity of light is greatest when the angle between the normal and the viewpoint is 180. In the angles that range from 0 to 90 and 270 to 360, there is no light falling on them, so they are black. We can use the dot product (and some other operations) to get the cosine of the angle between two vectors. We don't even need the angle itself... With some clever maths (not so clever, but...) we use just the cosine. And because the normal and the ligth vector are unitary, we just need the dot product. Notice that the cosine of the angle between the light vector and the displayable polygons (those who make an angle between 90 to 270) is always negative... Check that when you do the code... Now, if we set the first 16 colors of the palette to be tones of any color (I'll use greys in the first example), the code necessary to know the color of a grey polygon will be: CosineOfAngle:=(Light.X*Poly.Xn)+(Light.Y*Poly.Yn)+(Light.Z*Poly.Zn); If CosineOfAngle>=0 Then Color:=0 Else Color:=-Round(CosineOfAngle*16); This works because of the proprieties of the cosine (it is never larger than 1). We must negate the color because the cosines of the displayable polygons are always negative. This is pseudo-code. Light is a record with three fields which indicate a vector with the light. Notice that in this kind of lighting, the true position of the lightsource doesn't matter. It just matters it's orientation and direction. So, let's do a bit of coding... This is one of the examples of article 4, rewriten to include the lightsourcing. It is actually the example of backface removal. In the beggining, the ligth will be fixed, and it will point towards the lower left corner of the screen, facing into the screen. Then, after you press a key, the cube stops rotating and the ligthsource will 'rotate' around the cube. In that part, a small graphic will appear in the lower left corner, showing the position of the light. The yellow dot in the center represents the cube. The Color field in the Poly3d structure is ignored in this example (but not in the next...). Program TestLambert; Uses Mode13h,Crt; Const MaxPolys=6; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Xn,Yn,Zn:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Cube:Solid3d; RotCentX,RotCentY,RotCentZ:Real; Light:Record X,Y,Z:Real; End; A,Angle:Integer; C:Char; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Var Module:Real; Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; { Calc the normals } Polygons[N].Xn:=(Z2-Z1)*(Y3-Y1)-(Y2-Y1)*(Z3-Z1); Polygons[N].Yn:=(Z2-Z1)*(X3-X1)-(X2-X1)*(Z3-Z1); Polygons[N].Zn:=(Y2-Y1)*(X3-X1)-(X2-X1)*(Y3-Y1); { Normalize the normals } Module:=Sqrt(Sqr(Polygons[N].Xn)+Sqr(Polygons[N].Yn)+ Sqr(Polygons[N].Zn)); Polygons[N].Xn:=Polygons[N].Xn/Module; Polygons[N].Yn:=Polygons[N].Yn/Module; Polygons[N].Zn:=Polygons[N].Zn/Module; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); { This rotates around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point, and the normal of the polygon. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-RotCentX; P.Y1:=P.Y1-RotCentY; P.Z1:=P.Z1-RotCentZ; P.X2:=P.X2-RotCentX; P.Y2:=P.Y2-RotCentY; P.Z2:=P.Z2-RotCentZ; P.X3:=P.X3-RotCentX; P.Y3:=P.Y3-RotCentY; P.Z3:=P.Z3-RotCentZ; P.X4:=P.X4-RotCentX; P.Y4:=P.Y4-RotCentY; P.Z4:=P.Z4-RotCentZ; P.Xc:=P.Xc-RotCentX; P.Yc:=P.Yc-RotCentY; P.Zc:=P.Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zc; P.Zc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zn; P.Zn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Zc*Sines^[Angle]; P.Zc:=P.Zc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Zn*Sines^[Angle]; P.Zn:=P.Zn*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+RotCentX; P.Y1:=P.Y1+RotCentY; P.Z1:=P.Z1+RotCentZ; P.X2:=P.X2+RotCentX; P.Y2:=P.Y2+RotCentY; P.Z2:=P.Z2+RotCentZ; P.X3:=P.X3+RotCentX; P.Y3:=P.Y3+RotCentY; P.Z3:=P.Z3+RotCentZ; P.X4:=P.X4+RotCentX; P.Y4:=P.Y4+RotCentY; P.Z4:=P.Z4+RotCentZ; P.Xc:=P.Xc+RotCentX; P.Yc:=P.Yc+RotCentY; P.Zc:=P.Zc+RotCentZ; End; Procedure InitCube; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,6); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,5); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; CosAngle:Real; C:Byte; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); { Find out the color } CosAngle:=(Light.X*P.Xn)+(Light.Y*P.Yn)+(Light.Z*P.Zn); If CosAngle>0 Then C:=1 Else C:=-Trunc(CosAngle*15)+1; FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,C,Where); End; Procedure DrawSolid(S:Solid3d;Where:Word); Var A:Word; Begin For A:=1 To S.NumPolys Do { Draw if the normal is negative } If (Polygons[S.Polys[A]].Zn<0) Then Draw3dPoly(Polygons[S.Polys[A]],Where); End; Procedure RotateSolid(S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; For A:=1 To S.NumPolys Do Rotate3dPoly(Polygons[S.Polys[A]],XAng,YAng,ZAng); End; Begin InitGraph; InitVirt; InitTables; { Set the greyscale... This must be done carefully, or else the display will be very ugly. } SetColor(0,0,0,0); For A:=1 To 8 Do SetColor(A,18+A*2,18+A*2,18+A*2); For A:=9 To 15 Do SetColor(A,A*4,A*4,A*4); SetColor(16,63,0,0); SetColor(17,63,63,0); Cls(0,VGA); InitCube; { First part, fixed light } Light.X:=-0.58; Light.Y:=0.58; Light.Z:=0.58; Repeat Cls(0,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); RotateSolid(Cube,5,5,5); Until Keypressed; { Second part, moving light } C:=ReadKey; Light.Y:=0.0; Angle:=0; Repeat Cls(0,Vp[1]); { Calc the light } Light.X:=Cosines^[Angle]; Light.Z:=Sines^[Angle]; Inc(Angle,5); If Angle>360 Then Dec(Angle,360); { Draw the graphic in the lower left corner } PutPixel(30,170,17,Vp[1]); PutPixel(30+Round(-Light.X*20),170+Round(Light.Z*20), 16,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); Until Keypressed; ClearTables; CloseGraph; End. Now, I'll play some more with the concept of flat-shading. Now, let's add the color factor. Now, the Color field in the Poly3d structure defines the base color. That value is added to the Color finded with the algorithm we've used to give the correct colors. Now, we just have to adjust the palette correctly to give the impression of several colors. The next example does this. It is a variation of the last example in article 4. As in the previous examples, we have two parts... The first with the fixed light, and the in the other the light turns around with the orbiting cube). Program TestLambert; Uses Mode13h,Crt; Const MaxPolys=18; { For 3 cubes } Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Xn,Yn,Zn:Real; Color:Byte; End; Type Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; RotX,RotY,RotZ:Integer; { The orientation of the solid } End; Var Polygons:Array[1..MaxPolys] Of Poly3d; { The accurate polys } TPolys:Array[1..MaxPolys] Of Poly3d; { The inaccurate ones } RotCentX,RotCentY,RotCentZ:Real; Order:Array[1..MaxPolys] Of Word; Light:Record X,Y,Z:Real; End; Var Cube1,Cube2,Cube3:Solid3d; XInc:Integer; C:Char; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Var Module:Real; Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; { Calc the normals } Polygons[N].Xn:=(Z2-Z1)*(Y3-Y1)-(Y2-Y1)*(Z3-Z1); Polygons[N].Yn:=(Z2-Z1)*(X3-X1)-(X2-X1)*(Z3-Z1); Polygons[N].Zn:=(Y2-Y1)*(X3-X1)-(X2-X1)*(Y3-Y1); { Normalize the normals } Module:=Sqrt(Sqr(Polygons[N].Xn)+Sqr(Polygons[N].Yn)+ Sqr(Polygons[N].Zn)); Polygons[N].Xn:=Polygons[N].Xn/Module; Polygons[N].Yn:=Polygons[N].Yn/Module; Polygons[N].Zn:=Polygons[N].Zn/Module; End; Procedure Rotate3dPoly(N:Word;XAng,YAng,ZAng:Integer); { This rotates polygon N around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point, and the normal of the polygon. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } TPolys[N].X1:=TPolys[N].X1-RotCentX; TPolys[N].Y1:=TPolys[N].Y1-RotCentY; TPolys[N].Z1:=TPolys[N].Z1-RotCentZ; TPolys[N].X2:=TPolys[N].X2-RotCentX; TPolys[N].Y2:=TPolys[N].Y2-RotCentY; TPolys[N].Z2:=TPolys[N].Z2-RotCentZ; TPolys[N].X3:=TPolys[N].X3-RotCentX; TPolys[N].Y3:=TPolys[N].Y3-RotCentY; TPolys[N].Z3:=TPolys[N].Z3-RotCentZ; TPolys[N].X4:=TPolys[N].X4-RotCentX; TPolys[N].Y4:=TPolys[N].Y4-RotCentY; TPolys[N].Z4:=TPolys[N].Z4-RotCentZ; TPolys[N].Xc:=TPolys[N].Xc-RotCentX; TPolys[N].Yc:=TPolys[N].Yc-RotCentY; TPolys[N].Zc:=TPolys[N].Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=TPolys[N].X1; TPolys[N].X1:=Temp*Cosines^[Angle]-TPolys[N].Y1*Sines^[Angle]; TPolys[N].Y1:=TPolys[N].Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X2; TPolys[N].X2:=Temp*Cosines^[Angle]-TPolys[N].Y2*Sines^[Angle]; TPolys[N].Y2:=TPolys[N].Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X3; TPolys[N].X3:=Temp*Cosines^[Angle]-TPolys[N].Y3*Sines^[Angle]; TPolys[N].Y3:=TPolys[N].Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X4; TPolys[N].X4:=Temp*Cosines^[Angle]-TPolys[N].Y4*Sines^[Angle]; TPolys[N].Y4:=TPolys[N].Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xc; TPolys[N].Xc:=Temp*Cosines^[Angle]-TPolys[N].Yc*Sines^[Angle]; TPolys[N].Yc:=TPolys[N].Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xn; TPolys[N].Xn:=Temp*Cosines^[Angle]-TPolys[N].Yn*Sines^[Angle]; TPolys[N].Yn:=TPolys[N].Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=TPolys[N].Z1; TPolys[N].Z1:=Temp*Cosines^[Angle]-TPolys[N].Y1*Sines^[Angle]; TPolys[N].Y1:=TPolys[N].Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z2; TPolys[N].Z2:=Temp*Cosines^[Angle]-TPolys[N].Y2*Sines^[Angle]; TPolys[N].Y2:=TPolys[N].Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z3; TPolys[N].Z3:=Temp*Cosines^[Angle]-TPolys[N].Y3*Sines^[Angle]; TPolys[N].Y3:=TPolys[N].Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Z4; TPolys[N].Z4:=Temp*Cosines^[Angle]-TPolys[N].Y4*Sines^[Angle]; TPolys[N].Y4:=TPolys[N].Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Zc; TPolys[N].Zc:=Temp*Cosines^[Angle]-TPolys[N].Yc*Sines^[Angle]; TPolys[N].Yc:=TPolys[N].Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Zn; TPolys[N].Zn:=Temp*Cosines^[Angle]-TPolys[N].Yn*Sines^[Angle]; TPolys[N].Yn:=TPolys[N].Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=TPolys[N].X1; TPolys[N].X1:=Temp*Cosines^[Angle]-TPolys[N].Z1*Sines^[Angle]; TPolys[N].Z1:=TPolys[N].Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X2; TPolys[N].X2:=Temp*Cosines^[Angle]-TPolys[N].Z2*Sines^[Angle]; TPolys[N].Z2:=TPolys[N].Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X3; TPolys[N].X3:=Temp*Cosines^[Angle]-TPolys[N].Z3*Sines^[Angle]; TPolys[N].Z3:=TPolys[N].Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].X4; TPolys[N].X4:=Temp*Cosines^[Angle]-TPolys[N].Z4*Sines^[Angle]; TPolys[N].Z4:=TPolys[N].Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xc; TPolys[N].Xc:=Temp*Cosines^[Angle]-TPolys[N].Zc*Sines^[Angle]; TPolys[N].Zc:=TPolys[N].Zc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=TPolys[N].Xn; TPolys[N].Xn:=Temp*Cosines^[Angle]-TPolys[N].Zn*Sines^[Angle]; TPolys[N].Zn:=TPolys[N].Zn*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } TPolys[N].X1:=TPolys[N].X1+RotCentX; TPolys[N].Y1:=TPolys[N].Y1+RotCentY; TPolys[N].Z1:=TPolys[N].Z1+RotCentZ; TPolys[N].X2:=TPolys[N].X2+RotCentX; TPolys[N].Y2:=TPolys[N].Y2+RotCentY; TPolys[N].Z2:=TPolys[N].Z2+RotCentZ; TPolys[N].X3:=TPolys[N].X3+RotCentX; TPolys[N].Y3:=TPolys[N].Y3+RotCentY; TPolys[N].Z3:=TPolys[N].Z3+RotCentZ; TPolys[N].X4:=TPolys[N].X4+RotCentX; TPolys[N].Y4:=TPolys[N].Y4+RotCentY; TPolys[N].Z4:=TPolys[N].Z4+RotCentZ; TPolys[N].Xc:=TPolys[N].Xc+RotCentX; TPolys[N].Yc:=TPolys[N].Yc+RotCentY; TPolys[N].Zc:=TPolys[N].Zc+RotCentZ; End; Procedure CopySolid(Origin:Solid3d;Var Dest:Solid3d;StartPoly:Word); { This procedure copys the Origin solid to the Dest solid, copying the polygons belonging Origin to the polygons array (as every polygon must be stored there), from StartPoly } Var A:Byte; Begin Move(Origin,Dest,SizeOf(Origin)); For A:=StartPoly To (StartPoly+Origin.NumPolys-1) Do Begin Move(Polygons[A-StartPoly+1],Polygons[A],SizeOf(Poly3d)); Dest.Polys[A-StartPoly+1]:=A; End; End; Procedure TranslatePoly(Var P:Poly3d;Xt,Yt,Zt:Real); { This translates a polygon } Begin P.X1:=P.X1+Xt; P.Y1:=P.Y1+Yt; P.Z1:=P.Z1+Zt; P.X2:=P.X2+Xt; P.Y2:=P.Y2+Yt; P.Z2:=P.Z2+Zt; P.X3:=P.X3+Xt; P.Y3:=P.Y3+Yt; P.Z3:=P.Z3+Zt; P.X4:=P.X4+Xt; P.Y4:=P.Y4+Yt; P.Z4:=P.Z4+Zt; P.Xc:=P.Xc+Xt; P.Yc:=P.Yc+Yt; P.Zc:=P.Zc+Zt; End; Procedure TranslateSolid(Var S:Solid3d;Xt,Yt,Zt:Real); { This translates the solid } Var A:Byte; Begin For A:=1 To S.NumPolys Do TranslatePoly(Polygons[S.Polys[A]],Xt,Yt,Zt); S.Xc:=S.Xc+Xt; S.Yc:=S.Yc+Yt; S.Zc:=S.Zc+Zt; End; Procedure ScaleSolid(Var S:Solid3d;Xt,Yt,Zt:Real); { This scales a solid } Var A:Byte; Xs,Ys,Zs:Real; Begin Xs:=S.Xc; Ys:=S.Yc; Zs:=S.Zc; For A:=1 To S.NumPolys Do Begin With Polygons[S.Polys[A]] Do Begin X1:=((X1-Xs)*Xt)+Xs; Y1:=((Y1-Ys)*Yt)+Ys; Z1:=((Z1-Zs)*Zt)+Zs; X2:=((X2-Xs)*Xt)+Xs; Y2:=((Y2-Ys)*Yt)+Ys; Z2:=((Z2-Zs)*Zt)+Zs; X3:=((X3-Xs)*Xt)+Xs; Y3:=((Y3-Ys)*Yt)+Ys; Z3:=((Z3-Zs)*Zt)+Zs; X4:=((X4-Xs)*Xt)+Xs; Y4:=((Y4-Ys)*Yt)+Ys; Z4:=((Z4-Zs)*Zt)+Zs; Xc:=((Xc-Xs)*Xt)+Xs; Yc:=((Yc-Ys)*Yt)+Ys; Zc:=((Zc-Zs)*Zt)+Zs; End; End; End; Procedure InitCubes; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the first cube } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,17); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,33); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,49); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,65); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,81); { Define the main cube solid } Cube1.NumPolys:=6; For A:=1 To 6 Do Cube1.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube1.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube1.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube1.Zc:=Average/6; Cube1.RotX:=0; Cube1.RotY:=0; Cube1.RotZ:=0; { Create the other two cubes } CopySolid(Cube1,Cube2,7); TranslateSolid(Cube2,100,0,0); ScaleSolid(Cube2,0.3,0.3,0.3); CopySolid(Cube1,Cube3,13); TranslateSolid(Cube3,-200,0,300); XInc:=10; { Initialize the rotation array } Move(Polygons,TPolys,SizeOf(Polygons)); End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; CosAngle:Real; C:Byte; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); CosAngle:=(Light.X*P.Xn)+(Light.Y*P.Yn)+(Light.Z*P.Zn); If CosAngle>0 Then C:=P.Color Else C:=P.Color+Trunc(-CosAngle*15); FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,C,Where); End; Procedure DrawPolys(Where:Word); Var A:Word; Begin For A:=MaxPolys DownTo 1 Do Draw3dPoly(TPolys[Order[A]],Where); End; Procedure RotateSolid(Var S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; S.RotX:=S.RotX+XAng; If S.RotX>360 Then S.RotX:=S.RotX-360; If S.RotX<-360 Then S.RotX:=S.RotX+360; S.RotY:=S.RotY+YAng; If S.RotY>360 Then S.RotY:=S.RotY-360; If S.RotY<-360 Then S.RotY:=S.RotY+360; S.RotZ:=S.RotZ+ZAng; If S.RotZ>360 Then S.RotZ:=S.RotZ-360; If S.RotZ<-360 Then S.RotZ:=S.RotZ+360; { Put accurate values in the rotation array } For A:=1 To S.NumPolys Do Begin Move(Polygons[S.Polys[A]],TPolys[S.Polys[A]],SizeOf(Poly3d)); Rotate3dPoly(S.Polys[A],S.RotX,S.RotY,S.RotZ); End; End; Procedure RotateSolidP(Var S:Solid3d; X,Y,Z:Real; XAng,YAng,ZAng:Integer); { This is similar to the RotateSolid procedure. The only diferences is that this one allows you to specify the rotation center, and the rotation is not stored in the rotation variables. } Var A:Word; Begin RotCentX:=X; RotCentY:=Y; RotCentZ:=Z; S.RotX:=S.RotX+XAng; If S.RotX>360 Then S.RotX:=S.RotX-360; If S.RotX<-360 Then S.RotX:=S.RotX+360; S.RotY:=S.RotY+YAng; If S.RotY>360 Then S.RotY:=S.RotY-360; If S.RotY<-360 Then S.RotY:=S.RotY+360; S.RotZ:=S.RotZ+ZAng; If S.RotZ>360 Then S.RotZ:=S.RotZ-360; If S.RotZ<-360 Then S.RotZ:=S.RotZ+360; { Put accurate values in the rotation array } For A:=1 To S.NumPolys Do Begin Move(Polygons[S.Polys[A]],TPolys[S.Polys[A]],SizeOf(Poly3d)); Rotate3dPoly(S.Polys[A],S.RotX,S.RotY,S.RotZ); End; End; Procedure SortPolys; Var Flag:Boolean; I,J:Integer; X:Integer; N:Real; T:Real; Procedure SortSubArray(Left,Right:Byte); Begin { Partition } I:=Left; J:=Right; N:=TPolys[Order[(Left+Right) Div 2]].Zc; Repeat { Find first number from the left to be < N } While Tpolys[Order[I]].Zc N } While Tpolys[Order[J]].Zc>N Do Dec(J); { Exchange } If I<=J Then Begin X:=Order[J]; Order[J]:=Order[I]; Order[I]:=X; Inc(I); Dec(J); End; Until J200) Or (Cube3.Xc<-200) Then XInc:=-Xinc; Until Keypressed; C:=ReadKey; Light.Y:=0; Repeat Cls(0,Vp[1]); RotateSolid(Cube1,2,3,4); { Rotate Cube 2 around Cube 1 } RotateSolidP(Cube2,Cube1.Xc,Cube1.Yc,Cube1.Zc,0,5,0); Light.X:=-Cosines^[Cube2.RotY]; Light.Z:=-Sines^[Cube2.RotY]; { Display light direction } PutPixel(30,170,17,Vp[1]); PutPixel(30+Round(-Light.X*20),170+Round(Light.Z*20), 16,Vp[1]); { Move Cube 3 in ping-pong fashion } RotateSolid(Cube3,4,6,5); TranslateSolid(Cube3,XInc,0,0); If (Cube3.Xc>200) Or (Cube3.Xc<-200) Then XInc:=-Xinc; SortPolys; DrawPolys(Vp[1]); CopyPage(Vp[1],VGA); Until Keypressed; ClearTables; CloseGraph; End. This is fairly easy... Just one more thing before I wrap up this article: double lightsources ! Well, if you have two light sources, you just have to sum their effects (making sure they do not cross a threshould). This is a short example. This is the first example, but with two lights. When the polygon gets light cyan, it means that the the light is very intense (the combination of the two... One light alone can never get the polygon cyan). Program TestLambertWithTwoLights; Uses Mode13h,Crt; Const MaxPolys=6; Type Poly3d=Record X1,Y1,Z1, X2,Y2,Z2, X3,Y3,Z3, X4,Y4,Z4, Xc,Yc,Zc:Real; Xn,Yn,Zn:Real; Color:Byte; End; Solid3d=Record NumPolys:Word; Polys:Array[1..MaxPolys] Of Word; Xc,Yc,Zc:Real; End; Light=Record X,Y,Z:Real; End; Var Polygons:Array[1..MaxPolys] Of Poly3d; Cube:Solid3d; RotCentX,RotCentY,RotCentZ:Real; Light1,Light2:Light; A,Angle:Integer; C:Char; Procedure Gen3dPoly(N:Word; X1,Y1,Z1,X2,Y2,Z2, X3,Y3,Z3,X4,Y4,Z4:Real; C:Byte); Var Module:Real; Begin Polygons[N].X1:=X1; Polygons[N].Y1:=Y1; Polygons[N].Z1:=Z1; Polygons[N].X2:=X2; Polygons[N].Y2:=Y2; Polygons[N].Z2:=Z2; Polygons[N].X3:=X3; Polygons[N].Y3:=Y3; Polygons[N].Z3:=Z3; Polygons[N].X4:=X4; Polygons[N].Y4:=Y4; Polygons[N].Z4:=Z4; Polygons[N].Xc:=(X1+X2+X3+X4)/4; Polygons[N].Yc:=(Y1+Y2+Y3+Y4)/4; Polygons[N].Zc:=(Z1+Z2+Z3+Z4)/4; Polygons[N].Color:=C; { Calc the normals } Polygons[N].Xn:=(Z2-Z1)*(Y3-Y1)-(Y2-Y1)*(Z3-Z1); Polygons[N].Yn:=(Z2-Z1)*(X3-X1)-(X2-X1)*(Z3-Z1); Polygons[N].Zn:=(Y2-Y1)*(X3-X1)-(X2-X1)*(Y3-Y1); { Normalize the normals } Module:=Sqrt(Sqr(Polygons[N].Xn)+Sqr(Polygons[N].Yn)+ Sqr(Polygons[N].Zn)); Polygons[N].Xn:=Polygons[N].Xn/Module; Polygons[N].Yn:=Polygons[N].Yn/Module; Polygons[N].Zn:=Polygons[N].Zn/Module; End; Procedure Rotate3dPoly(Var P:Poly3d;XAng,YAng,ZAng:Integer); { This rotates around a certain point, specified by the variables RotCentX, RotCentY and RotCentZ. We also rotate the center of the polygons, that change in order to the rotation point, and the normal of the polygon. } Var Angle:Integer; Temp:Real; Begin { Transform negative angles into positive ones } If XAng<0 Then XAng:=XAng+360; If YAng<0 Then YAng:=YAng+360; If ZAng<0 Then ZAng:=ZAng+360; { Make the coordinates of the points relative to the center } P.X1:=P.X1-RotCentX; P.Y1:=P.Y1-RotCentY; P.Z1:=P.Z1-RotCentZ; P.X2:=P.X2-RotCentX; P.Y2:=P.Y2-RotCentY; P.Z2:=P.Z2-RotCentZ; P.X3:=P.X3-RotCentX; P.Y3:=P.Y3-RotCentY; P.Z3:=P.Z3-RotCentZ; P.X4:=P.X4-RotCentX; P.Y4:=P.Y4-RotCentY; P.Z4:=P.Z4-RotCentZ; P.Xc:=P.Xc-RotCentX; P.Yc:=P.Yc-RotCentY; P.Zc:=P.Zc-RotCentZ; { Rotate all points of poly around Z axis } Angle:=ZAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around X axis } Angle:=XAng; Temp:=P.Z1; P.Z1:=Temp*Cosines^[Angle]-P.Y1*Sines^[Angle]; P.Y1:=P.Y1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z2; P.Z2:=Temp*Cosines^[Angle]-P.Y2*Sines^[Angle]; P.Y2:=P.Y2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z3; P.Z3:=Temp*Cosines^[Angle]-P.Y3*Sines^[Angle]; P.Y3:=P.Y3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Z4; P.Z4:=Temp*Cosines^[Angle]-P.Y4*Sines^[Angle]; P.Y4:=P.Y4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zc; P.Zc:=Temp*Cosines^[Angle]-P.Yc*Sines^[Angle]; P.Yc:=P.Yc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Zn; P.Zn:=Temp*Cosines^[Angle]-P.Yn*Sines^[Angle]; P.Yn:=P.Yn*Cosines^[Angle]+Temp*Sines^[Angle]; { Rotate all points of poly around Y axis } Angle:=YAng; Temp:=P.X1; P.X1:=Temp*Cosines^[Angle]-P.Z1*Sines^[Angle]; P.Z1:=P.Z1*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X2; P.X2:=Temp*Cosines^[Angle]-P.Z2*Sines^[Angle]; P.Z2:=P.Z2*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X3; P.X3:=Temp*Cosines^[Angle]-P.Z3*Sines^[Angle]; P.Z3:=P.Z3*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.X4; P.X4:=Temp*Cosines^[Angle]-P.Z4*Sines^[Angle]; P.Z4:=P.Z4*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xc; P.Xc:=Temp*Cosines^[Angle]-P.Zc*Sines^[Angle]; P.Zc:=P.Zc*Cosines^[Angle]+Temp*Sines^[Angle]; Temp:=P.Xn; P.Xn:=Temp*Cosines^[Angle]-P.Zn*Sines^[Angle]; P.Zn:=P.Zn*Cosines^[Angle]+Temp*Sines^[Angle]; { Transform the coordinates again } P.X1:=P.X1+RotCentX; P.Y1:=P.Y1+RotCentY; P.Z1:=P.Z1+RotCentZ; P.X2:=P.X2+RotCentX; P.Y2:=P.Y2+RotCentY; P.Z2:=P.Z2+RotCentZ; P.X3:=P.X3+RotCentX; P.Y3:=P.Y3+RotCentY; P.Z3:=P.Z3+RotCentZ; P.X4:=P.X4+RotCentX; P.Y4:=P.Y4+RotCentY; P.Z4:=P.Z4+RotCentZ; P.Xc:=P.Xc+RotCentX; P.Yc:=P.Yc+RotCentY; P.Zc:=P.Zc+RotCentZ; End; Procedure InitCube; Var A:Byte; Average:Real; Begin { Define the polygons that will be part of the solid } Gen3dPoly(1,-30,-30,226,30,-30,226,30,30,226,-30,30,226,1); Gen3dPoly(2,30,-30,226,30,-30,286,30,30,286,30,30,226,2); Gen3dPoly(3,30,-30,286,-30,-30,286,-30,30,286,30,30,286,3); Gen3dPoly(4,-30,-30,286,-30,-30,226,-30,30,226,-30,30,286,4); Gen3dPoly(5,30,-30,286,-30,-30,286,-30,-30,226,30,-30,226,6); Gen3dPoly(6,-30,30,286,30,30,286,30,30,226,-30,30,226,5); { Define the cube solid } Cube.NumPolys:=6; For A:=1 To 6 Do Cube.Polys[A]:=A; { Calc the center point } Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Xc; Cube.Xc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Yc; Cube.Yc:=Average/6; Average:=0; For A:=1 To 6 Do Average:=Average+Polygons[A].Zc; Cube.Zc:=Average/6; End; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Draw3dPoly(P:Poly3d;Where:Word); Var Tx1,Tx2,Tx3,Tx4, Ty1,Ty2,Ty3,Ty4:Integer; CosAngle1,CosAngle2:Real; C:Byte; Begin Conv3d(P.X1,P.Y1,P.Z1,Tx1,Ty1); Conv3d(P.X2,P.Y2,P.Z2,Tx2,Ty2); Conv3d(P.X3,P.Y3,P.Z3,Tx3,Ty3); Conv3d(P.X4,P.Y4,P.Z4,Tx4,Ty4); { Find out the color } CosAngle1:=(Light1.X*P.Xn)+(Light1.Y*P.Yn)+(Light1.Z*P.Zn); CosAngle2:=(Light2.X*P.Xn)+(Light2.Y*P.Yn)+(Light2.Z*P.Zn); If CosAngle1>0 Then If CosAngle2>0 Then C:=1 Else C:=-Trunc(CosAngle2*15)+1 Else If CosAngle2>0 Then C:=-Trunc(CosAngle1*15)+1 Else C:=-Trunc(CosAngle1*15)-Trunc(CosAngle2*15)+1; If C>15 Then C:=18; FPoly(Tx1,Ty1,Tx2,Ty2,Tx3,Ty3,Tx4,Ty4,C,Where); End; Procedure DrawSolid(S:Solid3d;Where:Word); Var A:Word; Begin For A:=1 To S.NumPolys Do { Draw if the normal is negative } If (Polygons[S.Polys[A]].Zn<0) Then Draw3dPoly(Polygons[S.Polys[A]],Where); End; Procedure RotateSolid(S:Solid3d;XAng,YAng,ZAng:Integer); Var A:Word; Begin RotCentX:=S.Xc; RotCentY:=S.Yc; RotCentZ:=S.Zc; For A:=1 To S.NumPolys Do Rotate3dPoly(Polygons[S.Polys[A]],XAng,YAng,ZAng); End; Begin InitGraph; InitVirt; InitTables; { Set the greyscale... This must be done carefully, or else the display will be very ugly. } SetColor(0,0,0,0); For A:=1 To 8 Do SetColor(A,18+A*2,18+A*2,18+A*2); For A:=9 To 15 Do SetColor(A,A*4,A*4,A*4); SetColor(16,63,0,0); SetColor(17,63,63,0); SetColor(18,0,63,63); Cls(0,VGA); InitCube; { First part, fixed lights. The two lights are shining from oposite directions along the X axis } Light1.X:=-1; Light1.Y:=0; Light1.Z:=0; Light2.X:=1; Light2.Y:=0; Light2.Z:=0; Repeat Cls(0,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); RotateSolid(Cube,5,5,5); Until Keypressed; { Second part, one light moves, the other not } C:=ReadKey; Light1.Y:=0.0; Angle:=0; Repeat Cls(0,Vp[1]); { Calc the light } Light1.X:=Cosines^[Angle]; Light1.Z:=Sines^[Angle]; Inc(Angle,5); If Angle>360 Then Dec(Angle,360); { Draw the graphic in the lower left corner } PutPixel(30,170,17,Vp[1]); PutPixel(30+Round(-Light1.X*20),170+Round(Light1.Z*20), 16,Vp[1]); DrawSolid(Cube,Vp[1]); CopyPage(Vp[1],VGA); Until Keypressed; ClearTables; CloseGraph; End. Well, that's it for this article on flat-shading... This one took 2.5 hours to get ready... Now, I'll do the article on crossfading... If the Gods are on my side, tomorrow this issue is ready to go to everyone out there... It is just 4 months late !! :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 6. The optimization bible Hello, welcome to this article about optimization... In this article, I will teach a number of tricks to speed up the execution of your programs. Most of this tricks will only work properly in assembly, but don't let that stop you from using them in your programs. Notice that this isn't how to do nice little structured code... This is how to do fast code... This often colide... So, without further due, let's do it... :) (A) The powers of two are great for aritmetics in the computer. I already talked about the Shr/Shl trick in other issue, but I'll talk about it again now, and I'll extend the concept. In unsigned numbers, doing binary shifts left and right will multiply and divide by two the number, respectivelly. Example: 100 shl 1 will shift the binary description of 100 one bit to the left, giving us 200... Let's see this in binary: 50 = 00110010 50 shl 1 = 01100100 = 100 !!! 100 shl 1 = 50 shl 2 = 11001000 = 200 !!! See ? You can do the inverse (shift right): 200 = 11001000 200 shr 1 = 01100100 = 100 !!! 100 shr 1 = 200 shr 2 = 00110010 = 50 !!! Cool, isn't it... The rule is : X Shl N = X * 2^N X Shr N = X Div 2^N Don't forget that the division is integer! You can't use this trick with real numbers... If you use it in Pascal directly (i.e. A:=2 Shl 1;), you can use signed numbers (i.e. A:=-5 Shl 2). In assembly, for signed numbers, you must use Sar to shift right, because that preserves the sign bit. The Shl can be used, altough you should check the carry flag afterwards... If it is set, an overflow has ocurred, because the carry flag stores the last bit shifted out. Signed numbers in a binary computer are stored in a special format. For example, a signed byte is stored as eight bits, the leftmost one storing the sign (0=positive, 1=negative), and the remaining seven bits store the numbers... That's why the signed byte (shortint type) have a range from -128 to 127, while a unsigned byte (byte type) has a range from 0 to 255 (an extra bit in a numeric representation gives double the range). This isn't all you can do with the powers of two... Another neat trick to do is X Mod N, while N is a power of two. You can use the AND function and a little trick to do this VERY fast. For example: X Mod 2 = X And 00000001 = X And 1 X Mod 4 = X And 00000011 = X And 3 X Mod 8 = X And 00000111 = X And 7 X Mod 16 = X And 00001111 = X And 15 ......................... Got the ideia ? How this works ? Simple: If you use AND with normal numbers, you will get something that is sometimes refered as masking. With AND you can clear the bits you want from a number... Imagine the following number and operation: 50 = 00110010 3 = 00000011 50 And 3 = 00000010 = 2 = 50 Mod 4 ! See ? You clear the bits that don't matter to the operation. The X Mod 4 = X And 3 trick is very usefull in ModeX operations, so don't forget this one... (B) Use pregenerated arrays and tables as much as you can. If you can precalc all the values before you start, your program will be internally just made of memory reads and writes ! Of course that's an ideal situation, an impossible one, but that can speed up things like hell! Of course that it takes large amounts of memory to do so, but who cares ? Almost everytime there is the speed/memory tradeoff ! You'll have to see what is more important in your programs. In games and demos, it is the speed, so screw the memory ! :) (C) Think about your calculations over and over again... There is speed gains to be made in clever thinking. For example, a trick I will be using in to speed up the PutPixel procedure. At a certain point, you'll have to do the following multiplication: 320*Y. How can you speed up mults ? Using shift left... But 320 isn't a power of two... But if you think a bit, you notice that 320=64+256. So, the mult will look like this: (64+256)*Y that is equal to 64*Y+256*Y, that is equal Y Shl 6+Y Shl 8. So, we speeded up the calculations a bit... :) (D) Do the calcs only once. Imagine the following piece of a program: For A:=1 To 50 Do Begin For B:=1 To 100 Do Begin C:=A*10+B; WriteLn(C); End; End; This can be speeded up a bit by using an extra variable. Notice that the A*10 parcel of the calculus will only be changed after 100 changes in the B parcel. So, what you can use is this: For A:=1 To 50 Do Begin D:=A*10; For B:=1 To 100 Do Begin C:=D+B; WriteLn(C); End; End; In the original piece of code, you used 5000 mults. In the second piece, you use 50... That is what I call speeding ! :) (E) Cripple the code... By that, I mean that you can change the routine to be less flexible, but more efficient. For example, in a game I'm working on now, I have a routine that just draws a 30x30 sprites screen, given the offset. I do all the calculations regarding the position of the sprite using offsets because it is faster than calculating the offset everytime you need it. (F) Use fixed point maths... Floating point operations (i.e. those who use Pascal's Real type) are VERY slow, even with a FPU (Floating Point Unit). But, I hear you cry, I need the floating part of the number! What do I do with it ? Well, you keep it... I won't go too deep in the issue of fixed point maths, since a friend of mine wrote an whole article on the subject. That article will be released in issue 12. This is just enough to get you going... For example, take the number 9.5... This number has an integer part (9) and a fractional part (.5). So it is a real number. We have to convert this real number to integer, do the calculations with integers (it's many times faster) and then convert it back to real (or not... Depending on the aplication, you may choose to maintain the number in integer format). How do we convert the number ? Well, if you multiply the number by 10, you get the number 95. Now, let's multiply 95 by 2. You get 190. Now, let's divide that result by 10. You get 19, which is the right result for 9.5*2, but this was many times faster. If you multiply by 10, you get a 0.1 precision. That isn't enough for most purposes. And a multiplication by 10 is a slow operation even so. So, let's try another thing: Back to 9.5... Let's multiply the number by 256. We get 9.5*256=2432. Now, let's multiply that number by 3. We get 7296. Now, let's shift the bits of the number 8 steps to the right. We get 7296/256=28.5, that is equal to 9.5*3... But, although the operation itself is many times faster, the conversions aren't much. One way to get around this is never to convert back to real. Do all calculations with integer math. That include sines and cosines tables, that can be multiplied by 256 in the initialization stages, and you can use that value throughout the program. Usually you only need the integer part of the number in a game or demo, although you need to calc real stuff (like a rotation). Although you need an integer (x,y) pair of numbers in a rotation, you need to multiply by a sine or cosine, real numbers. If you convert those real numbers to integer, you get a BIG speed increase. Just one more thing... Why we use 256 in the multiplication. Well, that depends on the precision you want. With 256 you get something like a 0.004 precision. I use 256 because if you just want the integer part of a number (as most times you do), you just have to do a fast shift right 8 bits operation to get it. Much faster than a division by 256. Another thing... I think that Pentium processors can actually do floating point aritmetic faster than fixed point, because of the use of the FPU. But to use that in Pascal (or in ASM, for the matter of fact), you have to use some strange opcodes and stuff, and the programs wouldn't probably be compatible with 486 computers... But, if you want to learn more about this, read the newsgroups dedicated to hardware... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 7. How to do crossfading Crossfading is the effect of an image fading out while other fades in. It has been used countless in movies, altough it isn't much used in games and demos. The ideia of crossfading is to have two pictures, and fade in the palette of one while fading out the palette of another. Obviously enough, the two pictures must be on screen at the same time... And this is when the troubles begin !!! :) In this article we will cover two types of crossfading (there are more, but most of them use what is known as ModeX. From next issue forth I will start a tutorial on ModeX. Stay tuned...): 16 color crossfading, and 256 color crossfading. 7.1. 16 color crossfading To display a 16 color picture, we just need to use 4 bits (4^2=16). If you think that the normal VGA screen uses 1 byte (=8 bits) to store a picture, you quickly arrive to the conclusion that you can store two 16 color pictures on a normal VGA screen ! So, how do we do this ? Well, for this tutorial, I've done two pictures, each one with 16 colors and diferent palettes. They are called (originally) Pic1.Pcx and Pic2.Pcx. Notice that I've only used the lower 16 colors (0-15), even though the files are 256 color PCX. Now, to be perfectly honest, I'm so fed up with this issue that I'm just going to tell you how it is done, without much explanation. Study it to understand it... You can do it ! ;) So, now you have to load the two pictures to the virtual screens, saving the palettes in diferent variables. Then, you have to combine the screens in a way that one of them occupy the lower 4 bits and the other the upper 4 bits. Then, you readjust the palettes in this way. Palette of screen 1 (the one that occupies the lower bits): The 16 colors (0-15) must be copied to the other registers, like this: Color no. | Color value 0-15 | A B C D E F G H I J K L M N O P 16-31 | A B C D E F G H I J K L M N O P 32-47 | A B C D E F G H I J K L M N O P ........................................... 240-255 | A B C D E F G H I J K L M N O P Palette of screen 2 (the one that occupies the higher bits): It has color 0 occupying positions 0-15, color 2 occupying positions 16-31, etc, like this: Color no. | Color value 0-15 | A A A A A A A A A A A A A A A A 16-31 | B B B B B B B B B B B B B B B B 32-47 | C C C C C C C C C C C C C C C C ............................................ 240-255 | P P P P P P P P P P P P P P P P Now, all you have to do is to set palette 1, then fade it to palette 2, and then to palette 1 again, etc... Let's see the code: Program Crossfade16; Uses Mode13h,Crt; Var Pal1,Pal2:RgbList; Procedure InitAll; Var A,B,C,D:Word; Begin { Clear the VGA screen and put it's palette black } Cls(0,VGA); For A:=0 To 255 Do SetColor(A,0,0,0); { Load the two pictures and store their palettes } LoadPcx('Pic1.Pcx',Vp[1]); Move(PCXPal,Pal1,768); LoadPcx('Pic2.Pcx',Vp[2]); Move(PCXPal,Pal2,768); { Combine the two pictures in VGA screen } For A:=0 To 199 Do For B:=0 To 319 Do Begin C:=GetPixel(B,A,Vp[1]); D:=GetPixel(B,A,Vp[2]); PutPixel(B,A,(D Shl 4)+C,VGA); End; { Adjust palette 1 } For A:=1 To 15 Do For B:=0 To 15 Do Pal1[A*16+B]:=Pal1[B]; { Adjust palette 2 } For A:=0 To 15 Do Pal2[A*16]:=Pal2[A]; For A:=0 To 15 Do For B:=0 To 15 Do Pal2[A*16+B]:=Pal2[A*16]; End; Begin InitGraph; InitVirt; InitAll; Repeat Fade(Pal1); Fade(Pal2); Until Keypressed; CloseVirt; CloseGraph; End. Neat, isn't it ? If you have a slow video card, as I have, you'll notice LOTS of snow... That's because of the slownest of the computer in setting the colors. But with a faster computer and/or card, you won't notice it. So, let's move on: 7.2. 256 color crossfading Well, this isn't actually 256 color crossfading. It's... some more colors crossfading... It is very slow (it requires some precalculation), but it enables to have more than 16 colors in both pictures (a maximum of 256 when we combine them), as long as they don't overlap. The ideia is this: Screen 1 Screen 2 Combined Screen ...1.... ..2..... ..12.... ..3...4. ..1..4.. ..3..45. ..2..3.. .111111. .676636. The dot represents color 0. The colors that appear in the combined screen are a combination of the colors of the two screens: ________________________________________ | Color no. | Screen 1 | Screen 2 | ---------------------------------------- | 0 | 0 | 0 | | 1 | 0 | 2 | | 2 | 1 | 0 | | 3 | 3 | 1 | | 4 | 0 | 4 | | 5 | 4 | 0 | | 6 | 0 | 1 | | 7 | 2 | 1 | ---------------------------------------- What does this mean after all ? Well, we now have to create two palettes (the palettes we are going to fade around). In pallete 1, color 0 will have the same value as color 0 of screen 1, color 1 will have the same value as color 0 of screen 1, color 2 will have the same value of color 1 of screen 1, and so forth (compare the values with the table above). Palette 2 will have color 0 equal to color 0 of screen 2, color 1 equal to color 2 of screen 2, color 2 equal to color 0 of screen 2, etc... And this is it... This requires some computation in the beggining of the program, but you can even store the combined image and their palettes. When you use this type of crossfading, you should ensure that the images overlap as little as possible. There are three pictures I made to examplify this. Change the name of the files in the program to access them. They are called Pic3.Pcx, Pic4.Pcx and Pic5.Pcx. I made them in a way that both Pic3.Pcx and Pic4.Pcx have more than 100 colors, but they do not overlap, while Pic5.Pcx overlaps with any of the pictures, but has only 4 colors (so I can probably crossfade them). Here's the code: Program Crossfade256; Uses Mode13h,Crt; Var Dx,Dy,Dc:Word; Pal1,Pal2:Rgblist; Pal3,Pal4:rgblist; W:Word; R1,G1,B1,R2,G2,B2:Byte; Change:Boolean; P1,P2:Byte; F:File; Begin InitGraph; InitVirt; LoadPcx('Pic3.Pcx',Vp[1]); Move(PCXPal,Pal1,768); LoadPcx('Pic4.Pcx',Vp[2]); Move(PCXPal,Pal2,768); Cls(0,VGA); W:=1; FillChar(Pal3,768,0); FillChar(Pal4,768,0); For Dx:=0 To 319 Do Begin For Dy:=0 To 199 Do Begin P1:=GetPixel(Dx,Dy,Vp[1]); P2:=GetPixel(Dx,Dy,Vp[2]); If (P1<>0) Or (P2<>0) Then Begin Change:=False; R1:=Pal1[P1].R; G1:=Pal1[P1].G; B1:=Pal1[P1].B; R2:=Pal2[P2].R; G2:=Pal2[P2].G; B2:=Pal2[P2].B; { Check if this combination was already made } For Dc:=1 To W Do If (Pal3[Dc].R=R1) And (Pal3[Dc].G=G1) And (Pal3[Dc].B=B1) And (Pal4[Dc].R=R2) And (Pal4[Dc].G=G2) And (Pal4[Dc].B=B2) Then Begin Change:=True; PutPixel(Dx,Dy,Dc,VGA); End; If Not Change Then Begin Inc(W); If W=256 Then Begin CloseGraph; WriteLn('Too many colors'); ReadLn; Halt; End; Putpixel(Dx,Dy,W,VGA); Pal3[W].R:=R1; Pal3[W].G:=G1; Pal3[W].B:=B1; Pal4[W].R:=R2; Pal4[W].G:=G2; Pal4[W].B:=B2; End; End; End; End; Repeat Fade(Pal4); Fade(Pal3); Until Keypressed; CloseVirt; CloseGraph; End. Just change the name of the files to see the effects... :)) Don't try to crossfade with this method image Pic3.Pcx and Pic2.Pcx... It won't work... This is it for crossfading... In a future issue, I'll teach a diferent kind of crossfading, that requires ModeX... 'till then... Adios... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 8. Sprites Part IV - Rotating and scaling This is an article about two cool things to use in demos and games. They are quite simple to do, so this will be a fast article. :) 8.1. Rotation In issue 10 of 'The Mag', in the 3d article, I derived that the equations for 2d rotation were: X' = X*Cos(B) - Y*Sin(B) Y' = Y*Cos(B) + X*Sin(B) You should check out the derivation, because that's important, if you want to be a good coder. Normally, you can use the following procedure to put a sprite (this procedure isn't the same we used in previous issues... It's just so I can demonstrate the rotation principle...): Procedure PutImage(X,Y:Word;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Xp,Yp:Integer; Color:Byte; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; For A:=1 To Dy Do For B:=1 To Dx Do Begin Xp:=X+B; Yp:=Y+A; Color:=Mem[Segm:Offs]; PutPixel(Xp,Yp,Color,Where); Inc(Offs); End; End; Basically, he will traverse the square region where the image must be and he places the right color there... The coordinates where to put the current pixel are (Xp,Yp). If we want to rotate the image, we should place the pixels in a different place, according to the angle... So, using the above formulas and using the code given above, we do the PutRotatedImage procedure: Procedure PutRotatedImage(X,Y,Angle:Word;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Xp,Yp:Integer; Xr,Yr:Integer; Color:Byte; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; For A:=1 To Dy Do For B:=1 To Dx Do Begin Xp:=X+B; Yp:=Y+A; { Calculate rotated coordinates } Xr:=Trunc(Xp*Cos(Angle)-Yp*Sin(Angle)); Yr:=Trunc(Yp*Cos(Angle)+Xp*Sin(Angle)); Color:=Mem[Segm:Offs]; PutPixel(Xr,Yr,Color,Where); Inc(Offs); End; End; The problem of this routine is that accounts that the rotation will be around point (0,0) of the screen ! That won't do... We must do the rotation according to the center of the object... To do so, we must use the coordinates relatively to the center of rotation. The center of rotation will be the center of the sprite: (X+Dx Div 2,Y+Dy Div 2). Then, Xp and Yp must be calculated in respect to those coordinates. And finally, after the rotated points are calculated, we must add the value of the center, to achieve the right rotation... The inner loop will look like this: { Calculate relative coordinates } Xp:=(X+B)-(X+Dx Div 2); Yp:=(Y+A)-(Y+Dy Div 2); { Calculate rotated coordinates } Xr:=Trunc(Xp*Cos(Angle)-Yp*Sin(Angle)); Yr:=Trunc(Yp*Cos(Angle)+Xp*Sin(Angle)); { Add the value of the center } Xr:=Xr+(X+Dx Div 2); Yr:=Yr+(Y+Dy Div 2); { Put the pixel } Color:=Mem[Segm:Offs]; PutPixel(Xr,Yr,Color,Where); Inc(Offs); Now this works fine... Let's now speed this up... The center of rotation is something used twice for loop... There's no use of calculating it time after time... Let's calc it once, in the beggining, save the result in two variables, and then use the variables... That's faster... Also, you can use lookup tables for the sines and cosines... In the end, the procedure will look like this: Procedure PutRotatedImage(X,Y,Angle:Word;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Xp,Yp:Integer; Xr,Yr:Integer; Xc,Yc:Integer; Color:Byte; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; Xc:=X+(Dx Div 2); Yc:=Y+(Dy Div 2); For A:=1 To Dy Do For B:=1 To Dx Do Begin { Calculate relative coordinates } Xp:=(X+B)-Xc; Yp:=(Y+A)-Yc; { Calculate rotated coordinates } Xr:=Trunc(Xp*Cosines^[Angle]-Yp*Sines^[Angle]); Yr:=Trunc(Yp*Cosines^[Angle]+Xp*Sines^[Angle]); { Add the value of the center } Xr:=Xr+Xc; Yr:=Yr+Yc; { Put the pixel } Color:=Mem[Segm:Offs]; PutPixel(Xr,Yr,Color,Where); Inc(Offs); End; End; Don't forget to call the InitTables procedure before doing this, because it needs it... Check out the example program: Program TestRotation; Uses Crt,Mode13h,Sprites; Var F:File; Image:Pointer; A:Word; Procedure PutRotatedImage(X,Y,Angle:Word;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Xp,Yp:Integer; Xr,Yr:Integer; Xc,Yc:Integer; Color:Byte; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; Xc:=X+(Dx Div 2); Yc:=Y+(Dy Div 2); For A:=1 To Dy Do For B:=1 To Dx Do Begin { Calculate relative coordinates } Xp:=(X+B)-Xc; Yp:=(Y+A)-Yc; { Calculate rotated coordinates } Xr:=Trunc(Xp*Cosines^[Angle]-Yp*Sines^[Angle]); Yr:=Trunc(Yp*Cosines^[Angle]+Xp*Sines^[Angle]); { Add the value of the center } Xr:=Xr+Xc; Yr:=Yr+Yc; { Put the pixel } Color:=Mem[Segm:Offs]; PutPixel(Xr,Yr,Color,Where); Inc(Offs); End; End; Begin { Init } InitGraph; InitTables; { Set the colors } SetColor(0,0,0,0); SetColor(1,0,0,63); SetColor(2,63,0,0); SetColor(3,63,63,0); SetColor(4,0,63,0); SetColor(5,0,63,63); { Load image } Assign(F,'Example.Img'); Reset(F,1); LoadImage(F,Image); Close(F); { Rotate the sprite } For A:=0 To 359 Do Begin PutRotatedImage(160,100,A,Image,VGA); End; ReadKey; { Shut Down } KillImage(Image); ClearTables; CloseGraph; End. So, what are the advantages and disadvantages of this method ? Well, it is simpler to understand, it works with any sized images, it doesn't require extra memory, and it doesn't clips the images. On the other hand it is slow and leaves holes in the image. So, how can we speed this up and take out the holes ? Well, to speed up, we'll have to use something pregenarated. To fill up the holes we'll have to understand first why there are holes in the image... The holes appear because the rotated points don't fill up the image completely. So, how to prevent it ? We'll invert this thing !!! So, instead of calculating where the points will be, we'll calculate were the points should be !! Confusing ?! Think about this: you now have the starting coordinates of the points and you want to calculate the destination. So, know you'll have the destination points and you'll calculate the starting points, avoiding the holes. Notice that the inversion of the transformation equations is just the same thing, but with negated angles. As you are working with a pregenerated table for the sines and cosines, you'll have to convert the negative angles into positive ones. So, if the angle you want to rotate by is -x, the position in the table will be 360-x. So, to build the procedure, let's go by steps. First, let's assume you're plotting a rectangular sprite with corners that would be in coordinates (x1,y1)(x2,y2)(x3,y3)(x4,y4) if they weren't rotated. Don't forget that, if (x1,y1) is the upper-left coordinates of the sprite, they are given by the procedure call. So: x2=x1+Xsize x3=x1+Xsize x4=x1 y2=y1 y3=y1+Ysize y4=y1+Ysize The color of a point (x,y) inside that rectangle will be the value in the memory position [Seg(Image):(y*xsize)+x]. Now, let's imagine the following coordinates: (x'1,y'1),(x'2,y'2),(x'3,y'3),(x'4,y'4). They will be calculated by our procedure. They are the corners of the sprite after the rotation. Then, after this conversion, we calculate the maximum and minimum X and Y of these rotated coordinates, and then we do two For cicles from the minimums to the maximums and we do the inversed rotation, calculating the color to put in each of the points. I'm a tad late with this issue of 'The Mag' (about two or three months, actually), so I don't have time to do the code for this one, but with the info I gave you, you shouldn't have any problems. If you do have problems, mail me and I'll try to sort it out. On to... 8.2. Scaling Scaling is the process of changing the size of something. In our case, a sprite. There a number of processes to do this, but the one I'll teach you is very simple and efficient, if well optimized. Imagine you have the following 2x2 sprite: 12 32 If you scale it by two, you would get the following 4x4 sprite: 1122 1122 3322 3322 So, what has happened ? Well, every pixel in the image became 4, not 2. Why ? Because you are scaling the X and Y factors. Now, let's imagine we wanted to fit that second image in a 3x3 square. We would get something like this: 112 or this: 112 or this: 112 or... well, you get the 132 112 332 picture... :) 322 332 322 How can we make this come true ? Good question... We'll have to use a step value for each the x and y coordinates. Confused ? No wonder... Let's go one step at a time. First, we need to do the definition of the procedure: Procedure Scale(Image:Pointer;DeltaX,DeltaY:Word;Var Target:Pointer); The Deltas are the size of the final sprite. Now, from the original sprite let's get the original size: Segm1:=Seg(Image^); Offs1:=Ofs(Image^); Move(Mem[Segm1:Offs1],Xsize,2); Move(Mem[Segm1:Offs1+2],Ysize,2); Now, we create the necessary space to store the scaled image: GetMem(Target,DeltaX*DeltaY+4); And we store it's size in the first 4 bytes: Segm2:=Seg(Target^); Offs2:=Ofs(Target^); Move(DeltaX,Mem[Segm2:Offs2],2); Move(DeltaY,Mem[Segm2:Offs2+2],2); Offs2:=Offs2+4; Then we find the step mentioned earlier... What is this step ? It is the ammount you must increase the pointer so... Well, nevermind, this would be complicated to explain by text with my limited english... :) Let's try an example... We have picture 1 and we want to double it's size: ab <- This is picture 1 aabb cd This is the doubled picture 1 -> aabb ccdd ccdd Now, let's have an index pointing to the first pixel in picture 1, and call it 'Joe'... :) Now, let's have another index pointing to the first pixel of the target memory area. Let's call that index 'John'. First, we copy the byte indexed by 'Joe' to the place 'John' points. Then, we increment John by one, and we increase Joe by 0.5 (the step in this case). Then we copy the byte indexed by Joe to the place John points. This is the real trick. We use only the integer part of Joe. In the above case, it copies the same thing ('a'), because the integer part is still 1, though Joe is really 1.5 !!! :))) We do this for the X and Y axis and we have a scalled image in no time ! Let's get the ratio: XRatio:=XSize/DeltaX; YRatio:=YSize/DeltaY; Now, let's create 'Joe', and call it IndexX and IndexY (because there are two axis): IndexX:=0; IndexY:=0; And let's create 'John', and call it Dx and Dy (also two axis): For Dy:=1 To DeltaY Do Begin For Dx:=1 To DeltaX Do Begin Now, let's get the byte indexed by Joe: Offs1:=Trunc(IndexY)*XSize+Trunc(IndexX)+4; C:=Mem[Segm1:Offs1]; And let's put that byte in John. As John is sequential (remember what was said about the storage of a sprite in the first parts of the Sprite Tut), we can do that without much fuss: Mem[Segm2:Offs2]:=C; And let's update the variables: Inc(Offs2); IndexX:=IndexX+XRatio; End; IndexX:=0; IndexY:=IndexY+YRatio; End; And let's finish up the procedure: End; And voil , a complete scaling procedure... Let's check out the whole code: Procedure Scale(Image:Pointer;DeltaX,DeltaY:Word;Var Target:Pointer); Var Segm1,Segm2,Offs1,Offs2:Word; XSize,YSize:Word; IndexX,IndexY,XRatio,YRatio:Real; Dx,Dy:Word; C:Byte; Begin Segm1:=Seg(Image^); Offs1:=Ofs(Image^); Move(Mem[Segm1:Offs1],Xsize,2); Move(Mem[Segm1:Offs1+2],Ysize,2); GetMem(Target,DeltaX*DeltaY+4); Segm2:=Seg(Target^); Offs2:=Ofs(Target^); Move(DeltaX,Mem[Segm2:Offs2],2); Move(DeltaY,Mem[Segm2:Offs2+2],2); Offs2:=Offs2+4; XRatio:=XSize/DeltaX; YRatio:=YSize/DeltaY; IndexX:=0; IndexY:=0; For Dy:=1 To DeltaY Do Begin For Dx:=1 To DeltaX Do Begin Offs1:=Trunc(IndexY)*XSize+Trunc(IndexX)+4; C:=Mem[Segm1:Offs1]; Mem[Segm2:Offs2]:=C; Inc(Offs2); IndexX:=IndexX+XRatio; End; IndexX:=0; IndexY:=IndexY+YRatio; End; End; This could have been done faster, if I used integers instead of reals, but I'll leave that for a future issue, when I cover fixed-point maths. A complete working example of this can be seen in the file SPRSCAL.PAS. I already included the rotation and scaling procedures in the SPRITE unit, so the programs don't include them. This is the end of this article, and I really don't know what I'm gonna do for the next one on Sprites... If no one asks me a specific matter, I may do an article on detecting collisions between sprites. -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 9. Graphics Part X - Assembling it all This article is the last of the standart Mode 13h series. it is about how you can convert your code to be faster and more efficient, by use of assembly and of some optimizations. To understand this issue, a basic knowledge of assembler is needed. That knowledge can be found in previous issues of 'The Mag'. So, let's get started: let's begin with the all time favorite, Mr.PutPixel ! The original version was something like this: Procedure PutPixel(X,Y:word;Col:Byte;Where:Word); Begin Mem[Where:(y*320)+x]:=Col; End; We can do an assembler version. It looks like this (I will number the line so that it is easier to explain): 1 Procedure PutPixel(X,Y:Word;Col:Byte;Where:Word); Assembler; 2 Asm 3 Mov Ax,[Where] 4 Mov Es,Ax 5 Mov Ax,[Y] 6 Mov Bx,320 7 Mul Bx 8 Add Ax,[X] 9 Mov Di,Ax 10 Mov Al,[C] 11 StosB 12 End; This is quite simple. Let's see it in reverse. Line 11 is the line that really puts the pixel. It places the value stored in AL in the memory position Es:Di (Es is the Segment, Di is the Offset), and increments Di. So, we must initialize those three registers (AL,ES,DI). We initialize AL in line 10 (we load the color value in it). ES is initialized in lines 3 and 4. Why move to Ax in order to move to Es, and not move directly ? Because you can't. Because of the way the internal architecture of the PC is designed, you can't load memory positions into segment registers (ES,SS,CS,DS,FS and GS). You can only load registers into those registers. That is why we do two steps. Then, we must initialize DI. DI will be equal to (Y*320)+X. In line 7 we do the multiplication needed. The MUL opcode multiplies the contents of AX with the contents of operand given, and stores the result in AX. So, line 7 is really (in this case): AX=AX*BX. The explanation I gave on MUL isn't accurate. Check issue 5 of 'The Mag' for a more thorought explanation on the opcodes. I don't have time here to explain every opcode I'll use. So, lines 5 and 6 prepare the multiplication. And that's it for this procedure. But, we can still optimize it. How ? By using a nifty trick. Multiplications are slow, even if done in assembler. If it was a way to multiply without using the multiplication ? There is !!!! Using binary shifts. We can shift left to get a times two multiplication. But, 320 isn't a shiftable number, that is, there isn't any n such as 320=2^n. That is true, but look at this: 320 = 256+64 Y*320 = Y*(256+64) = Y*256+Y*64 See ? We first shift Y by 8 (2^8=256) and store that result in a register. Then, we shift Y by 6 (2^6=64) and store that result in another register. Then, we add up the both of them, and we got Y*320 !! And faster (timing proves that this is almost 3 times faster !!! And that's a major improvement in speed, because a Putpixel is a very used routine (well, really not much, but it doesn't matter). So, the final procedure is like this: Procedure PutPixel(X,Y:Word;C:Byte;Too:Word); Assembler; Asm Mov Ax,[Too] Mov Es,Ax Mov Bx,[X] Mov Dx,[Y] Mov Di,Bx Mov Bx,Dx Shl Dx,8 { Shift Dx left 8 times (Dx=Dx*256) } Shl Bx,6 { Shift Bx left 6 times (Bx=Bx*64) } Add Dx,Bx Add Di,Dx Mov Al,[C] Stosb End; This is about 6 times faster than the original putpixel. Now, let's move to... clearing the screen. This is very simple, actually. You just do it like this: Procedure Cls(Col:Byte;Where:Word); Assembler; Asm Mov Ax,[Where] Mov Es,Ax Mov Al,[Col] Mov Cx,64000 Mov Di,0 Rep StosB End; The REP prefix makes the following command to be repeat as many times as there are indicated. Actually, what that line does is something like this: Loop: Move value from AL to ES:DI Increment Di Decrement Cx Is Cx is greater than 0 then jump to beggining of loop There are two things you can do to increase the speed of this code. One is exchange the Mov Di,0 instruction with Xor Di,Di. This clears the Di register in one clock tick (the clock tick is the unit of speed in assembler), while the Mov instruction used 4 clock ticks. This might not seem much, but after some frames of calculation, it can be pretty good. Another thing is using the StosW command, instead of the StosB. The StosW command is similar to the StosB, except in some aspects: -> It moves a word instead of a byte -> It increments Di by two, instead of one So, if you load AL with the Col value and the AH with that same value, and you use the StosW opcode, you only need to do the loop 32000 times. You may ask yourselfs why is this faster... The answear is rather simple. Because of the architecture of our friend, the 80x86 processor, it is ALWAYS faster to move and manipulate words than bytes. Everytime you can, use words. So, the final CLS procedure will look like this: Procedure Cls(Col:Byte;Where:Word); Assembler; Asm Mov Ax,[Where] Mov Es,Ax Mov Al,[Col] Mov Ah,Al Mov Cx,32000 Xor Di,Di Rep StosW End; Now, let's toy around with the procedure that sets the color of the screen palette: SetColor. The original is like this: Procedure SetColor(Col,R,G,B:Byte); Begin Port[$3C8]:=Col; Port[$3C9]:=R; Port[$3C9]:=G; Port[$3C9]:=B; End; We'll do the same thing exactly, but in assembly language: Procedure SetColor(Col,R,G,B:Byte); Assembler; Asm Mov Dx,3c8h { Dx:=3c8h } Mov Al,[Col] { Al:=Col } Out Dx,Al { Port[Dx]:=Al } Inc Dx { Dx:=Dx+1 (Dx=3c9h) } Mov Al,[R] { Al:=R } Out Dx,Al { Port[Dx]:=Al } Mov Al,[G] { Al:=G } Out Dx,Al { Port[Dx]:=Al } Mov Al,[B] { Al:=B } Out Dx,Al { Port[Dx]:=Al } End; The comments in the side should get this right for you. It's fairly simple this one. Now, if we wanted to set the whole palette ? Normally, we would use the above procedure to do so. But we can take advantage of the linearity of memory in an array and set it all much faster. You just make a 'pointer' (this is not the correct name for it in asm) point to the array where the colors are stored, and then you blast the 768 bytes directly into video memory: 1 Procedure SetPalette(Var Palette:RgbList);Assembler; 2 Label L1,L2; 3 Asm 4 Mov Dx,3dah 5 L1: 6 In Al,Dx 7 And Al,08h 8 Jnz L1 9 L2: 10 In Al,Dx 11 And Al,08h 12 Jz l2 13 Push Ds 14 Lds Si,Palette 15 Mov Dx,3c8h 16 Mov Al,0 17 Out Dx,Al 18 Inc dx 19 Mov Cx,768 20 Rep OutsB 21 Pop Ds 22 End; Lines 4 to 12 are to implement the WaitVBL procedure, to avoid the fuzz on the screen. Then, we save the content of the Ds register (never loose this one, or else you're f***** when you return to Pascal). Line 14 loads the 'pointer'. It makes DS:SI point to the beggining of the palette. In line 15/17, we tell the VGA board that we want to start setting colors from color 0. Then, you use line 19/20 to blast the data to the video port, using the Rep prefix with the OutsB, which is the equivelent to Stosb for ports. Then we restore the Ds register, because it points to Pascal's data segment, where we store all the variables. Let's move on to the filled polygon procedure. We aren't going to convert it to Pascal (is a bit complicated and I'm not up to it... :))). But I'll tell you how to do a small optimization that will really speed this up. When we draw the polygon, we have to draw lots of horizontal lines. For this we are using the general line procedure. But we don't need that !!! We now from the start that the line is horizontal, and by taking advantage of the memory linearity of mode 13h, we can do a special procedure to draw horizontal lines !!!! Procedure HLine(Y,X1,X2,Color,Where); Assembler; Asm Mov Ax,[Where] Mov Es,Ax Mov Dx,[Y] Mov Bx,Dx Shl Dx,8 Shl Bx,6 Add Dx,Bx Mov Bx,[X1] Mov Di,Bx Add Di,Dx { Es:Di now points to the place where we must start drawing the horizontal line } Mov Cx,[X2] Sub Cx,Bx Mov Al,[C] Rep Stosb End; Now, you just change the code that draws the line in the FPoly procedure to get this working pretty fast compared to the older. We can speed up this, by putting down two pixels at a time. We just have to put the color value in Ah as well as in Al, and divide by two the number of loops (shift right Cx one time). We can only do this optimization, when we know that X2-X1 is an even number (we just need to check the last bit in Cx to do so, but I'll leave that to you). The same principle applies to the Filled Ellipse. NOTE: I've found an error when I was testing this SHIT !! :((( The FPoly procedure would not work in some cases. It was just a problem of typecasting. Change all variables that are not integer to integer type, in the FPoly procedure, and all will work out fine... Sorry about that... Now, one the most important things to be optimized in a program: the pagefliping procedure. For this one, we'll use the MovsW opcode, which moves the word indexed by Ds:Si to the memory location indexed by Es:Di. This is fairly easy also: Procedure CopyPage(From,Too:Word); Assembler; Asm Push Ds Mov Ds,[From] Mov Es,[Too] Xor Di,Di Xor Si,Si Mov Cx,32000 Rep MovsW Pop Ds End; We transfer whole words at a time because (as I said earlier) it is faster that way. Did I mention that the VGA board is built in such a fashion that putting one pixel is twice as slow as putting two ? Effectively speaking, using words in graphics is 4 times faster than working with bytes !!! So, I guess this is the end of this article. We could do ALL the procedures in the Mode13h unit in Asm, but some of them (as the filled poly) are quite hard, and don't have the time to get it on... But try it yourself. If you succeed and have the time, make a small article explaining it and send it to me !! I would love it !! :)))) Farewell, and don't forget to experiment... Probably I'll start a new series of graphic articles in the next issue, dedicated to ModeX !! Don't miss it !!! :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 10. Hints and Tips * - Begginners tip ** - Medium tip *** - Advanced tip - Timing a routine (**) How many times did you asked yourself: "Is this routine faster than the other one ?", and you hurried to get the chronometer ? Well, there's a simple way to get the time a routine took to execute. First thing, we should have a variable... But a special variable, this one. This variable is addressed in a way that it overlaps the computer's timer. There is a position in memory that stores a long integer that every 1/18th of a second increases it's value by one. You make a variable point to it, using the ABSOLUTE keyword. That keyword enables you to tell Turbo Pascal where to address the variable. So, you define a longint like this: Var MemClock:LongInt Absolute $40:$6C; $40:$6C is the memory position ocuppied by the computer's clock. So, let's time some routines: Var Start,Finish:LongInt; Time:LongInt; ....................................... ....................................... Start:=MemClock; ....................................... { Put the routine you want to time here... Usually it is associated with a loop or something. } ....................................... Finish:=MemClock; Time:=(Finish-Start) Div 18; Writeln('The routine took ',Time,' seconds...'); We divide the value by 18 because the variable is updated 1/18th times a second. Just one word of caution. Although you can, you shouldn't change the MemClock variable, because it can screw up the time of your computer, until you reset it... Other use for this trick is to ensure that a game or demo runs at the same speed in all computers, but I'll let you figure out how to do it... Ehehhe... :))) - Mapping a screen (**) As we saw in the previous tip, we can specify where in memory a variable will be. We can use this concept to 'map' a screen. That is used in the Smooth Scrolling in Text Mode program made by Brent Hostetler. In the beggining he has something like (this is adapted for simplicity's sake): Type VirSCR=Array[0..4999] Of Byte; Var VirtualScreen : VirSCR absolute $B800:0000; Now, anything that we send to the VirtualScreen variable will appear on screen. For example VirtualScreen[0]:=Ord('A'); will make an A appear on the top-left corner of the screen. And: VirtualScreen[1]:=14; will transform that character from white to yellow. You can use this instead of pointers to simplify the operations. To see how the textmode screen works, go to issue 9, and there's an article that covers it in a ligth way. I think you can also map the Mode 13h screen, but I'm not sure, since I never tried it out... Try it... :))) - Byte problem (*) In the text adventure article this issue, I had to change from a byte to a shortint. Why ? Because of this: If you define A as a byte, and you do this: A:=5-10 You won't get -5 as an answear, because byte only ranges from 0 to 255. You'll get 251. That isn't good for some applications, like the game. To use the same memory and maintain a similar range, redefine A as a shortint, that ranges from -128 to 127. -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 11. Points of View Well, this issue is finally done !!! It just... well... er... 4 months late... I bet you gave up on me... But I think it was worth the wait !! 260 Kb (give or take a bit) of information. My finger ache just thinking about it... Just think of this... By my calcu- lations, it took me about 10 hours to get article 4 ready. That's lot of work... But school and the exams also postponed this issue time and time again... Oh, well... I'll try to do the next issue faster (and it probably will, because 3 articles are already made, not by me). This issue was so late already that I didn't do the control file for SpellView... Sorry about that, but that takes more time than you think... I'm thinking of adopting another way of doing this, but I'll see that in the future... Maybe releasing this in PostScrip and Txt file type... What do you think ? Next issue we'll have the continuation of the Text Adventure saga, this time dedicated to interaction with the gamescape. We'll have in the 3d front an article about texture mapping, and how to combine this with flat-shading (and if I'm up to it, an article on Gourad shading). Also, the first article about the Soundblaster (by Viriato), an article on DMA (made by Eyal Lotem) and an article about fixed point maths (made by Gh8). In the Sprites tutorial, I'll maybe talk about colision detection, and in the Graphics tutorial, I'll start to murk about in ModeX land! I'm thinking also on including an article about interfaces (mouse or keyboard, you choose). Of course there will be more tricks and tips, adventures of Spellcaster, and all that... :) Well, this issue is late enough already, so I'll just bid you all farewell, 'till the next issue and all that crap... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x 12. The adventures of Spellcaster, the rebel programmer of year 2018. Episode 11 - Salvation - Spell... What's with you ? - Kristie asked, approaching me from behind, while I gazed at the red sky above. - Nothing... Just thinking of my doings... - You did the right thing. You saved him! - Yes... But how long will he thank me ?! - How come ? - Don't you understand what went in there ? He lost his humanity... All of it... He lost everything except his mind and his feelings... He even lost his mortallity... - What ?! - He cannot die... Not even pulling the plug... His mind will be always stored in the HexaMind's EPROM. - So what ?! - How long can he stand life... This artificial life... - I told her, closing shut my eyes, as if I wanted to block out the darkness of my soul. - Well... I don't know... But at least he will be happy until we get our revenge... I turned to Kristie and looked her into her dark brown eyes. I wanted to speak, but words didn't come out... - Kristie, it's no secret for nobody why me and Karl want to strike against COMPTEL... But why do you want to get even ? - I'd rather not talk about it, Diogo... - Diogo ?!!! We're improving... - I said with a light laugh - The only person that calls me that is my mother... - Well, I'm not your mother !!!!!! - she yelled, turning her back on me and taking one hard step. I streched my arm and I grabbed hers. She looked at me with a mixed expression of fear and surprise. I pulled her towards me, and then, without thinking, I kissed her in the lips. She got loose and she looked at me, with a strange look on her face. I tried to unveil her emotions, but it was useless... I took a step towards her, and she also took one. Then, she punched me in the face... Really hard... See you in the next issue Diogo "SpellCaster" Andrade