直线段扫描转换算法

本文原创首发于我的个人博客
由于本文存在大量 LaTeX 代码,建议原站查看

数学里的直线上的点是无穷多的,但是光栅显示器的像素点是有限的,所以只能使用有限的像素去尽可能逼近直线

直线段扫描转换算法

为了在光栅显示器上用这些离散的像素点去逼近直线,需要知道这些像素点的 xx, yy 坐标

求法:

1. 求出过点 P0P_{0}, P1P_{1} 的直线段的方程:

y=kx+by = kx + b

k=(y1y0)(x1x0)      (x1x0)k=\frac{ \left ( y_{1} - y_{0}\right ) }{ \left ( x_{1} - x_{0}\right ) } \;\;\; \left ( x_{1} \neq x_{0}\right )

2. 求 yy 的值

假设 xx 已知,即从 xx 的起点 x0x_{0} 开始,沿着 xx 方向前进一个像素(步长 = 1),可以计算出相应的 yy

因为像素的坐标是整数,所以 yy 值还需要进行取整处理

取整规律:

P(1.7,  0.8)(+0.5)P(2,  1)P\left(1.7,\;0.8\right) \rightarrow \left(+0.5\right) \rightarrow 取整 \rightarrow P\left(2,\;1\right)

直线是最基本的图形,一个动画或者真实感图形往往需要调用成千上万次画线程序,因此直线算法的好坏与效率将直接影响图形的质量和显示速度

算法优化:把算法中的乘法消去可以减少计算量,从而提高算法效率

三个直线绘制常用算法:

  1. 数值微分法(DDA)
  2. 中点画线法
  3. Bresenham 算法

1. 数值微分法

引进了增量思想,算法直观、容易实现

原理解析:

直线段扫描转换算法

在上图中点 (xi,  yi)\left(x_{i},\;y_{i}\right), (xi+1,  yi+1)\left(x_{i+1},\;y_{i+1}\right) 可以表示为:

yi=kxi+by_{i} = kx_{i} + b

yi+1=kxi+1+by_{i+1} = kx_{i+1} + b

因为存在假设:xx 已知,且每次前进步长为 1,所以:xi+1=xi+1x_{i+1} = x_{i} + 1

则:

yi+1=kxi+1+by_{i+1} = kx_{i+1} + b

                          =k(xi+1)+b\;\;\;\;\;\;\;\;\;\;\;\;\; = k \left(x_{i} + 1\right) + b

                    =kxi+k+b\;\;\;\;\;\;\;\;\;\;= kx_{i} + k + b

                    =kxi+b+k\;\;\;\;\;\;\;\;\;\;= kx_{i} + b + k

    =yi+k\;\;= y_{i} + k

即:当前的 yy 值等于前一步的 yy 值 + 斜率 kk

这里的斜率 kk 也就是式子 yi+1=yi+ky_{i+1} = y_{i} + k 的增量

这样就把原来一个乘法和加法变成了一个加法

eg. 用 DDA 扫描转换连接两点 P0(0,  0)P_{0} \left(0,\;0\right)P1(5,  3)P_{1} \left(5,\;3\right) 的直线段

直线段扫描转换算法

1. 计算 kk

k=y1y0x1x0=3050=0.6<1k = \frac{y_{1} - y_{0}}{x_{1} - x_{0}} = \frac{3 - 0}{5 - 0} = 0.6 < 1

2. 计算像素点坐标

xx yy int(y+0.5)int \left(y + 0.5\right)
00 00 00
11 0+0.60+0.6 11
22 0.6+0.60.6+0.6 11
33 1.2+0.61.2+0.6 22
44 1.8+0.61.8+0.6 22
55 2.4+0.62.4+0.6 33

直线段扫描转换算法

注意:

k>1|k| > 1

使用上述计算方式可能不能画出直线,因为任意两点之间通过 DDA 算法计算直线,当 k>1|k| > 1 时,结果只有三个点,如果两点之间距离比较远,就会导致光栅点太稀疏,从而不能描绘直线

2. 中点画线法

对 DDA 的改进方案:

1. 提高算法效率

yi+1=yi+ky_{i+1} = y_{i} + k

因为在计算机中,加法运算是最快的,加法运算里整数加法又是最快的

一般情况下,yykk 都是小数,而且每一步运算都要对 yy 进行四舍五入后取整,所以可以通过把浮点运算变成整数运算来改进

2. 改变直线方程类型

中点画线法:利用直线的一般式方程

F(x,  y)=0F \left(x,\;y\right) = 0

Ax+By+C=0      (A=(Δy);    B=(Δx);    C=B(Δx))Ax + By + C = 0 \;\;\;\left(其中:A = - \left(\Delta y\right); \;\; B = \left(\Delta x\right); \;\; C = - B \left(\Delta x\right)\right)

这样一来,对于直线上的点:F(x,  y)=0F \left(x,\;y\right) = 0

对于直线上方的点:F(x,  y)>0F \left(x,\;y\right) > 0

对于直线下方的点:F(x,  y)<0F \left(x,\;y\right) < 0

原理解析:

每一次在最大位移方向上走一步,而另一个方向走不走取决于中点误差项的判断

直线段扫描转换算法

假设:0k10 \leq |k| \leq 1,所以每次在 xx 方向上 +1+1yy 方向上的变化情况需要通过判断 PuP_{u}PdP_{d} 的中点来决定

如图,此时 ideal line 与直线 PuPdP_{u}P_{d} 有一个交点 QQ,直线 PuPdP_{u}P_{d} 有一个中点:M(xi+1,  yi+0.5)M \left(x_{i}+1,\;y_{i}+0.5\right)

判断:

  • QQMM 上方时,PuP_{u} 距离直线更近,则 PuP_{u} 为下一像素点
  • QQMM 下方时,PdP_{d} 距离直线更近,则 PdP_{d} 为下一像素点
  • QQMM 是同一个点,则 PuP_{u}PdP_{d} 都可以是下一像素点

如何判断 QQMM 上方还是下方?

1. 把 MM 代入理想直线方程:

F(xm,  ym)=Axm+Bym+CF \left(x_{m},\;y_{m}\right) = Ax_{m} + By_{m} + C

d=F(xm,  ym)=F(xi+1,  yi+0.5)令:d = F \left(x_{m},\;y_{m}\right) = F \left(x_{i} + 1,\;y_{i} + 0.5\right)

    =A(xi+1)+B(yi+0.5)+C\;\;= A \left(x_{i}+1\right) + B \left(y_{i} + 0.5\right) + C

2. 判断

  • d<0d < 0 时:QQMM 上方,下一像素点为 PuP_{u}
  • d>0d > 0 时:QQMM 下方,下一像素点为 PdP_{d}
  • d=0d = 0 时:QQ 等于 MM,下一像素点为 PuP_{u}PdP_{d} 都可以

算法:

y={y+1(d<0)y(d0)y = \left\{\begin{matrix} y+1 & \left(d < 0\right) \\ y & \left(d \geq 0 \right) \end{matrix}\right.

算法计算量:为了求得 dd 值,需要 44 个乘法,22 个加法

对中点画线法的改进:因为 ddxx, yy 的线性函数,所以可以采用增量计算

推导过程:

直线段扫描转换算法

代入中点坐标求得 d0d_{0}

d0=F(xm0,  ym0)d_{0} = F \left(x_{m0},\;y_{m0}\right)

                            =F(xi+1,  yi+0.5)\;\;\;\;\;\;\;\;\;\;\;\;\;\;= F \left(x_{i}+1,\;y_{i}+0.5\right)

                                                          =A(xi+1)+B(yi+0.5)+C\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A \left(x_{i}+1\right) + B \left(y_{i}+0.5\right) + C

假设 d<0d < 0

则第一个取的像素点 P0P_{0}Pu(xi+1,  yi+1)P_{u} \left(x_{i} + 1,\;y_{i} + 1\right)

再往下计算一个像素点 P1P_{1} 的位置:

P1P_{1} 可能是点 Pa(xi+2,  yi+1)P_{a} \left(x_{i}+2,\;y_{i}+1\right) 或者 Pb(xi+2,  yi+2)P_{b} \left(x_{i}+2,\;y_{i}+2\right)

则直线 PaPbP_{a}P_{b} 的中点为 M1(xi+2,  yi+1.5)M_{1} \left(x_{i}+2,\;y_{i}+1.5\right)

所以:

d1=F(xm1,  ym1)d_{1} = F \left(x_{m1},\;y_{m1}\right)

                              =F(xi+2,  yi+1.5)\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = F \left(x_{i}+2,\;y_{i}+1.5\right)

                                                            =A(xi+2)+B(yi+1.5)+C\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A\left(x_{i}+2\right) + B\left(y_{i}+1.5\right) + C

                                                                                        =A(xi+1)+B(yi+0.5)+C+A+B\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A\left(x_{i}+1\right) + B\left(y_{i}+0.5\right) + C + A + B

      =d0+A+B\;\;\;= d_{0} + A + B

假设 d0d \geq 0

则第一个取的像素点 P0P_{0}'Pd(xi+1,  yi)P_{d} \left(x_{i} + 1,\;y_{i}\right)

再往下计算一个像素点 P1P_{1}' 的位置:

P1P_{1}' 可能是点 Pa(xi+2,  yi+1)P_{a} \left(x_{i}+2,\;y_{i}+1\right) 或者 Pc(xi+2,  yi)P_{c} \left(x_{i}+2,\;y_{i}\right)

则直线 PaPcP_{a}P_{c} 的中点为 M1(xi+2,  yi+0.5)M_{1}' \left(x_{i}+2,\;y_{i}+0.5\right)

所以:

d1=F(xm1,  ym1)d_{1} = F \left(x_{{m1}'},\;y_{{m1}'}\right)

                          =F(xi+2,  yi+0.5)\;\;\;\;\;\;\;\;\;\;\;\;\; = F \left(x_{i}+2,\;y_{i}+0.5\right)

                                                        =A(xi+2)+B(yi+0.5)+C\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A\left(x_{i}+2\right) + B\left(y_{i}+0.5\right) + C

                                                                        =A(xi+1)+B(yi+0.5)+C+A\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A\left(x_{i}+1\right) + B\left(y_{i}+0.5\right) + C + A

=d0+A          = d_{0} + A \;\;\;\;\;

计算 d0d_{0}

直线的第一个像素点坐标为:P(x0,  y0)P\left(x_{0},\;y_{0}\right)

d0=F(x0+1,  y0+0.5)d_{0} = F \left(x_{0}+1,\;y_{0}+0.5\right)

                                        =A(x0+1)+B(y0+0.5)+C\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = A \left(x_{0}+1\right) + B \left(y_{0}+0.5\right) + C

                                  =Ax0+By0+C+A+0.5B\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = Ax_{0} + By_{0} + C + A + 0.5B

因为 P(x0,  y0)P \left(x_{0},\;y_{0}\right) 在直线上,所以满足 Ax0+By0+C=0Ax_{0} + By_{0} + C = 0

所以:

d0=A+0.5Bd_{0} = A + 0.5B

中点画线法算法:

dnew={dold+A+B(d<0)dold+A(d0)      d0=A+0.5Bd_{new} = \left\{\begin{matrix} d_{old} + A + B & \left(d < 0\right) \\ d_{old} + A & \left(d \geq 0\right) \end{matrix}\right. \;\;\; d_{0} = A + 0.5B

至此,中点画线法算法与 DDA 的算法效率一样好

注意:

一般情况下,AABB 都是整数,而 d0d_{0} 的计算中存在浮点数,浮点数的运算更复杂

但是 d0d_{0} 的作用只是与 00 比较大小,所以完全可以用 2d02d_{0} 代替 d0d_{0},以此可以避免浮点数运算

此时中点画线法算法中只包含整数运算,所以更优于 DDA 算法

3. Bresenham 算法

Bresenham 拥有更优的效率和更广泛的适用范围

基本思想:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5cEgkci-1586352023419)(https://cdn.jsdelivr.net/gh/TUFZ/ImgHosting//TUFZ-Img/article/20Apr08A/06.png)]

通过各行、各列像素中心构造一组虚拟网格线,按照直线起点到终点的顺序,计算直线与各垂直网格线的交点,然后根据误差项(交点距离像素点的距离)的符号确定该列像素中与此交点最近的像素

假设每次 x+1x+1yy 的递增(减)量为 0011,它取决于实际直线与光栅网格点的距离,这个距离的最大误差是 0.50.5

  • 如果 d<0.5d < 0.5yy 的递增(减)量为 00
  • 如果 d>0.5d > 0.5yy 的递增(减)量为 11
  • 如果 d=0.5d = 0.5yy 的递增(减)量为 0011 中任意一个

误差项 dd 的初值 d0=0d_{0} = 0

初始之后的 d=d+kd = d + k

一旦 d1d \geq 1,就将其减 11,以保证 dd 的相对性,且始终在 0011 之间

计算下一个象素点的原理:

Pnext={xi+1=xi+1yi+1={yi+1(d>0.5)yi(d0.5)P_{next = }\left\{\begin{matrix} x_{i+1} = x_{i} + 1 \\ y_{i+1} = \left\{\begin{matrix} y_{i} + 1 & \left(d > 0.5\right) \\ y_{i} & \left(d \leq 0.5\right) \end{matrix}\right. \end{matrix}\right.

算法效率优化:

1. 令 e=d0.5e = d - 0.5

将值的大小替换成正负号

Pnext={xi+1=xi+1yi+1={yi+1(e>0)yi(e0)P_{next = }\left\{\begin{matrix} x_{i+1} = x_{i} + 1 \\ y_{i+1} = \left\{\begin{matrix} y_{i} + 1 & \left(e > 0\right) \\ y_{i} & \left(e \leq 0\right) \end{matrix}\right. \end{matrix}\right.

  • e=0.5e_{初} = -0.5
  • 每当 x+1x + 1,则 e=e+ke = e + k
  • 如果 e>0e > 0,则 e=e1e = e - 1

2. 令 2eΔx=e2e\Delta x = e

因为 k=dydxk = \frac{dy}{dx}dx=Δxdx = \Delta x

  • e=Δxe_{初} = -\Delta x
  • 每当 x+1x + 1,则 e=e+2Δye = e + 2\Delta y
  • 如果 e>0e > 0,则 e=e2Δxe = e - 2\Delta x

至此,算法全部改进为整数加法,且不受直线方程类型的限制

算法步骤:

  1. 输入直线的两端点 P0(x0,  y0)P_{0}\left(x_{0},\;y_{0}\right)P1(x1,  y1)P_{1}\left(x_{1},\;y_{1}\right)
  2. 计算初始值 Δx\Delta xΔy\Delta ye=Δxe = -\Delta xx=x0x = x_{0}y=y0y = y_{0}
  3. 绘制点 (x,  y)\left(x,\;y\right)
  4. ee 替换为 e+2Δye + 2\Delta y,判断 ee 的符号
    • e>0e > 0,则将 (x,  y)\left(x,\;y\right) 替换为 (x+1,  y+1)\left(x+1,\;y+1\right),同时将 ee 替换为 e2Δye - 2\Delta y
    • 否则,将 (x,  y)\left(x,\;y\right) 替换为 (x+1,  y)\left(x+1,\;y\right)
  5. 当直线没有画完时,重复步骤 3 和 4,否则结束

Bresenham 算法很像 DDA 算法,都是加斜率,但 DDA 算法是每次求出一个新的 yy 以后取整来画,而 Bresenham 算法是判符号来决定上下两个点,所以该算法集中了 DDA 和中点算法的优点,而且应用范围更广泛

4. 结

以上是基础原理,实现代码还是一个没填????

我的个人博客还有更多文章