灵活地应用画点功能可以绘制出更复杂的图形。刚刚讨论的"画文字"是画点功能的一个具体地应用,我们下面的任务就是要解决如何画出任意方向直线的问题。
解析几何告诉我们坐标系中任意一条直线都可以用方程"y=kx+b"表示,其中k是这条直线的斜率,b是这条直线在y轴上的截距。我们只要根据这个公式编制程序就能在屏幕上画出直线来。请看下面这个程序:
code segment
assume cs:code,ds:code
org 100h
start: jmp go_on ;跳过数据区
k db 0,0 ;保存斜率
x0 db 0 ;起始点的X坐标
y0 db 0 ;起始点的Y坐标
x1 db 200 ;终止点的X坐标
y1 db 200 ;终止点的Y坐标
go_on: mov al,y1 ;计算Y1-Y0
sub al,y0
mov k,al ;将Y1-Y0保存至K
mov al,x1 ;计算X1-X0
sub al,x0
mov k+1,al ;将X1-X0保存至K+1
mov ax,0004h ;设置图形显示模式4
int 10h
pixel:
mov cl,x0 ;将起始点的(X0,Y0)送至CX-DX
mov dl,y0
mov ch,0
mov dh,0
mov al,1 ;选择青色
mov ah,0ch ;在CX-DX指定的位置画一个点
int 10h
inc x0 ;X坐标加1,得下一个点的X坐标
mov al,x0 ;将新的X坐标送入AL寄存器
cmp al,x1 ;已经画到最后一个点了吗?
ja end_line ;已经画到最后一个点,转END_LINE
mul byte ptr k ;计算(K x X)
div byte ptr k+1
mov y0,al ;将算出的Y坐标存入Y0
jmp pixel ;转PIXEL继续画点
end_line:
mov ah,0 ;等待键盘输入
int 16h
mov ax,0003h ;设置字符显示模式3
int 10h
mov ah,4ch ;结束进程
int 21h
code ends
end start
此程序并未直接给出直线的斜率K,而是给出直线起始点与终止点的坐标,通过两个点的坐标计算出K。这好象有些繁琐,程序的指令增多,计算量也大了,这样设计程序不是自找麻烦吗?
大家平时一想起直线首先想到公式"y=Kx+b",然而真正在纸上画直线时几乎没有人考虑什么斜率、截距之类的东西。谁都愿意在知道起止点的情况下画线,直尺一连即可,这是最省事的。
任何一个成功的图形制作软件在画直线时都不会要求用户输入斜率、截距这样的参数,都是要求用户输入起止点坐标,或者让用户用鼠标器直接定起止点,而对于用户来说这种画线方式也是最直观,最方便的。这也就是说,把程序设计成这样一种形式其实是为程序的用户着想,虽然这个程序不可能有什么用户。
上面这个程序可以画出从点(0,0)至(199,199)这样一条对角线来,看上去效果还能让人接受。然而如果将程序中Y1的值改成1的话那么这个程序可就有问题了。
当然这个程序仍然可以运行并能画出线来,但是这条线的效果可就不能让人接受了。仔细观察这条线可以看到,组成线条的前199个点都位于屏幕第一行,只有最后一个点位于下面一行。这又是为什么呢?
问题的关键在于电脑显示器的分辨率是有限的,计算机只能在"整数"坐标位置显示点,不可能在两个扫描行之间显示出点来。所以当程序计算出某个点的Y坐标为199/200时,计算机也只能将此点画在第一行或下一行,具体要画在哪一行和程序取整的方法有关。
如果程序对所有计算出来的Y坐标都直接取整的话,那么理论上位于(199,199/200)处的点就会被画在屏幕上的第一行,因为199/200直接取整为0。上面给出的程序其实就是直接将计算结果取整,结果造成了画"拐棍儿"的效果。
这样的问题可以通过改进程序加以解决,就上面这个程序而言,只要在计算Y坐标时采用四舍五入的方法取整就能很好地解决这个问题。然而这样做并不是最理想的改进方法,因为这个程序还有其它一些缺点:比如我们将程序中的续止点X、Y坐标交换一下,即由(200,1)变成(1,200),那么这个程序就会产生十分尴尬的结果:整条线变成了两个点。看来"弱智"也是这个程序的一个缺陷。它至少应该能对(Y1-Y0)和(X1-X0)这两个差值做些比较,如果(Y1-Y0) (X1-X0)那么程序应该以(Y1-Y0)作为循环计数,而采用公式X=Y/K计算X坐标。
列举了以上一些不足之处,意在说明采用公式"Y=Kx+b"来画直线并不适用于计算机。其实上述这些缺点都是次要因素,最主要的一个缺点就是这种算法要使用乘法和除法运算。乘除法运算要消耗大量的时间,所以说这种算法是极低效的,这是这种算法的致命的缺陷。因此我们必须设计出既有效率又有效果的画线算法。
当我们给出一条直线的两个端点后,比如起点为(0,0),终点为(5,2),屏幕上应该显示出什么样的图形才能给人感觉是一条直线呢?毫无疑问,屏幕上应该显示出如图8-4(a)所示的几个点,即每画两个点之后Y坐标值要加1。如果起点仍为(0,0),终点为(11,3),那么屏幕显示出的图形就应该如图8-4(b)所示的样子了。不难看出如果我们所设计的程序能够在画第3、5点(对于图8-4b来说是画第3、7、11点)时自动将Y坐标值加上1,那么就能产生另人满意的效果来。看来要解决的一个关键问题就是要判断在什么样的情况下所画点的Y坐标值需要加1。

图9-5给出了一种算法,可以看到程序只要能够判断某个点的ΔY值是否大于1/2就可以决定是否将这个点的Y坐标值加1。具体来说,当ΔY>1/2时,所画点的Y坐标值就要加1,而当ΔY≤1/2时所画点的Y坐标值保持不变。这里就出现了两个问题:第一,各个点的ΔY值应如何计算?第二,怎样将ΔY与1/2相比较?
第一个问题比较好解决,从图中我们可以看到,直线的起点A(0,0)的ΔYa=0,第二个点B的ΔYb=0+K,第三个点C的ΔYc=0+K+K,第四个点D的ΔYd=0+K+K+K-1,第五个点E的ΔYe=0+K+K+K+K-1,以此类推,其中的K就是这条直线的斜率,K=ΔY'/ΔX'=(Y1-Y0)/(X1-X0)。可以看出计算ΔY的过程都包括一个"斜率累加"的过程,只是在Y坐标值加1之后要从累加值中减去1。如果我们把K=ΔY'/ΔX'代入公式,则有ΔYa=0,ΔYb=ΔY'/ΔX',ΔYc=(ΔY'+ΔY')/ΔX',ΔYd=(ΔY'+ΔY'+ΔY'-ΔX')/ΔX',"斜率累加"就变成"ΔY'累加"了。
由ΔY的计算式我们还可以看出每个点的ΔY都是分数,那么如何判断一个分数是否大于1/2呢?很明显若一个分数满足"(分子x2)>分母",则这个分数就大于1/2。对于图中的B点而言,如果2ΔY'>ΔX',就说明其ΔY大于1/2。对于C点,如果2ΔY'+2ΔY'>ΔX',则其ΔY大于1/2。而D 点就更麻烦一些,当2ΔY'+ΔY'+2ΔY'-2ΔX'>ΔX'时其ΔY值是大于1/2的。从这样一个判断的过程中我们能近一步发现在计算ΔY时真正需要累加的既不是"K",也不是"ΔY'",而是"2ΔY'"。

对于图中这条直线,线上各点有如表9-2这样的计算结果:
表9-2 决定最近点的计算实例 ΔY'=2,ΔX'=5,2ΔY'=4,2ΔX'=10
| 点 | 2ΔY'的累加值 | 累加值与ΔX'比较 | 实际点的坐标值 | 累加值调整 |
| A | 0 | (0,0) | ||
| B | 0+4=4 | 4<5 | (1,0) | |
| C | 4+4=8 | 8>5 | (2,1) | 8-10=-2 |
| D | -2+4=2 | 2<5 | (3,1) | |
| E | 2+4=6 | 6>5 | (4,2) | 6-10=-4 |
| F | -4+4=0 | 0<5 | (5,2) |
通过以上计算,我们清楚地看到当表中第三栏内出现">"时,实际所画点的Y坐标值都加上了1,同时2 Y 的累加值根据2 X 作了相应调整。而在整个过程之中X坐标值始终都是递增的。
这个画线算法是由一位名为Bresenham的程序员首创,因此这个算法被命名为Bresenham算法。因为" 2"的操作可以由左移位代替,所以在这个算法中实际上是没有乘法、除法运算的,因而这个算法十分快速,这是其最突出的优点,也是它长盛不衰的原因。
程序LOVE.ASM演示了Bresenham直线算法的一种应用,这个程序有些复杂,它将屏幕中央连续画6个封闭的多边形,由这些多边形组成一个图案。
start_x equ 100 ;起点X坐标
start_y equ 0 ;起点Y坐标
data segment
assume ds:data
dotbuff dw 26h ;第一部分总的点数
dw 48h,09h,42h,0dh,3ch,11h,37h,14h,31h,16h,29h,18h,28h,19h,2eh,1ah
dw 31h,1bh,35h,1ah,3eh,18h,45h,16h,48h,15h,4ah,19h,4ch,1dh,4ch,1eh
dw 4ah,22h,48h,25h,44h,2ah,40h,2dh,3eh,2fh,48h,2eh,4dh,2ah,50h,27h
dw 54h,22h,54h,21h,53h,1eh,51h,1ah,4fh,18h,4ah,15h,4eh,14h,53h,13h
dw 55h,11h,55h,10h,53h,0ch,4fh,09h,4bh,07h,45h,06h
dw 20h ;第二部分总的点数
dw 38h,1eh,3ah,21h,3bh,26h,3dh,2ah,3fh,2bh,40h,2bh,42h,29h,44h,26h
dw 44h,24h,42h,21h,44h,1eh,46h,1ah,42h,1fh,3fh,1ch,3dh,1bh,39h,1bh
dw 36h,1ch,33h,1fh,2fh,24h,2dh,24h,2ah,23h,26h,22h,26h,23h,29h,27h
dw 2bh,2ah,2bh,2eh,2ch,33h,34h,31h,33h,2bh,34h,24h,34h,22h,36h,20h
dw 3ch ;第三部分总的点数
dw 61h,2fh,5dh,33h,5ch,33h,57h,36h,51h,38h,4bh,3bh,51h,39h,56h,3bh
dw 58h,3ah,5fh,39h,62h,3ah,64h,39h,69h,3ah,6bh,38h,6ch,36h,6ch,35h
dw 6bh,32h,69h,2fh,66h,2dh,64h,2bh,60h,2ah,5eh,2ah,5ch,2bh,5ah,2ah
dw 55h,2bh,4ch,2ch,40h,2dh,3ch,2eh,31h,2fh,2eh,30h,29h,33h,24h,35h
dw 21h,38h,20h,34h,1dh,31h,1bh,30h,1dh,35h,1ch,36h,1bh,3bh,19h,3fh
dw 17h,41h,17h,42h,18h,48h,19h,4bh,1bh,4dh,1ch,4bh,1fh,46h,20h,43h
dw 21h,3fh,23h,3ch,27h,3ah,2ch,36h,2fh,37h,31h,37h,35h,36h,3dh,34h
dw 43h,33h,4ch,31h,53h,30h,5dh,2fh
dw 2ch ;第四部分总的点数
dw 33h,49h,32h,4dh,2fh,52h,2ch,57h,29h,59h,29h,5ah,27h,5dh,23h,62h
dw 21h,65h,1fh,68h,1eh,6dh,1fh,6dh,22h,6ch,25h,6ah,29h,65h,2dh,62h
dw 31h,5dh,33h,59h,37h,53h,3ch,4ah,40h,42h,45h,41h,4eh,3eh,51h,3dh
dw 50h,39h,4ch,37h,4bh,37h,46h,38h,43h,39h,42h,36h,40h,33h,3ch,31h
dw 3bh,31h,35h,34h,3bh,37h,3ah,39h,39h,3dh,35h,3fh,31h,41h,2ah,44h
dw 26h,43h,28h,47h,2bh,4ah,2eh,4bh
dw 33h ;第五部分总的点数
dw 3fh,5fh,3ch,62h,38h,65h,35h,67h,2eh,69h,2ch,69h,24h,68h,2bh,6ah
dw 2fh,6ch,32h,6dh,34h,6dh,3ah,6ch,3fh,6bh,43h,68h,45h,65h,4dh,6ah
dw 50h,6dh,56h,71h,5ah,73h,63h,74h,68h,74h,6ch,73h,6ch,72h,6bh,6fh
dw 67h,6ch,64h,6ah,5eh,68h,5ah,67h,56h,65h,52h,63h,4fh,61h,4ah,5eh
dw 4ch,58h,4eh,53h,52h,4dh,52h,4bh,51h,48h,4fh,44h,4bh,41h,4ah,41h
dw 47h,43h,43h,46h,3eh,4ah,39h,4dh,34h,4fh,2bh,51h,26h,53h,28h,53h
dw 2bh,54h,32h,57h,3ah,5bh
dw 09h ;第六部分总的点数
dw 48h,4bh,48h,4fh,45h,54h,44h,59h,40h,57h,3fh,57h,3ah,53h,3fh,51h
dw 44h,4eh
x0 dw ? ;第一特征点X坐标
y0 dw ? ;第一特征点Y坐标
x1 dw ? ;直线起始点的X坐标
y1 dw ? ;直线起始点的Y坐标
x2 dw ? ;直线终止点的X坐标
y2 dw ? ;直线终止点的Y坐标
color db 2 ;直线的颜色
deltax dw ? ;保存ΔX′
deltay dw ? ;保存ΔY′
delta1 dw ? ;保存2ΔX′(也可能是2ΔY′,依递增方向定)
delta2 dw ? ;保存2ΔY′(也可以是2ΔX′)
data ends
code segment
assume cs:code
main proc far
mov ax,data ;初始化DS寄存器
mov ds,ax
mov ax,0004h ;设置图形显示模式4
int 10h
mov si,offset dotbuff ;SI寄存器指向多边形的顶点表
mov cx,6 ;显示6个封闭的多边形
drloop: ;显示循环开始
push cx ;暂存CX寄存器中的计数值
lodsw ;取得一个多边形的顶点总数并保存至堆栈
push ax
lodsw ;取多边形的第一点X坐标
add ax,start_x ;起点要显示在合适的位置
mov x0,ax ;第一点的X坐标送入X0及X1
mov x1,ax
lodsw ;取多边形的第一点Y坐标
add ax,start_y ;调整位置
mov y0,ax ;第一点的Y坐标送入Y0及Y1
mov y1,ax
pop cx ;取每个多边形包含的顶点总数
dec cx ;减去起始点
drloop1:
push cx
lodsw ;取下一点X坐标
add ax,start_x ;调整位置
mov x2,ax ;将下一点的X坐标存入X2
lodsw ;取下一点Y坐标
add ax,start_y ;调整位置
mov y2,ax ;将下一点的Y坐标存入Y2
mov color,2 ;选择洋红色
call line ;从(X1,Y1)画一直线到(X2,Y2)
mov ax,x2 ;将X2移至X1
mov x1,ax
mov ax,y2 ;将Y2移至Y1
mov y1,ax
pop cx
loop drloop1 ;转DRLOOP1继续画直线
mov ax,x0 ;将第一点的X坐标送入X2
mov x2,ax
mov ax,y0 ;将第一点的Y坐标送入Y2
mov y2,ax
call line ;画至第一点,形成封闭图形
mov ah,0 ;等待键盘输入
int 16h
pop cx
loop drloop ;画下一个多边形
exit:
mov ax,03h ;设置字符显示模式3
int 10h
mov ah,4ch ;结束进程
int 21h
main endp
line proc near ;画直线的子过程
push ax ;保存寄存器
push bx
push cx
push dx
push si
push di
mov si,1 ;初设X、Y方向上的步进增量为+1
mov di,1
mov ax,x2 ;取得终止点的X坐标
sub ax,x1 ;减去起始点的X坐标
jge cont1 ;若终止点在起始点右边,转CONT1
neg si ;否则X方向上步进增量改为-1
neg ax ;获得X方向上的总点数(ΔX′)
cont1:
mov deltax,ax ;保存ΔX′
mov ax,y2 ;取得终止点的Y坐标
sub ax,y1 ;减去起始点的Y坐标
jge cont2 ;若终止点在起始点下方,转CONT2
neg di ;否则Y方向上的步进增量改为-1
neg ax ;获得Y方向上的总点数(ΔY′)
cont2:
mov deltay,ax ;保存ΔY′
cmp ax,deltax ;将ΔY′与ΔX′进行比较
jl directx ;若ΔY′< ΔX′,则将X方向作为递增方向
mov bp,0 ;若Y方向为递增方向,则准备调整X坐标
mov dx,si ;X方向上的增量值送入DX寄存器备份
mov si,0 ;X方向的增量值清0
mov ax,deltax ;ΔX′送入AX寄存器
mov bx,deltay ;ΔY′送入BX寄存器
jmp cont3 ;转CONT3继续
directx:
mov bp,2 ;若X方向为递增方向,则准备调整Y坐标
mov dx,di ;Y方向的增量值送入DX寄存器备份
mov di,0 ;Y方向的增量值清0
mov bx,deltax ;ΔX′送入BX寄存器
cont3: ;注:以下的说明将假定X方向为递增方向
mov cx,bx ;暂存BX寄存器中的ΔX′值
shl ax,1 ;计算2ΔY′
mov delta1,ax ;将2ΔY′保存至DELTA1
shl bx,1 ;计算2ΔX′
mov delta2,bx ;将2ΔX′保存至DELTA2
mov bx,cx ;将ΔX′送回BX寄存器
lineloop:
call point ;在(X1,Y1)处画一个点
add x1,si ;X坐标加一个增量值
add y1,di ;Y坐标加0
cmp ax,bx ;将AX中的2ΔY′累加值与BX中的ΔX′比较
jg adjust ;若2ΔY′大于ΔX′则转ADJUST进行调整
jmp next ;否则转NEXT继续
adjust:
add [x1+bp],dx ;将Y1坐标加上一个增量值
sub ax,delta2 ;同时AX中2ΔY′的累加值减去一个2ΔX′
next:
add ax,delta1 ;继续在AX寄存器中累加2ΔY′
loop lineloop ;转LINELOOP继续画点
pop di ;恢复各寄存器初值
pop si
pop dx
pop cx
pop bx
pop ax
ret ;返回主过程
line endp
point proc near ;画点子过程
push ax ;保存寄存器
push cx
push dx
mov cx,x1 ;X坐标送入CX寄存器
mov dx,y1 ;Y坐标送入BX寄存器
mov al,color ;颜色值送入AL寄存器
mov ah,0ch ;调用10H中断的0CH功能画一个点
int 10h
pop dx ;恢复寄存器
pop cx
pop ax
ret ;返回LINE子过程
point endp
code ends
end main
这个程序很长,数据量很大,一旦录入出了错很不好查找。不过只要大家有耐心,最终还是会被这个程序的输出所吸引的。