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 PaletteIn 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.