We need QBCM to be available on as many Qbasic related websites as possible. That way ALL Qbasic coders can benefit from it. If you have a website dedicated to Qbasic then please become a host of QBCM too! All you have to do to become a host is join the QB Cult Ring, put up all the previous issues. You will recieve the new issues every month via e-mail, and your site will be listed on the QBCM host list!
Copyright (C) 2000 Christopher S. Charabaruk and Matthew R. Knight. All rights reserved. All articles, tutorials, etc. copyright (c) by the original authors. QB Cult Magazine is the exclusive property and copyright of Christopher Steffan Charabaruk and Matthew R. Knight.
Welcome to the fifth jampacked issue of QB Cult Magazine. This is the first issue with me, Chris Charabaruk (EvilBeaver) as editor, and wow am I nervous! But the delay of this issue has allowed me to fill it with all sorts of great articles.
This month sees the beginning of the serialization of BASIC Techniques and Utilities, a very well written book by Ethan Winer; the first of QbProgger's excellent engine tutorials; two 3D articles; and a superb tutorial on 2D rotation by Matthew R. Knight. I was planning on writing something for this issue, but, oh well, I didn't get anything done. Maybe next issue.
Usually, near the end of each issue, we have a nice little column. But because I wasn't able to find someone to write it for this issue, you'll have to do without. Sorry. Next issue, it'll come back.
Regarding BASIC Techniques and Utilities, it was originally published by Ziff-Davis Press, producers of PC Magazine and other computer related publications. After ZD stopped printing it, they released the rights to Ethan Winer, the author. After communicating with Ethan by e-mail, he allowed me to reproduce his book, chapter by chapter, in QBCM. You can find a full text version of BASIC Techniques and Utilities at Ethan's website <www.ethanwiner.com>, or wait until the serialization is complete, when I will have an HTML version ready.
Well, I really don't know what else to write here, but thank you to all who help keep QBCM as the best QB e-zine around, and all those who read it. Have a great time programming, and I hope this issue helps you just as much as all the previous ones have! And remember, if you want to contribute something to QB Cult Magazine, I'm only an e-mail away at <qbcm@tekscode.com>!
Chris Charabaruk (EvilBeaver), editor
P.S.: Sorry about this issue being so late. Being over 3 months late isn't very good for a first issue. QBCM is going to be either bimonthly, or only release one issue for July and August. Please don't hurt me. (=
P.P.S.: Don't forget to send letters in to QBCM, or questions for Ask QBCM. Send them to qbcm@tekscode.com, our new e-mail address.
This issue doesn't have any letters. But you can always send one in by e-mailing us at qbcm@tekscode.com (subject Letters).
Isn't it annoying when there's something in QB that you just can't figure out? If you get stuck on anything in Qbasic, just send an e-mail to us at qbcm@tekscode.com (subject Ask QBCM), and in the next issue, your problem will be solved by an expert QB programmer.
Hello, is it possible to incorporate mp3 files into a qbasic 4.5 program?
If not, is it possible to play large .wav files (40mb+ and in high quality)?
Any help is greatly appreciated,
Thanks.
- Maniac
There are no "natural" ways of playing MP3s in QB yet.
As far as WAV files of that size, the only way I know of is through a wav-playing program found on qbasic.com. Future Library is able to play large WAVs as well, but that's a library and not usable in QBasic 1.1.
I have written DS4QB, which is a program that allows a QB program to access a Win32 slave. It plays MP3s and WAVs, plus modules and MIDIs. But MP3 playing in QB, even with the use of assembly, would be silly, IMHO.
- Nekrophidius
Usually there's more news, but with a three month delay... Sorry again.
By Ethan Winer <ethan@ethanwiner.com>
This chapter explores the internal workings of the BASIC compiler. Many people view a compiler simply as a "black box" which magically transforms BASIC source files into executable code. Of course, magic does not play a part in any computer program, and the BC compiler that comes with Microsoft BASIC is no exception. It is merely a program that processes data in the same way any other program would. In this case, the data is your BASIC source code.
You will learn here what the BASIC compiler does, and how it does it. You will also get an inside glimpse at some of the decisions a compiler must make, as it transforms your code into the assembly language commands the CPU will execute. By truly understanding the compiler's role, you will be able to exploit its strengths and also avoid its weaknesses.
No matter what language a program is written in, at some point it must be translated into the binary codes that the PC's processor can understand. Unlike BASIC commands, the CPU within every PC is capable of acting on only very rudimentary instructions. Some typical examples of these instructions are "Add 3 to the value stored in memory location 100", and "Compare the value stored at address 4012 to the number -12 and jump to the code at address 2015 if it is less". Therefore, one very important value of a high-level language such as BASIC is that a programmer can use meaningful names instead of memory addresses when referring to variables and subroutines. Another is the ability to perform complex actions that require many separate small steps using only one or two statements.
As an example, when you use the command PRINT X% in a program, the value of X% must first be converted from its native two-byte binary format into an ASCII string suitable for display. Next, the current cursor location must be determined, at which point the characters in the string are placed into the screen's memory area. Further, the cursor position has to be updated, to place it just past the digits that were printed. Finally, if the last digit happened to end up at the bottom-right corner of the screen, the display must also be scrolled up a line. As you can see, that's an awful lot of activity for such a seemingly simple statement!
A compiler, then, is a program that translates these English-like BASIC source statements into the many separate and tiny steps the microprocessor requires. The BASIC compiler has four major responsibilities, as shown in Figure 1-1 below.
|
As the compiler processes a program's source code, it translates only the most basic statements directly into assembly language. For other, more complex statements, it instead generates calls to routines in the BASIC run-time library that is supplied with your compiler. When designing a BASIC program you would most likely identify operations that need to be performed more than once, and then create subprograms or functions rather than add the same code in-line repeatedly. Likewise, the compiler takes advantage of the inherent efficiency of using called subroutines.
For example, when you use a BASIC statement such as PRINT Work$, the compiler processes it as if you had used CALL PRINT(Work$). That is, PRINT really is a called subroutine. Similarly, when you write OPEN FileName$ FOR RANDOM AS #1 LEN = 1024, the compiler treats that as a call to its Open routine, and it creates code identical to CALL OPEN(FileName$, 1, 1024, 4). Here, the first argument is the file name, the second is the file number you specified, the third is the record length, and the value 4 is BASIC's internal code for RANDOM. Because these are BASIC key words, the CALL statement is of course not required. But the end result is identical.
While the BC compiler could certainly create code to print the string or open the file directly, that would be much less efficient than using subroutines. Indeed, all of the subroutines in the Microsoft-supplied libraries are written in assembly language for the smallest size and highest possible performance.
The second important job the compiler must perform is to identify all of the variables and other data your program is using, and allocate space for them in the object file. There are two kinds of data that are manipulated in a BASIC program--static data and dynamic data. The term static data refers to any variable whose address and size does not change during the execution of a program. That is, all simple numeric and TYPE variables, and static numeric and TYPE arrays. String constants such as "Press a key to continue" and DATA items are also considered to be static data, since their contents never change.
Dynamic data is that which changes in size or location when the program runs. One example of dynamic data is a dynamic array, because space to hold its contents is allocated when the program runs. Another is string data, which is constantly moved around in memory as new strings are assigned and old ones are erased. Variable and array storage is discussed in depth in Chapter 2, so I won't belabor that now. The goal here is simply to introduce the concept of variable storage. The important point is that BC deals only with static data, because that must be placed into the object file.
As the compiler processes your source code, it must remember each variable that is encountered, and allocate space in the object file to hold it. Further, all of this data must be able to fit into a single 64K segment, which is called DGROUP (for Data Group). Although the compiled code in each object file may be as large as 64K, static data is combined from all of the files in a multi-module program, and may not exceed 64K in total size. Note that this limitation is inherent in the design of the Intel microprocessors, and has nothing to do with BC, LINK, or DOS.
As each new variable is encountered, room to hold it is placed into the next available data address in the object file. (In truth, the compiler retains all variable information in memory, and writes it to the end of the file all at once following the generated code.) For each integer variable, two bytes are set aside. Long integer and single precision variables require four bytes each, while double precision variables occupy eight bytes. Fixed-length string and TYPE variables use a varying number of bytes, depending on the components you have defined.
Static numeric and TYPE arrays are also written to the object file by the compiler. The number of bytes that are written of course depends on how many elements have been specified in the DIM statement. Also, notice that no matter what type of variable or array is encountered, only zeroes are written to the file. The only exceptions are quoted string constants and DATA items, in which case the actual text must be stored.
Unlike numeric, TYPE, and fixed-length variables, strings must be handled somewhat differently. For each string variable a program uses, a four-byte table called a *string descriptor* is placed into the object file. However, since the actual string data is not assigned until the program is run, space for that data need not be handled by the compiler. With string arrays--whether static or dynamic--a table of four-byte descriptors is allocated.
Finally, each array in the program also requires an array descriptor. This is simply a table that shows where the array's data is located in memory, how many elements it currently holds, the length in bytes of each element, and so forth.
In order to fully appreciate how the translation process operates, you will first need to understand what assembly language is all about. Please understand that there is nothing inherently difficult about assembly language. Like BASIC, assembly language is comprised of individual instructions that are executed in sequence. However, each of these instructions does much less than a typical BASIC statement. Therefore, many more steps are required to achieve a given result than in a high-level language. Some of these steps will be shown in the following examples. If you are not comfortable with the idea of tackling assembly language concepts just yet, please feel free to come back to this section at a later time.
Let's begin by examining some very simple BASIC statements, and see how they are translated by the compiler. For simplicity, I will show only integer math operations. The 80x86 family of microprocessors can manipulate integer values directly, as opposed to single and double precision numbers which are much more complex. The short code fragment in Listing 1-1 shows some very simple BASIC instructions, along with the resulting compiled assembly code. In case you are interested, disassemblies such as those you are about to see are easy to create for yourself using the Microsoft CodeView utility. CodeView is included with the Macro Assembler as well as with BASIC PDS.
A% = 12 MOV WORD PTR [A%],12 ;move a 12 into the word variable A% X% = X% + 1 INC WORD PTR [X%] ;add 1 to the word variable X% Y% = Y% + 100 ADD WORD PTR [Y%],100 ;add 100 to the word variable Y% Z% = A% + B% MOV AX,WORD PTR [B%] ;move the contents of B% into AX ADD AX,WORD PTR [A%] ;add to that the value of A% MOV WORD PTR [Z%],AX ;move the result into Z%
The first statement, A% = 12, is directly translated to its assembler equivalent. Here, the value 12 is *moved* into the word-sized address named A%. Although an integer is the smallest data type supported by BASIC, the microprocessor can in fact deal with variables as small as one byte. Therefore, the WORD PTR (word pointer) argument is needed to specify that A% is a full two-byte integer, rather than a single byte. Notice that in assembly language, brackets are used to specify the contents of a memory address. This is not unlike BASIC's PEEK() function, where parentheses are used for that purpose.
In the second statement, X% = X% + 1, the compiler generates assembly language code to increment, or add 1 to, the word-sized variable in the location named X%. Since adding or subtracting a value of 1 is such a common operation in all programming languages, the designers of the 80x86 included the INC (and complementary DEC) instruction to handle that.
Y% = Y% + 100 is similarly translated, but in this case to assembler code that adds the value 100 to the word-sized variable at address Y%. As you can see, the simple BASIC statements shown thus far have a direct assembly language equivalent. Therefore, the code that BC creates is extremely efficient, and in fact could not be improved upon even by a human hand-coding those statements in assembly language.
The last statement, Z% = A% + B%, is only slightly more complicated than the others. This is because separate steps are required to retrieve the contents of one memory location, before manipulating it and assigning the result to another location. Here, the value held in variable B% is moved into one of the processor's registers (AX). The value of variable A% is then added to AX, and finally the result is moved into Z%. There are about a dozen registers within the CPU, and you can think of them as special variables that can be accessed very quickly.
The next example in Listing 1-2 shows how BASIC passes arguments to its internal routines, in this case PRINT and OPEN. Whenever a variable is passed to a routine, what is actually sent is the address (memory location) of the variable. This way, the routine can go to that address, and read the value that is stored there. As in Listing 1-1, the BASIC source code is shown along with the resultant compiler-generated assembler instructions.
It may also be worth mentioning that the order in which the arguments are sent to these routines is determined by how the routines are designed. In BASIC, if a SUB is designed to accept, say, three parameters in a certain order, then the caller must pass its arguments in that same order. Parameters in assembler routines are handled in exactly the same manner. Of course, any arbitrary order could be used, and what's important is simply that they match.
PRINT Work$ MOV AX,OFFSET Work$ ;move the address of Work$ into AX PUSH AX ;push that onto the CPU stack CALL B$PESD ;call the string printing routine OPEN FileName$ FOR OUTPUT AS #1 MOV AX,OFFSET FileName$ ;load the address of FileName$ PUSH AX ;push that onto the stack MOV AX,1 ;load the specified file number PUSH AX ;and push that as well MOV AX,-1 ;-1 means that a LEN= was not given PUSH AX ;and push that MOV AX,2 ;2 is the internal code for OUTPUT PUSH AX ;pass that on too CALL B$OPEN ;finally, call the OPEN routine
When you tell BASIC to print a string, it first loads the address of the string into AX, and then pushes that onto the stack. The stack is a special area in memory that all programs can access, and it is often used in compiled languages to hold the arguments being sent to subroutines. In this case, the OFFSET operator tells the CPU to obtain the address where the variable resides, as opposed to the current contents of the variable. Notice that the words offset, address, and memory location all mean the same thing. Also notice that calls in assembly language work exactly the same as calls in BASIC. When the called routine has finished, execution in the main program resumes with the next statement in sequence.
Once the address for Work$ has been pushed, BASIC's B$PESD routine is called. Internally, one of the first things that B$PESD does is to retrieve the incoming address from the stack. This way it can locate the characters that are to be printed. B$PESD is responsible for printing strings, and other BASIC library routines are provided to print each type of data such as integers and single precision values.
In case you are interested, PESD stands for Print End-of-line String Descriptor. Had a semicolon been used in the print statement--that is, PRINT Work$;--then B$PSSD would have been called instead (Print Semicolon String Descriptor). Likewise, printing a 4-byte long integer with a trailing comma as in PRINT Value&, would result in a call to B$PCI4 (Print Comma Integer 4), where the 4 indicates the integer's size in bytes.
In the second example of Listing 1-2 the OPEN routine is set up and called in a similar fashion, except that four parameters are required instead of only one. Again, each parameter is pushed onto the stack in turn, followed by a call to the routine. Most of BASIC's internal routines begin with the characters "B$", to avoid a conflict with subroutines of your own. Since a dollar sign is illegal in a BASIC procedure name, there is no chance that you will inadvertently choose one of the same names that BASIC uses.
As you can see, there is nothing mysterious or even difficult about assembly language, or the translations performed by the BASIC compiler. However, a sequence of many small steps is often needed to perform even simple calculations and assignments. We will discuss assembly language in much greater depth in Chapter 12, and my purpose here is merely to present the underlying concepts.
As you have seen, some code is translated by the compiler into the equivalent assembly language statements, while other code is instead converted to calls to the language routines in the BASIC libraries. Some statements, however, are not translated at all. Rather, they are known as *compiler directives* that merely provide information to the compiler as it works. Some examples of these non-executable BASIC statements include DEFINT, OPTION BASE, and REM, as well as the various "metacommands" such as '$INCLUDE and '$DYNAMIC. Some others are SHARED, BYVAL, DATA, DECLARE, CONST, and TYPE.
For our purposes here, it is important to understand that DIM when used on a static array is also a non-executable statement. Because the size of the array is known when the program is compiled, BC can simply set aside memory in the object file to hold the array contents. Therefore, code does not need to be generated to actually create the array. Similarly, TYPE/END TYPE statements also merely define a given number of bytes that will ultimately end up in the program file when the TYPE variable is later dimensioned by your program.
The last compiler responsibility I will discuss here is the generation of additional code to test for events and debugging errors. This occurs whenever a program is compiled using the /d, /w, or /v command line switches. Although event trapping and debugging are entirely separate issues, they are handled in a similar manner. Let's start with event trapping.
When the IBM PC was first introduced, the ability to handle interrupt- driven events distinguished it from its then-current Apple and Commodore counterparts. Interrupts can provide an enormous advantage over polling methods, since polling requires a program to check constantly for, say, keyboard or communications activity. With polling, a program must periodically examine the keyboard using INKEY$, to determine if a key was pressed. But when interrupts are used, the program can simply go about its business, confident that any keystrokes will be processed. Here's how that works:
Each time a key is pressed on a PC, the keyboard generates a hardware interrupt that suspends whatever is currently happening and then calls a routine in the ROM BIOS. That routine in turn reads the character from the keyboard's output port, places it into the PC's keyboard buffer, and returns to the interrupted application. The next time a program looks for a keystroke, that key is already waiting to be read. For example, a program could begin writing a huge multi-megabyte disk file, and any keystrokes will still be handled even if the operator continues to type.
Understand that hardware interrupts are made possible by a direct physical connection between the keyboard circuitry and the PC's microprocessor. The use of interrupts is a powerful concept, and one which is important to understand. Unfortunately, BASIC does not use interrupts in most cases, and this discussion is presented solely in the interest of completeness.
BASIC provides a number of event handling statements that perhaps *could* be handled via interrupts, but aren't. When you use ON TIMER, for example, code is added to periodically call a central event handler to check if the number of seconds specified has elapsed. Because there are so many possible event traps that could be active at one time, it would be unreasonable to expect BASIC to set up separate interrupts to handle each possibility. In some situations, such as ON KEY, there is a corresponding interrupt. In this case, the keyboard interrupt. However, some events such as ON PLAY(Count), where a GOSUB is made whenever the PLAY buffer has fewer than Count characters remaining, have no corresponding physical interrupt. Therefore, polling for that condition is the only reasonable method.
The example in Listing 1-3 shows what happens when you compile using the /v switch. Notice that the calls to B$EVCK (Event Check) are not part of the original source code. Rather, they show the additional code that BC places just before each program statement.
DEFINT A-Z CALL B$EVCK 'this call is generated by BC ON TIMER(1) GOSUB HandleTime CALL B$EVCK 'this call is generated by BC TIMER ON CALL B$EVCK 'this call is generated by BC X = 10 CALL B$EVCK 'this call is generated by BC Y = 100 CALL B$EVCK 'this call is generated by BC END HandleTime: CALL B$EVCK 'this call is generated by BC BEEP CALL B$EVCK 'this call is generated by BC RETURN
At five bytes per call, you can see that using /v can quickly bloat a program to an unacceptable size. One alternative is to instead use /w. In fact, /w can be particularly attractive in those cases where event handling cannot be avoided, because it lets you specify where a call to B$EVCK is made: at each line label or line number in your source code. The only downside to using line numbers and labels is that additional working memory is needed by BC to remember the addresses in the code where those labels are placed. This is not usually a problem, though, unless the program is very large or every line is labeled.
All of the various BASIC event handling commands are specified using the ON statement. It is important to understand, however, that ON GOTO and ON GOSUB do not involve events. That is, they are really just an alternate form of GOTO and GOSUB respectively, and thus do not require compiling with /w or /v.
The last compiler option to consider here is the /d switch, because it too generates extra code that you might not otherwise be aware of. When a program is compiled with /d, two things are added. First, for every BASIC statement a call is made to a routine named B$LINA, which merely checks to see if Ctrl-Break has been pressed. Normally, a compiled BASIC program is immune to pressing the Ctrl-C and Ctrl-Break keys, except during an INPUT or LINE INPUT statement. Since much of the purpose of a debugging mode is to let you break out of an errant program gone berserk, the Ctrl-Break checking must be performed frequently. These checks are handled in much the same way as event trapping, by calling a special routine once for each line in your source code.
Another important factor resulting from the use of /d is that all array references are handled through a special called routine which ensures that the element number specified is in fact legal. Many people don't realize this, but when a program is compiled without /d and an invalid element is given, BASIC will blindly write to the wrong memory locations. For example, if you use DIM Array%(1 TO 100) and then attempt to assign, say, element number 200, BASIC is glad to oblige. Of course, there *is* no element 200 in that case, and some other data will no doubt be overwritten in the process.
To prevent these errors from going undetected, BC calls the B$HARY (Huge Array) routine to calculate the address based on the element number specified. If B$HARY determines that the array reference is out of bounds, it invokes an internal error handler and you receive the familiar "Subscript out of range" message. Normally, the compiler accesses array elements using as little code as possible, to achieve the highest possible performance. If a static array is dimensioned to 100 elements and you assign element 10, BC knows at the time it compiles your program the address at which that element resides. It can therefore access that element directly, just as if it were a non-array variable.
Even when you use a variable to specify an array element such as Array%(X) = 12, the starting address of the array is known, and the value in X can be used to quickly calculate how far into the array that element is located. Therefore, the lack of bounds checking in programs that do not use /d is not a bug in BASIC. Rather, it is merely a trade-off to obtain very high performance. Indeed, one of the primary purposes of using /d is to let BC find mistakes in your programs during development, though at the cost of execution speed.
The biggest complication from BASIC's point of view is when huge (greater than 64K) arrays are being manipulated. In fact, B$HARY is the very same routine that BC calls when you use the /ah switch to specify huge arrays (hence the name HARY). Since extra code is needed to set up and call B$HARY compared to the normal array access, using /ah also creates programs that are larger and slower than when it is not used. Further, because B$HARY is used by both /d and /ah, invalid element accesses will also be trapped when you compile using /ah.
The final result of using /d is that extra code is generated after certain math operations, to check for overflow errors that might otherwise go undetected. Overflow errors are those that result in a value too large for a given data type. For example, if you multiply two integers and the result exceeds 32767, that causes an overflow error. Similarly, an underflow error would be created by a calculation resulting a value that is too small.
When a floating point math operation is performed, errors that result from overflow are detected by the routines that perform the calculation. When that happens there is no recourse other than halting your program with an appropriate message. Integer operations, however, are handled directly by 80x86 instructions. Further, an out of bounds result is not necessarily illegal to the CPU. Thus, programs compiled without the /d option can produce erroneous results, and without any indication that an error occurred.
To prove this to yourself, compile and run the short program shown in Listing 1-4, but without using /d. Although the correct result should be 90000, the answer that is actually displayed is 24464. And you will notice that no error message is displayed! As with illegal array references, BC would rather optimize for speed, and give you the option of using /d as an aid for tracking down such errors as they occur. If you compile the program in Listing 1-4 with the /d option, then BASIC will report the error as expected.
Since an overflow resulting from integer operations is not technically an error as far as the CPU is concerned, how, then, can BASIC trap for that? Although an error in the usual sense is not created, there is a special flag variable within the CPU that is set whenever such a condition occurs. Further, a little-used assembler instruction, INTO (Interrupt 4 if Overflow), will generate software Interrupt 4 if that flag is set. Therefore, all BC has to do is create an Interrupt 4 handler, and then place an INTO instruction after every integer math operation in the compiled code. The interrupt handler will receive control and display an "Overflow" message whenever an INTO calls it. Since the INTO instruction is only one byte and is also very fast, using it this way results in very little size or performance degradation.
X% = 30000 Y% = X% * 10 PRINT Y%
Designing a compiler for a language as complex as BASIC involves some very tricky programming indeed. Although it is one thing to translate a BASIC source file into a series of assembly language commands, it is another matter entirely to do it well! Consider that the compiler must be able to accept a BASIC statement such as X! = ABS(SQR((Y# + Z!) ^ VAL(Work$))), and reduce that to the individual steps necessary to arrive at the correct result.
Many, many details must be accounted for and handled, not the least of which are syntax or other errors in the source code. Moreover, there are an infinite number of ways that a programmer can accomplish the same thing. Therefore, the compiler must be able to recognize many different programming patterns, and substitute efficient blocks of assembler code whenever it can. This is the role of an *optimizing compiler*.
One important type of optimization is called *constant folding*. This means that as much math as possible is performed during compilation, rather than creating code to do that when the program runs. For example, if you have a statement such as X = 4 * Y * 3 BC can, and does, change that to X = Y * 12. After all, why multiply 3 times 4 later, when the answer can be determined now? This substitution is performed entirely by the BC compiler, without your knowing about it.
Another important type of optimization is BASIC's ability to remember calculations it has already performed, and use the results again later if possible. BC is especially brilliant in this regard, and it can look ahead many lines in your source code for a repeated use of the same calculations. Listing 1-5 shows a short fragment of BASIC source code, along with the resultant assembler output.
X% = 3 * Y% * 4 MOV AX,12 ;move the value 12 into AX IMUL WORD PTR [Y%] ;Integer-Multiply that times Y% MOV WORD PTR [X%],AX ;assign the result in AX to X% A% = S% * 100 MOV BX,AX ;save the result from above in BX MOV AX,100 ;then assign AX to 100 IMUL WORD PTR [S%] ;now multiply AX times S% MOV WORD PTR [A%],AX ;and assign A% from the result Z% = Y% * 12 MOV WORD PTR [Z%],BX ;assign Z% from the earlier result
As you can see in the first part of Listing 1-5, the value of 3 times 4 was resolved to 12 by the compiler. Code was then generated to multiply the 12 times Y%, and the result is in turn assigned to X%. This is similar to the compiled code examined earlier in Listing 1-1. Notice, however, that before the second multiplication of S% is performed, the result currently in AX is saved in the BX register. Although AX is destroyed by the subsequent multiplication of S% times 100, the result that was saved earlier in BX can be used to assign Z% later on. Also notice that even though 3 * 4 was used first, BC was smart enough to realize that this is the same as the 12 used later.
While the compiler can actually look ahead in your source code as it works, such optimization will be thwarted by the presence of line numbers and labels, as well as IF blocks. Since a GOTO or GOSUB could jump to a labeled source line from anywhere in the program, there is no way for BC to be sure that earlier statements were executed in sequence. Likewise, the compiler has no way to know which path in an IF/ELSE block will be taken at run time, and thus cannot optimize across those statements.
Microsoft compiled BASIC lets you create two fundamentally different types of programs. Those that are entirely self-contained in one .EXE file are compiled with the /o command line switch. In this case, the compiler creates translations such as those we have already discussed, and also generates calls to the BASIC language routines contained in the library files supplied by Microsoft. When your compiled program is subsequently linked, only those routines that are actually used will be added to your program.
When /o is not used, a completely different method is employed. In this case, a special .EXE file that contains support for every BASIC statement is loaded along with the BASIC program when the program is run from the DOS command line. As you are about to see, there are advantages and disadvantages to each method. For the purpose of this discussion I will refer to stand-alone programs as BCOM programs, after the BCOMxx.LIB library name used in all versions of QuickBASIC. Programs that instead require the BRUNxx.LIB library to be present at run time will be called BRUN programs.
Beginning with BASIC 7 PDS, the library naming conventions used by Microsoft have become more obscure. This is because PDS includes a number of variations for each method, depending on the type of "math package" that is specified when compiling and whether you are compiling a program to run under DOS or OS/2. These variations will be discussed fully in Chapter 5, when we examine all of the possible options that each compiler version has to offer. But for now, we will consider only the two basic methods--BCOM and BRUN. The primary differences between these two types of programs are shown in Figure 1-2.
|
Stand-alone BCOM programs are always larger than an equivalent BRUN program because the library code for PRINT, INSTR, and so forth is included in the final .EXE file. However, less *memory* will be required when the program runs, since only the code that is really needed is loaded into the PC. Likewise, a BRUN program will take less disk space, because it contains only the compiled code. The actual routines to handle each BASIC statements are stored in the BRUNxx.LIB library, and that library is loaded automatically when the main program is run from DOS.
You might think that since a BRUN program is physically smaller on disk it will load faster, but this is not necessarily true. When you execute a BRUN program from the DOS command line, one of the first things it does is load the BRUN .EXE support file. Since this support file is fairly large, the overall load time will be much greater than the compiled BASIC program's file size would indicate. However, if the main program subsequently chains to another BASIC program, that program will load quickly because the BRUN file does not need to be loaded a second time.
One other important difference between these two methods is the way that the BASIC language routines are accessed. When a BCOM program is compiled and linked, the necessary routines are called in the usual fashion. That is, the compiler generates code that calls the routines in the BCOM library directly. When the program is subsequently linked, the procedure names are translated by LINK into the equivalent memory addresses. That is, a call to PRINT is in effect translated from CALL B$PESD to CALL ####:####, where ####:#### is a segment and address.
BRUN programs, on the other hand, instead use a system of interrupts to access the BASIC language routines. Since there is no way for LINK to know exactly where in memory the BRUNxx.EXE file will be ultimately loaded, the interrupt vector table located in low memory is used to hold the various routine addresses. Although many of these interrupt entries are used by the PC's system resources, many others are available. Again, I will defer a thorough treatment of call methods and interrupts until Chapter 11. But for now, suffice it to say that a direct call is slightly faster than an indirect call, where the address to be called must first be retrieved from a table.
As an interesting aside, the routines in the BRUNxx.EXE file in fact modify the caller's code to perform a direct call, rather than an interrupt instruction. Therefore, the first time a given block of code is executed, it calls the run-time routines through an interrupt instruction. Thereafter, the address where the BRUN file has been loaded is known, and will be used the next time that same block of code is executed. In practice, however, this improves only code that lies within a FOR/NEXT, WHILE, or DO loop. Further, code that is executed only once will actually be much slower than in a BCOM program, because of the added self- modification (the program changes itself) instructions.
Notice that when BC compiles your program, it places the name of the appropriate library into the object file. The name BC uses depends on which compiler options were given. This way you don't have to specify the correct name manually, and LINK can read that name and act accordingly. Although QuickBASIC provides only two libraries--one for BCOM programs and one for BRUN--BASIC PDS offers a number of additional options. Each of these options requires the program to be linked with a different library. That is, there are both BRUN and BCOM libraries for use with OS/2, for near and far strings, and for using IEEE or Microsoft's alternate math libraries. Yet another library is provided for 8087-only operation.
Until now, we have examined only the actions and methods used by the BC compiler. However, the process of creating an .EXE file that can be run from the DOS command line is not complete until the compiled object file has been linked to the BASIC libraries. I stated earlier that when a stand-alone program is created using the /o switch, only those routines in the BCOM library that are actually needed will be added to the program. Unfortunately, that is not entirely accurate. While it is true that LINK is very smart and will bring in only those routines that are actually called, there is one catch.
Imagine that you have written a BASIC program which is comprised of two separate modules. In one file is the main program that contains only in- line code, and in the other are two BASIC subprograms. Even if the main program calls only one of those subprograms, both will be added when the program is linked. That is, LINK can resolve routines to the source file level only, but cannot extract a single routine from an object module which contains multiple routines. Since an .LIB library file is merely a collection of separate object modules, all of the routines that reside in a given module will be added to a program, even if only one has been accessed. This property is called *granularity*, and it determines how finely LINK can remove routines from a library.
In the case of the libraries supplied with BASIC, the determining factor is which assembly language routines were combined with which other routines in the same source file by the programmers at Microsoft. In QuickBASIC 4.5, for example, when a program uses the CLS statement, the routines that handle COLOR, CSRLIN, POS(0), LOCATE, and the function form of SCREEN are also added. This is true even if none of those other statements have been used. Fortunately, Microsoft has done much to improve this situation in BASIC PDS, but there is still room for improvement. In BASIC PDS, CLS is stored in a separate file, however POS(0), CSRLIN, and SCREEN are still together, as are COLOR and LOCATE.
Obviously, Microsoft has their reasons for doing what they do, and I won't attempt to second guess their expertise here. The BASIC language libraries are extremely complex and contain many routines. (The QuickBASIC 4.5 BCOM45.LIB file contains 1,485 separate assembler procedures.) With such an enormous number of assembly language source files to deal with, it no doubt makes a lot of sense to organize the related routines together. But it is worth mentioning that Crescent Software's P.D.Q. library can replace much of the functionality of the BCOM libraries, and with complete granularity. In fact, P.D.Q. can create working .EXE programs from BASIC source that are less than 800 bytes in size.
In this chapter, you learned about the process of compiling, and the kinds of decisions a sophisticated compiler such as Microsoft BASIC must make. In some cases, the BASIC compiler performs a direct translation of your BASIC source code into assembly language, and in others it creates calls to existing routines in the BCOM libraries. Besides creating the actual assembler code, BASIC must also allocate space for all of the data used in a program.
You also learned some basics about assembly language, which will be covered in more detail in Chapter 12. However, examples in upcoming chapters will also use brief assembly language examples to show the relative efficiency of different coding styles. In Chapter 2, you will learn how variables and other data are stored in memory.
By Terry Cavanagh <terrycavanagh@eircom.net>
It's no secret that QuickBasic isn't the ideal enviorment for programming games. You're stuck with a maximum of 256 colors, 16 if you want smooth animation, the standard palette is utterly useless, and the only command that exists in the language to modify the standard palette is the painfully slow PALETTE Command. So one of the first things QB programmers should really concentrate on is inproving thier palette. The tutorial is aimed at programmers who know some QB, but who still think they've got a lot to learn. You have to admit, most palette affects look cool. But there's more to it than that. A well kept secret is that palette effects are also quite easy to do, and require little programming knowelge save what most intermediate QuickBasic Programmers already know.
Ok, Let's get started. QuickBasic has one palette command. And here it is.
PALETTE OldColor%, NewColor%
It's slow, but it gets the job done. Let me explain how it works.
The "OldColor%" is the Palette Slot to be changed. Say you wanted the Palette Slot 15 to be green instead of white.
Copy this into QB and run it to see the effect. I'll explain how it works after.
SCREEN 13 COLOR 15 PRINT "This Text is White - Or is it?" SLEEP PALETTE 15, 16128
Huh? You're probably saying :) Ok, Let's go through EXACTLY how the Palette command works.
The OldColor% is the Slot. If you've run this program, you'll have seen the text change from white to bright Green when you hit the slot.
But the 16128? That probably confused a few people... In an EGA screen mode, the Palette command simply changes the OldColor% to the contents of the color in the NewColor% slot. In a VGA screen mode, however, like Screen 13, it's a little more complecated.
Here's the forumla.
PALETTE OldColor%, Red% + (Green% * 256) + (Blue% * 65536)
Bearing in mind that the maximum intensities for Red%, Green% and Blue% are 63, being the brightest, and 0 being pitch dark, you can create any color with these. You can mix combinations, et cetera. Let's look at that code above again.
SCREEN 13 COLOR 15 PRINT "This Text is White - Or is it?" SLEEP PALETTE 15, 16128
Screen 13 simply changes to a 256 VGA screen mode (QB's only). Color 15 tells the computer that you will be using this color for your text. The Print Command shows the text in the brackets on screen. And the Sleep command waits for a keypress before contining. All this you should have already known. But the Palette command is the key. What exactly is 16128? It's 63 * 256 - i.e, the brightest shade of Green! Let's play around with the Palette some more :
SCREEN 13 COLOR 15 PRINT "This Text is white - or is it?" SLEEP PALETTE 15, 0 SLEEP PALETTE 15, 63 SLEEP PALETTE 15, 256 * 63 SLEEP PALETTE 15, (256 * 63) + 63 SLEEP PALETTE 15, 65536 * 63 SLEEP
Run that to see its effects.
Alright, You've gotten the jist of the Palette command. But there's so much more to learn!
What'll we do first? Something we should really try to improve on is the palette's speed. Sure, That runs at a reasonable speed, but what if you wanted to change the entire palette? Try to do this, and the program slows down considerbly, to such an extend that on my Pentium 433 Mhz, the entire palette takes a wopping 3 seconds to replace. sigh.. In this day and age, that's much too slow. Lucky for QB, we've always gotten by on pokes to speed things up. Here's a listing that will have the same effect as the palette command.
OUT &H3C8, Slot% OUT &H3C9, Red% OUT &H3C9, Green% OUT &H3C9, Blue%
You simply write those four lines exactly like that, one after the other. I advise that you write it down rather than cut and paste it, because it's good habit to type it in everytime you need it - It'll stick in you head that way.
Now for the main part of the tutorial - I'm going to mentain a trick that can be done with palettes, give a listing, and then explain how it works.
' Trick One - Chosing the background text color in a screenmode other than 0. SCREEN 13 OUT &H3C8, 0 OUT &H3C9, 63 OUT &H3C9, 63 OUT &H3C9, 63 COLOR 8 PRINT "Look See!" PRINT "The Text is gray! PRINT "The Background is White!"
Well, we all have to start somewhere, don't we? Let's explain the code word for word. The reason why we might want to do this is simple - The Background of the text is always black - in screen 0 you can select the background text color with the color command, but that's a different story. To get the background text color to be something other than black, you need to change the color that resides in slot zero.
The first line changes the screenmode.
The next four lines change color 0 to white.
The next line changes the foreground color to dark gray.
And the last few lines demonstrate the effects.
If you're totally lost at this stage, you havn't a hope of catching up now. I'd advise testing your skills now, and coming back to the tutorial at this point. Things are just about to get complicated.
' Trick Two - Fading the Entire Screen RANDOMIZE TIMER DIM Red%(255), Green%(255), Blue%(255) SCREEN 13 FOR I% = 0 TO 255 OUT &H3C7, I% Red%(I%) = INP(&H3C9) Green%(I%) = INP(&H3C9) Blue%(I%) = INP(&H3C9) NEXT I% FOR Y% = 0 TO 199 FOR X% = 0 TO 319 PSET (X%, Y%), RND * 256 NEXT X% NEXT Y% LOCATE 10, 4 PRINT "Example Of QuickBasic Fade Effect" LOCATE 11, 12 PRINT "By Terry Cavanagh" SLEEP GOSUB FadeOut SLEEP GOSUB FadeIn SLEEP END FadeOut: FOR I% = 0 TO 63 FOR X% = 0 TO 255 OUT &H3C7, X% R% = INP(&H3C9) - 1: IF R% < 0 THEN R% = 0 G% = INP(&H3C9) - 1: IF G% < 0 THEN G% = 0 B% = INP(&H3C9) - 1: IF B% < 0 THEN B% = 0 OUT &H3C8, X% OUT &H3C9, R% OUT &H3C9, G% OUT &H3C9, B% NEXT X% NEXT I% RETURN FadeIn: FOR I% = 0 TO 63 FOR X% = 0 TO 255 OUT &H3C7, X% R% = INP(&H3C9) + 1: IF R% > Red%(X%) THEN R% = Red%(X%) G% = INP(&H3C9) + 1: IF G% > Green%(X%) THEN G% = Green%(X%) B% = INP(&H3C9) + 1: IF B% > Blue%(X%) THEN B% = Blue%(X%) OUT &H3C8, X% OUT &H3C9, R% OUT &H3C9, G% OUT &H3C9, B% NEXT X% NEXT I% RETURN
Ok, Loads of new stuff here. Run this program to see what happens.
To explain how this works, bear a few points in mind.
First of all, If you fade the screen out, the current palette is destroyed. This is why arrays exist to store the current palette. That explains the line:
DIM Red%(255), Green%(255), Blue%(255)
Also, There's a new trick in action here. You can read the Red, Green and Blue intensities of any palette slot with this code:
OUT &H3C7, Slot% R% = INP(&H3C9) G% = INP(&H3C9) B% = INP(&H3C9)
Simple! You'll notice it bears more than a passing resemblence to the code to set a palette color. Try to memerize this also. Here's a full explaination.
RANDOMIZE TIMER ' Ensures that a completely new set of random numbers are generated each time the program is run. DIM Red%(255), Green%(255), Blue%(255) ' Arrays to hold the current pallette SCREEN 13 ' Change to VGA Screenmode FOR I% = 0 TO 255 OUT &H3C7, I% Red%(I%) = INP(&H3C9) Green%(I%) = INP(&H3C9) Blue%(I%) = INP(&H3C9) NEXT I% ' Reads the contents of the current palette into the arrays. FOR Y% = 0 TO 199 FOR X% = 0 TO 319 PSET (X%, Y%), RND * 256 NEXT X% NEXT Y% LOCATE 10, 4 PRINT "Example Of QuickBasic Fade Effect" LOCATE 11, 12 PRINT "By Terry Cavanagh" ' Draws a Quick Screen to be Faded. SLEEP GOSUB FadeOut SLEEP GOSUB FadeIn SLEEP END ' The Main Engine. Waits for a key, then Fades Out. Waits again, then fades back ' in and exits the program. Have a look at the sections which perform the Fades. FadeOut: FOR I% = 0 TO 63 FOR X% = 0 TO 255 OUT &H3C7, X% R% = INP(&H3C9) - 1: IF R% < 0 THEN R% = 0 G% = INP(&H3C9) - 1: IF G% < 0 THEN G% = 0 B% = INP(&H3C9) - 1: IF B% < 0 THEN B% = 0 OUT &H3C8, X% OUT &H3C9, R% OUT &H3C9, G% OUT &H3C9, B% NEXT X% NEXT I% RETURN ' Creates a loop for each color intensity, and inside creates a loop for every ' palette color. Inside this loop, the current contents of each palette color is ' observed, reduced by one in intensity, then changed. The first loop ensures ' that the screen is faded the whole way to black, and errors are avoided by ' making sure that if an intensity is reduced below zero that it is kept at zero. FadeIn: FOR I% = 0 TO 63 FOR X% = 0 TO 255 OUT &H3C7, X% R% = INP(&H3C9) + 1: IF R% > Red%(X%) THEN R% = Red%(X%) G% = INP(&H3C9) + 1: IF G% > Green%(X%) THEN G% = Green%(X%) B% = INP(&H3C9) + 1: IF B% > Blue%(X%) THEN B% = Blue%(X%) OUT &H3C8, X% OUT &H3C9, R% OUT &H3C9, G% OUT &H3C9, B% NEXT X% NEXT I% RETURN ' Almost the same, except that instead of reducing the color it increases it. ' Also, instead of stopping the program going below 0, it stops it going above ' the color that the palette should be.
There! I bet you all thought that fading was a difficult thing to do. Let's try something more fancy.
' Trick Three - Static RANDOMIZE TIMER SCREEN 13 FOR I% = 0 TO 255 X% = RND * 64 OUT &H3C8, I% OUT &H3C9, X% OUT &H3C9, X% OUT &H3C9, X% NEXT I% FOR Y% = 0 TO 199 FOR X% = 0 TO 319 PSET (X%, Y%), RND * 256 NEXT X% NEXT Y% SLEEP DO FOR I% = 0 TO 256 X% = RND * 64 OUT &H3C8, I% OUT &H3C9, X% OUT &H3C9, X% OUT &H3C9, X% NEXT I% LOOP UNTIL INKEY$ = CHR$(27)
This is a lot easier to do than fading the screen, but all this stuff is so easy anyway that there's little or no difference! Let's go through this step by step...
RANDOMIZE TIMER ' Sigh.. I though I already explained what this line does! Pay more attentian! SCREEN 13 ' Assigns 13 different screens that can be used to flip between. What do you think it does? FOR I% = 0 TO 255 X% = RND * 64 OUT &H3C8, I% OUT &H3C9, X% OUT &H3C9, X% OUT &H3C9, X% NEXT I% ' Sets the entire palette to shades of White. FOR Y% = 0 TO 199 FOR X% = 0 TO 319 PSET (X%, Y%), RND * 256 NEXT X% NEXT Y% ' Draws random dots all over the screen. SLEEP ' Drift into never never land. DO FOR I% = 0 TO 256 X% = RND * 64 OUT &H3C8, I% OUT &H3C9, X% OUT &H3C9, X% OUT &H3C9, X% NEXT I% LOOP UNTIL INKEY$ = CHR$(27) ' The main part, Keep changing the shades of white until escape is pressed!
The effect is pretty pleasing, as you can see! Try a few different things! An invasion of color can be managed by adding a delay between changes of each palette color. A red static looks pretty cool too!
Anyway, so ends this tutorial. I was going to show how to do translucantcy also, but instead I decided to leave it, because to do a full explaination of translucantcy, and the many different ways to do it would fill a tutorial of this size. Maybe If I get some nice feedback, I'll write another tutorial explaining it (hint hint nudge nudge). This thing is probably littered with spelling mistakes, because I didn't read over it. Incedently, I wrote all this code myself, so If you want to use it in your programs, let me know about it and give me credit. Hopefully this tutorial will show you just how easy these sort of things are.
Till next Time!
Terry Cavanagh, Dark Legends Software
terrycavanagh@eircom.net , www.darklegends.com.
By QbProgger <qbprogger@tekscode.com>
If you are new to RPG's this article is not about how to make a tile engine so close this damned browser right now. Yeah, you heard me, this isn't for new people, so close this now. Yup.
The hardest thing that RPG makers face nowadays is the object system. The object system is everything from a soda can to the guy who sells magazines behind the counter in the dirty book store (yeah, we all know you put one in your game, don't be shy ;)). Anyways, I see awful examples of such an object system every day of my life. I see NPC's that just sit there...and don't do anything. I see NPC's that are incapable of nothing more than a random movement and message. I see objects that are limited to their original script. I see many things that are horrible and should be eliminated from each and every engine on this planet.
The first thing that a person should do if they want to make a complete RPG is make a complete and totally functional object system. A functional and complete object system will allow NPC's to walk through doors, activate scripts, be controlled, hold items, talk, and drop items. Instead of having one character and other "npcs" which are controlled through some other routine, we should put all of the objects and the character on the same exact level. Here's an example.
(Psuedo code, do not copy into your program or it will screw up )
SUB UpdateNPCS FOR uNF=1 to ActiveNPCS SELECT CASE uNF CASE CurrentlyControlledCharacter MoveNPC uNF, Keyboardismovinghorizontal, Keyboardismovingvertical CASE ELSE SELECT CASE ObjectData(uNF) CASE RandomlyMoves MoveNPC uNF, RandomPlaceX, RandomPlaceY CASE IsMovingToAPoint MoveNPC uNF, ObjectPointToMoveTo(uNf,0), ObjectPointToMoveTo(uNF,1) CASE IsStandingStill AnimateNPC uNF END SELECT END SELECT NEXT END SUB SUB MoveNPC Number,MoveToX,MoveToY Test=Map(MoveToX,MoveToY) SELECT CASE Test CASE Walkable ObjectLocation(Number,0)=MoveToX:ObjectLocation(number,1)=MoveToY CASE IsAScriptTile ActivateScript END SELECT END SUB
(End of psuedo code)
With this example (you REALLY need to change it to suit your own coding style) you can EASILY make moving npcs and npcs that can do everything from activate scripts to whatever. And, to control a different object, just change the CurrentlyControlledCharacter variable. This allows a person to update over 200 npcs in a VERY short time, causing a very professional look to your object system. Also, in the SELECT CASE ObjectData(unf) you can make it so that objectdata(5) (NPC #5) is a value of 3. Let's say that an NPC with the attribute of 3 goes after the player and tries to attack him. All you have to do is add in code under there and it's nice, neat, and organized and will work very quickly without sacrificing code speed. Now you can have differently behaving NPCs in your engine, which will impress your players =].
Not so hard, eh? I bet you didn't think that a totally efficient and active object system would be so short. Now that you have this out of the way, you can add things like an items list for each NPC (watch your RAM usage!!). Then, you could possibly trade and sell items to every NPC.
By Toshi Horie <horie@ocf.berkeley.edu>
The first step toward 3D graphics in QB is to find out how to convert 3D points to screen coordinates. Graphics people call this process "perspective projection." In online tutorials, we see formulas like xs = x/z, ys = y/z without explanation. Why divide by z? Is it just an approximation? Or is there really a geometric reason behind it?
The first thing to do to figure out the answers to these questions is to draw a nice diagram. Imagine yourself looking down from the ceiling at your monitor and where you usually sit. Here is my little ASCII diagram to help you.
The 3D object, say a baseball is at point P, and it is displayed on the screen of the monitor at point S. The eye is at E, and the center of the screen is at point C. All points are defined so that the top left corner of the screen is the origin (0,0,0), and +y is down and +x is to the right and +z is into the monitor (up in the following diagram). The units for position are in SCREEN 13 pixels, since that's teh screen mode the sample code will be working in.
' [top-down view of screen, sliced at y=100] ' (160,100,zp) Q+--------- * P(xp,100,zp) behind screen | / (a point in 3D - assume y is 100 for now) | / 0.. (160,100,zs) | / (320,100,zs) |=================C======S===============| <-- screen | / (xs,100,zs) ^+z | / where the pixel is lit | | / +-->+x | / | / E|/ eye(160,100,zeye)
In this figure,
Now you have to notice that we have two similar triangles - Triangle ECS and Triangle EQP are similar triangles. (In case you don't know what similar triangles are, they are triangles with the same shape but of different sizes**. They have the property that their corresponding sides are proportional, meaning they are magnified by the same amount, and thus the ratio between the corresponding sides is the same.)
** There's a special case when similar triangles have the same size as well, but those are usually called "congruent triangles."
This means that the ratio of the corresponding sides of the triangle is the same for ECS and EQP! Which means:
EC CS ---- = ------ .... Eq. 1 EQ QP
Notice, that
Now we want to find out what xs is, because that's the x coordinate of the point we want to plot with PSET.
Substituting the values above into equation 1, we get: (remember the distance between the eye and center of the screen is 640)
640 xs-160 ----------- = ----------- ... Eq. 2 (zp-zs)+640 xp-160
Now, if we assume the screen is at z=0, then zs drops out and things get easy.
640 xs-160 ----------- = ----------- ... Eq. 3 zp+640 xp-160
'[first figure with more numbers filled in] ' ' (160,100,zp) Q+--------- * P(xp,100,zp) behind screen | / (a point in 3D) | / (160,100,0) | / (320,100,0) |=================C======S===============| :| / (xs,100,0) 6| / pixel for point 4| / 0| / :| / E:|/ eye(160,100,zeye)
We want to solve this for xs, so here it goes: multiplying both sides by the (xp-160), we get
640*(xp-160) xs-160 = ----------------------- ... Eq. 3b 640+zp
adding 160 to both sides of the equation, we get
640*(xp-160) xs = ----------------------- + 160 ... Eq. 4 (origin at top left corner of screen) 640+zp
Next, we will find the formula for ys, then we can plot 3D points on the screen using PSET(xs,ys),colour.
HOW COME WE CAN ASSUME Y=100?Okay, we got the formula for xs when y=100, but this same formula actually works for y<>100. Why is this? Here is an intuitive explanation: <tek`> if i was standing on a cliff ... <tek`> looking into oblivion <tek`> and there's this giant orb that just floats <tek`> say it's "30 units to the right of the center of my FOV" <tek`> and it moves along the (vertical) y-axis <tek`> no matter how far up or down it goes that x-coord is staying the same
Because y does not have to be 100, the formula for xs, given in equation 4 can be used any time we need to project 3D points to the screen. |
This gives us a formula for xs. But what about ys? It turns out that ys can be found in almost the exact same way!
Now you can get off the ceiling :) Sit back in your seat, and rotate the monitor sideways so you can't see what's on the screen. Before you do that, you might want to copy the diagram below, so you can compare how the monitor looks to the diagram. Okay, since the screen is sideways, the +z axis points .to the right in the diagram, and the +y axis points down. The baseball is now at point P' (pronounced "pee-prime") this time.
'[side view of monitor and eye] ' in front of <--- screen --> behind screen ==== ||::::::::: +-->into +z || :::::::::: | screen || (assume x = 160 +y v || for all points) down || :::::::: || ::: || ::: Eye(160,100,zeye) || behind screen ::: E----------C------+ Q' ::: \ || | ::: \ || | ::: \ || | ::: \ || | ::: S' | ::: || \ | ::: || \ | ::: || * P' (160,yp,zp) ::: || ::::::::::: || ::::::::::::::::::: ||:::::::::: ====
In this figure,
Now you have to notice that we have two similar triangles - Triangle ECS' and Triangle EQ'P' are similar.
This means that the ratio of the corresponding sides of the triangle is the same for ECS' and EQ'P'! So we have:
EC CS' ---- = ------ ... Eq. 5 EQ' Q'P'
Looks just like equation 3, huh? I told you that the x and y's can be solved in the same way!
The rest of the derivation looks similar too! Just keep the numbers straight, and you'll be fine. Plugging in the lengths of the sides of the triangle into equation 5, we get something that looks a lot like equation 2: (remember the distance between the eye and center of the screen is 640 pixels for SCREEN 13.)
640 ys'-100 ------------- = ----------- ... Eq. 6 640+(zp-zs') yp-100
Again, the screen is at z=0, so zs=0 and things get easier.
640 ys'-100 ----------- = ----------- ... Eq. 7 640+zp yp-100
We want to solve this for ys, so here it goes: multiplying both sides by the (yp'-100), we get
640*(yp-100) ys'-100 = ----------------------- ... Eq. 7b 640+zp
adding 100 to both sides, we get
640*(yp-100) ys' = ---------------- + 100 ... Eq. 8 (origin at top left corner of screen) 640+zp
HOW COME WE CAN ASSUME X=160 ?When we solve for ys, why can we forget about the x coordinate and assume it is 160? I can say, it works by analogy, but that's not a proof. Here is a physics-based explanation: If I was standing on the side of a flat street looking toward the other side, while the cars were passing by in the x direction (horizontally), I wouldn't see the cars moving up and down, would I? [Now if this was a sloped street, cars going horizontally would be either taking off or crashing into the ground, like in "Back to the Future," but that's another story.] |
Because of the above reasoning, once again, we can generalize our equation to one that projects any 3D point to the screen, without doing any extra work! So ys = ys' if point P' is at the same position as point P above.
640*(yp-100) ys = ys' = ------------------- + 100 ... Eq. 8a (origin at top left corner of screen) 640+zp
Together, Equation 4 and equation 8a give us the complete formula for plotting 3D points (which have their origin on the top left corner of the screen, with +x axis going to the right, the +y axis pointing down, and the +z axis pointing into the monitor) onto the screen.
Here they are again.
640*(xp-160) xs = ------------------- + 160 ... Eq. 4 640+zp 640*(yp-100) ys = ------------------- + 100 ... Eq. 8a 640+zp
Wait! "Top left corner of the screen?" That means (1,-1,1) will be plotted off the screen! Ok, we'll fix this, but there's another problem for people used to y axis pointing up. The y-axis on our coordinate system points down!
To correct this, we have to return to equation 7b. (don't worry, it's only a small change!)
640*(yp-100) -(ys'-100) = ------------- ... Eq. 7b [+y axis is up in 3D point, down on screen] 640+zp
Look, all we had to do was add a minus sign! Now this makes a small change in the equation 8 and 8a. Here it is:
640*(yp-100) ys = 100 - --------------- ... Eq. 8a [y axis fix] 640+zp
We didn't have to change equation 4 because the screen coordinate (abbreviated "screen coord" below) agrees with the Cartesian coordinate system (defined by the x, y and z axes) we used.
[to make the origin of points at center of screen] (Note: These xp and yp variables have values different from the xp and yp in Eq. 4 and 8a.)
640*(xp+160-160) xs = 160 + ---------------------- ... Eq. 4c (origin at C) 640+zp [y axis fix, origin at C] 640*(yp-100+100) ys = 100 - --------------------- ... Eq. 8c (origin at C) 640+zp [y axis fix, origin at C]
Simplifying, we get a formula that works pretty well for plotting 3D points in SCREEN 13.
640*xp xs = 160 + ----------- ... Eq. 4c' (origin at C) 640+zp [y axis fix, units in pixels] 640*yp ys = 100 - ---------- ... Eq. 8c' (origin at C) 640+zp [y axis fix]
[How things look with the origin at C (orthogonal projection)] (160,100,zp) Q+--------- * P(xp,yp,zp) | / (note: values of xp,yp,zp are different than before) | / (-160,ys,0) (0,0,0) | / (160,ys,0) |=================C======S===============| | / (xs,ys,0) | / pixel for point | / | / | / E |/ eye(160,100,-640) ////////////////behind eye///////////////// ///////////////////////////////////////////
Likewise, we can move the orgin to the eye, if you want, although usually this *isn't* the always the best thing, because a point at the origin will crash your 3D engine (it's equivalent to poking yourself in the eye), unless you write an IF statement to handle the special case! (In fact, all points with z coordinates on or behind the eye shouldn't be displayed!) But this is actually what most 3D engines do (including OpenGL) when doing perspective transform.
(Note: LET xp3d = xp from Eq. 4c' yp3d = yp from Eq. 8c' zp3d = zp+640 ) 640*xp3d xs = 160 + -------------- ... Eq. 4e' (origin at E, y-axis fix) zp3d 640*yp3d ys = 100 - -------------- ... Eq. 8e' (origin at E, y-axis fix) zp3d
Well, if we take a quick look at the xs = x/z, ys = y/z in the introduction, you'll see that 4e' and 8e' are very close. (just take off the centering addition and the *640 which multiplies the x and y by the eye to screen distance). To really get that, you have to measure everything in special units so that the distance from the eye to screen is defined to be 1, and use the coordinate system with the origin (0,0,0) at the eye and do WINDOW SCREEN (-160,100)-(160,100) to center the screen at (0,0,zs). Although that is nice in theory, when you write a game engine, you don't want to be doing extra divide operations, so the forms presented in equation 4e'+8e' or 4c'+8c' works the best. I suggest that you work out the math to prove to yourself that is true.
Well, we have derived several formulas for perspective projection in SCREEN 13, and we found out that the x/z and y/z are accurate ways to do perspective projection when we use the correct coordinate system and units. We will finish this time by writing a simple 3D parametric function plotter.
' QBasic code (finally!) DEFINT A-Z SCREEN 13: CLS '===================================== ' 3D Perspective Projection Test '===================================== 'set grayscale palette FOR i = 0 TO 255: OUT &H3C9, i \ 4: OUT &H3C9, i \ 4: OUT &H3C9, i \ 4: NEXT 'draw wavy thing around zp=100 axis FOR t! = 0 TO 6 STEP .001 xp = INT(100 * COS(t!)) yp = INT(100 * SIN(8 * t!)) zp = INT(99 * SIN(t!) + 100) zdenom = (zp + 640) 'perspective projection (world space to screen space) IF zdenom > 0 THEN xs = (160 + xp * 640& \ zdenom) 'using equation 4c'. ys = (100 - yp * 640& \ zdenom) 'using equation 8c'. r = (640 \ zdenom) 'find size of point CIRCLE (xs, ys), r, 200 - zp 'plot it on the screen! END IF NEXT t! 'draw helix around the y axis FOR t! = 0 TO 60 STEP .001 xp = INT(100 * COS(t!)) yp = INT(t! + .5) zp = INT(100 * SIN(t!) + 100) xp3d = xp yp3d = yp zp3d = zp + 640 'perspective projection (world space to screen space) 'note how zdenom = zp3d IF zp3d > 0 THEN 'if point is in front of eye, then 'project the 3D point to the screen xs = (160 + xp3d * 640& \ zp3d) 'using equation 4e'. ys = (100 - yp3d * 640& \ zp3d) 'using equation 8e'. r = (640 \ zp3d) 'find size of point CIRCLE (xs, ys), r, 200 - zp 'plot it on the screen! END IF NEXT t!
Next time, I'll talk about how to change the field of view, so you can get panoramic scenes or binocular zoom vision in your perspective code.
By Matthew R. Knight <horizonsqb@hotmail.com>
If you have visited the NeoBASIC (http://www.neozones.com) discussion board lately, then I'm sure I need not even mention that the knowledge required for two-dimensional rotation is in sudden demand. I have thus taken it upon myself to write a tutorial of this nature, aiming to introduce the basic concepts and mathematical formulas behind this most interesting field of graphics programming.
In a two-dimensional setting, it is easy to rotate a coordinate pair (defined by an x value and a y value) about a point. The key to two-dimensional rotation, however, is that the rotation must be about the origin of the coordinate system. Initially, this seems like a severe limitation when using QuickBASIC, because the upper left-hand corner of the screen is the physical origin of the screen's two dimensional coordinate system.
Fortunately, there is a way around this. With some simple programming, we will effectively be able to support two coordinate systems. These coordinate systems are the physical coordinate system and the view coordinate system. By creating two variables containing the x and y location on the screen which we require to serve as the origin, and adding those x and y values to the values we obtain from our calculations relative to the physical origin, we can rotate a coordinate pair around literally any point on the screen. If this seems a little fuzzy at the moment, fear not, an example program will follow shortly! ^_^
In a two dimensional coordinate system, you can rotate coordinate pairs in either a clockwise or counterclockwise direction. Simple trigonometric algorithms are used to rotate these coordinate pairs. These formulas are as follows:
x = (x' * cos(angle)) + (y' * sin(angle)) y = (y' * cos(angle)) - (x' * sin(angle))
where x' and y' are the values of x and y prior to transformation.
When describing the amount of rotation desired, and angle argument is used in the equations above. For example, if you wanted a 45-degree clockwise rotation, and angle parameter of -45 degrees would be used. An angle parameter of 45 degrees would be used for counterclockwise rotation.
The trigonometric functions provided by QuickBASIC (SIN, COS, etc.) require the angle argument to be in radians. Because it is easier and more natural to think in degrees, a simple conversion can be used to convert degrees to radians.
Radians = Degrees * .017453 Degrees = Radians * 57.29577
The following example program will simply rotate a pixel around a pre-defined virtual-origin. It is fully commented, and should thus be very simple to understand.
You must, however, consider several issues when doing this seemingly simple example. The first issue is that in order to make a circular pattern (as opposed to an elliptical pattern) you must use an aspect ratio. For the video mode in use, multiplying the rotated y coordinate by .75 works well. The use of .75 is due to the aspect ratio of the pixel. The second issue is the proper use of variable types. When using the trigonometric functions, the angle argument must be expressed as type single and in radians. If this is not done properly, the rotational results will be unpredictable.
SCREEN 9 'Now what on Earth does this line do?! ;) VX = 250 'Virtual-origin X coordinate. VY = 150 'Virtual-origin Y coordinate. X = 100 Y = 100 Rotation = 0 'The rotation counter. Angle = -5 * .017453 DO OldX = X OldY = Y X = (OldX * COS(Angle)) + (OldY * SIN(Angle)) Y = (OldY * COS(Angle)) - (OldX * SIN(Angle)) PSET (X + VX, (.75 * Y) + VY), 15 CIRCLE (X + VX, (.75 * Y) + VY), 5, 15 Rotation = Rotation + 5 LOOP WHILE Rotation < 360
And there you have it! A nice piece of example code, and a concise 65 lines of techno-babble! ^_^ Have fun with your new knowledge! ^_^
By Eclipzer <eclipzer@aol.com>
As we all know, the greatest part about doing 3D graphics is being able to view your models from any angle. If you know anything about 3D rotation at all, then you'll know that it is nothing more than three 2D rotations, one rotation for each axis. You simply supply the number of degrees to rotate about each axis, and calculate the rotated position of each point based on that. For this document we shall use the following terms for the number of degrees to rotate about each axis:
a = z-angle 'degrees to rotate about z-axis b = y-angle 'degrees to rotate about y-axis c = x-angle 'degrees to rotate about x-axis
The key to doing faster rotation is to eliminate as much math as possible within the point rotation routine. Before we can do this we must first take a look at the standard method of point rotation. Our initial point will be (x,y,z) and our final point will be (x'',y'',z'') [read "x double prime, y double prime, z double prime"]:
x' = x*cos(a) - y*sin(a) 'rotate about the y' = y*cos(a) + x*sin(a) 'z-axis x'' = x'*cos(b) - z *sin(b) 'rotate about the z' = z *cos(b) + x'*sin(b) 'y-axis y'' = y'*cos(c) - z'*sin(c) 'rotate about the z'' = z'*cos(c) + y'*sin(c) 'x-axis
Notice that once we have rotated two points about an axis we must save their values to be used in the next axis rotation. This prevents the axes from rotating independently of each other. You should also notice that to get from the inital point (x,y,z) to the final point (x'',y'',z'') it requires 12 multiplications. So for every 3D point that you wish to rotate you must perform 12 multiplications. Ultimately our goal is to reduce the number of multiplications performed per point, thereby increasing speed. Our solution? Simplify! To do this, first we will substitute the actual calculations for x',y',z' into our equations for x'',y'',z'':
x'' = x'*cos(b) - z*sin(b) 'standard x'' calculation = [x*cos(a) - y*sin(a)]*cos(b) - z*sin(b) 'substitute for x' y'' = y'*cos(c) - z'*sin(c) 'standard y'' calculation = [y*cos(a) + x*sin(a)]*cos(c) - [z*cos(b) + x'*sin(b)]*sin(c) 'substitute for y' and z' z'' = z'*cos(c) + y'*sin(c) 'standard z'' calculation = [z*cos(b) + x'*sin(b)]*cos(c) + [y*cos(a) + x*sin(a)]*sin(c) 'substitute for z' and y'
Alright, we've done the first substitution and it seems our equations are even uglier! We've still got x' in both the y'' and z'' equations. It's going to get worse before it gets better. Our next step is to use the distributive property to expand our equations into addition and subtraction of products. From there we will substitute for x' where it exists and then use the distributive property once again:
x'' = [x*cos(a) - y*sin(a)]*cos(b) - z*sin(b) 'substitute for x' = x*cos(a)cos(b) - y*sin(a)cos(b) - z*sin(b) 'distribute cos(b) y'' = [y*cos(a) + x*sin(a)]*cos(c) - [z*cos(b) + x'*sin(b)]*sin(c) 'substitute for y' and z' = y*cos(a)cos(c) + x*sin(a)cos(c) - 'distribute cos(c) z*cos(b)sin(c) - x'*sin(b)sin(c) 'distribute -sin(c) = y*cos(a)cos(c) + x*sin(a)cos(c) - z*cos(b)sin(c) - [x*cos(a) - y*sin(a)]*sin(b)sin(c) 'substitute for x' = y*cos(a)cos(c) + x*sin(a)cos(c) - z*cos(b)sin(c) - x*cos(a)sin(b)sin(c) + y*sin(a)sin(b)sin(c) 'distribute -sin(b)sin(c) z'' = [z*cos(b) + x'*sin(b)]*cos(c) + [y*cos(a) + x*sin(a)]*sin(c) 'substitute for z' and y' = y*cos(a)sin(c) + x*sin(a)sin(c) + 'distribute sin(c) z*cos(b)cos(c) + x'*sin(b)cos(c) 'distribute cos(c) = y*cos(a)sin(c) + x*sin(a)sin(c) + z*cos(b)cos(c) + [x*cos(a) - y*sin(a)]*sin(b)cos(c) 'substitute for x' = y*cos(a)sin(c) + x*sin(a)sin(c) + z*cos(b)cos(c) + x*cos(a)sin(b)cos(c) - y*sin(a)sin(b)cos(c) 'distribute sin(b)cos(c)
Now all our equations are represented by the addition and subtraction of products. Also we have totally eliminated x',y',z' from the equations meaning x'',y'',z'' are only in terms of x,y,z. The next step is to factor out any multiple x,y,z terms. Note that x'' has no terms to be factored:
x'' = x*cos(a)cos(b) - y*sin(a)cos(b) - z*sin(b) 'no factoring needed y'' = y*cos(a)cos(c) + x*sin(a)cos(c) - z*cos(b)sin(c) - x*cos(a)sin(b)sin(c) + y*sin(a)sin(b)sin(c) 'distribute -sin(b)sin(c) = x*[sin(a)cos(c) - cos(a)sin(b)sin(c)] + y*[cos(a)cos(c) + sin(a)sin(b)sin(c)] - z*cos(b)sin(c) 'factor out x and y terms z'' = y*cos(a)sin(c) + x*sin(a)sin(c) + z*cos(b)cos(c) + x*cos(a)sin(b)cos(c) - y*sin(a)sin(b)cos(c) 'distribute sin(b)cos(c) = x*[sin(a)sin(c) + cos(a)sin(b)cos(c)] + y*[cos(a)sin(c) - sin(a)sin(b)cos(c)] + z*cos(b)cos(c) 'factor out x and y terms
Well now our equations look much cleaner, but it seems as if we have more math going on now then before! However, you should notice that the x,y,z terms in each equation are only multiplied by sines and cosines. These sin/cos values are constants and can therefore be precalculated. Not only that but many of the sin/cos products are repeated in each equation so we have even less to do less. For now we will simply calculate our constants and rewrite our equations:
xx = cos(a)cos(b) 'calc xx constant xy = -sin(a)cos(b) 'calc xy constant xz = -sin(b) 'calc xz constant yx = sin(a)cos(c) - cos(a)sin(b)sin(c) 'calc yx constant yy = cos(a)cos(c) + sin(a)sin(b)sin(c) 'calc yy constant yz = -cos(b)sin(c) 'calc yz constant zx = sin(a)sin(c) + cos(a)sin(b)cos(c) 'calc zx constant zy = cos(a)sin(c) - sin(a)sin(b)cos(c) 'calc zy constant zz = cos(b)cos(c) 'calc zz constant x'' = x*xx + y*xy + z*xz 'final x'' equation y'' = x*yx + y*yy + z*yz 'final y'' equation z'' = x*zx + y*zy + z*zz 'final z'' equation
As you can see our final equations look a whole lot nicer now, and we have reduced the number of multiplications per rotation from 12 to 9. This may not seem like a whole lot but with our original method we were doing three times the number of points more multiplications each time we rotated. Food for thought. Also, we only need to calculate the constants once per rotation. So any time we change the angles a,b,c we must recalculate the constants. This is truly a trivial price to pay for the increase in speed. Well that's all there is to it. Enjoy!
-Eclipzer
In the next issue of QB Cult Magazine, the serialization of BASIC Techniques and Utilities will continue with chapter 2, "Variables and Constant Data." We will also see another RPG tutorial from QbProgger, some game reviews, and a whole bunch of good articles and tutorials to help you be the best QB programmers.
Until next issue!
Chris Charabaruk (EvilBeaver), editor