文章

计算机图形学笔记——画直线算法

画线算法主要用来解决如下问题:对于某个直线方程,已知起点重点,计算机如何绘制出这个线段。计算机绘制线段其实就是绘制一系列的像素点。

DDA算法

思路: 对于直线方程

y=kx+by = kx + b

, 在一个坐标轴以单位长度步进,并计算得到另一坐标轴的步进,之后根据起点+步进得到一个点的坐标,这个点就是需要绘制的点。为了保证绘制的精度,选择步进较大的坐标轴为基础坐标轴,例如每次步进​

Δx>Δy\Delta x > \Delta y

, 那么以x轴为单位长度步进的坐标轴。

实现代码:

	//四舍五入 
	inline int round(float a) {return int(a+0.5f)}  
	
	void lineDDA(int startX, int startY, int endX, int endY)
	{
		float dx = endX - startX;
		float dy = endY - startY;
		float steps = fabs(dx) > fabs(dy) ? fabs(dx) : fabs(dy);
		float incX = dx / steps;
		float incY = dy / steps;

		for (int i = 0; i <= steps; i++)
		{
			float newX = (float)startX + (float)(i) * incX;
			float newY = (float)startY + (float)(i) * incY;
			drawPixel(round(newX), round(newY)); //画像素点
		}
	}

通过代码也可以看到,运算使用了浮点运算,取整,不利于用硬件实现。

Bresenham算法

同DDA算法一样选择步进坐标轴,我们假设步进坐标轴为

xx

轴,即斜率

k<1|k| < 1

, 因为绘制的像素点坐标必须是整数, 那么对于

(xn,yn)(x_n, y_n)

的下一个点

(xn+1,yn+1)(x_{n+1}, y_{n+1})

,有

xn+1=xn+1x_{n+1} = x_n + 1

,

yn+1<1y_{n+1} < 1

, 该点绘制的像素坐标会是

(xn+1,yn)(x_n + 1, y_n)

(xn+1,yn+1)(x_n + 1, y_n+1)

中的一个。

每次绘制下一个点时,我们期望有这样一个变量

pnp_n

, 如果

pn>0p_n > 0

下一个点的坐标为

(xn+1,yn+1)(x_n + 1, y_n+1)

,否则下一个点的坐标为

(xn+1,yn)(x_n + 1, y_n)

我们期望绘制的像素点离实际的坐标点距离越小越好,那么两个候选的像素坐标到实际坐标的距离分别为:

dupperd_{upper}

=

yn+1yn+1y_n+1 - y_{n+1}


dlowerd_{lower}

=

yn+1yny_{n+1} - y_n

对于直线方程

y=kx+by = kx + b


Δd=dlowerdupper=2yn+12yn1=2(kxn+1+b)2yn1\Delta d = d_{lower} - d_{upper} = 2y_{n+1} - 2y_n -1 = 2(kx_{n+1} + b) - 2y_n -1

其中

xn+1=xn+1x_{n+1} = x_n + 1

, 推导可得:

Δd=2(kxnyn)+2k+2b1\Delta d = 2(kx_n - y_n) + 2k + 2b - 1

注意:不能再继续用

yn=kxn+by_n = kx_n + b

代如上式推导下去了,因为点

(xn,yn)(x_n, y_n)

不一定
精确地
在直线上。

如果

Δd>0\Delta d > 0

, 那么下个点取

(xn+1,yn+1)(x_n + 1, y_n+1)

会准确一些,反之亦然,但

Δd\Delta d

还不能作为变量

pnp_n

使用,因为计算还存在浮点运算和除法。

到这一步的式子中,为了消除

kk

这个存在除法运算的变量,我们可以用

k=XendXstartYendYstart=ΔyΔxk = \frac{X_{end} - X_{start}}{ Y_{end} - Y_{start}} = \frac{\Delta y}{\Delta x}

代如上式, 然后等式两边同时乘以

Δx\Delta x

可得:

ΔxΔd=2(ΔyxnΔxyn)+2Δy+2bΔxΔx=2(ΔyxnΔxyn)+c\Delta x \Delta d = 2(\Delta y x_n - \Delta x y_n) + 2\Delta y + 2b\Delta x - \Delta x = 2(\Delta y x_n - \Delta x y_n) + c

因为

Δx\Delta x

在我们的设定中始终大于0, 这样我们就得到了需要的变量

pn=ΔxΔdp_n = \Delta x \Delta d

,计算该变量无需浮点运算和除法。

进一步,我们可以通过

pnp_n

pn+1p_{n+1}

的关系来继续简化运算:

pn+1pn=2(Δy(xn+1xn)Δx(yn+1yn))=2(ΔyΔx(yn+1yn))p_{n+1} - p_n = 2(\Delta y(x_{n+1} - x_n) - \Delta x(y_{n+1} - y_n)) = 2(\Delta y - \Delta x(y_{n+1} - y_n))

其中

yn+1y_n+1

,

yny_n

是我们要话的两个像素点,它们的差为

00

或者

1-1

, 得到:

pn+1={pn+2Δy\mboxpn0pn+2(ΔyΔx)\mboxpn>0p_{n+1} = \begin{cases} p_n + 2\Delta y &\mbox{p_n \leq 0} \ p_n + 2(\Delta y - \Delta x) &\mbox{p_n > 0} \end{cases}

其中:

p0=2(ΔyxnΔxyn)+c=2(ΔyxnΔx(ΔyΔxx0+b))+c=2ΔyΔxp_0 = 2(\Delta y x_n - \Delta x y_n) + c = 2(\Delta y x_n - \Delta x (\frac{\Delta y}{\Delta x} x_0 + b)) + c = 2\Delta y - \Delta x

下面给出斜率

k<1|k| < 1

的代码:

	void lineBresenham(int startX, int startY, int endX, int endY)
	{
		int x = startX;
		int y = startY;
		if (x > endX) //保证dx > 0
		{
			x = endX;
			endX = startX;
			y = endY;
			endY = startY;
		}
	
		int dx = endX - x;
		int dy = endY - y;
		int p = 2*dy - dx;
	
		drawPixel(x, y);
	
		while(x < endX)
		{
			x++;
			if (p > 0)
			{
				y++;
				p += 2 * (dy - dx);
			}
			else
			{
				p += 2 * dy;
			}
			drawPixel(x, y);
		}
	}

k>1|k|>1

时,选择y方向的步进即可,对于一些特殊斜率,如0, 1, 无穷大,直接使用特殊处理会更简单。