1. 程式人生 > >計算機圖形學基礎 : 基本圖形生成演算法之直線的掃描轉換

計算機圖形學基礎 : 基本圖形生成演算法之直線的掃描轉換

學習了三種常用的直線掃描轉換演算法 :數值微分法(DDA)、中點畫線法和Bresenham畫線演算法.

注 : 本文中的程式都是假定斜率在0~1之間,其他斜率類似,做相應的簡單處理就好

數值微分法(DDA,  Digital Differential Analyzer)

直線掃描轉換最簡單的方法是先算出直線的斜率:  k = △y / △x △x = x1 - x0,△y = y1 - y0, (x0, y0)和(x1, y1)是線段的兩個端點座標。然後從線的起點開始,確定最近逼近於直線的y座標。假設端點座標都為整數,讓x從起點開始到終點,步增為1,計算對應的y座標,y = kx + B,並取畫素(x, round(y))。用這種方法可行、直觀,但是效率很差,因為每步運算都需要一個浮點乘法和一個舍入運算

yi+1 = kxi+1 + B = k(xi + △x) + B = kxi + B + k△x = yi + k△x 因此,當△x = 1時,有yi+1 = yi + k,每當x步增1,y遞增k。 DDA演算法程式為:
int round(float a) { return int(a + 0.5); }

void DDAline(int x0, int y0, int x1, int y1, int color)
{
	int x;
	float dx, dy, k, y;
	dx = x1 - x0;
	dy = y1 - y0;
	k = dx / dy;
	y = y0;
	for (x = x0; x <= x1; ++x)
	{
		DrawPixel(x, round(y), color);	
		y = y + k;
	}
}


在這個演算法中,y和k必須用浮點數表示,每一步運算又需要將y進行舍入取整,這使得該演算法不利於硬體的實現。

中點畫線法

中點畫線法的基本原理是:
直線在x方向增加一個單位,則在y方向上的增量只能是0和1之間。如圖,假設x座標為xp的的各畫素點中,與直線最近者已確定,為(xp, yp),用實心小圓表示;那麼下一個與直線最近的畫素只能是正右方P1(xp + 1, yp) 或者是右上方的點P2(xp + 1, yp + 1)兩者之一。 M(xp + 1, yp + 0.5) 表示P1和P2的中點,Q是理想直線與垂直線x = xp + 1的交點,如果M點在Q點下方,很明顯P2離直線更新一些,如果M在Q的上方,則P1離直線更新一些,我們取離直線最近的那個畫素點,如果M和Q重合,我們約定取正右方的那個點。 假設起點和終點為 (x0, y0)和(x1, y1),則直線方程為: F(x, y) = ax + by + c, 其中a = y0 - y1, b = x1 - x0,c = x0y1 - x1y0
對於直線上的點,F(x, y) = 0;直線上方的點,F(x, y) > 0;直線下方的點,F(x, y) < 0.  因此,判斷M在Q的上方還是下方,只需將M點帶入方程計算判別式: d = F(M) = F(xp + 1, yp + 0.5) = a(xp + 1) + b(yp + 0.5) + c 對於每一個畫素的判別式d,根據它的符號來判斷下一個畫素點。 由於d是xp和yp的線性函式,可採用增量計算來提高運算效率。 在d >= 0的情況下,取正右方畫素p1,欲判斷再下一個畫素應取哪個,應計算: d1 = F(xp +1 + 1, yp + 0.5) = a(xp + 1 + 1) + b(yp + 0.5) + c  = d + a 注:下一個點的座標是(xp + 1, yp),所以再下一個中點就是(xp + 1 +1, yp + 0.5) 故d的增量為a; 在d < 0 ,則取右上方的畫素p2,要判斷再下一個畫素,則應該計算: d2 = F(xp +1 + 1, yp + 1 + 0.5) = a(xp + 1 + 1) + b(yp + 1 + 0.5) + c  = d + a + b 注:下一個點的座標是(xp + 1, yp + 1),所以再下一個中點就是(xp + 1 +1, yp + 1 + 0.5)
d的增量為 a + b. d的初始值,第一個畫素應取左端點(x0, y0),相應的判別式為: d0 =F(x0 + 1, y0 + 0.5) = a(x0 + 1) + b(yp + 0.5) + c =  ax0 + by0 + c + a + 0.5b = F(x0, y0) + a + 0.5b 由於(x0, y0)在直線上,所以F(x0, y0) = 0,因此,d的初始值為a + 0.5b。 d的初始值包含小數因此可以用2d來代替d,寫出僅包含整數運算的演算法:
void MidPointLine(int x0, int y0, int x1, int y1, int color)
{
	int a, b, delta1, delta2, d, x, y;
	a = y0 - y1;
	b = x1 - x0;
	d = 2 * a + b;
	delta1 = 2 * a;
	delta2 = 2 * (a + b);
	x = x0;
	y = y0;
	DrawPixel(x, y, color);
	while (x < x1)
	{
		if (d < 0)
		{
			++x;
			++y;
			d += delta2;
		}
		else
		{
			++x;
			d += delta1;
		}
		DrawPixel(x, y, color);
	}
}


Bresenham畫線演算法


該演算法檢查一個誤差項的符號就可以確定該列的畫素。 如圖,假設x列的畫素已確定,其行下標為y,那麼下一個畫素的列座標比為x + 1,而行座標要麼不變,要麼遞增1,是否遞增1取決於誤差項d的值(如上圖所示)。因為直線起始點在畫素中心,所以誤差項d的初始值為0,x下標每增加1,d的值相應遞增直線的斜率值,即d = d + k.  一旦d >= 1,將它的值減去1,使得d的值總保持在0到1之間。當d >= 0.5時,直線與x + 1列垂直網格線交點最接近於當前畫素的右上方畫素點(x + 1, y + 1),d < 0.5時則為正右方畫素(x + 1, y)。 令 e = d - 0.5,當e >= 0時 , 下一個畫素值y遞增1,e < 0時y不遞增,e初始值為-0.5. 演算法如下:
void Bresenham(int x0, int y0, int x1, int y1, int color)
{
	int x, y, dx, dy;
	float k, e;
	dx = x1 - x0;
	dy = y1 - y0;
	k = dx / dy;
	e = -0.5; // 因為d的初始值為0
	x = x0;
	y = y0;
	for (int i = 0; i <= dx; ++i)
	{
		DrawPixel(x, y, color);
		x += 1;
		e = e + k;
		if (e >= 0) // 虛擬碼,實際比較浮點數不這樣進行比較
		{
			y += 1;
			e = e - 1;
		}
	}
}

寫完會發現,該演算法會用到小數和除法,為了利於硬體實現,可以改用整數以避免除法,演算法中只用到誤差項的符號,因此可以作如下替換: e' = 2 * e * dx; 整數Bresenham演算法為:
void Bresenham(int x0, int y0, int x1, int y1, int color)
{
	int x, y, dx, dy;
	float k, e;
	dx = x1 - x0;
	dy = y1 - y0;
	e = -dx
	x = x0;
	y = y0;
	for (int i = 0; i <= dx; ++i)
	{
		DrawPixel(x, y, color);
		x += 1;
		e = e + 2 * dy;
		if (e >= 0) // 虛擬碼,實際比較浮點數不這樣進行比較
		{
			y += 1;
			e = e - 2 * dx;
		}
	}
}

以上所有程式均是考慮斜率在0~1之間的時候~  初學者,歡迎各位大牛指出錯誤!