计算机图形学——OPENGL应用

计算机图形学——OPENGL多边形填充

一 实验目的

通过实践对多边形填充算法有更充分的认识,让同学们上完计算机图形学这门课之后都不是仅仅停留在理论,通过自己动手对opengl,c++都有更好的使用,同时也为我们打开了新世界的大门。帮助我们学习计算机图形学的知识。

二 实验内容

1 OpenGL 实现
2 通过橡皮筋交互输入多边形
3 清屏重置多边形
4 多边形扫描算法中的顶点处理以每条边减去一个像素方法处理
5 要类(或模版类)来表示数据结构
6 多文档组织,至少要用头(.h)文件表示数据结构
7 通过菜单交互
8 自相交多边形、多个多边形的扫描填充
9 通过文件存储和读出已经交互输入的多边形

三 算法实现

. 1. 活性边表的具体变化过程
“活动边表”是扫描线填充算法的核心,整个算法都是围绕着这张表进行处理的。要完整的定义“活动边表”,需要先定义边的数据结构。每条边都和扫描线有个交点,扫描线填充算法只关注交点的x坐标。每当处理下一条扫描线时,根据△x直接计算出新扫描线与边的交点x坐标,可以避免复杂的求交计算。一条边不会一直待在“活动边表”中,当扫描线与之没有交点时,要将其从“活动边表”中删除,判断是否有交点的依据就是看扫描线y是否大于这条边两个端点的y坐标值,为此,需要记录边的y坐标的最大值。根据以上分析,边的数据结构可以定义如下:

  //“活动边表(AET)”和“新边表(NET)”
    typedef struct XET {
        float x;
        float dx, ymax;
        XET* next;
    }AET, NET;



A(288,769) B(293,530) C(714,784) D(734,506)
图(1) 多边形与扫描线示意图

扫描线算法的核心就是围绕“活动边表(AET)”展开的,为了方便活性边表的建立与更新,我们为每一条扫描线建立一个“新边表(NET)”,存放该扫描线第一次出现的边。当算法处理到某条扫描线时,就将这条扫描线的“新边表”中的所有边逐一插入到“活动边表”中。“新边表”通常在算法开始时建立,建立“新边表”的规则就是:如果某条边的较低端点(y坐标较小的那个点)的y坐标与扫描线y相等,则该边就是扫描线y的新边,应该加入扫描线y的“新边表”。 图(1)所示多边形中各扫描线的“新边表”如下图所示:

图(2) 各扫描线的新边表
然后对边表进行排序,具体实现代码见下面功能模块。
四 实验结果

  1. 通过橡皮筋交互输入多边形
    通过橡皮筋交互输入相应的多边形,鼠标左键为输入点,右键为结束划线(大于两个点,否则撤销输入),最后一点与开始的点连线形成多边形。

    图(3)橡皮筋输入的多边形
  2. 对输入的多边形进行填充
    在交互输入多边形的过程中,把相应点的坐标存放到多边形结构体中,然后将多边形在算法中扫描建立NET表,然后进行排序,最后将其填充。

    图(4)填充之后的多边形 算法步骤:
  3. 首先对输入的多边形求出最高点和最低点,减少扫描线扫描的次数,提高计算速度。

    vector<point> polypoint = s.p;</point>

      int POINTNUM = polypoint.size();
      int MinY=1000, MaxY = 0;//扫描开始,结束的位置
      for (int i = 0; i < POINTNUM; i++) {
          if (polypoint[i].y > MaxY) {
              MaxY = polypoint[i].y;
          }
          if (polypoint[i].y < MinY) {
              MinY = polypoint[i].y;
          }
      }

2.预先初始化NET,AET,原理上讲,填充的时候是根据活性边表AET进行填充的,但是活性边表AET的更新又是依据边表NET。然后自上而下扫描并建立NET。

for (int i = MinY; i < MaxY; i++) {
    for (int j = 0; j < POINTNUM; j++) {
        if (polypoint[j].y == i) {
            /*前面的那个点*/
            if (polypoint[(j - 1 + POINTNUM) % POINTNUM].y > polypoint[j].y) {
                NET* p = new NET;
                p->x = polypoint[j].x;
                p->ymax = polypoint[(j - 1 + POINTNUM) % POINTNUM].y;
                p->dx = (polypoint[(j - 1 + POINTNUM) % POINTNUM].x - polypoint[j].x) / (polypoint[(j - 1 + POINTNUM) % POINTNUM].y - polypoint[j].y);//斜率的倒数
                p->next = pNET[i]->next;
                pNET[i]->next = p;
            }
            /*后面的那个点*/
            if (polypoint[(j + 1 + POINTNUM) % POINTNUM].y > polypoint[j].y) {
                NET* p = new NET;
                p->x = polypoint[j].x;
                p->ymax = polypoint[(j + 1 + POINTNUM) % POINTNUM].y;
                p->dx = (polypoint[(j + 1 + POINTNUM) % POINTNUM].x - polypoint[j].x) / (polypoint[(j + 1 + POINTNUM) % POINTNUM].y - polypoint[j].y);
                p->next = pNET[i]->next;
                pNET[i]->next = p;
            }
        }
    }
}
  1. 计算新的交点x,更新AET。更新完以后,对活性边表AET按照x值从小到大排序。
  NET* p = pAET->next;
            while (p) {
                p->x = p->x + p->dx;
                p = p->next;
            }
        //更新完以后,对活性边表AET按照x值从小到大排序
        AET* tq = pAET;
        p = pAET->next;
        tq->next = NULL;
        while (p) {
            while (tq->next && p->x >= tq->next->x)//重新排序
                tq = tq->next;
            NET * s = p->next;
            p->next = tq->next;
            tq->next = p;
            p = s;
            tq = pAET;
        }

4.顶点处理,删除y=ymax的节点,避免出现奇顶点。

AET* q = pAET;
        p = q->next;
        while (p) {
            if (p->ymax == i) {
                q->next = p->next;
                delete p;
                p = q->next;
            }
            else {
                q = q->next;
                p = q->next;
            }
  1. 将NET中的新点加入AET,并用插入法按x递增排序
    p = pNET[i]->next;
            q = pAET;
            while (p) {
                while (q->next && p->x >= q->next->x)
                    q = q->next;
                NET * s = p->next;
                p->next = q->next;
                q->next = p;
                p = s;
                q = pAET;
            }
            p = pAET->next;
  1. 改写像素的颜色值
    while (p && p->next) {
                for (float j = p->x; j <= p->next->x; j++) {
                    glColor3f(1.0, 0.0, 0.0);//填充颜色
                    glVertex2i(static_cast<int>(j), i);
                }
                p = p->next->next;////考虑端点情况
        }
  1. 自相交多边形、多个多边形的扫描填充
    循环调用一个多边形的填充方法即可,下图中五角星中间为空,是因为填充时,按照排序后的边表进行填充的,每一个扫描线对应填充的点是1和2,3和4……之间的空白区域,故刚好中间为空。

图(5)自相交多边形及填充 图(6)多个多边形输入及填充
主要代码为:
  if (!s.empty())
        {
            for (i = 0; i < s.size(); i++)   //对多边形类向量循环,该向量中的每个元素代表一个多边形
            {
                GLfloat a = s[i].red, b = s[i].green, c = s[i].blue;
                glColor3f(a, b, c);
                int k = s[i].p.size();  //将一个多边形的点的个数
                for (j = 0; j < k; j++) //画多边形
                {
                    glBegin(GL_LINES); //将当前的点与后一个点连线
                    glVertex2i(s[i].p[j].x, s[i].p[j].y);
                    glVertex2i(s[i].p[(j + 1) % k].x, s[i].p[(j + 1) % k].y);//,通过取模操作来避免越界问题,该思路来源于循环队列.
                    glEnd();
                }
                PolygonScanning (s[i]);
            }
        }

. 4. 通过文件存储和读出已经交互输入的多边形
首先通过橡皮筋算法画出多边形时,将该多边形所有的点集放入一个多边形,然后进行填充。然后继续画图,都放入多边形集中,这样保存所有已画多边形数据到多边形集中
,循环调用填充函数进行填充,之后然后显示出来。

图(7)画出的多边形 图(8)txt文件中保存的信息

实现一个循环,将多个多边形点集写入文件中,具体代码为:
/将已经画好了的多边形保存到文件中/

  void SavePolygon()
{
    int polygonnum = s.size();//获得多边形的数量,着一保存
    ofstream out;//文件输出流
    out.open("data.txt");
    if (!out.is_open())//不存在该文件时
        cout << "error:不存在该文件,无法打开!" << endl;
    else
        for (int i = 0; i < polygonnum; i++)
        {
            polygon poly = s[i];
            int pointsize = poly.p.size();
            for (int j = 0; j < pointsize; j++)
                out << poly.p[j].x << "," << poly.p[j].y << " ";//多边形点与点之间用空格间隔开
            out << endl;//当一个多边形输入完毕,换行输入下一个多边形
        }
    out.close();
    cout << "成功保存多边形! " << endl;
}

从txt文件中按行读取相应点的坐标,我自己写了一个分割函数,将字符串按照输入的字符进行分割。分割之后,循环将每行得到的点集将放进一个多边形。将获得的多边形集,循环调用填充函数将所有的多边形都显示出来。

图(9)txt文件中保存的信息 图(10)读取、填充之后的多边形

//根据指定字符对字符串进行分割,从而能从文件中得到所需要的坐标值,返回的是一个存放分割后的每一个字符串矢量容器
vector<string> SplitString(string srcStr, const string& delim)
{
    int nPos = 0;
    vector<string> vec;
    nPos = srcStr.find(delim.c_str());
    while (-1 != nPos)
    {
        string temp = srcStr.substr(0, nPos);
        vec.push_back(temp);
        srcStr = srcStr.substr(nPos + 1);
        nPos = srcStr.find(delim.c_str());
    }
    vec.push_back(srcStr);
    return vec;
}

/载入上次画好的多边形/

void ReadPolygon() {
    ifstream in("data.txt");
    string bufline;//保存从文件中读出的字符串
    if (!in.is_open())
        cout << "Error:无法打开该文件!"<<endl;
    else
        while (!in.eof())
        {
            while (getline(in, bufline))
            {
                polygon poly;
                vector<string> line = SplitString(bufline, " ");//根据空格识别每个点的坐标
                for (int i = 0; i < line.size() - 1; i++)
                {
                    point ppp;
                    vector<string> tempString = SplitString(line[i], ",");
                    ppp.x = atoi(tempString[0].c_str());
                    ppp.y = atoi(tempString[1].c_str());
                    poly.p.push_back(ppp);
                }
                s.push_back(poly);
                poly.p.clear();//清除p中信息
            }
        }
    //从文件中读出多边形,然后进行赋值显示
    for (int i = 0; i < s.size(); i++)
        PolygonScanning(s[i]);
    //s.push_back(temp[i]);
    in.close();
    glFlush();
    cout << "成功读取多边形文件!"<<endl;
}

5. 清屏重置多边形

将屏幕上所有的多边形都清除,以及多边形的点集,多边形。刚开始实现清除函数的时候,总是需要点一下屏幕然后才能屏幕清空,不知道为什么,后来改了算法,按照扫描线那样,一行一行的清空屏幕,然后把多边集全部清空,否则下次刷新时仍会出现已经消失的多边形。


图(11)清空之前的图形 图(12)清空之后的屏幕
具体代码为:

void myClean() {
    glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBegin(GL_LINE_LOOP);
    for (int i = 0; i <= 1000; i++) {
        glVertex2f(0.0, 1000.0);
    }
    p.clear();
    s.clear();//避免出现遗留
    glEnd();
    glFlush();
}

6. 通过菜单交互

原本是打算做一个菜单的,后来感觉用键盘交互一个道理,所以就做了用键盘交互(改进之后做了菜单)。将键盘几个固定键绑定函数,点击相应按键的时候,就会调用相应的函数来实现特殊的功能。(建议读者能尝试自己动手做一下菜单)
r表示read,从文件中读出多边形顶点集
s表示save,将多边形的顶点集写到文件中
c表示clear,将屏幕显示的所有多边形都删除,还有多边形的顶点集以及多边形。
具体代码为:

void keyboard(unsigned char key, int x, int y) {
    switch (key)
    {
    case 's'|'S':
        cout << "save" << endl;
        SavePolygon();//保存多边形到txt文件中
        break;
    case'c'|'C':
        cout << "clean" << endl;
        myClean();//清除屏幕所有多边形
        break;
    case 'r'|'R':
        ReadPolygon();//读出存入的多边形
        break;
    }
}

五 总结与体会

本例还有很多可以改进的地方,可以添加多边形的颜色,把多边形的颜色也写入多边形的结构体,这样保存(读出)的时候也可以保存(读出)多边形的颜色,还可以增加多种填充模式,比如等间隔、斜扫描线填充。最后,这是我第一次发布博客,做的不好的地方希望大家在评论里提些意见。希望大家能一起学习进步。
附:源码(仅供参考)

全部评论

相关推荐

牛客316659795号:不是,证明hr初筛已经过了,要投给部门筛一遍
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务