@cxm-2016
2016-11-03T02:31:52.000000Z
字数 10095
阅读 3456
算法
版本:3
作者:陈小默
声明:禁止商用,禁止转载
场景中的直线由其两端点的坐标位置来定义。要在光栅监视器中显示一条线段,图形系统必须先将两端点投影到整数屏幕坐标,并确定离两端点间的直线路径最近的像素位置。接下来才是将颜色填充到相应的像素坐标。[1]
文章最后的演示代码使用的是C++语言,函数库使用的是以GLUT为基础的自定义封装库。本章内容将介绍生成直线的直线方程算法、DDA算法以及重要的Bresenham算法。
以下仅仅展示算法的计算过程,具体实施请参考示例程序部分。
对于绘制直线来说,使用直线方程无疑是一种最直接的算法。在二维笛卡尔坐标系中,直线方程为:
通过使用直线方程绘制的点,其优点是算法简单且精确,但是其在绘制每一个点的过程中都需要计算一次乘法和加法,显而易见,由于乘法的存在,导致运算时间大幅度增加。接下来介绍的DDA算法将弥补直线方程的乘法缺陷。
从上可知,在绘制大量点的过程中,我们要尽可能的减少每一个点的计算时间。在计算机中加法运算是最简单的运算之一了。我们可以利用直线的微分特性将每一步的乘法运算替换为加法运算。数字微分分析法(Digital Differential Analyzer,DDA)是一种线段扫描转换算法,基于式 或 来计算 或 。
对于斜率 的线段来说,我们仍以单位 间隔(考虑到屏幕设备坐标为连续整数)取样,并逐个计算每一个 值。
于是,我们便将乘法运算合理的转换为了加法运算。但是需要注意的是,在屏幕设备中的坐标均是整数,所以我们在绘制时的y需要取整。
对于具有大于1的正斜率线段,则需要交换 和 的位置。也就是以单位 间隔 取样,顺序计算每一个 的值
此时,每一个计算出的 要沿着 扫描线舍入到最近的像素位置。
该算法只需要计算出一个 值( 或者 ),然后就可以沿着路径的方向计算出下一位像素。然而,该算法的缺点显而易见,在数学上,该算法能够保证计算结果准确无误,但是,由于计算机中的数据类型具有精度限制,这将会导致大量数据处理的误差积累。并且,其虽然消除了直线方程中的乘法运算,但是对于浮点数的运算和取整仍然十分耗时。
接下来我们介绍由布莱森汉姆(Bresenham)提出的精确且高效的光栅线生成算法,该算法仅仅使用整数增量计算,除此之外,该算法还能应用于圆或者其他曲线。
我们将分别介绍四种斜率( 、 、 和 )的计算过程(以下示例均为从左至右画线)。

首先,在斜率大于1的情况下,沿路径像素以单位 间隔取样。假设线段以开始对于其路径上已绘制的点我们需要判定下一个点的绘制位置是还是。
在取样位置 我们使用 和 来标识两个像素位置(与)与数学位置的水平偏移量。根据式 可得在像素列 处的 坐标计算值为
所以
且
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。
设线段终点位置为,可得
设决策参数
其中
在第 步,决策参数可以由式计算得出
将上述等式减去式可得决策值的增量
其中的取值可能为0,也可能为1,这由前一个决策值的正负决定,也就是说如果,则下一个绘制的点是 否则绘制。并且绘制的点的位置又决定了该位置的决策值大小。
目前我们有了决策值之间的增量关系,仅需求出第一个决策值即可,由式、式、式和式联立可得
绘制过程解析:由于 所以当时(此时), 代表当前数学点更接近与否则更接近与。我们仅仅需要求出第一个决策值 其后的决策值都是前一个决策值与决策增量的和。我们可以在沿线路径的每一个 处,进行下列检测:
如果 下一个要绘制的点是 ,并且
否则,下一个要绘制的点是 ,并且
在斜率大于0小于1的情况下,沿路径像素以单位 间隔取样。假设线段以开始对于其路径上已绘制的点我们需要判定下一个点的绘制位置是还是。
在取样位置 我们使用 和 来标识两个像素位置(与)与数学位置的水平偏移量。根据式 可得在像素列 处的 坐标计算值为
所以
且
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。
设决策参数
其中
在第 步,决策参数可以由式计算得出
将上述等式减去式可得决策值的增量
由式、式、式和式联立可得第一个决策值
在斜率大于-1小于0的情况下,沿路径像素以单位 间隔取样。假设线段以开始对于其路径上已绘制的点我们需要判定下一个点的绘制位置是还是。
在取样位置 我们使用 和 来标识两个像素位置(与)与数学位置的水平偏移量。
且
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。
设决策参数
其中
在第 步,决策参数可以由式计算得出
将上述等式减去式可得决策值的增量
由式、式、式和式联立可得第一个决策值
在斜率小于-1的情况下,沿路径像素以单位 间隔取样。假设线段以开始对于其路径上已绘制的点我们需要判定下一个点的绘制位置是还是。
在取样位置 我们使用 和 来标识两个像素位置(与)与数学位置的水平偏移量。根据式 可得在像素列 处的 坐标计算值为
所以
且
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。
设线段终点位置为,可得
设决策参数 (在从左到右的绘制过程中 为了保持符号统一,所以交换和位置)
其中
在第 步
将上述等式减去式可得决策值的增量
目前我们有了决策值之间的增量关系,仅第一个决策值
在上面得的算法分析中,我们已经了解了三种算法的基本思想与设计思路。这里通过C++程序演示其过程(也可以通过其他图形软件包比如Java的swing或者Android的Canvas实现),实现方式各异,不必拘泥于细节。
首先定义一个显示用来显示图形的View
#ifndef lineview_h#define lienview_h#include"cxm.h"class LineView:public View{private:_Paint _paint;//画笔指针_StringArray _names;//字符串数组指针List<IntArray> *_arrays;//存放了整型数组的链表对象,数组中为计算出的点的坐标位置public:int bottom,top;LineView();~LineView();void LineEquation(double x1,double y1,double x2,double y2);//使用直线方程画线void DDA(double x1,double y1,double x2,double y2);//使用DDA算法画线void Bresenham(double x1,double y1,double x2,double y2);//布莱森汉姆算法virtual void onDraw(Canvas &canvas);//图案绘制方法};typedef LineView* _LineView;typedef LineView& LineView_;#endif
接下来实现其中的构造与析构函数
LineView::LineView(){_paint = new Paint;_names = new StringArray(4);_arrays = new LinkedList<IntArray>;StringArray_ names_=*_names;names_[0]="OpenGL";names_[1]="Equation";names_[2]="DDA";names_[3]="Bresenhan";}LineView::~LineView(){delete _paint;delete _names;delete _arrays;}
接下来我们实现使用直线方程的方法,该方法将计算出的点保存在了整型数组中,并将数组存储到链表
void LineView::LineEquation(double x1,double y1,double x2,double y2){//y=mx+b_IntArray _arr;double dy = y2-y1;double dx = x2-x1;double m = dy/dx;double b = y1 - m*x1;if(abs(m)>1){//以y轴像素为单位 x=(y-b)/mint size = abs(2*int(dy));//存放坐标的数组长度_arr = new IntArray(size);IntArray_ arr=*_arr;int start = int(y1<y2?y1:y2);//取最小起始位置int end = start+abs(int(dy));for(int i=start,j=0;i<end;i++){arr[j++]=int((i-b)/m);//x坐标arr[j++]=i;//y坐标}}else{//以x轴像素为单位 y=mx+bint size = abs(2*int(dx));//存放坐标的数组长度_arr = new IntArray(size);IntArray_ arr = *_arr;int start = int(x1<x2?x1:x2);//取最小起始位置int end = start+abs(int(dx));for(int i=start,j=0;i<end;i++){arr[j++]=i;//x坐标arr[j++]=int(b+m*i);//y坐标}}_arrays->add(_arr);}
然后实现使用DDA画线的算法计算出各点
void LineView::DDA(double x1,double y1,double x2,double y2){_IntArray _arr;double dy = y2-y1;double dx = x2-x1;double m = dy/dx;if(abs(m)>1){//以单位y取样double rate = 1/m;int size = abs(2*int(dy));//存放坐标的数组长度_arr = new IntArray(size);IntArray_ arr = *_arr;int start = int(y1<y2?y1:y2);//取最小起始位置int end = start+abs(int(dy));double rx = y1<y2?x1:x2;for(int i=start,j=0;i<end;i++){rx+=rate;arr[j++]=int(rx);arr[j++]=i;}}else{//以单位x取样double rate = m;int size = abs(2*int(dx));//存放坐标的数组长度_arr = new IntArray(size);IntArray_ arr = *_arr;int start = int(x1<x2?x1:x2);//取最小起始位置int end = start+abs(int(dx));double ry = x1<x2?y1:y2;for(int i=start,j=0;i<end;i++){ry+=rate;arr[j++]=i;arr[j++]=int(ry);}}_arrays->add(_arr);}
最后是Bresenham算法的简单实现
void LineView::Bresenham(double x1,double y1,double x2,double y2){_IntArray _arr;double dy=y2-y1;double dx=x2-x1;double m = dy/dx;if(m>1){//以单位y取样int size = abs(2*int(dy));_arr = new IntArray(size);IntArray_ arr = *_arr;double p = 2*dx-dy;//podouble left = 2*dx;double right = 2*dx-2*dy;int start = int(y1);int end = start+abs(int(dy));int x = int(x1);for(int y=start,i=0;y<=end;y++){if(p<0){arr[i++]=x;p+=left;}else{arr[i++]=++x;p+=right;}arr[i++]=y;}}else if(m>0&&m<1){//以单位x取样int size = abs(2*int(dx));_arr = new IntArray(size);IntArray_ arr = *_arr;double p = 2*dy-dx;double upper = 2*dy;double lower = 2*dy-2*dx;int start = int(x1);int end = start + abs(int(dx));int y = int(y1);for(int x=start,i=0;x<=end;x++){arr[i++]=x;if(p<0){arr[i++]=y;p+=upper;}else{arr[i++]=++y;p+=lower;}}}else if(m<0&&m>-1){//以单位x取样int size = abs(2*int(dx));_arr = new IntArray(size);IntArray_ arr = *_arr;double p = dx-2*dy;double upper = -2*dy;double lower = -2*dx-2*dy;int start = int(x1);int end = start + abs(int(dx));int y = int(y1);for(int x=start,i=0;x<=end;x++){arr[i++]=x;if(p<0){arr[i++]=y;p+=upper;}else{arr[i++]=--y;p+=lower;}}}else{//以单位y取样int size = abs(2*int(dy));_arr = new IntArray(size);IntArray_ arr = *_arr;double p = 2*dx+dy;double left = 2*dx;double right = 2*dx+2*dy;int start = int(y1);int end = start-abs(int(dy));int x = int(x1);for(int y=start,i=0;y>=end;y--){if(p<0){arr[i++]=x;p+=left;}else{arr[i++]=++x;p+=right;}arr[i++]=y;}}_arrays->add(_arr);}
然后在onDraw方法中将所有的点绘制出来
void LineView::onDraw(Canvas &canvas){canvas.drawLine(_paint,50,bottom,150,top);for(int i=0;i<_arrays->size();i++){canvas.drawPoints(*_paint,*(*_arrays)[i]);}for(int i=0;i<_names->size();i++){canvas.drawString(*_paint,i*200+40,25,(*_names)[i]);}}
最后就是在main方法中创建视图窗口,并将刚刚创建的View添加进去
int _tmain(int argc, char* argv[]){GlutWindow window(100,300,800,300);String title = "直线算法";window.setTitle(title);window.setBackgroundColor(BLUE_SKY);_LineView _view = new LineView();LineView_ view=*_view;view.bottom=50;view.top=290;view.LineEquation(250,view.bottom,350,view.top);view.DDA(450,view.bottom,550,view.top);view.Bresenham(650,view.bottom,750,view.top);window.addView(view);window.show(&argc,argv);return 0;}
结果图展示
