Abe's Demoschool: Part I, mod 13h
Welcome to part I of the Demoschool. In the demoschool I'll guide you through different demoeffects such as paletterotation, plasma, fire, sprites, 3d- vector-graphics, shading, warping, mode-X, Soundblaster and whatever else that might show up. Most parts of the demoschool will be in mode 13h or MCGA-mode as it is also called. That immediately brings up the first question:
What is mode 13h?
Well, the answer is easy, mode 13h is the graphic mode of the PC that most games and demos are written for. The resolution of mode 13h is 320 pixels horizontally and 200 pixels vertically. that makes 320*200 = 64000 pixels totally on the screen. The pixels on the screen can have 256 different colours (each pixel can actually only have one colour at a time, but that colour can be one out of 256 colours in the palette). As you should know, one byte (8 bits) can take on 256 different values. This means that each pixel in mode 13h could be represented by one byte, and that is also the case.
Like I mentioned mode 13h has 320*200 = 64000 pixels which is 64000 bytes. The screen-memory in mode 13h begins at address A000:0000h, and ends at address A000:FFFFh. This is 65535 bytes which leaves about 4 invisible rows below the bottom of the screen.
To write a pixel to the screen, first set segmentregister ES to A000h and calculate an offset and put it in di then write one colorbyte to es:di.
The offset is calculated like this:
Offset = 320*y + x
where x is the x-coordinate and y is the y-coordinate. (0,0) is at the upper left corner of the screen and (319,199) is at the bottom right corner of the screen. Here is a table of the coordinates and their respective offsets.
(0,0) (1,0) (2,0) (3,0) . . . (319,0)
0 0 1 2 3 . . . 319 X-axis
1 320 321 322 323 . . . 639 (319,1)
2 640 641 642 643 . . . 959 (319,2)
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
199 63680 63681 63682 63683 . . . 63999
(0,199) (1,199) (2,199) (3,199) . . . (319,199)
Y-axis
Mode 13h is said to be in a linear memory model that means that the bytes follow each other one after another.
To write a pixel with colour 1 (default blue) at (3,2) you should write value 1 to A000:(320*2 + 3) = A000h:643. Try it, it works.
Here comes an unoptimized example in C:
void putpixel(int x,int y,char color)
{
int offs;
offs=320*y + x; //calculate the offset using the given formula
asm mov ax,0a000h //you cant move a value directly to es
asm mov es,ax //move the screenaddress to es
asm mov di,offs //and the calculated offset to di
asm mov al,color //and the colorbyte to al
asm mov [es:di],al //finally move the pixel to the screen
}
Before putting a pixel to the screen you've gotta be in the MCGA-mode 13h.
Switching into mode 13h is done like this:
mov ax,13h ;move the number of the mode to ax
int 10h ;and enter the mode using the videointerrupt 10h
Switching back to textmode 3 is done like this
mov ax,3 ;mode 3 is the dos standard mode 80*25 16 color textmode
int 10h ;enter textmode 3
In C you could write a function to change the mode like this:
void setmode(int mode)
{
asm mov ax,mode
asm int 10h
}
To switch into mode 13h you would write setmode(0x0013); at the beginning of main and to switch back into textmode write setmode(3);
To optimize the putpixel routine you could replace the multiplication, which is very slow, with shifts and adds. As you should know, shifting left 1 bit is the same as multiplicating by 2, shiftleft 2 bits is the same as multiplicating by 4 and so on. The trick is to split 320 up into powers of 2, 320 is the same as (256 + 64). Y*320 is the same as (y leftshift 8) + (y leftshift 6). Look at the source code for an implementation of the optimized putpixelroutine using inline-assembler in C.
What colour the pixel will have depends on two things: the colourvalue you write to the screen (0-255) and what colour that value represents in the palette. The palette has 256 entries (0 to 255). Each colour in the palette is represented by three bytes, one each for the Red, Green and Blue component of the colour. Each value can vary between 0 and 63. The value gives the intensity of the colour, 0 no intensity, 63 strong intensity. This gives 64^3 possible colours.
Examples of common colours
Red Green Blue Colour
______________________________
0 0 0 BLACK
63 0 0 RED
20 0 0 DARK RED
0 63 0 GREEN
0 0 63 BLUE
63 63 0 YELLOW
63 32 0 ORANGE
63 0 63 PURPLE
0 63 63 CYAN
63 63 63 WHITE
32 32 32 GRAY
10 10 10 DARKGRAY
And So On
The MCGA mode 13h is always started with the same palett (0 black, 1 blue . . .).
But it is easy to change the palett by your own. It is done by writing to ports 3c8h and 3c9h. This is not as hard as it sounds. You just write the indexnumber of the first color you intend to change to port 3c8h. And then write the Red Green and Blue values to port 3c9h. After writing 3 bytes to port 3c9h the index automatically increments itself.
If the palett is stored in an array pal (3*256 bytes long). Let's say that colour 0 is black, colour 1 is blue and colour 2 is gray, then pal will start like this: 0,0,0, 0,0,63, 32,32,32, . . .
Here is a C-function which sets the palette to the values of the array pal:
void setpal(char*pal)
{
int i;
outp(0x3C8,0); //begin with colour 0
for(i=0;i<256*3;i++)
outp(0x3C9,pal[i]); //send the palette-data to port 3c9h
}
The most common way of handling the palette is to make the array pal in a specially written program for the purpose and save the palette to a file on disk. This palette file is easy to recognise because it is 3*256 = 768 bytes big. In the demo you just load the palette file from disk to an array and sets it with the function above.
In assembler you can use a very fast instruction, rep outsb, to set the palette, rep outsb sends CX number of bytes from DS:SI and sends them to port DX.
SI is incremented after each byte.
Here is an assembler routine to set the entire palette.
mov dx,03c8h ;move portnumber to dx
xor al,al ;reset ax
out dx,al ;send 0 to port 3c8h
lds si,pal ;setup ds:si to point to pal
mov cx,3*256 ;move number of bytes to cx
inc dx ;dx = 3c9h
rep outsb ;send 768 bytes from pal to port 3c9h
A common demoeffect is to repeatedly rotate the array pal and calling setpal(pal). Among other effects you could use this technique to make a static plasma effect. First setup the array pal to contain smooth colour runs between some nice colours. Then draw a nice picture on the screen using the sine and cosine functions and at last rotate the palette and set it over and over again.
The picture doesn't have to be a plasma picture you could draw just any picture and watch how it looks when the palette starts rotating. To rotate the array pal just save the first colour (the first three bytes) and then shift all colours down one step in the array (three bytes) and at last set the last colour to the saved colour.
Here's the C-code for rotating the palette.
(it actually rotates colours first to last in the palette)
void rotpal(char* pal, int first, int last)
{
char r,g,b;
int i;
r=pal[first*3 + 0]; //set r,g,b to the first colour
g=pal[first*3 + 1];
b=pal[first*3 + 2];
for(i=first*3;i<(last+1)*3;i++) //move all colours down one step
pal[i]=pal[i+3];
pal[last*3+0]=r; //set the last colour to the saved first colour
pal[last*3+1]=g;
pal[last*3+2]=b;
wtsync(); //wait until the electron beam finishes this frame
setpal(pal); //set the rotated pal
}
You don't have to send first and last as parameters to the function but it has some advantages. In the example program I use an array that has more than 256 colours and each time I rotate the entire palette, except colour 0, but I only set the 256 first colours. I don't rotate colour 0 because I dont want the background colour to change. Try including colour 0 in the rotation to see what I mean.
The wtsync function should always be called before changing the palette to avoid "snow" due to bad syncronization with the electron beam.
Wtsync waits for the vertical retrace which is when the electron beam has finished one frame and jumps back to draw the next.
You could say that the example program is halfway to a real plasma effect, I'll show you a real plasma in a later part of the demoschool.
That should be all for now, view the C-source for comments and implementation.
951213 Abe Raham
mail: dat94avi@bilbo.mdh.se
s-mail:
Albert Veli
Spisringsg. 9
724 76 VÑsterÜs
SWEDEN
DEMO1.C
/********************************************************/
/* */
/* Sourcecode for PART I in Abe's Demoschool */
/* */
/* compile with : */
/* */
/* bcc -1 demo1.c (Borland C) */
/* tcc -1 demo1.c (Turbo C) */
/* */
/* the -1 option allows 286-specific code */
/* (ie shl di,6). */
/* If you are using another compiler remember to */
/* set the settings of the compiler to allow */
/* 286-specific instructions. */
/* */
/* 13/12-95 Dirty Abe */
/* */
/********************************************************/
#include<conio.h>
#include<math.h>
#define PI 3.1415926 /* I suppose most of you've heard of this little weazel */
#define KX 2*PI/320 /* KX & KY are constants for the picture drawing */
#define KY 2*PI/200
/* wtsync waits for the vertical retrace
That is when the electron beam jumps back from the bottom right corner
of the screen to the upper left corner of the screen
This gives us a chance to draw things on the screen, changing the palette and
stuff like that while the electron beam aint drawing */
void wtsync(void)
{
asm mov dx,3DAh
WaitVR1:
asm in al,dx
asm test al,8
asm jne WaitVR1
WaitVR2:
asm in al,dx
asm test al,8
asm je WaitVR2
}
/* sets the 256 colours of the palette to the 256 first colours of pal */
void setpal(char*pal)
{
int i;
outp(0x3c8,0); /* set colorindex to 0 */
for(i=0;i<256*3;i++)
outp(0x3c9,pal[i]); /* send palett-data to the palett */
}
/* reads the current palette and saves it in the array pal */
void getpal(char*pal)
{
int i;
outp(0x3c7,0);
for(i=0;i<256*3;i++)
pal[i]=inp(0x3c9);
}
/* this function sets up a pal with smooth colorruns between some neat colours
feel free to change te colours and see what it looks like */
void coolpal(char*pal)
{
int i,r=0,g=0,b=0,col=0;
outp(0x3c8,0);
for(i=0;i<64;i++) // BLACK - WHITE
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(r<63)r++;
if(g<63)g++;
if(b<63)b++;
}
for(i=0;i<64;i++) // WHITE - YELLOW
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(b>0)b--;
}
for(i=0;i<64;i++) // YELLOW - DARK BLUE
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(r>0)r--;
if(g>0)g--;
if(b<63 && i%2==0)b++;
}
for(i=0;i<64;i++) // DARKBLUE - GREEN
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(g<63)g++;
if(b>0 && i%2==0)b--;
}
for(i=0;i<64;i++) // GREEN - DARK RED
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(r<63 && i%2==0)r++;
if(g>0)g--;
}
for(i=0;i<64;i++) // DARKRED - 63 00 31
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(r<63 && i%2==0)r++;
if(b<63 && i%2==0)b++;
}
for(i=0;i<64;i++) // 63 00 31 - 00 31 63
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(b<63 && i%2==0)b++;
if(g<63 && i%2==0)g++;
if(r>0)r--;
}
for(i=0;i<64;i++) // 00 31 63 - BLACK (00 00 00)
{
pal[col++]=r;
pal[col++]=g;
pal[col++]=b;
if(b>0)b--;
if(g>0 && i%2==0)g--;
}
setpal(pal); // set the first 256 colours of pal
}
/* rotates the colours first - last in the array pal */
void rotpal(char*pal,int first, int last)
{
char r,g,b;
int i;
r=pal[first*3 + 0]; //save the first color in r,g,b
g=pal[first*3 + 1];
b=pal[first*3 + 2];
for(i=first*3;i<(last+1)*3;i++) //move all colors down one step (3 bytes)
pal[i]=pal[i+3];
pal[last*3+0]=r; //set the last color to r,g,b
pal[last*3+1]=g;
pal[last*3+2]=b;
wtsync(); //wait for the vertical retrace
setpal(pal); //and set the first 256 colours of pal
}
/* putpixel draws a pixel with colour col at (x,y) of the screen */
/* the adress of the coordinate (x,y) is 0A000h:offset */
/* the offset is calculated like this: offset = WIDTH*Y + X */
/* the width of the screen is 320 bytes in mode 13h */
void putpixel(int x, int y, char col)
{
asm{
mov ax,0a000h //ax = segment adress of the screen
mov es,ax //es = A000h
mov bx,y //bx = y
mov di,bx //di = y
xchg bh,bl //bx = 256*y
shl di,6 //di = 64*y
add di,bx //di = (256 + 64)*y = 320*y
add di,x //di = 320*y + x
mov al, col //move the colorbyte to al
mov [es:di],al // and move it to the screen
}
}
/* get pixel is pretty similar to putpixel.
It reads the colour at (x,y) and returns it */
char getpixel(int x, int y)
{
char col;
asm{
mov ax,0a000h
mov es,ax
mov bx,y //bx = y
mov di,bx //di = y
xchg bh,bl //bx = 256*y
shl di,6 //di = 64*y
add di,bx //di = (256 + 64)*y = 320*y
add di,x //di = 320*y + x
mov al,[es:di] //move the colour to al
mov col,al //and move it to the char col (a char is one byte)
}
return col; //return the color
}
/* sets the graphic mode to mode mode !? */
void setmode(int mode)
{
asm mov ax,mode
asm int 10h
}
void drawsincos(void)
{
int x,y;
char col;
for(x=0;x<320;x++) //loop across all pixels on the screen
{ // 320 in the x-direction
for(y=0;y<200;y++) // 200 in the y-direction
{
/* calculate a color with the following formula */
col=(sin(x*KX*0.5)*sin(y*KY*0.5))*(127-20)+128;
/* try changing the formula and see how the picture changes */
putpixel(x,y,col); // draw the pixel to the screen
} // end for y
} // end for x
} // endfunction
void filtersincos(void)
{
int x,y;
char col;
for(x=0;x<320;x++) //loop through all pixels but this time
{ // read the existing colours from the screen
for(y=0;y<200;y++) //and modify them with the formula below
{ //this way a "filter" is put on the image
col=getpixel(x,y); //read the colour at (x,y)
col+=(sin(x*KX*10)*sin(y*KY*10))*20; //modify the read color
putpixel(x,y,col); //and write the modified color at the same place
} // end for y
} // end for x
} // endfunction
void main(void)
{
char pal[512*3]; //Use 512 colours 4 the palette (only 256 visible at a time)
setmode(0x0013); // Enter demomode (320*200 256 colors)
// also called MCGA and mode 13h
coolpal(pal); //setup pal to smooth colorruns
drawsincos(); //draw a sincos picture to the screen
getch(); //wait for a keypress
filtersincos(); //change the screen with a sincos "filter"
getch(); //wait for a keypress again
do
{
/* now just rotate the palette */
/* until the keyboard is hit */
rotpal(pal,1,511);
/* notice that colour 0 is not rotated, that is because I don't */
/* want the backgroundcolor to change */
}while(!kbhit());
setmode(3); //get back into textmode
}