欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。
本期话题:给定多边形的边如何填充多边形内部区域
朴素做法
- 枚举法
枚举屏幕上的每一点,判断点是否在多边形内。点在多边形内判断方法
运算量太大。 - 扫描线求交法
从小到大枚举平等于x轴的直线, 直线与多边形的边求交,所有交点x轴从小到大排序,每两点之间画线 (根据射线法可知)。
用一条线(平行于x轴)与多边形求交,会得到偶数个交点,每两点之间作线段。
复杂度 (ymax-ymin)*n, n为边数量。
基于有序活动边表优化
扫描线求交法的缺点是每次都要与所有边求交。但实际上,从y 到 y+1求交时,可能求交的还是原来的线段,或者改变的边就几条。
从上面图可以看出,y+1时相交的线有可能不变化(a), 当y与一条线交点是线段的最高点时,就会有变化(b)。
基于这个思想,我们只要维护一个动态边表,当前与哪些线段,每次y增加,对交点进行变化,并检查是否有边到达最高点进行替换。y+1, x的变化可以参考DDA画线算法。
数据结构设计
// 直线定义
class Line {
public:
Point low; // 低点 y较小的点
Point high; // 高点 y较大的点
};
// 动态边表数据结构
class ActiveEdge {
public:
double x; // 与该边交点的x,每次+k
int maxY; // 边最高点y轴坐标
int deltaX, deltaY; // 最左最右距离,最高最低距离。
double k ; // 线段斜率, ∆x/∆y
};
// 活动边表有序,先比较x, 相同情况下比较斜率
vector<ActiveEdge> activeEdgeList;
算法过程
step 1 对所有线段按照low.y 从低到高排序
step 2
for y 从ymin开始遍历到ymax {
1. 对activeEdgeList所有边的x+=k
2. 对activeEdgeList每两个交点作线段
3. 查看activeEdgeList边中有没有ymax<=y的边,如果有就删除
4. 查看线段集合中有没有ymin=y的边,如果有就加入到activeEdgeList(平行于x轴的边不考虑),并排序
}
上图中蓝色边是中活动边表中的边。绿色线为已经填充区域。每次到达一条线的最高点,时会有新的边更新。
算法实现
#include "glew/2.2.0_1/include/GL/glew.h"
#include "glfw/3.3.4/include/GLFW/glfw3.h"
#include <iostream>
#include <vector>
using namespace std;
double esp = 1e-6;
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
//如果按下ESC,把windowShouldClose设置为True,外面的循环会关闭应用
if(key==GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
std::cout<<"ESC"<<mode;
}
int cmpDouble(double a, double b) {
if (fabs(a-b)<esp)return 0;
if (a<b)return -1;
return 1;
}
class ActiveEdge {
public:
double x; // 每次+k
int maxY;
int deltaX, deltaY;
double k ; // ∆x/∆y
};
int curY;
bool cmpActiveEdge(ActiveEdge a, ActiveEdge b) {
if(curY>=a.maxY)return false;
if(curY>=b.maxY)return true;
int eq = cmpDouble(a.x, b.x);
if(eq) return eq<0; // x小的排前面
eq = cmpDouble(a.k, b.k); // 斜率小说明x在减小得快
return eq<0;
}
class Point {
public:
int x, y;
Point(int xx, int yy):x(xx), y(yy){}
Point(){}
};
class Line {
public:
Point low, high;
Line(){}
Line(Point tp1, Point tp2):low(tp1),high(tp2){
if (tp2.y<tp1.y){
low=tp2;
high=tp1;
}
}
void Out(){
printf("%d,%d-%d,%d\n", low.x, low.y, high.x, high.y);
}
};
bool cmpLine(Line a, Line b) {
return a.low.y<b.low.y;
}
void setPixel(Point p) {
// cout<<p.x<<","<<p.y<<endl;
glPointSize(1);
glBegin(GL_POINTS);
glVertex2i(p.x, p.y);
glEnd();
glFlush();
}
void drawHorizon(int start, int end, int y) {
for(;start<=end;start++){
setPixel(Point(start, y));
}
}
/*
* 扫描线从低到高扫描
* 先更新活动边表x值,每一对点画一条线段。
* 查看当前y值是否有新线段加入,有则入加,并排序。
*/
void fillPolygen(vector<Line> polygen) {
//1. 对线段按照底端从低到高排序
sort(polygen.begin(), polygen.end(), cmpLine);
// 2. 查找最低点与最高点
int minY = polygen[0].low.y;
int maxY = polygen[0].high.y;
for (Line l : polygen){
maxY = max(maxY, l.high.y);
}
// 定义活动边表
vector<ActiveEdge> ae;
for(int h=minY, j=0;h<=maxY;h++) {
bool needSort=false;
// 更新提有活动边x值。
for(int i=0;i<ae.size();i++) {
if (h>ae[i].maxY)continue;
if (ae[i].deltaX) ae[i].x += ae[i].k;
// 每两点画一条线
if (i&1 && h<=ae[i].maxY) {
drawHorizon(ae[i-1].x, ae[i].x, h);
}
if (h>=ae[i].maxY && !needSort) needSort=true; // 有边需要删除,删除采用假删除,将无用边排到最后边去
}
// 查看当前是否有边加入
for(;j<polygen.size() && polygen[j].low.y==h;j++){
// 平行边不需要
if (polygen[j].low.y== polygen[j].high.y)continue;
if (!needSort) needSort=true; // 有新边加入,需要排序
ActiveEdge newEdge;
newEdge.maxY = polygen[j].high.y;
newEdge.x = polygen[j].low.x;
newEdge.deltaX = polygen[j].high.x-polygen[j].low.x;
newEdge.deltaY = polygen[j].high.y-polygen[j].low.y;
newEdge.k = double(newEdge.deltaX) / double(newEdge.deltaY); // ∆x/∆y
ae.push_back(newEdge);
}
if (needSort) {
curY=h;
sort(ae.begin(), ae.end(), cmpActiveEdge);
}
}
}
vector<Line> getTriangle() {
vector<int> pList = {
-100,-200,200,50,
-100,-200,0,100,
0,100,200,50,
};
vector<Line> sq;
for(int i=0;i+3<pList.size();i+=4){
sq.push_back(Line(Point(pList[i]-350,pList[i+1]-250),Point(pList[i+2]-350,pList[i+3]-250)));
}
return sq;
}
vector<Line> get5Triangle() {
vector<int> pList = {
-100,-200,150,0,
-100,-200,0,100,
-150,0,150,0,
100,-200,-150,0,
100,-200,0,100,
};
vector<Line> sq;
//glBegin(GL_LINES);
for(int i=0;i+3<pList.size();i+=4){
sq.push_back(Line(Point(pList[i]-350,pList[i+1]+50),Point(pList[i+2]-350,pList[i+3]+50)));
//glVertex2i(pList[i],pList[i+1]);
//glVertex2i(pList[i+2],pList[i+3]);
}
//glEnd();
return sq;
}
vector<Line> getUnNor() {
vector<int> pList = {
-10,10,100,-50,
120,51,150,-58,
170,100,200,-50,
300,150,250,120,300,170,270,200,
210,180,200,250,
150,210,
110,230,
};
vector<Line> sq;
// glBegin(GL_LINES);
for(int i=0;i+3<pList.size();i+=2){
sq.push_back(Line(Point(pList[i],pList[i+1]),Point(pList[i+2],pList[i+3])));
//glVertex2i(pList[i],pList[i+1]);
//glVertex2i(pList[i+2],pList[i+3]);
}
int n = pList.size();
sq.push_back(Line(Point(pList[n-2],pList[n-1]),Point(pList[0],pList[1])));
//glVertex2i(pList[n-2],pList[n-1]);
//glVertex2i(pList[0],pList[1]);
//glEnd();
return sq;
}
vector<Line> getSquare() {
vector<int> pList = {
0,0,100,0,
0,0,0,100,
100,0,100,100,
0,100,100,100,
};
vector<Line> sq;
for(int i=0;i+3<pList.size();i+=4){
sq.push_back(Line(Point(pList[i]-350,pList[i+1]+250),Point(pList[i+2]-350,pList[i+3]+250)));
}
return sq;
}
int main(void) {
//初始化GLFW库
if (!glfwInit())
return -1;
//创建窗口以及上下文
GLFWwindow *window = glfwCreateWindow(1024, 900, "hello world", NULL, NULL);
if (!window) {
//创建失败会返回NULL
glfwTerminate();
}
//建立当前窗口的上下文
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, key_callback); //注册回调函数
//glViewport(0, 0, 400, 400);
gluOrtho2D(-512, 512, -450, 450);
//循环,直到用户关闭窗口
cout<<123<<endl;
while (!glfwWindowShouldClose(window)) {
/*******轮询事件*******/
glfwPollEvents();
//选择清空的颜色RGBA
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
vector<Line> polygen = getSquare();
glColor3f(1,0, 0);
fillPolygen(polygen);
polygen = getTriangle();
glColor3f(1,1, 0);
fillPolygen(polygen);
glColor3f(1,0, 1);
polygen = get5Triangle();
fillPolygen(polygen);
polygen = getUnNor();
fillPolygen(polygen);
/******交换缓冲区,更新window上的内容******/
glfwSwapBuffers(window);
//break;
}
glfwTerminate();
return 0;
}
算法效果
该算法适用于规则图形,也适用于复杂的凸多边形与凹多边形。
本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。