直线段扫描转换算法

本文内容总结自孙家广《计算机图形学》

直线段绘制算法的发展基本是从数值微分法

到中点画线法

再到Bresenham画线算法

目前流行的渲染器使用的都是Bresenham画线算法,前两个也可以了解一下

数值微分直线算法

众所周知我们生活的现实世界的一切都是离散的(我胡说的),DDA直线算法就是用离散像素点去逼近直线段的一种算法。

我们先来看一下直线的函数表达,最常用的斜截式(顺便练练Markdown的数学公式= =)

y=kx+by=kx+b

k=ΔyΔxk=\frac{\Delta y}{\Delta x}

显示屏幕无论分辨率有多少总之还是一个一个点,在画线段的时候,已知两个端点,就是要把中间的点画出来,我们假设起点(x1,y1)(x1,y1),终点(x2,y2)(x2,y2),画线段时我们从x1开始,再怎么样x轴的增量也得是整数,1的整数倍,那么很自然的可以得出以下结论:

yi=kxi+by_i=kx_i+b

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

由于步长一定是1的倍数

yi+1=k(xi+1)+b=kxi+k+b=kxi+b+k=yi+kyi+1=yi+k\begin{aligned} y_{i+1}&=k(x_i+1)+b\\ &=kx_i+k+b\\ &=kx_i+b+k\\ &=y_i+k\\ y_{i+1}&=y_i+k \end{aligned}

可以发现yy的步长增量是kk

举个例子:

我们要画出P0(0,0)P_0(0,0)P1(5,3)P_1(5,3)的直线段。

用PS随便画了一下,长这个样子:

20211226172334

推算过程如下:

20211226172421

加0.5是为了向下取整的时候减少误差,比如P(1.7,0.8)P(1.7,0.8)这个点更接近的整数点应该是(2,1)(2,1),直接向下取整会变成(1,0)(1,0)

那为啥不四舍五入呢

四舍五入也是加0.5再向下取整

DDA算法有比较明显的缺点,斜率大于1的线段取点会不正常:

y=1.5x+1y=1.5x+1

我们画一个P(0,0)P(0,0)P(5,8.5)P(5,8.5)

20211226211219

已经很难说这是个线了……取点太稀疏,中间直接断掉了

中点画线算法

这个算法在数值微分算法上改进,数值微分法步进增量一般情况下都是小数,中点画线可以改进为整数。

我们知道直线的一般式方程是这样的:

Ax+By+C=0Ax+By+C=0

A=(Δy)A=-(\Delta y)

B=ΔxB=\Delta x

C=B(Δx)C=-B(\Delta x)

对于直线F(x,y)F(x,y),点有三种情况

F(x,y)=0线F(x,y)>0线F(x,y)<0线\begin{aligned} F(x,y)&=0\quad点在直线上\\ F(x,y)&>0\quad点在直线上方\\ F(x,y)&<0\quad点在直线下方\\ \end{aligned}

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

假定0k10\leqslant\vert k \vert \leqslant 1,每次在xx方向上加1,yy方向上只有两种情况,加1或者不加1,也就是说P(xi,yi)P(x_i,y_i)的下一个点有可能是Pu(xi+1,yi+1)P_u(x_{i+1},y_{i+1})或者是Pd(xi+1,yi)P_d(x_{i+1},y_{i})

20211226215524

如图所示,Q是直线的理想点,M是PuP_uPdP_d的中点M(xi+1,yi+0.5)M(x_{i+1},y_{i+0.5})

如果QyQ_y大于MyM_y说明PuP_u距离理想直线更近,否则是PdP_d更理想

那么问题就变成了怎么判断Q与M的关系

假设我们有点P(x0,y0)P(x_0,y_0),那么我么下一个点P1P_1虽然不确认y值,但是下一个理想的中点M1(x1+1,y1+0.5)M_1(x_1+1,y_1+0.5)是确定的,那么我们就有步进增量:

d0=F(xi+1,yi+0.5)=A(xi+1)+B(yi+0.5)+C=A+0.5B\begin{aligned} d_{0}&=F(x_{i}+1,y_{i}+0.5) \\&= A(x_{i}+1)+B(y_{i}+0.5)+C\\&= A+0.5B \end{aligned}

d1=F(xi+2,yi+1.5)=A(xi+2)+B(yi+1.5)+C=A(xi+1)+B(yi+0.5)+C+A+B=d0+A+B\begin{aligned} d_{1} &= F(x_{i}+2,y_{i}+1.5)\\ &=A(x_{i}+2)+B(y_{i}+1.5)+C \\ &= A(x_{i}+1)+B(y_{i}+0.5)+C+A+B\\ &=d_{0}+A+B \end{aligned}

实际上起点是(0,0)(0,0),所以有:

di=F(xi+1,yi+0.5)=A(xi+1)+B(yi+0.5)+C=A+0.5B\begin{aligned} d_{i}&=F(x_{i}+1,y_{i}+0.5)\\ &= A(x_{i}+1)+B(y_{i}+0.5)+C\\ &= A+0.5B \end{aligned}

d0=A+0.5Bd_0 = A+0.5B

d<0d<0则取点PuP_u

d>0d>0则取点PdP_d

等于0随便取一个

然后就能得到关于d的步进:

dnew={dold+A+B,d<0dold+A,d0d_{new}=\left\{ \begin{aligned} d_{old}+A+B \quad ,d<0\\ d_{old}+A \quad ,d\geqslant0 \end{aligned} \right.

当然,为了提高效率,避免浮点数加法,一般把d扩大一倍,就得到2d0=2A+B2d_0=2A+B

这样就是,每到下一步,先计算dnewd_{new},然后就可以判断选yiy_{i}还是yi+1y_{i+1}

接下来画一个中点画线法的直线:

(0,0)(0,0)(5,2)(5,2)

x y d
0 0 1(初始值)
1 0 -3
2 1 3
3 1 -1
4 2 5
5 2 1

Bresenham画线算法

这个算法描述起来很简单。

我们计算机画点都得是整数,第一个点直接四舍五入是一定可以确认的,那么下一个点的话,用xx增量的话下一个必定是x+1x+1,我们需要确定yy

Bresenham画线算法计算y的误差,就是斜率k,第一个点画完后画第二个点,y的增量是k,x每增加1,误差增加k,假设误差为d,那么递增直线的误差d=d+kd = d+k,当d0d \geqslant 0的时候就需要减去1,始终保证d在0和1之间,这样在d小于0.5的时候,y不变,否则y增加1

思路就是这个思路,当然书上对该算法进行硬件实现的时候用整数改进了除法

既然是判断d和0.5的关系,那么就是判断d减去0.5之后与0的关系,但这样开始要计算k

我们用e来表示d减去0.5

比较e和0的关系其实就只是关注e的正负号而已

代码实现的时候e=0.5e = - 0.5(初始值),递增k,大于等于0的时候减1

首先乘2,浮点数没了,还要想一下怎么把k弄掉,k还是需要一个浮点数除法

e=e+k2e=2e+2ΔyΔxΔx2e=Δx2e+Δy\begin{aligned} e &= e+k\\ 2e &= 2e + 2\frac{\Delta y}{\Delta x}\\ \Delta x * 2e &= \Delta x * 2e + \Delta y\\ \end{aligned}

也就是说我们定义一个gg

g=2eΔxg = 2e * \Delta x

那么就有了新的递增计算:

g=g+2Δyg = g + 2* \Delta y

新的初始值:

e=0.52eΔx=Δxg=Δx\begin{aligned} e &= -0.5\\ 2e*\Delta x &= - \Delta x\\ g &= - \Delta x \end{aligned}

到这里就完成了整数算法


直线段扫描转换算法
http://muchenhen.com/posts/51990/
作者
木尘痕
发布于
2021年12月31日
许可协议