The following procedure (SetPalBlock) sets a block of the palette, utilizing the auto-increment feature of the palette. It is much faster than using the equivelent BIOS function, and is useful for setting the entire palette at once...
First, we need to have a procedure that initializes the fade. I called this function StartPalFade. You pass to it the destination palette (The palette you want to end up with), and the number of steps you want to take to get there. After you initialize it, you can fade the palette as fast as you would like. When you want to update the palette (Step to the next position) call PalFadeContinue. It returns a TRUE if the fade is done, or a FALSE if it is continuing.
Instead of explaining how to fade the entire palette, I will explain how to fade one color, and then extend that to a whole palette....
To start, you (StartPalFade) are given three numbers: An initial value, a final value, and the number of steps you are allowed to take in between them. With this information, we need to calculate the differance in between each step (the delta). The math is pretty simple: (ColorF is the Final color, ColorI is the Initial color)
I use two arrays for this set of two functions... One is the delta array, and the other holds the current palette. Both have 768 entries and both are composed of integers. Why didn't I just read the palette entries every time instead of saving them? If we did that, we would lose all of our fractional data that we worked so hard for! We have already covered how to initialize the delta array (Just repeat 768 times: one for each color), but how do we initialize the CurrentPalette? We simply read the current palette out of the VGA (using regs $3C7 and $3C9 remember?) and shift
it left nine times.
With all of this, we can now write the PalFadeContinue function. All it has to do is decrement step (If zero, quit with a TRUE), add all of Delta to Cur (the current palette), and output Cur SHR 9 to the palette registers. This would look something like this:
I use these routines with my Timer unit the most. This makes sure that the palette fades smoothly and constantly (And it looks the same on all computers). Check out: Particle for an example...
We need a function to load a palette file from disk. It doesn't need to be fancy, but having one is a neccessity. The following procedure does just that:
The units it requires are at The download page
On a VGA, the palette controls how color numbers are displayed. For example, when you draw a line in color #1, it turns out blue. How does the VGA decide that color #1 is blue? It turns out that it uses an array of bytes to determine the RGB (Red-Green-Blue) values to send to the monitor. The default RGB color for color #1 is blue. You can display all of the default colors with the following short program (Notice that many of the colors are uninteresting green shades... ):
PROGRAM DisplayAllColors;
USES
GraphPro;
VAR I : INTEGER;
BEGIN
InitGraph;
FOR I := 0 TO 255 DO
SetPixel(I, 0, I);
READLN;
CloseGraph;
END.
The interface between the VGA card and the CPU is suprisingly simple. It consists of three ports (Ports can be accessed in TP with the Port[] array.), $3C7, $3C8 and $3C9. Port $3C9 is the data port. Reading or writing to it updates a palette entry. Port $3C8 sets the current color number for writing. Port $3C7 sets the current color number of the next port to read. An example will make this more clear:
PROCEDURE SetColor(Index, R, G, B : BYTE);
BEGIN
Port[$3C8] := Index; { Set the port to write to }
Port[$3C9] := R; { Set the Red component }
Port[$3C9] := G; { Set the Green component }
Port[$3C9] := B; { Set the Blue component }
END;
Variables R, G, and B can vary from 0 to 63. 63 is the brightest and 0 is the darkest. You can see from this example that the data port is automatically incrementing. After you write to the data port ($3C9), it increments to point to the next color slot. Here is an implementation of ReadColor...
PROCEDURE ReadColor(Index : BYTE; VAR R, G, B : BYTE);
BEGIN
Port[$3C7] := Index; { Set the port to read from }
R := Port[$3C9]; { Read the Red component }
G := Port[$3C9]; { Read the Green component }
B := Port[$3C9]; { Read the Blue component }
END;
These two routines, while very simple, are very powerful. When you change the attribute of a color, it changes all of that color all over the screen to the new color. This means that you can do animation without redrawing anything!
PROCEDURE SetPalBlock(Start : BYTE; Number : WORD; Pal : PalArrayPtr); ASSEMBLER;
ASM
les DI, Pal
mov AL, Start
mov DX, 3C8h
out DX, AL
inc DX
mov CX, Number
@SetPalLoop:
mov AL, [ES:DI]
out DX, AL
mov AL, [ES:DI+1]
out DX, AL
mov AL, [ES:DI+2]
out DX, AL
add DI, 3
dec CL
jnz @SetPalLoop
END;
Call this function with Start = to the first color you want to change, Number = to the number of
colors you want to change, and Pal = to the data to set it to... For example:
SetPalBlock(0, 256, MyPal); { Sets the entire palette to MyPal }
Fading the palette is as easy as changing an entry smoothly, starting at the source and ending at that target. The kind of fade I will describe is a very flexible one. You can fade from any palette to any other palette. This can be used to fade to black, white, black and white, purple, or anything else.
Delta := (ColorF - ColorI) / Steps;
Since we don't want to use REAL numbers, we'll use nice fixed point numbers (Imaginary numbers are no fun either! :). This turns the equation into:
Delta := ((ColorF - ColorI) SHL 9) DIV Steps;
How did I decide to SHL by 9? Well I'm using palette entries that range from 0 to 63. That's six bits. I need a sign bit to step backwards. That's seven bits total. I'm going to use a 16 bit integer to hold the number. 16-7 = 9 bits. I've found that nine bits of fixed point fraction is more that enough for this application, whereas one bit (Using a byte instead of an integer) is WAY to little.
FUNCTION PalFadeContinue : BOOLEAN;
VAR I : INTEGER;
BEGIN
IF Step = 0 THEN
BEGIN
PalFadeContinue := TRUE;
EXIT;
END;
Port[$3C8] := 0;
FOR I := 0 TO 767 DO
BEGIN
Cur[I] := Cur[I] + Delta[I];
Port[$3C9] := Cur[I] SHR 9;
END;
PalFadeContinue := FALSE;
END;
Pretty cool huh? Not only is it fast, but it's flexible. You can fade to any palette, which can cause neat (well planned) effects that boggle the mind.
I'm making steering this discussion into creating an indepentant unit that works with all 256 color video modes (From 320x200 to 1280x1024...). So, it makes sense to not put these functions into the GraphPro unit that we're developing. Therefore, this unit needs to have some support functions that don't have an affect on the display. This section is for them.
PROCEDURE LoadPal(Filename : STRING; Pal : PalArrayPtr);
VAR F : FILE;
BEGIN
ASSIGN(F, Filename);
RESET(F, 3);
BlockRead(F, Pal^, FileSize(F));
CLOSE(F);
END;
As you can see, it just reads the entire palette file into a palette array specified by Pal. The file is just an RGB triplet file with one byte for R, G and B. Therefore, a complete palette with 256 colors would be 3*256=768 bytes long.
This program does some palette animation, and it demonstrates fading...
See TP Examples for other (more exciting) examples.
PROGRAM PaletteExample;
USES
GraphPro, { InitGraph, CloseGraph, VLine }
Palette, { Lots of functions... }
CRT; { Uses KeyPressed & ReadKey }
VAR
NewPal, BlackPal : PalArrayPtr;
I, J : INTEGER;
BEGIN
InitGraph;
New(BlackPal);
New(NewPal);
FillChar(BlackPal^, SIZEOF(PalArrayType), 0);
SetPalBlock(0, 256, BlackPal);
FOR I := 0 TO 319 DO
VLine(I, 0, 199, I AND 31);
J := 0;
WHILE NOT KeyPressed DO
BEGIN
WHILE (Port[$3DA] AND 8) = 0 DO; { Wait for vertical retrace }
WHILE (Port[$3DA] AND 8) = 8 DO; { Wait while vertical retrace }
FOR I := 0 TO 15 DO
SetPal((I+J) AND 31, I*4, I*4, 0);
FOR I := 0 TO 15 DO
SetPal((I+J+16) AND 31, 63-I*4, 63-I*4, 0);
INC(J);
END;
ReadKey;
StartPalFade(BlackPal, 60*1);
FinishFade; { Does the same thing as FadeToBlack }
FOR I := 0 TO 15 DO
BEGIN
NewPal^[I].R := I*4;
NewPal^[I].G := I*4;
NewPal^[I].B := I*4;
NewPal^[I+16].R := 63-I*4;
NewPal^[I+16].G := 63-I*4;
NewPal^[I+16].B := 63-I*4;
END;
StartPalFade(NewPal, 60*2);
FinishFade;
FadeToWhite(1);
FadeToBlack(0.5);
Dispose(BlackPal);
CloseGraph;
END.