我是用MFC框架進行測試的,由於本人也沒有專門系統學習MFC框架,代碼若有不足之處,請指出。
一,先來一個最簡單的DDA算法
DDA算法全稱為數值微分法,基於微分方程來繪制直線。
①推導微分方程如下:
②,dM時間步長的倒數的詳解:
可以看到
當|k|<=1時
dx=1或者-1,此時的x為計長方向
當|k|>1時
dy=1或者-1,此時的y為計長方向
繪制時需要用dM來控制繪制的點數
③繪制像素的問題:
為了“方便”管理算法,我為不同的繪制函數新建了一個類了。。。(其實可以寫到一個類里面。。。。)
④代碼實現:
MyDDA.cpp
1 #include "stdafx.h" 2 #include "MyDDA.h" 3 4 5 MyDDA::MyDDA() 6 { 7 } 8 9 10 MyDDA::~MyDDA() 11 { 12 } 13 14 void MyDDA::SetDc(CDC * dc) 15 { 16 this->pdc = dc; 17 } 18 19 void MyDDA::Moveline(CPoint start, CPoint end) 20 { 21 int x1, x2; 22 int y1, y2; 23 x1 = start.x; 24 x2 = end.x; 25 y1 = start.y; 26 y2 = end.y; 27 float dm = 0; 28 if (abs(x2 - x1) >= abs(y2 - y1)) 29 dm = abs(x2 - x1); 30 else 31 dm = abs(y2 - y1); 32 float dx = (float)(x2 - x1) / dm; 33 float dy = (float)(y2 - y1) / dm; 34 float x = x1 + 0.5; 35 float y = y1 + 0.5; 36 int i = 0; 37 while (i<dm) { 38 this->pdc->SetPixel((int)x, (int)y, RGB(0, 0, 0)); 39 x += dx; 40 y += dy; 41 i += 1; 42 } 43 44 }
總結:
其實這個算法還算是挺簡單的,就是要確定是X還算Y為計長方向,需要注意的是循環過程中計長方向自增1,另一個方向按照當直線在計長方向自增1時,直線在軸上的投影的大小自增就可以了。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
二,逐點比較法
逐點比較法是在畫線的過程中,每畫一點就與理想直線比較,以決定下一點的走向,以一步步逼近的方法點亮最接近直線的一組像素。
①繪制前先將直線平移
使y坐標較小的點位於坐標原點(繪制的時候再平移回去,跟幾何變換一個道理)
如圖:
②推導過程:
先來談一下直線的偏移值怎么表示
可以看到:
如果d是正值說明當前繪制點在繪制直線的上方。
反之,在下方。
③由於每次計算都需要計算兩次乘法,計算工作量大,所以可以利用遞推公式來,借助當前點的偏移值,獲取下一個點的偏移值。
遞推公式推導過程如下圖所示:
④終點判斷:
⑤整理下我們在編程時需要的參數:
⑥實現代碼:
注釋部分的代碼比較容易理解一點,但是太長了,注釋下面的代碼是簡化的版本,兩者的作用是一樣的。
PtoP.cpp
1 void PtoP::Moveline(CPoint start, CPoint end) 2 { 3 int xA, yA; 4 if (start.y > end.y) { 5 xA = start.x - end.x; 6 yA = start.y - end.y; 7 } 8 else 9 { 10 xA = end.x - start.x; 11 yA = end.y - start.y; 12 } 13 14 int n = abs(xA) + abs(yA); 15 16 int x = 0, y = 0, F = 0; 17 /* 18 if (xA > 0)//1 19 { 20 for (int i = 0; i < n; i++) 21 { 22 if (F >= 0) 23 { 24 x += 1; 25 F -= yA; 26 if(start.y>end.y) 27 this->pdc->SetPixel(x+end.x,y+end.y, RGB(0, 0, 0)); 28 else 29 this->pdc->SetPixel(x + start.x, y + start.y, RGB(0, 0, 0)); 30 } 31 else 32 { 33 y += 1; 34 F += xA; 35 if (start.y > end.y) 36 this->pdc->SetPixel(x + end.x, y + end.y, RGB(0, 0, 0)); 37 else 38 this->pdc->SetPixel(x + start.x, y + start.y, RGB(0, 0, 0)); 39 } 40 } 41 42 } 43 else//2 44 { 45 for (int i = 0; i < n; i++) 46 { 47 if (F >= 0) 48 { 49 y += 1; 50 F += xA; 51 if (start.y > end.y) 52 this->pdc->SetPixel(x + end.x, y + end.y, RGB(0, 0, 0)); 53 else 54 this->pdc->SetPixel(x + start.x, y + start.y, RGB(0, 0, 0)); 55 } 56 else 57 { 58 x-= 1; 59 F += yA; 60 if (start.y > end.y) 61 this->pdc->SetPixel(x + end.x, y + end.y, RGB(0, 0, 0)); 62 else 63 this->pdc->SetPixel(x + start.x, y + start.y, RGB(0, 0, 0)); 64 } 65 66 } 67 68 } 69 */ 70 for (int i = 0; i < n; i++) { 71 if (xA > 0) { 72 if (F >= 0) 73 { 74 x++; 75 F -= yA; 76 } 77 else 78 { 79 y++; 80 F += xA; 81 } 82 } 83 else { 84 if (F >= 0) 85 { 86 y++; 87 F += xA; 88 } 89 else 90 { 91 x--; 92 F += yA; 93 } 94 } 95 if (start.y > end.y) 96 this->pdc->SetPixel(x + end.x, y + end.y, RGB(0, 0, 0)); 97 else 98 this->pdc->SetPixel(x + start.x, y + start.y, RGB(0, 0, 0)); 99 } 100 }
⑦總結:
逐點比較法繪制最重要的是遞推公式的推導,以及避免原偏移值公式的無理運算。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
三,Bresenham畫線法
①引入
Bresenham畫線算法是應用最廣泛的直線生成算法,它采用加減以及乘2運算(即移位運算)來實現。
首先先從直線斜率為0<=k<=1的八分之一象限來開始討論。
如圖:
②如何判別選取點S還是點T
③迭代公式
④起點的判別值
⑤其他情況的判別
⑥整理所需變量
⑦代碼實現
Bresenham.cpp
1 #include "stdafx.h" 2 #include "Bresenham.h" 3 4 5 Bresenham::Bresenham() 6 { 7 } 8 9 10 Bresenham::~Bresenham() 11 { 12 } 13 14 void Bresenham::SetDc(CDC * dc) 15 { 16 this->pdc = dc; 17 } 18 19 void Bresenham::Moveline(CPoint start, CPoint end) 20 { 21 int x1=start.x, y1=start.y; 22 int x2=end.x, y2=end.y; 23 24 this->pdc->SetPixel(start.x,start.y,RGB(0,0,0)); 25 int dx, dy; 26 dx = abs(x2-x1); 27 dy = abs(y2-y1); 28 int flag=0; 29 if (dx == 0 && dy == 0) 30 return; 31 if (dy > dx) 32 { 33 flag = 1; 34 swap_value(x1,y1); 35 swap_value(x2,y2); 36 swap_value(dx,dy); 37 } 38 int tx = (x2 - x1) > 0 ? 1 : -1; 39 int ty = (y2 - y1) > 0 ? 1 : -1; 40 int curx = x1 + 1; 41 int cury = y1; 42 int dS = 2 * dy; 43 int dT = 2 * (dy-dx); 44 int d =dS-dx; 45 while (curx != x2) 46 { 47 if (d >= 0) { 48 d += dT; 49 cury += ty; 50 } 51 else 52 { 53 d += dS; 54 } 55 if (flag) 56 this->pdc->SetPixel(cury,curx,RGB(0,0,255)); 57 else 58 this->pdc->SetPixel(curx, cury, RGB(0, 0, 255)); 59 curx+=tx; 60 } 61 } 62 63 void Bresenham::swap_value(int & a, int & b) 64 { 65 /* 66 a ^= b; 67 b ^= a; 68 a ^= b; 69 */ 70 int temp = a; 71 a = b; 72 b = temp; 73 74 }
總結:Bresenham算法關鍵點還是在於迭代公式的推導,以及如何選擇下一個點,判斷x軸和y軸的步長是自增還是自減。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
四,中點畫線法
中點畫線法的思路跟Bresenham算法的思路極為相似,在學習完之后,我就覺得兩者的算法不同之處就是取點的判斷標准。
①構造判別式
②遞推公式推導
③其他斜率情形
④整理變量
⑤代碼實現
1 #include "stdafx.h" 2 #include "MidpointLine.h" 3 4 5 MidpointLine::MidpointLine() 6 { 7 } 8 9 10 MidpointLine::~MidpointLine() 11 { 12 } 13 14 void MidpointLine::SetDc(CDC * dc) 15 { 16 this->pdc = dc; 17 } 18 19 void MidpointLine::Moveline(CPoint start, CPoint end) 20 { 21 int x0 = start.x, x1 = end.x, y0 = start.y, y1 = end.y; 22 int a, b, d2, x, y, flag = 0; 23 if (abs(x1 - x0) < abs(y1 - y0)) 24 { 25 swap_value(x0,y0); 26 swap_value(x1, y1); 27 flag = 1; 28 } 29 if (x0 > x1) {//保證x0<x1,方便判別斜率 30 swap_value(x0, x1); 31 swap_value(y0, y1); 32 } 33 a = y0 - y1; 34 b = x1 - x0; 35 d2 = 2*a + b;//擺脫小數點,提高效率 36 if (y0 < y1) {//k>0 37 x = x0; y = y0; 38 this->pdc->SetPixel(x,y,RGB(0,0,0)); 39 while (x < x1) 40 { 41 if (d2 < 0) 42 { 43 x++; 44 y++; 45 d2 =d2+ 2*a + 2*b; 46 } 47 else { 48 x++; 49 d2 =d2+ 2 * a; 50 } 51 52 if(flag)//|k|>1 53 this->pdc->SetPixel(y, x, RGB(0, 0, 0)); 54 else 55 this->pdc->SetPixel(x, y, RGB(0, 0, 0)); 56 } 57 } 58 else {//k<0 59 x = x1; 60 y = y1; 61 this->pdc->SetPixel(x, y, RGB(0, 0, 0)); 62 while (x >x0) 63 { 64 if (d2 < 0) 65 { 66 x--; 67 y++; 68 d2 = d2-2 * a + 2 * b; 69 } 70 else { 71 x--; 72 d2 =d2- 2 * a; 73 } 74 75 if (flag)//|k|>1 76 this->pdc->SetPixel(y, x, RGB(0, 0, 0)); 77 else 78 this->pdc->SetPixel(x, y, RGB(0, 0, 0)); 79 } 80 } 81 } 82 83 void MidpointLine::swap_value(int & a, int & b) 84 { 85 /* 86 a ^= b; 87 b ^= a; 88 a ^= b; 89 */ 90 int temp = a; 91 a = b; 92 b = temp; 93 }
總結:
判斷的准則不同也導致函數的寫法跟之前Bresenham函數有很大的差別,最明顯的就是中點判斷法需要保證繪制直線的走向,以方便判斷直線的斜率,而Bresenham算法則不需要這一點,它只需要知道tx,ty就能知道直線的走向,對於起點,終點在哪里。
並沒有太大的約束。
最后來一張四種算法同時繪制直線的合照吧。。。。
最左邊是DDA,依次是逐點比較法,Bresenham,中點畫線法。
該工程的github鏈接:https://github.com/Thousandyearsofwar/DrawLine