繼續圖形學之旅,咱們已經解決了如何畫線和畫圓的問題,接下來要解決的是,如何往一個區域內填充顏色?對一個像素填充顏色只需調用SetPixel之類的函數就好了,因此這個問題其實就是:如何找到一個區域內的全部像素?c++
定義一個區域能夠有兩種方法,即內點表示法
和邊界表示法
,內點表示就是指用一種顏色表示區域內的點,只要當前像素是這種顏色就在區域內,邊界表示就是用一種顏色表示區域邊界,只要當前像素是這種顏色就表示到達了區域邊界。算法
最簡單暴力的填充算法便是從區域內一點出發,向四周擴散填充,到達區域邊界時中止,常見的有四鄰法
和八鄰法
兩種,顧名思義,一個是向上下左右四個方向擴散填充,另外一個是向周圍八個方向擴散,四鄰法能夠確保不溢出區域邊界,但有可能出現一次填不滿區域的狀況,八鄰法則相反,必定能填充滿當前區域,但有從對角線溢出邊界的危險。ide
邊界表示的四鄰法代碼實現:函數
void BoundaryFill4(HDC hdc, int x, int y, COLORREF boundaryColor, COLORREF newColor) { COLORREF c = GetPixel(hdc, x, y); if (c != newColor && c != boundaryColor) { SetPixel(hdc, x, y, newColor); BoundaryFill4(hdc, x + 1, y, boundaryColor, newColor); BoundaryFill4(hdc, x - 1, y, boundaryColor, newColor); BoundaryFill4(hdc, x, y + 1, boundaryColor, newColor); BoundaryFill4(hdc, x, y - 1, boundaryColor, newColor); } }
咱們用以前學習的Bresenham畫線算法畫一個矩形,而後用這個算法填充它。學習
Bresenham_Line(150, 150, 150, 200, hdc, RGB(0, 0, 0)); Bresenham_Line(150, 200, 200, 200, hdc, RGB(0, 0, 0)); Bresenham_Line(200, 200, 200, 150, hdc, RGB(0, 0, 0)); Bresenham_Line(200, 150, 150, 150, hdc, RGB(0, 0, 0)); BoundaryFill4(hdc, 175, 175, RGB(0, 0, 0), RGB(255, 0, 0));
運行效果:3d
很顯然,這種遞歸的填充算法簡單好理解,但效率是不可接受的,實際上我運行時填充100*100像素的區域就直接GG了(堆棧溢出),顯然咱們須要提升算法效率,避免過多的遞歸調用。code
爲了提升效率可使用掃描線種子填充算法,這裏的掃描線就是與x軸相平行的線,該算法能夠由如下4個步驟實現:blog
代碼實現:排序
void ScanLineFill4(HDC hdc, int x, int y, COLORREF oldColor, COLORREF newColor) { int xl, xr; bool SpanNeedFill; pair<int, int> seed; stack<pair<int, int>> St; seed.first = x; seed.second = y; St.push(seed); while (!St.empty()) { seed = St.top(); St.pop(); y = seed.second; x = seed.first; while (GetPixel(hdc,x,y) == oldColor)//向右填充 { SetPixel(hdc, x, y, newColor); x++; } xr = x - 1; x = seed.first - 1; while (GetPixel(hdc, x, y) == oldColor)//向左填充 { SetPixel(hdc, x, y, newColor); x--; } xl = x + 1; //處理上方的一條掃描線 x = xl; y = y + 1; while (x<xr) { SpanNeedFill = false; while (GetPixel(hdc,x,y)==oldColor) { SpanNeedFill = true; x++; } if (SpanNeedFill) { seed.first = x - 1; seed.second = y; St.push(seed); SpanNeedFill = false; } while (GetPixel(hdc, x, y) != oldColor && x < xr)x++; } //處理下方的一條掃描線 x = xl; y = y - 2; while (x < xr) { SpanNeedFill = false; while (GetPixel(hdc, x, y) == oldColor) { SpanNeedFill = true; x++; } if (SpanNeedFill) { seed.first = x - 1; seed.second = y; St.push(seed); SpanNeedFill = false; } while (GetPixel(hdc, x, y) != oldColor && x < xr)x++; } } }
此次畫一個不太規則的圖形試試吧。遞歸
Bresenham_Line(100, 100, 150, 150, hdc, RGB(0, 0, 0)); Bresenham_Line(150, 150, 200, 100, hdc, RGB(0, 0, 0)); Bresenham_Line(200, 100, 200, 300, hdc, RGB(0, 0, 0)); Bresenham_Line(200, 300, 100, 300, hdc, RGB(0, 0, 0)); Bresenham_Line(100, 300, 100, 100, hdc, RGB(0, 0, 0)); ScanLineFill4(hdc, 150, 175, RGB(255, 255, 255), RGB(0, 255, 0));
此次因爲對每個待填充區段只須要壓棧一次,因此效率提升了,也沒有堆棧溢出的危險,但說實話上面的填充進行了接近十秒鐘才完成,若是畫圖軟件使用這種填充算法估計是沒人會用了吧......
接下來是最複雜的一種的掃描線算法,須要多邊形的全部邊信息,主要思想是求得每一條掃描線與多邊形的交點,從而兩兩配對獲得處在多邊形內的區間,對這些區間進行上色,但要求掃描線與多邊形的交點,直接暴力地遍歷每條邊確定是不可行的,咱們須要引入活性邊表AET
和新邊表NET
來輔助計算。
NET中存放的是在該掃描線第一次出現的邊,也就是最低端點的y值等於當前掃描線位置的邊,對每個結點,須要存儲當前x值、直線斜率倒數和直線最高點y值,以下圖所示:
經過NET就能夠容易地獲得AET,AET中存放的是掃描線與多邊形的交點。咱們從下往上遍歷每條掃描線,對於掃描線i來講,將NET[i]中結點插入,將AET[i-1]中ymax=i的結點刪除,其他結點將x值加上斜率倒數以後插入,就獲得了AET[i]。
有了AET以後,只須要配對每兩個交點,把區間內像素上色就能夠了,但要注意,因爲要從左到右配對,因此
AET表應時刻保持按x座標遞增排序。
僞代碼:
void PolyFill(polygon,color) { 初始化新邊表NET和活性邊表AET; for(每條掃描線i) { 把ymin=i的邊放進邊表NET[i]; } for(每條掃描線i) { 把新邊表NET[i]中結點插入AET[i](x座標遞增有序排列); AET[i-1]中ymax!=i的結點加入AET[i]; 遍歷AET[i],把配對交點區間中像素上色; } }
還有一種基於掃描線思想的邊界標誌算法,比較適合用硬件實現。基本思想是對多邊形每條邊進行掃描轉換,找到多邊形邊界的全部像素,對每條與多邊形相交的掃描線按從左到右的順序掃描每一個像素,用一個布爾值inside表示當前點是否在多邊形內(初始爲false),只要掃描到多邊形邊界像素,就把inside取反,若inside爲真,則表示該點在多邊形內,則填充該像素。
僞代碼:
void edgemark_fill(polydef,color) { 對多邊形每條邊掃描轉換; inside=false; for(每條掃描線) { for(掃描線上每一個像素) { if(該像素是邊界像素) inside=!inside; else if(inside==true) SetPixel(x,y,color); } } }