2 直 线
数学上,理想的直线是由无数个点构成的集合,没有宽度。计算机绘制直线是在显示器所给定的有限个像素组成的矩阵中,确定最佳逼近该直线的一组像素,并且按扫描线顺序,对这些像素进行写操作,实现显示器绘制直线,即通常所说的直线的扫描转换,或称直线光栅化。由于一个图形中可能包含成千上万条直线,所以要求绘制直线的算法应尽可能地快。本节介绍一个像素宽直线的常用算法:数值微分法(DDA)、中点画线法、Bresenham 算法。
2.2.1 DDA(数值微分)算法
(1) DDA算法原理:
问题:如图1-1所示,已知过端点P0(x0, y0) ,P1 ( x1, y1)的直线段为P0P1,则直线斜率为k=( y1- y0)/ ( x1- x0),要在输出设备上显示该直线段,我们需要计算出经过该直线段P0P1上所有像素点的集合。
根据几何知识,我们只需要将x从左端点x0开始,向右端点x1步进,步长=1(个像素),将x代入直线方程 y=kx+B 计算出相应的坐标即可;
这里有一个问题那就是,我们x的取值是离散的,但是计算出来的y值不一定是整数,所以需要我们对y进行四舍五入,像素点 [x, round(y)] 作为当前点的坐标。

(2) 代码实现:
根据上面的分析,我们开始编写代码,首先我们在the_application类中增加四舍五入取整函数
//四舍五入为整数
int Round(float f)
{
return int(f+0.5);
}
在直线的斜截式方程中,我们知道k=( y1- y0)/ ( x1- x0),但是我们还缺少B,这里我们还需要从直线的两点式方程中推导出B的值。下面我们一起推导下,以帮助大家熟悉下还给数学老师的知识:
(y - y0)/( y1- y0)=(x- x0)/( x1- x0)
y ( x1- x0)= (x- x0) ( y1- y0)+y0( x1- x0)
y ( x1- x0)= ( y1- y0)x-( y1- y0) x0+y0( x1- x0)
y ( x1- x0)= ( y1- y0)x +y0 x1- y1 x0
因此直线截矩B=( y0 x1- y1 x0)/ ( x1- x0),k=( y1- y0)/ ( x1- x0),实现数值微分法画直线的代码如下:
//该算法仅仅适应于|k|<=1,传递参数时,必须x0<=x1
void DDALine(int x0,int y0,int x1,int y1,COLORREF color)
{
float y, k, b,dx;
dx = x1-x0;
//计算直线斜率
k=(y1-y0)/dx;
//计算B
b= (y0*x1-y1*x0)/dx;
for (intx=x0;x<=x1;x++)
{
DrawPixel(x,Round(y),color);
y=k*x+b;
}
}
(3) 代码优化:
我们回过头来看,在我们的代码中存在一次乘法运算,大家都知道乘法指令是比较占用cpu资源,当系统需要渲染大量的直线段时,造成的效率低下是无法估算的。
我们考虑下看能不能优化掉该条乘法指令,由于直线方程是线性的,可以考虑用增量方式来优化,即当x每递增1,y递增k(即直线斜率),数学推导如下:
yi+1 = k (xi+1)+B
= k xi+B+ k
= yi+ k
按照我们的分析,我们经过优化后DDALine函数,代码如下:
//该算法仅仅适应于|k|<=1,传递参数时,必须x0<=x1
void DDALine(int x0,int y0,int x1,int y1,COLORREF color)
{
float y, k, dx;
//计算直线斜率
dx = x1-x0;
k=(y1-y0)/dx;
y=y0;
for (int x=x0;x<=x1;x++)
{
DrawPixel(x,Round(y),color);
y+=k;
}
}
(4) 算法分析:
根据上面的理论分析,我们知道x每递增一个像素,y递增k个像素。
当|k|>1时,x是按1步进的,因此x在整数范围内时连续,但是y将不再连续,这样导致我们画出来的直线就成了虚线,如下图所示:
当|k|<=1时,在这种情况下,x每增加1,y最多增加1,此时x,y在整数范围内都是连续的,所以我们画出的直线没有问题,因此我们上述的算法仅适用于|k|<=1。
那么当|k|>1时,我们该怎么办了?其实也简单,我们把x,y地位互换,这样y每增加1,x相应增加|1/k|,此时 |Dx|<=1也是成立的。具体代码实现就不写,这里我们主要理解算法原理就可以了。
(5) 运行效果截图: