The aim of this tutorial is to show newbies how they can
achieve simple color cycling effects in assembly.
It assumes that you are familiar with the basics of ASM on the PC (if not,
you should read Cruehead's fine tutorial on ASM first).
When you draw a picture on the screen you use a defined set of colors, called the palette. Of course you can change the values of the colors defined in the standard palette, to be able to use your own set of colors. If you rotate the value of colors continuosly, you do 'color-cycling'... Now you might ask, "What the hell can i use that for ???". Okay, apart from showing off with it ("Hah, you lamer can't even do a simple color-cycle !" :-P) you can give the impression of "movement" in a picture. Or you can let something glow (eg. cycling from dark colors to light colors). Amiga-freaks should be familiar with it anyway (the Amiga had color-cycling implemented in one of its graphic-chips). In this tutorial, i'll give you an example of a color cycle which looks like something is being moved.
First thing you should know is how to get into a proper graphic mode. For this tutorial we will be messing around in Mode 13h (this is 320x200 with 256 colors) coz it's very simple to work with it. To get into Mode 13h, we have to call the BIOS Interrupt 10h (which is used for Video/Display routines). Switching Video-modes is done via function 00 of Int 10h:
AH = 00 AL = 00 40x25 B/W text (CGA,EGA,MCGA,VGA) = 01 40x25 16 color text (CGA,EGA,MCGA,VGA) = 02 80x25 16 shades of gray text (CGA,EGA,MCGA,VGA) = 03 80x25 16 color text (CGA,EGA,MCGA,VGA) = 04 320x200 4 color graphics (CGA,EGA,MCGA,VGA) = 05 320x200 4 color graphics (CGA,EGA,MCGA,VGA) = 06 640x200 B/W graphics (CGA,EGA,MCGA,VGA) = 07 80x25 Monochrome text (MDA,HERC,EGA,VGA) = 08 160x200 16 color graphics (PCjr) = 09 320x200 16 color graphics (PCjr) = 0A 640x200 4 color graphics (PCjr) = 0B Reserved (EGA BIOS function 11) = 0C Reserved (EGA BIOS function 11) = 0D 320x200 16 color graphics (EGA,VGA) = 0E 640x200 16 color graphics (EGA,VGA) = 0F 640x350 Monochrome graphics (EGA,VGA) = 10 640x350 16 color graphics (EGA or VGA with 128K) 640x350 4 color graphics (64K EGA) = 11 640x480 B/W graphics (MCGA,VGA) = 12 640x480 16 color graphics (VGA) = 13 320x200 256 color graphics (MCGA,VGA) - if AL bit 7=1, prevents EGA,MCGA & VGA from clearing displayYou call Int 10h to set mode 13h with the following code:
mov ah, 00h ; Set video mode mov al, 13h ; Use mode 13h int 10h ; ... let's go !The Palette
In Mode 13h, we have 256 colors to play with. That gives us in hexadecimal the colour numbers from 00h to FFh. Each color is represented through 3 byte sized values: the Red, Green and Blue components (RGB). The RGB values range from 0 to 63 decimal or 0h to 40h in hexadecimal. So the color Black would have 0,0,0 as RGB values and White would be 63,63,63 .
Now we need a method to change the colors of the standard VGA palette. This is done with the VGA registers. Here is a description of the VGA registers concerning the palette:
VGA-Register 03C7h Sets the palette in read mode VGA-Register 03C8h Sets the palette in write mode VGA-Register 03C9h Read or write 3 RGB values, after every third write the color you are setting is incremented by one.
"How can i access these strange registers ?" you might yell... Well, you use the ASM commands IN and OUT. IN and OUT are used to send/receive data from one of the 65536 possible hardware ports of the PC (like the serial port for instance). Here is the syntax for both commands:
IN ACCUMULATOR, PORT Description: Reads some data from PORT into ACCUMULATOR where ACCUMULATOR can be AL (byte), AX (word) or EAX (double-word) depending on the size of the data you want to read and PORT can either be an immediate value from 0 to 0FFh or DX. e.g.: in ax, 0A0h ;read word from port 0A0h into ax or mov dx, 03C7h ;put the VGA-PaletteRead Register in dx mov al, 1 ;put the color number to read in al out dx, al ;set palette in READ mode for color 1 mov dx, 03C9h ;put the VGA-PaletteData Register in dx in al, dx ;get Red value of color 1 in al OUT PORT, ACCUMULATOR Description: Sends some data from ACCUMULATOR to PORT where ACCUMULATOR can be AL (byte), AX (word) or EAX (double-word) depending on the size of the data you want to send and PORT can either be an immediate value from 0 to 0FFh or DX. e.g.: out 0A0h, ax ;send word in ax to port 0A0h or mov dx, 03C8h ;put the VGA-PaletteWrite Register in dx mov al, 3 ;put the color number to write in al out dx, al ;Set palette in WRITE mode for color 3 mov dx, 03C9h ;put the VGA-PaletteData Register in dx mov al, 63 ;use 63 as new Red value for color 3 in al out dx, al ;send it ...Now we have the 'tools' to change colors in the palette. A nice feature if you want read or write several colors in a row is the automatic color increasing after every 3rd read/write when you use the VGA-PaletteData Register. You can use it like this:
mov dx, 03C8h ;put the VGA-PaletteWrite Register in dx mov al, 3 ;put the color number to write in al out dx, al ;Set palette in WRITE mode for color 3 mov dx, 03C9h ;put the VGA-PaletteData Register in dx mov al, 63 ;use 63 as new Red value for color 3 in al LoopCol: out dx, al ;send it ... out dx, al out dx, al dec al cmp al, 0 jnz LoopCol
This code starts with setting the palette in WRITE mode for color number 3 (we want to change the colors) and then writes three times the value 63 to the VGA-PaletteData register. After that, the value in al is decreased by one and the new value is sent again 3 times to the PaletteData Register, then again decreased, then again sent 3 times and so and so on ... until finally al is ZERO. What does that do ? After the value 63 is sent 3 times to the VGA register, the VGA automatically increases the color number you are about to change by one. So the next time the value 62 from al is sent, color number 4 is changed accordingly. This goes on until al reaches zero. As a result you have changed colors 3 to 67 in shades from white to black.
Ha ha, i bet you wondered about the "other things" in the topic line. Well, other things are Arrays in this case ;-)
What the heck is an array ? And what do i need it for ?
Don't panic, i'll tell you (almost) everything 'bout arrays... An Array is a group of data items which all have the
same type. For example a book is an array of pages (since each page is generally the same type and contains string
data :-)). Usually, you declare a single variable in ASM like this:
Name size initial-value MyVar DB 0This declares the variable MyVar with the size of one byte (with the statement DB) and the initial value of zero. If you want MyVar to have no specific value from the start on, you can declare it uninitialized with the "?" operator:
Name size initial-value MyVar DB ?Okay, now how can i declare an array ? Quite simple. You just use the number of elements along with the "dup" operator:
Name size No. dup initial-value MyArray DB 10 dup ?This declares a byte-sized array which has 10 uninitialized elements (or you could say MyArray is made up of 10 bytes uninitialized data).
.DATA Bored1 DB 0 Bored2 DW 0 Whazzup DB ? Badjoke DB 0You will agree with me that this must be the most boring data segment you have ever seen in a ASM source code. But now to a data segment which contains an array:
.DATA ;**** BEGIN OF NAUGHTY DATA DECLARATIONS **** Naughty DB 11 ;**** HOORAY - ARRAY ! **** Hooray DB 128 dup 0 ; HEY ! IT'S AN ARRAY ! Cheers DW 7 Drunk DB ?Isn't it wonderful ? Ain't it pretty ? Yes, arrays can do wonderful things for us all...
.DATA ;beginning of data segment... ;*** Virtual Screen *** VScreen DB 64000 dup 0Referencing one of the elements of an array is also very easy. There are 2 methods of referencing, direct and indirect. Lets say you have a BYTE-sized array of 20 elements which is called TestArray and you want to get the ninth element of it into register cl. The definition of the array is:
TestArray DB 20 dup 0Then the code for moving the contents of element 9 into cl will look like:
mov cl, [TestArray + 8] ;direct reference of element 9or for an indirect reference:
mov ax, 8 mov cl, [TestArray + ax] ;indirect reference of element 9Note that every array starts with element 0 so the ninth element is number 8 !!! Now referencing an array which has a size other than byte is a bit more difficult. You have to multiply the index with the size divided through bytes in order to get the right reference. Lets take a WORD-sized array this time:
TestArray DW 20 dup 0Then the code for moving the contents of element 9 into cx (WORD-sized) will look like:
mov ax, 8 ;ninth element mul 2 ;multiplied with (size/bytes): a word consists of 2 bytes mov cx, [TestArray + ax] ;indirect reference of element 9I hope you get the point. Of course it is possible to declare Arrays in other sizes than byte or word, but since this is NOT a tutorial on DATA STRUCTURES IN ASM i will skip this now.
Let's sum up again what we need for the color cycling routine:
This example will display a string on the screen and then cycles the color of the text through
shades of yellow. It is structured like this:
CyclePtr DW 0CyclePtr is obviously a WORD-sized variable (DW) with the initial value of zero. CyclePtr is used to store the current position in the array ColList.
Text1 DB 'PALETTE-CYCLE DEMO PART I',0Text1 holds the string that should be printed on the screen.
Credits DB 0Dh, 0Ah,'This was the Palette-Cycle Demo',0Dh,0Ah etc...Credits holds the string that should be printed on exit.
ColList DB 0 , 0 , 0 , 1 , 1 , 0 , 2 , 2 , 0 , 3 , 3 , 0 , 4 , 4 , 1 , 5 , 5 , 1 , 6 , 6 etc...This is the array that holds the color values which are going to be cycled. It ends with 3 times 0FFh as a marker for the end of the color table.
.MODEL SMALL .STACK 200h .DATA CyclePtr DW 0 Text1 DB 'PALETTE-CYCLE DEMO PART I',0 Credits DB 0Dh, 0Ah,'This was the Palette-Cycle Demo',0Dh,0Ah DB ' Part I ',0Dh,0Ah DB ' done by Icedragon from MiB ',0Dh,0Ah DB ' 1998/12/16 ',0Dh,0Ah DB ' ',0Dh,0Ah DB ' visit our homepage at ',0Dh,0Ah DB ' http:\\www.mib99.cjb.net ',0Dh,0Ah DB ' ',0Dh,0Ah,0 ; *** This is the sample palette for the cycled colors. *** ; *** The 3 0FFh bytes at the end are put in to make *** ; *** sure that the index gets the proper end value (0FFh) *** ; *** when it is moved 3 bytes forward. *** ColList DB 0 , 0 , 0 , 1 , 1 , 0 , 2 , 2 , 0 , 3 , 3 , 0 , 4 , 4 , 1 , 5 , 5 , 1 , 6 , 6 DB 1 , 7 , 7 , 2 , 8 , 8 , 2 , 9 , 9 , 2 , 10 , 10 , 3 , 11 , 11 , 3 , 12 , 12 , 3 , 13 DB 13 , 4 , 14 , 14 , 4 , 15 , 15 , 4 , 16 , 16 , 5 , 17 , 17 , 5 , 18 , 18 , 5 , 19 , 19 , 6 DB 20 , 20 , 6 , 21 , 21 , 6 , 22 , 22 , 7 , 23 , 23 , 7 , 24 , 24 , 7 , 25 , 25 , 8 , 26 , 26 DB 8 , 27 , 27 , 8 , 28 , 28 , 9 , 29 , 29 , 9 , 30 , 30 , 9 , 31 , 31 , 10 , 32 , 32 , 10 , 33 DB 33 , 10 , 34 , 34 , 11 , 35 , 35 , 11 , 36 , 36 , 11 , 37 , 37 , 12 , 38 , 38 , 12 , 39 , 39 , 12 DB 40 , 40 , 13 , 41 , 41 , 13 , 42 , 42 , 13 , 43 , 43 , 14 , 44 , 44 , 14 , 45 , 45 , 14 , 46 , 46 DB 15 , 47 , 47 , 15 , 48 , 48 , 15 , 49 , 49 , 16 , 50 , 50 , 16 , 51 , 51 , 16 , 52 , 52 , 17 , 53 DB 53 , 17 , 54 , 54 , 17 , 55 , 55 , 18 , 56 , 56 , 18 , 57 , 57 , 18 , 58 , 58 , 19 , 59 , 59 , 19 DB 60 , 60 , 19 , 61 , 61 , 20 , 62 , 62 , 20 , 63 , 63 , 21 , 62 , 62 , 20 , 61 , 61 , 20 , 60 , 60 DB 20 , 59 , 59 , 19 , 58 , 58 , 19 , 57 , 57 , 19 , 56 , 56 , 18 , 55 , 55 , 18 , 54 , 54 , 18 , 53 DB 53 , 17 , 52 , 52 , 17 , 51 , 51 , 17 , 50 , 50 , 16 , 49 , 49 , 16 , 48 , 48 , 16 , 47 , 47 , 15 DB 46 , 46 , 15 , 45 , 45 , 15 , 44 , 44 , 14 , 43 , 43 , 14 , 42 , 42 , 14 , 41 , 41 , 13 , 40 , 40 DB 13 , 39 , 39 , 13 , 38 , 38 , 12 , 37 , 37 , 12 , 36 , 36 , 12 , 35 , 35 , 11 , 34 , 34 , 11 , 33 DB 33 , 11 , 32 , 32 , 10 , 31 , 31 , 10 , 30 , 30 , 10 , 29 , 29 , 9 , 28 , 28 , 9 , 27 , 27 , 9 DB 26 , 26 , 8 , 25 , 25 , 8 , 24 , 24 , 8 , 23 , 23 , 7 , 22 , 22 , 7 , 21 , 21 , 7 , 20 , 20 DB 6 , 19 , 19 , 6 , 18 , 18 , 6 , 17 , 17 , 5 , 16 , 16 , 5 , 15 , 15 , 5 , 14 , 14 , 4 , 13 DB 13 , 4 , 12 , 12 , 4 , 11 , 11 , 3 , 10 , 10 , 3 , 9 , 9 , 3 , 8 , 8 , 2 , 7 , 7 , 2 DB 6 , 6 , 2 , 5 , 5 , 1 , 4 , 4 , 1 , 3 , 3 , 1 , 2 , 2 , 0 , 1 , 1 , 0 , 0 , 0 DB 0 , 0FFh, 0FFh, 0FFh .CODE .386 ;*** This is a macro to wait for the start of a vertical retrace *** ;*** During a vertical retrace you have flicker-free access to *** ;*** the VGA. *** ;*** It first checks if a Retrace is ALREADY in progress, *** ;*** coz in this case we have to wait for the NEXT one to start *** ;*** (this way you can be sure that you ALWAYS get the full time *** ;*** for the flicker-free access) *** WaitVRetrace MACRO LOCAL VRetrace,NoVRetrace push ax ;save contents of ax + dx push dx mov dx,03dah ;check the VRetrace: in al,dx ;VGA-Register 03DAh test al,00001000b ;if we are in the middle of a Vertical Retrace... jnz VRetrace ;if yes, then skip this one and NoVRetrace: in al,dx ;wait for the beginning of a new one... test al,00001000b jz NoVRetrace pop dx ;restore ax + dx pop ax ENDM ;******* This routine cycles the first color in the palette thru the array ColList CyclePal PROC push ax ;save regs push bx push cx push dx xor ax, ax ;clear regs xor bx, bx mov si, CyclePtr ;load cycle pointer lodsb ;get color value cmp al, 0FFh ;end of palette table ? jnz NotEnd mov CyclePtr, OFFSET ColList ;yes, so reset pointer mov si, CyclePtr ;and load it again lodsb ;and get the color value NotEnd: mov cl, al mov al, 1h ; Put the number of the color to cycle in AL mov dx, 03C8h ; Put the DAC write register in DX out dx, al ; Send color to port DX inc dx ; Now use port 03C9h mov al, cl NextCol: inc bx ; counter for loop out dx, al ; Send Red value to port DX lodsb ; get next color value out dx, al ; Send Green value to port DX lodsb ; get next color value out dx, al ; Send Blue value to port DX lodsb ; get next color value cmp al, 0FFh ; is pointer at the end of the palette ? jnz NotEnd2 ; if not, go on... mov si, OFFSET ColList ; at end, so reset to start of the palette table lodsb ;get value from table NotEnd2: add CyclePtr, 3 ;move pointer in Palette ahead to next color pop dx ;then restore regs... pop cx pop bx pop ax ret ;...and return ! CyclePal ENDP WriteChar PROC mov bp, OFFSET Text1 ;point bp to string Text1 LoopWrite: mov al, ds:[bp] ;load next char from string into al mov ah, 0Eh ;function 0Eh WriteCharTTY (Write Character in Teletype Mode) mov bh, 0h ;must be zero for graphics mode mov bl, 1h ;foreground color 1 int 10h ;call BIOS interrupt 10h function WriteCharTTY inc bp ;point bp to next char cmp al,0 ;string end ? jnz LoopWrite ;if not then do next char.... ret ;... otherwise return ! WriteChar ENDP START: mov ax, @data mov ds, ax mov CyclePtr, OFFSET ColList ;initialize the pointer for the ColList array ;Init 300x200 mov ah, 00h ; Set video mode mov al, 13h ; Use mode 13h int 10h ; ... let's go ! ;Set Cursor to middle of screen mov ah, 02h ;SetCursor function mov bh, 0h ;page number (0 for graphics) mov dh, 0ah ;row mov dl, 08h ;column int 10h ;call function SetCursor cli ; clear the interrupt flag call WriteChar ;print string on screen MainLoop: WaitVRetrace ;wait for a vertical retrace WaitVRetrace call CyclePal ;then cycle the palette... ;Keypress ? mov ah, 01h ; Check for keypress int 16h ; Is a key waiting in the buffer? jz MainLoop ; No, keep on going mov ah, 00h ; yes, so get the key int 16h sti ; set the interrupt flag again TextMode: ;Back to text mode mov ah, 00h mov al, 03h int 10h mov si, OFFSET Credits ;print Credits... WriteTxt: WaitVRetrace WaitVRetrace WaitVRetrace WaitVRetrace WaitVRetrace lodsb mov ah, 0eh xor bx, bx int 10h cmp al, 0 jnz WriteTxt ;Exit to DOS ExitDos: mov ax, 4c00h int 21h END START
Have some fun with the example code, modify it - rip it - whatever... Main thing is that you got the point of how to do a color cycle in the first place. I will actually add a second code example very soon, so check our homepage if interested. Of course there will also be tutorials about other demo-effects in assembly on the MiB-page (like scrolltext and other nifty FX you always wanted to implement in that patcher of yours...). If you have any questions/suggestions concerning the code/tutorial or my sense of humour then mail me at icedragon@thevortex.com.