[声明]:我对阿贝的例程学校(Abe’s Demoschool)和阿贝的智囊(Abe’s bag of tricks)的翻译得到了原作者的同意。任何人如果想使用阿贝的例程学校(Abe’s Demoschool)和阿贝的智囊(Abe’s bag of tricks)做学习交流之外的事情,请与原作者联系:Albert.Veli@systemdesign.af.se。
Abe's Demoschool Part 1 : mode 13h
阿贝的例程学校 第1部分:mode 13h
欢迎来到例程学校的第一部分内容。在例程学校中,我将带你领略各种各样的图形特效,包括palette rotation(滚动调色板)、plasma、火焰、sprites(精灵)、3D图形、阴影、卷屏、mode-X、soundblaster等等所有可以展示的效果。例程学校中的大部分内容都是基于mode 13h或称MCGA-mode。这就立刻引出了第一个问题:
[什么是mode 13h?]
好吧,答案很简单:mode 13h是PC机的一种图形模式,大部分游戏和图形演示都基于mode 13h完成。mode 13h的分辨率是水平方向320个像素,垂直方向200个像素。这使得屏幕上一共出现320×200=64000个像素。屏幕上的每个像素可以有256种不同的颜色(当然一个像素一次只能具有一种颜色)。你肯定知道,一个byte(8bit)可以取256种不同的值。这意味着mode 13h中的每一个像素都可以用一个byte来表示,而事实也正是如此。
如上所述,mode 13h拥有320×200=64000个像素即64000bytes。Mode 13h的screen-memory从地址A000:0000h开始,到A000:FFFFh结束。这总共有65535bytes,从而使屏幕底部还包含了4个不可见的行。
要往屏幕上写一个像素,须先把段寄存器ES设为A0000h,再计算出该像素的偏移量放入DI,然后向ES:DI写入一个colorbyte(即表示颜色的byte)。偏移量用下式计算:
偏移量 = 320 × y + x
其中x是x坐标值,y是y坐标值。坐标(0,0)位于屏幕的左上角,坐标(319,319)位于屏幕的右下角。下面列出了各个坐标及其相应的偏移量:
(0,0) (1,0) (2,0) (3,0) … … … (319,0)
0 0 1 2 3 … … … 319 ------x轴-----à
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轴
mode 13h属于一种线性存储模型(linear memory model),即byte一个接一个的顺序排列。
要在坐标(3,2)处用颜色1(缺省值是蓝色)写入一个像素,你应该向A000:(320×2+3)=A000:643处写入值1。试试吧,一定行。
下面是用C写的一个未优化的例子:
void putpixel(int x,int y,char color)
{
int offs;
offs=320*y + x; //使用公式计算偏移量
asm mov ax,0a000h //你不能把一个值直接放到ES中
asm mov es,ax //把屏幕地址放入ES
asm mov di,offs //把计算出来的偏移量放入DI
asm mov al,color //把colorbyte放入AL
asm mov [es:di],al //最后将像素写入屏幕
}
在向屏幕写像素之前,你必须已经处于MCGA-mode 13h模式。可以用下面的方法切换到这个mode 13h:
mov ax,13h ;把模式代号放入AX
int 10h ;使用videointerrupt 10h进入改模式
可以用下面的方法切换回我们常用的textmode 3:
mov ax,3 ;mode 3是DOS的标准80×25 16色文本模式
int 10h ;进入textmode 3
你可以用C编写一个切换屏幕模式的函数:
void setmode(int mode)
{
asm mov ax,mode
asm int 10h
}
这样,你就可以在main程序开头用setmode(0x0013);切换到mode 13h,再用setmode(3);切换回文本模式。
要优化putpixel函数,你可以将很慢的乘法替换为位运算和加法。你肯定也知道,向左移1位等同于乘以2,向左移2位等同于乘以4,等等。所使用的技巧是,将320分为几个以2为底的幂之和。320即(256+64);y×320即(y左移8位)+(y左移6位)。可以参看使用inline-assembler的优化过的putpixel实现之C代码。
一个像素具有什么颜色取决于两个因素:你向屏幕写入的颜色值(0至255)以及该值在调色板中所代表的颜色。调色板共有256个入口(0到255)。调色板中的每一种颜色由3个byte表示,分别代表红、绿、蓝三种颜色。每一个byte值都在0到63之间。值的大小表示了颜色的强度:0表示没有强度,63表示最大强度。这样就有了64的3次方种可能的颜色。
[常见的颜色]
红 绿 蓝 颜色
______________________________
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暗灰
MCGA-mode 13h总是以相同的调色板开始(即0表示黑,1表示蓝,等等)。但要自己改变调色板是很容易的。通过向3c8h和8c9h端口写入适当信息,我们就可以改变调色板。这并没有看上去那么难。你只要向3c8h端口写入你希望改变的起始颜色的索引值,再向3c9h端口写入红、绿、蓝的值就可以了。向3c9h写入了这三个colorbyte后,索引会自动递增。
如果调色板被存放在一个数组pal(3×256bytes长),我们假设0代表黑,1代表蓝,2代表灰,那么pal将会像这样开始:0,0,0;0,0,63;32,32,32;……
下面是一个C函数,它根据数组pal的值来设置调色板:
void setpal(char*pal)
{
int i;
outp(0x3C8,0); //从颜色值0开始
for(i=0;i<256*3;i++)
outp(0x3C9,pal[i]); //将调色板数据送到3c9h端口
}
控制调色板最常用的方法就是在特定的程序里使用数组pal,而把调色板的信息存到磁盘的一个文件中。这个调色板文件很容易辨认,因为它的大小总是3×256=768bytes。在程序中你只需要从此磁盘加载调色板文件并将信息放入数组,再用上面讲到的setpal函数设置调色板。
在汇编语言中,你可以使用一个非常快的指令rep outsb来设置调色板。rep outsb 指令从DS:SI将byte传到DX值指示的端口,传送的byte数目由CX的值指定。每传一个byte,SI就增加1。
下面是设置整个调色板的汇编程序:
mov dx,03c8h ;将端口号放到DX
xor al,al ;将AX清零
out dx,al ;将0传到3c8h端口
lds si,pal ;让DS:SI指向pal
mov cx,3*256 ;将要传送的byte数目存放在CX
inc dx ;DX = 3c9h
rep outsb ;从pal向3c9h端口传送768bytes
一种常见的图形效果就是不断的循环改变数组pal并调用setpal(pal)以形成颜色的循环变化。你可以在其它图形特效里使用这种技术来制造静态plasma效果。首先设置数组pal,使其包含一些好看颜色之间平滑的循环变化;然后使用sin或者cos函数在屏幕上画出优美的图案;最后不断的滚动调色板,使其不停的循环即可。
画出的图案并不必须是plasma picture,你可以画任意图案上去,看看当调色板滚动的时候会产生什么效果。要使数组pal循环滚动,先将第一个颜色(即前三个byte)保存起来,然后将数组中所有的颜色值向前移动一个位置(即3个byte),最后再将尾部空出来的位置设为刚才保存的颜色。
滚动调色板的C代吗如下:
(实际上就是每次将调色板中第一个颜色滚动到最末尾的位置)
void rotpal(char*pal,int first, int last)
{
char r,g,b;
int i;
r=pal[first*3 + 0]; //将第一个颜色的三个值分别赋给r,g,b
g=pal[first*3 + 1];
b=pal[first*3 + 2];
for(i=first*3;i<(last+1)*3;i++) //将所有颜色向前移动一个位置
pal[i]=pal[i+3];
pal[last*3+0]=r; //将最末尾设置为保存的原第一个颜色
pal[last*3+1]=g;
pal[last*3+2]=b;
wtsync(); //等待电子束扫完当前帧
setpal(pal); //根据滚动过的pal设置调色板
}
你并不一定非要将第一个和最末个颜色作为函数参数。但这样做的确有些好处。在例程中,我使用了一个拥有多于256种颜色的输组,每次滚动整个调色板时,都没有改变值为0的颜色,我只设置前256个颜色。我不让值为0的颜色参与滚动是因为,我不希望让背景颜色也改变。你可以让其也参加滚动试一试,这样就能明白我的意思了。
在每次改变调色板之前总应该调用wtsync函数,以避免因为与电子束不同步而产生的所谓“雪花”效果。wtsync等待垂直回溯,即电子束扫完一帧后跳回初始位置以便开始扫下一帧的时刻。
也许你会说本次的例程只能算是半个plasma特效——好吧,在例程学校以后的部分里,我会给你演示真正的plasma特效。
就到此为止,去看看例程的C源代码吧。
[例程在http://www.mds.mdh.se/f%C3%B6reningar/small/abe/abedemo1.zip下载。]