webgl与计算机图形学 | 青训营笔记

122 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

扫描线填充算法实现

通俗易懂的思路

就是一层一层(一个像素一个像素)的从低到高进行填充

算法基本原理(参照计算机图形学课本)

(1)求交:计算扫描线与多边形各边的交点;

(2)排序:把所有交点按x值递增顺序排序;

(3)配对:第一个与第二个,第三个与第四个等等;每对交点代表扫描线与多边形的一个相交区间;

(4)填色:把相交区间内的像素置成多边形颜色;

关键

扫描线多边形填充算法的主要步骤:

  1. 建立NET(NewEdgeList)
  2. 从最低扫描线开始到最高扫描线循环
  3. 建立或调整AET(ActiveEdgeList)
  4.  按照AET中的接点顺序填充

代码

//use:按鼠标左键确定一系列点,按回车进行填充,按delete擦除全部图形
#include <GLFW/glfw3.h>
#include <bits/stdc++.h>
#include <windows.h>

#define WIDTH 750.f
#define HEIGHT 750.f
#define length 0.02f
#define gap 20
#define pdd pair<double, double>
using namespace std;
struct edge {
    double x;     // 下端点的x坐标
    double delta; //斜率倒数
    double ymax;  //上端点y坐标
    double ymin;  //下端点y坐标
    edge *next;
};
bool cmpEdge(edge e1, edge e2) { //先比较最低点高度,再比较最低点的横坐标,再比较斜率
    // return e1.ymin == e2.ymin ? e1.x < e2.x : e1.ymin < e2.ymin;
    if (e1.ymin < e2.ymin) {
        return true;
    } else if (e1.ymin > e2.ymin) {
        return false;
    }
    if (e1.x < e2.x) {
        return true;
    } else if (e1.x > e2.x) {
        return false;
    }
    if (e1.delta * e2.delta > 0 && e1.delta < 0) { //同为负
        return e1.delta < e2.delta;
    } else if (e1.delta * e2.delta > 0 && e1.delta > 0) { //同为正
        return e1.delta > e2.delta;
    } else { //异号,负的放前
        return e1.delta < 0 ? true : false;
    }
}
vector<pdd> picture; //图形的顶点
bool finished = false;
void showLine() {
    glBegin(GL_LINE_LOOP); // 矩形
    glColor3f(1.f, 1.f, 1.f);
    for (auto it : picture) {
        double x = it.first, y = it.second;
        x = x / WIDTH * 2 - 1; //将像素坐标转换为标准化设备坐标
        y = 1 - y / HEIGHT * 2;
        glVertex3f(x, y, 0.0f);
    }
    glEnd(); // 矩形绘制结束
}
void drawYLine(double y, double x1, double x2) { //在特定高度y上绘制从x1到x2的一条直线
    glBegin(GL_LINES);                           // 直线
    glColor3f(1.f, 1.f, 1.f);
    // glLineWidth(5.0f);
    x1 = x1 / WIDTH * 2 - 1; //将像素坐标转换为标准化设备坐标
    x2 = x2 / WIDTH * 2 - 1;
    y = 1 - y / HEIGHT * 2;
    glVertex3f(x1, y, 0.0f);
    glVertex3f(x2, y, 0.0f);
    glEnd(); // 矩形绘制结束
}
void fillPicture() {
    deque<edge> net, aet;
    // 1.计算出所有的直线
    int n = picture.size();
    for (int i = 0; i < n; i++) {
        pdd p1 = picture[i % n], p2 = picture[(i + 1) % n]; //保证最后一个点和第一个点也参与
        if (p2.second < p1.second) {
            swap(p1, p2); //让p1是y较小的点
        }
        edge e;
        e.x = p1.first, e.ymin = p1.second, e.ymax = p2.second;    //加1保证交点处断开1个像素,出现两个点
        e.delta = (p2.first - p1.first) / (p2.second - p1.second); //斜率倒数
        net.push_back(e);
    }
    sort(net.begin(), net.end(), cmpEdge);
    //开始逐高度填充
    double curY = net.front().ymin;
    while (!net.empty() or !aet.empty()) {

        //删去AET中满足y=ymax的边
        for (auto it = aet.begin(); it != aet.end(); it++) {
            if (it->ymax == curY) {
                it = aet.erase(it);
                it--;
            }
        }

        //构造当前的活性边表
        bool isInsert = false;
        for (auto it = net.begin(); it != net.end(); it++) {
            if (it->ymin == curY) {
                edge tmp = *it;
                aet.push_back(tmp);
                it = net.erase(it);
                it--;
                isInsert = true;
            }
        }
        if (isInsert) { //只有加入新的边时才更新顺序
            sort(aet.begin(), aet.end(), cmpEdge);
        }
        //对于当前的活性边表,两两配对,画线
        for (int i = 0; i < aet.size(); i += 2) {
            drawYLine(curY, aet[i].x, aet[i + 1].x);
        }
        // y = y + 1,扫描线上移一像素。
        curY++;
        //对于AET中所有边,更新x = x + ∆x ymin
        for (auto &it : aet) {
            it.x += it.delta;
            it.ymin = curY;
        }
    }
}
// mouse button callback
void releaseLeftButton(GLFWwindow *window, int button, int action, int mods) {
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE) { //释放了鼠标左键
        double x, y;
        glfwGetCursorPos(window, &x, &y);
        cout << "当前位置 " << x << "," << y << std::endl;
        picture.push_back({x, y});
    }
}

void pressKeyBoard(GLFWwindow *window, int key, int scancode, int action, int mods) {
    if (key == GLFW_KEY_ENTER && action == GLFW_PRESS) { //回车确定当前图形
        cout << "开始填充" << endl;
        finished = true;
    }
    if (key == GLFW_KEY_DELETE && action == GLFW_PRESS) { //删除全部
        cout << "删除全部顶点并取消填充" << endl;
        picture.clear();
        finished = false;
    }
}
int main(void) {
    srand((int)time(NULL)); // 产生随机种子
    GLFWwindow *window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(WIDTH, HEIGHT, "计算机图形学实验_填充", NULL, NULL);
    glfwSetWindowPos(window, 150, 40);

    if (!window) {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window)) {
        /* Render here */
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // mouse
        glfwSetMouseButtonCallback(window, releaseLeftButton);

        // keyboard
        glfwSetKeyCallback(window, pressKeyBoard);

        // drawYLine(100, 200, 300);
        showLine(); // 展示全部线
        if (finished) {
            fillPicture(); //进行填充
        }

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

效果

image.png