Bresenham画直线算法(所有斜率)
Bresenham算法是图形学非常经典的光栅线生成算法,可用于显示直线、圆以及其他曲线。这里通过算法画直线过程,了解其工作原理。
问题描述
已知线段2端点\((x_0, y_0) (x_e, y_e)\),屏幕上画出该直线段。
由于屏幕是通过像素点显示的,只能通过像素点所在的整数坐标近似代表直线上点的位置。那么,该用什么点画出直线呢?这就是Bresenham算法要解决的问题。
Bresenham算法
基本思想
根据当前点,决定下一个(像素)点画在哪儿。要么在相邻点,要么在对角点。相邻点是指x坐标+1、y坐标不变,或者x不变、y+1;对角点是指x+1,同时y+1。
要确定2个备选点中的哪一个,可根据它们到直线的距离远近决定:如果相邻点更近,下一点选相邻点;如果对角点更近,下一点就选对角点。
为减少误差,用光栅直线逼近理论直线,Bresenham算法按变化大的轴(x轴或y轴)逐像素绘制线段。
算法推演
假设直线方程:\(y = mx + b\)
因为线段2端点在直线上,所以,\(y_0=mx_0+b, y_e=mx_e+b\)
线段x、y坐标变化:\(∆x=x_e-x_0, ∆y=y_e-y_0\)
斜率m、截距b:
如果现在画到了第k个像素点,位置\((x_k,y_k)\),那么第k+1个点\((x_{k+1},y_{k+1})\)在哪?
由于斜率影响算法的过程,可以分情况讨论:
特殊斜率:0<m<1
∵0 < m < 1
∴Δx>Δy
∴可按x轴递增绘制像素点,即\(x_{k+1}=x_k+1\);
∵m > 0且线段连续
∴y递增,\(y_{k+1}\)取值只能是\(y_k\)或\(y_k+1\)。
=> 第k+1点\((x_{k+1}, y_{k+1})\)有2个选择:\((x_k+1,y_k ) or (x_k+1,y_k+1)\)
tips: 哪个像素点距离直线更近,Bresenham算法就选择那个点作为下一个要绘制的点。然而,计算点到直线的距离方法复杂,而且涉及(斜率)浮点数运算。为了简化运算,Bresenham算法并没有用点到线的垂直距离,而是利用y轴或x轴方向的偏移替代。
根据\((1)\),直线在\(x_{k+1}\)位置的y值(理论值):
记2个备选点\(y_k, y_k+1\)到直线的竖直偏移分别为\(d_{1}, d_{2}\),有:
这里利用了m范围:0 < m < 1,有\(d_{1} > 0, d_{2} > 0\),可知,
要判断哪个点距离更近,将\((3)(4)\)做差值:
其中,斜率m、截距b都是常数。
将\(m=∆y/∆x\)代入\((5)\),两边同时乘∆x,可得,
令常数c:\(c=∆x(2b-1)\),可得第k步决策参数:
∵m > 0 ∴∆x>0 ∴\(p_k, d_{1}-d_{2}\)同号
问题转化为求\(p_k\)。
1)求\(p_k\)递推公式
根据\((6)\),当k取值k+1时,有
\((7)-(6)\)可得递推公式,
∵\(x_{k+1}=x_k+1\), \(y_{k+1}-y_k\)取值(0或1)取决于\(p_k\)的符号。
∴
2)求初值\(p_0\)
∵起始点\((x_0, y_0)\)在直线上
∴
又\(b=y_0-mx_0=y_0-x_0∆y/∆x\) => \(∆xb=∆xy_0-x_0∆y\)
∴
综上,可将0<m<1时,Bresenham画线算法归纳为如下步骤:
- 输入线段2端点,左端点存储在\((x_0, y_0)\);
- 将\((x_0, y_0)\)装入帧缓存,画出第一个点;
- 计算常量\(∆x, ∆y, 2∆y, 2∆y-2∆x\),得到决策参数初值:\(p_0=2∆y-∆x\)
- 从k=0开始,沿着线段路径,每个\(x_k\)处,计算下一个要绘制的点位置:
如果\(p_k<0\),下一个要绘制点\((x_k+1, y_k)\),且\(p_{k+1}=p_k+2∆y\);
否则,下一个要绘制点\((x_k+1, y_k+1)\),且\(p_{k+1}=p_k+2∆y-2∆x\); - 重复步骤4),共计∆x-1次。
可写出0<m<1时,Bresenham算法程序:
void setPixel(int x, int y)
{
glBegin(GL_POINTS);
glVertex2i(x, y);
glEnd();
}
// case 0 < m < 1
void lineBresenham(int x0, int y0, int xe, int ye)
{
int dx = abs(xe - x0), dy = abs(ye - y0);
int p = 2 * dy - dx;
int twoDy = 2 * dy, twoDx = 2 * dx, twoDyMinusDx = 2 * (dy - dx);
int x, y;
/* determine which endpoint to use as a start point */
if (x0 > xe) {
x = xe;
y = ye;
xe = x0;
}
else {
x = x0;
y = y0;
}
setPixel(x, y); // draw pixel
while (x < xe) {
x++;
if (p < 0) {
p += twoDy;
}
else {
y++;
p += twoDyMinusDx;
}
setPixel(x, y);
}
}
其他斜率
简化一下推理过程。
- 当0<m<1时
从像素点角度看,整个过程:
x: x0 -> xe, 递增(↑), x每次+1
y: y0 -> ye, 非递减(↑), y是否+1取决于p符号, 而p可由递推公式计算
有\(Δx > Δy\),可按x轴逐像素绘制,x每次+1
=> \(x_{k+1} = x_k + 1\)
∵y=f(x): y = mx + b连续且递增
∴\((x_k, y_k)\)下一像素点\((x_{k+1}, y_{k+1})\)选择:\((x_{k+1}, y_{k})\) or \((x_{k+1}, y_{k}+1)\)
=>
\(x_{k+1}\)处垂直偏移\(d_{1}\), \(d_{2}\)分别用来(近似)代表点\((x_k+1, y_k)\), \((x_k+1, y_k+1)\)到直线的距离(均>=0)。
\(d_{1}, d_{2}\)做差值,
两边乘以\(Δx\),可得决策参数\(p_k\):
k取k + 1,可得
根据\(y_{k+1}\)与\(y_k\)关系,可得,
注:Δx>0, Δy>0
而初值\(p_0\),
- 当-1<m<0时
从像素点角度看,整个过程:
x: x0 -> xe, 递增(↑), x每次+1
y: y0 -> ye, 非递增(↓), y是否-1取决于p符号,p由递推公式计算
\((x_k, y_k)\)下一像素点:\((x_k + 1, y_k)\) or \((x_k + 1, y_k - 1)\)
做差值可得,
发现与0<m<1情形一样。因此,对于决策参数\(p_k\):
将\(p_{k+1}\)与\(p_k\)做差值,求递推公式:
注: Δx>0, Δy<0
∵Δx>0
∴\(p_k\)与\(d_{1}-d_{2}\)同号
初值\(p_0\):\(p_0=-2Δy-Δx\)
综合 0<m<1以及-1<m<0,不难发现:
当m>1时,只需要将0<m<1情形的x、y规则对换即可;
当m<-1时,只需要将-1<m<0情形的x、y规则对换即可。
可以写出下面程序,适用于所有斜率的Bresenham算法:
// 在(x,y)位置绘制像素点
void set_pixel(int x, int y)
{
glBegin(GL_POINTS);
glVertex2i(x, y);
glEnd();
}
// 适用所有斜率的通用Bresenham画线算法
// (x1,y1) (x2, y2)是线段两端点
void bresenham_line(int x1, int y1, int x2, int y2)
{
int dx = x2 - x1;
int dy = y2 - y1;
int stepX = dx >= 0 ? 1 : -1;
int stepY = dy >= 0 ? 1 : -1;
dx = abs(dx);
dy = abs(dy);
if (dx > dy) { // |m| < 1
int p = 2 * dy - dx;
int y = y1;
for (int x = x1; x != x2; x += stepX) {
set_pixel(x, y);
if (p > 0) {
y += stepY;
p -= 2 * dx;
}
p += 2 * dy;
}
}
else { // |m| >= 1
int p = 2 * dx - dy;
int x = x1;
for (int y = y1; y != y2; y += stepY) {
set_pixel(x, y);
if (p > 0) {
x += stepX;
p -= 2 * dy;
}
p += 2 * dx;
}
}
}
参考
[1]DonaldHearn,M.PaulineBaker,赫恩,等.计算机图形学(第四版)[M].电子工业出版社,2014.
[2]KATEX公式编辑器符号大全-CSDN的Mardown公式支持 | CSDN