继续图形学之旅,咱们已经解决了如何画线和画圆的问题,接下来要解决的是,如何往一个区域内填充颜色?对一个像素填充颜色只需调用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); } } }