计算机图形学上机报告(四)

1.掌握计算机屏幕上多边形的各种绘制方法;

2.掌握用扫描线算法绘制多边形的原理;

3.利用C++语言编程实现利用扫描线算法绘制多边形。

Visual Studio 2022版

Windows 10/11

这次实验主要应用扫描线算法实现计算机屏幕上多边形的绘制,扫描线算法是多边形扫描转换算法的一种,即将多边形的顶点表示转化为点阵表示的一种算法。其主要原理是利用一条扫描线从多边形底部顶点出发,一直扫描到多边形的顶部顶点,每扫过一个坐标值,填充与扫描线相交的边之间的所有像素点,最终实现整个多边形的点阵填充。

整个算法过程可以分为三大部分,分别为建立多边形的边表、建立每一条扫描线的活动边表(Active Edge Table)(简称AET)和根据AET对多边形进行填充。

第一大部分是建立多边形的边表(Edge Table)(简称ET)。边表是一个包含多边形全部边记录的表,它按与扫描线一一对应的y坐标以递增或递减的顺序存放区域边界的所有边。每个y坐标值存放一个或者几个边记录。每当y的值碰到多边形边界的新边的底端y的值时,则在ET表中相应的y坐标值处写入一个边记录。当同时有多条边进入时,则在ET表中按链表结构写入相应数目的多个记录,这些记录按边线较低端点的x值增加的顺序排列。当没有新边加入时,表中对应的y坐标值处存储内容为空白。边表的建立需遵循以下规则:(1)与x轴平行的边不计入ET边表中;(2)多边形的顶点分为两大类:一类是极值点,即此点同时为两条边的最低点或最高点,顶点引出的两条边位于扫描线同侧。另外一类是非极值点,即此点只是一条边的最低点,是另一条边的最高点,顶点引出的两条边位于扫描线异侧。当扫面线与第一类顶点(极值点)相交时,应看作两个点;而当扫描线与第二类顶点(非极值点)相交时,应视为一个点;(3)当扫描线重合多边形某条边界水平线时,如与该水平边线相交两条边线是一上一下的,则该水平边线两个端点作为一个交点,即扫描线与该水平边界线相交了一次;如与水平边线相交两条边线是同为上或同为下的,则将该水平边线两个端点作为交点,即扫描线与该水平边界线相交了两次。要构建ET边表,首先需要将边以一定的数据结构进行表示,边可以采用其两个端点的属性进行表示,每一个边的结构内部包含:(1)该边y的最大值和最小值;(2)该边底端的x坐标;(3)该边的斜率的倒数1/m;(4)以及下一个边记录地址的指针。将所有边的结构按照输入顶点的顺序穿在一起,形成一个多边形的边数组。由此,就可以根据边数组根据边表的建立规则,进行各个条件的判断,将每一个y坐标位置符合条件的边数组中的边元素写入ET,并按照ET的建立规则进行排序,从而建立多边形的边表。

第二大部分是建立每一条扫描线的活动边表(Active Edge Table)(简称AET)。AET是一个只与当前扫描线相交的边记录链表,可以根据扫描线位置对于多边形边的相应参数进行实时更新。更新分为两个方面,第一个方面就是已有边交点坐标的更新,第二个方面就是边的变动。随着扫描线从一条到另一条的转换,AET表中与多边形边相交的交点x坐标也应随之变动,计算实时交点坐标可以采用直线的解析式,由前一个交点的坐标值,利用与DDA算法类似的原理进行计算,即y每增加1,x加斜率的倒数1/m,即yi+1=yi+1, xi+1=xi+1/m,由此可以算出AET表中x的新值xi。随着扫描线的y值增加,凡是与这一条扫描线相交的任何新边都加到AET表中,而与之不相交的边从AET表中删除。最后,按照边顶部端点x坐标从小到大的顺序对当前扫描线AET中的边元素进行排序。对于添加边的操作,首先需要判断有新的与当前扫描线相交的边,即该边的底部y坐标值小于或等于当前扫描线的y坐标,接着采用在单链表中插入结点的算法,将边元素插入到单链表的表尾,并返回新的AET链表。对于删除边的操作,首先需要遍历整个AET链表,找到满足与当前扫描线不相交的边在AET中的位置,即顶部端点y坐标的值小于当前扫描线的y坐标值,接着按照单链表中删除结点的操作,将该边元素从单链表中删除,并返回新的AET链表。对于变化之后的边,还需要根据顶部端点的x值从小到大重新进行排序,具体算法为对当前扫描线的AET中所有的边两两进行比较,保证所有边元素两两的x值是从小到大的,最终,整个AET中的所有边元素都是从小到大的。完成这两大块操作后就实现了AET的建立与实时更新。

对于x坐标值的更新数学原理如下,利用直线的解析式进行计算,由前一个点的坐标推得下一个点的坐标:

设直线方程为:

y=mx+b

xi+1=(yi+1-b)/m

yi+1=yi+1,可得

xi+1=(yi+1-b)/m

yi=mxi+b,所以

xi+1=mxi+b+1-bm=xi+1/m

第三大部分就是根据AET对多边形进行填充,在之前操作的基础上,每一个扫描线处,都会实时建立了一个活动边表AET,而其中储存的两条边的x坐标值也都是实时更新的,只需要填充两条边x值之间的位置的像素点即可。

通过对扫描线y坐标值由多边形最底部顶点到最顶部顶点,以一个像素点为一个步长进行增加,对第二大部分和第三大部分的操作进行循环操作,扫描线y值每变化一个步长,循环一次,最终填充多边形内部的所有像素点,实现将顶点表示转换为点阵表示。

 

图1 多边形扫描线填充算法流程图

而在C++中,可以综合前面学习过的类和对象、结构体、指针、数组、计算机图形绘制的知识,以及数据结构中学习过的单链表的插入以及按值删除算法,最终实现扫描线算法。

对于扫描线算法的实现,首先需要实现第一大部分的内容——建立多边形的边表。而要建立多边形的边表,首先就需要建立多边形所有边组成的数组,而建立这个数组,则需要利用一种结构来储存每一条边,在C++中可以用结构体来实现,所以需要建立一个结构体Edge,每个结构体中包含端点y坐标最大值和最小值,底部端点的x值,及斜率的倒数,由于最后所有边需要储存为一个结构体数组,所有还需要有指向下一条边的指针。将这些变量和指针封装在一个结构类型中,而为具体的结构对象起名为Edge。接着,需要对AET的类进行一个类似预定义的操作,AET是可以进行实时更新的,那么则将实时更新的每一个扫描线位置的AET边表都视为一个结构,利用结构体进行表示,结构体内部的变量包含该表项对应扫描线的y坐标值,内部包含的边元素类型的结点,和指向下一个位置AET的指针。而所有更新过的AET都会存到一个更大的结构中,即ET边表中,这就类似一个AET属性的统计器,能够得到AET当前的性质,这个结构体内部包含有所有更新过的AET,以及当前AET中边的个数。通过这两个结构体数组的建立,就可以实现AET实时更新的应有功能。接下来就是AET的初始化工作,让这些只包含抽象变量的结构体变成具体的对象。那么首先就需要对包含多边形所有边的边数组进行赋值,而多边形边的一些属性,就体现在两个端点之间的关系上,所以可以利用一个类,来定义对边的两个端点的操作,类内部的成员变量为两个端点的横纵坐标,成员函数为对坐标的一系列操作,即取两个点中最大的y坐标值、最小的y坐标值,取两个点中y坐标值较小的点对应的x坐标值,以及两个点组成直线的斜率。接着就可以构建多边形的边数组,采用InitEdges函数首先根据边数为边数组分配内存空间,输入的变量为多边形顶点坐标组成的数组和多边形边数,接着按照输入顺序构建对每一条边处理的对象,并由对边的两个端点的操作类的成员函数求得每一个边元素的四个要素最大的y坐标值y_max、最小的y坐标值y_min,两个点中y坐标值较小的点对应的x坐标值x,以及两个点组成直线的斜率分之一deltax,指向下一条边的指针置为空,并将每一个结构体存入到边数组中,直到所有输入的边扫描完毕,最后返回整个多边形的边数组。有了多边形的边数组,就可以开始构建AET了,构建AET需要将AET结构体和ET结构体的各变量均进行初始化,才能得到具体的AET对象。首先,可以由边数组和多边形边数,计算每一个扫描线对应的AET中的边元素数量item_count,并多边形所有边的最小值组成的数组ymins,item_count后续对于AET整体的扫描是必不可少的,ymins对于后续AET中结点的插入和删除也是必不可少的,假设所有边加入AET后都不进行删除,那么AET中表项最多的情况也就是把所有不同ymins对应边都加入时的情况,也就是说,count的数量就是ymins数组中元素的数量。所以建立TableItemCount函数,来实现上述功能,其输入变量为多边形边数组、多边形边数和待传出的参数ymins,对整个边数组进行遍历,如果当前边元素的端点坐标最小值在ymins数组中没有出现过,就将其加入到ymins中,每增加一个值,表项数量count自增1,最后返回count的值。而实现这个功能,就又需要一个判断一个整数是否在一个数组中的函数isInArray,这个函数的输入变量为数组,待判断整数值和数组的长度,函数操作为按数组长度从头到尾对数组进行遍历,若值与数组中某个元素相同,则返回逻辑真值1,否则返回逻辑错误值0。建立好ymins数组后,由于进行扫描线算法时,扫描线是根据y坐标值从小到大进行扫描的,所以需要对ymins数组进行排序,这里从第一个和第二个元素开始,两两进行比较,将小值放在前面,大值放在后面,直到遍历完整个数组为止,这样就实现了建立一个从小到大排序的边最小值数组ymins。有了这个函数,首先计算AET属性中的item_count变量,并建立ET链表,将其头结点进行初始化,头结点就是当前的一个AET,对其内部参数进行初始化,当前扫描线y坐标值就是多边形边中最底部端点的y坐标值,即ymins[0],由于还没有具体的边插入,所以当前AET的首个边的结点设置为空,指向下一个边元素的指针也置为空,接着将所有更新以后一定得到的AET都进行初始化,并连在一起。最后,为每个AET的空位添加相应的边元素。为了实现这个功能,需要建立函数AppendNode,输入参数为边表,待插入边元素的底部端点的y坐标值和边数组中的一个边元素,插入时按照单链表按值插入的方法,首先遍历初始化之后的ET链表,按照结点对应的y值与某个待插入y最小值相同的条件,由于ET中的初始AET是按y坐标最小值递增排序的,所以最先找到的是插入的位置所在的AET位置,如果插入的位置所在的AET还没有结点,那么直接插入即可,若插入位置的AET已经存在结点,则将其插入到当前结点的后面即可。通过循环,将多边形的所有边插入到ET边表的对应位置,这样就建立了具体的,可操作的ET和初始化的AET,为后续由扫描线建立AET的操作打下基础,这样第一大部分的操作就完成了。

接下来,则是第二大部分的操作——建立每一条扫描线的活动边表的编程实现。这一部分主要涉及了随着扫描线y坐标值的变化,AET中边元素的插入和删除操作,以及对于扫描线与多边形边交点x坐标值的更新。首先是插入边元素的操作,这部操作实际上就是从边表ET中取出符合条件的边元素,并将其放到当前AET中,建立函数GetCurrEdges,输入参数为多边形边表,当前扫描线的y坐标,和待传入的取出的边数edge_num,首先为取出的边元素组成的链表分配最大的内存空间,接着依次遍历ET中的所有AET中符合条件的边,条件为当前边底部端点的最小值小于等于扫描线的y坐标值,将取出边的次数存入到edge_num中,将所有边取出后,将其放入当前的AET中,接着对当前AET中存在的所有边元素按照底部端点x坐标从小到大进行排序,具体操作为从第一条边开始两两进行比较,如果存在排序不正确的情况,则修正排序,一直比较到最后一条边为止。这一步的目的就是保证第三大部分绘制两个交点x坐标之间的像素时,能够从左到右进行绘制。而实现比较操作,最核心的问题就是两个边元素按照底部端点x坐标进行排序,所以建立IsRightTo函数,输入参数为两个边,用于判断第一个边底部端点x坐标是否位于第二个边右侧,首先对于两个底部端点x坐标不同的边元素,直接进行判断,若符合条件,返回真值1,否则返回错误值0,接着判断两直线底部端点x坐标相同的情况,应该判断其y坐标增加1时两个x坐标的大小关系,首先根据相似三角形法则,计算出y坐标增加1时两个x坐标值,接着进行判断即可,若符合条件,返回真值1,否则返回错误值0。接着就是删除当前AET中结点的操作,这个操作与单链表按值删除的操作类似,建立DeleteNode函数进行操作,首先对整个ET中的所有AET进行遍历,找到符合边元素顶部端点的y值小于当前扫描线的y坐标值的边的位置,并移动指针,根据该位置是否为AET中头结点不同采用不同的操作,将该位置结点删除,接着继续向后遍历,直到ET表尾,删去所有符合条件的边。这样就实现了对当前AET中元素的插入和删除操作。最后,则是根据扫描线y坐标值变化修改当前扫描线与多边形边交点的x坐标,首先,需要获取需要修改的边元素并储存到边元素变量curr_edge中。建立一个函数GetThisEdge用于获取指定的边元素,输入变量为ET边表,待获取边元素的底部端点x坐标值,底部端点y坐标值,顶部端点y坐标值,对ET中的所有边元素进行遍历,找到同时以上与三个参数值都相等的边元素,并输出这个边元素即可。在修改x值时,按照索引首先在当前AET中找到位于左侧的边,并根据公式xi+1=xi+1/m,修改x的坐标值,右侧的边同理。至此,就完成了扫描线变化时需要进行的全部操作,第二大部分就完成了。

最后,是第三大部分的操作——AET对多边形进行填充,这一部分使用C++自带的图形库中的函数line绘制即可,这个函数输入参数为一条直线两端点的坐标值,可以绘制出连接这两个端点的直线。首先,编写graphx函数和graphy函数将我们认知中熟悉的笛卡尔坐标系转化为屏幕坐标系,编写realx函数和realy函数将屏幕坐标系转化为我们熟悉的笛卡尔坐标系,利用realy函数将扫描线坐标转化以后,自下而上开始移动扫描线,每次扫描线y坐标发生变化,都先画出与x轴平行的一条直线,再改变扫描线与多边形边交点的x坐标值,最终填充多边形内部所有像素点。

最后,编写Draw函数,实现对第二大部分和第三大部分内容的循环操作,每次扫描线y坐标值发生变化,都进行插入、填充、删除、改变x坐标值和y坐标值的操作,直到扫描到多边形最顶部的顶点或ET表为空为止。第二大部分和第三大部分的所有函数都必须存到解决方案类Solution中,这个类就包含了扫描线法绘制多边形的所有函数和操作,其对象可以调用任何与扫描线法绘制多边形有关的函数或使用有关变量。在主函数中建立edges数组输入多边形的顶点坐标值,注意起点坐标值和终点坐标值必须相同,多边形才能闭合,所以顶点坐标数量要比边数乘二多两个。接着,建立多边形的边数组,利用边数组初始化AET,建立图窗,再调用Draw函数,画出多边形并在屏幕上输出。至此,也就实现计算机屏幕上多边形的扫描线法绘制。

//头文件

#undef UNICODE

#undef _UNICODE//将之前定义的宏标识符取消定义

#include "graphics.h"//图形库

#include "stdio.h"//标准输入输出

#include "conio.h"//控制台输入输出

#include <stdlib.h>//标准函数库

#include <math.h>//数学运算库

#include <cmath>//数学函数库

#include <iostream>

using namespace std;

typedef struct Edge

{

    int y_max, y_min;//多边形边y的最大值和最小值

    double x, deltax;//每条边最低点的x坐标值和斜率

    struct Edge* next;//指向下一条边的指针

}Edge;//多边形边的数据结构,用于构建ET边表

typedef struct TableItem

{

    int curr_y;//扫描线y坐标的最小值

    Edge* firstNode;//该表首结点,如果没有则为NULL

    struct TableItem* next;//指向下一个表项的指针

}TableItem;//总活动边表表项,每一个为一个AET

typedef struct Table

{

    TableItem* itemHeader;//活动边表的第一个表项

    int item_count;//活动边表表项个数

}ET;//边表,typedef为新的结构体命名为ET

class Point {

private:

    int x1, x2, y1, y2;//两个点的平面坐标

public:

    Point(int x1, int y1, int x2, int y2) {

         this->x1 = x1;

         this->x2 = x2;

         this->y1 = y1;

         this->y2 = y2;

    }//构造函数,this指针用于访问本类的成员变量并对其进行赋值

    int YMax() {

         return (y1 > y2 ? y1 : y2);

    }//返回两个点中的ymax

    int YMin() {

         return (y1 <  y2 ? y1 : y2);

    }//返回两个点中的ymax

    int x() {

         return (y1 < y2 ? x1 : x2);

    }//返回最低点的x值

    double KOfLine() {

         return((y2 - y1) * 1.0 / (x2 - x1));

    }//返回两点构成直线的斜率

};//这个类为多边形的点类,可以完成扫描线算法中对点坐标的基础操作

class Solution {

public:

    Edge* InitEdges(int* edges, int edge_num) {

         Edge* newEdges = (Edge*)malloc(sizeof(Edge) * edge_num);//为所有边newEdges分配内存空间

         int j = 0;

         for (int i = 0; i < edge_num; ++i) {

             Point point(edges[2 * i], edges[2 * i + 1], edges[2 * (i + 1)], edges[2 * (i + 1) + 1]);//Point类中point函数传参,构建point对象

             Edge* newEdge = (Edge*)malloc(sizeof(Edge));//为newEdges表中每一项newEdge分配内存空间

             //初始化边表每一项的各参数

             newEdge->x = (double)point.x();//线最低点的x坐标值

             newEdge->y_max = point.YMax();//线的y坐标最大值

             newEdge->y_min = point.YMin();//线的y坐标最小值

             newEdge->deltax = 1.0 / point.KOfLine();//线的斜率分之一

             newEdge->next = NULL;

             newEdges[j++] = *(newEdge);//构建多边形边组成的表

         }

         return newEdges;//返回构建好的表

    }//构建多边形所有边组成的表,并进行初始化。输入参数为按顺序排列的定点坐标数组edges(终点坐标与起点相同,组成闭合图形),和多边形的边数edge_num,返回初始化之后的多边形所有边组成的表,边的顺序按照用户输入的各顶点坐标顺序确定

   

    bool isInArray(int* array, int e, int array_length) {

         for (int i = 0; i <= array_length; i++) {

             if (array[i] == e) {

                  return true;

             }

         }//对数组进行遍历,若在数组中,返回1

         return false;//否则,返回0

    }//判断一个整数是否在某个数组中

    int TableItemCount(Edge* edges, int edge_num, int* ymins) {

         int count = 0;//ymins数组初始长度

         for (int i = 0; i < edge_num; ++i) {

             if (!isInArray(ymins, edges[i].y_min, count))//判断条件为多边形所有边组成的表中的边y的最小值不在ymins数组中

             {

                  ymins[count++] = edges[i].y_min;//将多边形所有边组成的表中边的最小值加入到ymins数组中

             }

         }//构建ymins数组

         for (int j = 0; j < count - 1; ++j) {

             for (int k = j + 1; k < count; ++k) {

                  if (ymins[k] < ymins[j]) {

                      int tmp = ymins[k];

                      ymins[k] = ymins[j];

                      ymins[j] = tmp;

                  }

             }

         }//将ymins按升序排序

         return count;//返回ymins数组元素个数

    }//根据多边形所有边组成的表计算ET表项数量,edges为边表名称,edge_num为多边形边数,构建一个包含多边形所有边最小值的数组ymins,元素按从小到大排序,并返回ymins数组元素个数

   

    void AppendNode(ET& T, int place_y, Edge& e) {

         TableItem* p = T.itemHeader;//创建p指针,指向ET中首个AET

         for (int i = 0; i <= T.item_count; i++) {

             if (p->curr_y == e.y_min)//判断条件为待插入的边底部最小值与已有ET中当前AET的y值相同

                  break;

             p = p->next;

         }//将p指针移动到ET中待插入AET的位置

         //将边插入到该表项相邻链表中

         Edge* egp = p->firstNode;//创建指针egp,指向待插入AET的首个结点

         if (egp == NULL) {

             egp = (Edge*)malloc(sizeof(Edge));//为egp指针分配内存空间

             *(egp) = e;//将边插入到该位置

             egp->next = NULL;

             p->firstNode = egp;

         }//若该AET还没有边元素,则直接将这条边插入

         else {

             Edge* pre = egp;//创建一个前指针pre

             while (egp != NULL) {

                  pre = egp;

                  egp = egp->next;

             }//移动两个指针,使pre指向AET中现有的最后一个元素,而egp指向待插入位置

             Edge* newedge = (Edge*)malloc(sizeof(Edge));//为当前AET新边分配内存空间

             *(newedge) = e;

             pre->next = newedge;

             newedge->next = NULL;//和上一个分支操作相同

         }//该AET已经有边元素,将新的边元素插入到已有边元素的后面

    }//将新出现的边元素添加到ET中的当前AET里。输入参数为ET名称,待插入边元素的底部最小值,待插入边元素

    void Init(ET& T, Edge* edges, int edge_num) {

         //初始化AET结构体

         T.item_count = 0;//表项个数为0

         T.itemHeader = NULL;//初始化边表指针

         int ymins[20];//ymins为包含多边形所有边最小值的数组,元素按从小到大排序,这一步对其进行初始化

         T.item_count = TableItemCount(edges, edge_num, ymins);//所有AET的个数就是多边形所有边最小值ymins的个数

         T.itemHeader = (TableItem*)malloc(sizeof(TableItem));//为AET表项头指针分配内存空间

         //初始化头指针

         T.itemHeader->curr_y = ymins[0];//头指针的y坐标值在多边形所有边的最低点

         T.itemHeader->firstNode = NULL;

         T.itemHeader->next = NULL;

         TableItem* p = T.itemHeader;//创建指针p指向头结点,便于后续连接

         for (int i = 1; i < T.item_count; ++i) {

             TableItem *e= (TableItem*)malloc(sizeof(TableItem));//为各表项结点分配内存空间

             //为当前表项各参数赋值,进行初始化

             e->curr_y = ymins[i];

             e->firstNode = NULL;

             e->next = NULL;

             //连接下一个结点

             p->next = e;//p的下一个指针指向e,即前一个指针的下一个指针指向e

             p = e;//p移动到e的相应位置

         }//初始化ET及其包含的所有AET,各表项首结点和指针均为NULL

         p = T.itemHeader;//将p指针归位

         for (int j = 0; j < edge_num; ++j) {

             this->AppendNode(T, edges[j].y_min, edges[j]);//将edges数组对应的各边元素按照索引顺序添加到总AET表中的每个AET表中

         }//向初始化后的表中添加制定y值的结点

    }//构建边表,并对其进行初始化。输入参数为ET名称,多边形ET边表edges,多边形边数edge_num

//绘制多边形部分

//计算机坐标系和我们认知坐标系之间的坐标转换函数

double graphx(double x) {

    return x * 10 + 100;

}//认知x->计算机x

double realx(double gx) {

    return (gx - 100) * 1.0 / 10;

}//计算机x->认知x

int graphy(int y) {

    return 400 - y * 10;

}//认知y->计算机y

int realy(int gy) {

    return (400 - gy) / 10;

}//计算机y->认知y

void DeleteNode(ET& T, int curr_ymax) {

    TableItem* p = T.itemHeader;//创建p指针指向ET的第一个AET

    while (p != NULL) {

         Edge* item = p->firstNode;//创建指针item指向当前AET的首个结点

         Edge* itempre = p->firstNode;//创建指针preitem指向当前AET的首个结点

         while (item != NULL) {

             if (item->y_max <= curr_ymax)//当前AET中某条边的最大值小于扫描线的y值

             {

                  T.item_count--;//ET中所有边的数量减1

                  //删除这个边元素

                  if (item == p->firstNode)//判断条件为当item为当前AET首结点时

                  {

                      p->firstNode = (Edge*)malloc(sizeof(Edge));//为p分配内存空间

                      p->firstNode = item->next;

                      free(item);//删去该边元素

                      item = p->firstNode;//下一个边元素为当前边元素

                      itempre = p->firstNode;

                  }

                  else {

                      itempre->next = item->next;

                      free(item);//删去当前边元素

                      item = itempre->next;//继续遍历

                  }

             }

             else {

                  itempre = item;

                  item = item->next;//继续向后遍历

             }

         }

         p = p->next;//遍历整个ET表

    }

}//这个函数用于删除当前AET中最大值小于扫描线y值的边

Edge* GetCurrEdges(ET T, int curr_y, int& edge_num) {

    Edge* currEdges = (Edge*)malloc(sizeof(Edge) * 20);//构建当前扫描线中的所有边组成的链表currEdges

    int i = 0;//i为链表currEdges的索引,也是取出边的个数

    TableItem* p = T.itemHeader;//创建p指针指向ET的第一个AET

    while (p != NULL) {

         Edge* q = p->firstNode;//创建指针q指向每一个AET的首个结点

         while (q != NULL) {

             if (q->y_min <= curr_y)//AET中边元素的最小值小于当前扫描线的y坐标值

             {

                  currEdges[i++] = *q;//将这个边的值加入到链表currEdges中,构建链表currEdges

             }

             q = q->next;//遍历当前AET中的所有边元素

         }

         p = p->next;//遍历当前的ET表

    }

    edge_num = i;//利用参数egde_num传出当前扫描线链表currEdges在AET中取出边元素的个数

    return currEdges;//返回当前扫描线中的所有边组成的链表currEdges

}//这个函数可以构造当前扫描线中的所有边组成的链表,输入参数为ET名称,当前扫描线的y坐标值,待传出的参数数egde_num为当前扫描线链表currEdges在AET中取出边元素的个数

bool IsRightTo(Edge edge1, Edge edge2) {

    if (edge1.x > edge2.x)//判断条件边edge1底部x坐标值大于edge2

         return true;//返回1

    else {

         if (edge1.x < edge2.x)//边edge1底部x坐标值小于edge2

             return false;//返回0

         //剩下边edge1底部x坐标值等于edge2的情况

         double x_max1 = (edge1.y_max - (edge1.y_min - 1.0 / edge1.deltax * edge1.x)) * edge1.deltax;//计算边edge1上y移动1得到的下一个x坐标值x_max1

         double x_max2 = (edge2.y_max - (edge2.y_min - 1.0 / edge2.deltax * edge2.x)) * edge2.deltax;//计算边edge2上y移动1得到的下一个x坐标值x_max2

         if (x_max1 > x_max2)//x_max1大于x_max2

             return true;//返回1

    }

    return false;//所以情况都不符合,返回0

}//这个函数可以根据边底部的x坐标值判断两条边的左右位置关系。输入参数为两条边edge1和edge2

Edge* GetThisEdge(ET T, double x, int y_min, int y_max) {

    TableItem* p = T.itemHeader;//创建p指针指向ET的第一个AET

    while (p != NULL) {

         Edge* q = p->firstNode;//创建指针q指向每一个AET的首个结点

         while (q != NULL) {

             if ((q->x == x) && (q->y_max == y_max) && (q->y_min == y_min))//ET中某个AET中的某条边元素底部x值、y最大值、y最小值与需要查找的一致

             {

                  return q;//返回当前边

             }

             q = q->next;//遍历当前AET中所有边元素

         }

         p = p->next;//遍历ET中所有AET

    }

    return NULL;

}//在ET中找到符合条件的边元素,并返回。输入元素为ET名称,要查找的边元素底部x值x、y最大值y_max、y最小值y_min,返回值为符合条件的边元素

void Draw(ET T,int color) {

    int curr_y = 0, curr_edge_num = 0, curr_gy = graphy(curr_y);//curr_y为当前扫描线的y坐标

    Edge* currEdges = (Edge*)malloc(sizeof(Edge) * 20);//为AET中的边元素分配内存空间

    TableItem* p = T.itemHeader;//创建指针p指向ET中的第一个AET

    while (p != NULL) {

         Edge* q = p->firstNode;//创建指针q指向每一个AET的首个结点

         while (q != NULL) {

             q->x = graphx(q->x);//将每个边底部的x坐标值转化为计算机坐标

             q = q->next;//遍历当前AET的所有边

         }

         p = p->next;//遍历ET中的每一个AET

    }//将ET中包含的所有边元素底部的x坐标值转化为计算机坐标

    for (; curr_y < 30; curr_gy--, curr_y = realy(curr_gy)) {

         this->DeleteNode(T, curr_y);//删除当前AET中最大值小于扫描线y值的边

         currEdges = this->GetCurrEdges(T, curr_y, curr_edge_num);//向当前扫描线AET链表中添加y坐标最小值小于当前扫描线y坐标的边元素

         //对当前扫描线AET链表按照边底部的x坐标值由左到右排序

         for (int i = 0; i < curr_edge_num - 1; ++i) {

             for (int j = i + 1; j < curr_edge_num; ++j) {

                  if (this->IsRightTo(currEdges[i], currEdges[j])) {

                      Edge tmp = currEdges[i];

                      currEdges[i] = currEdges[j];

                      currEdges[j] = tmp;

                  }

             }

         }

         setcolor(color);//设置画线颜色

         for (int j = 0; j < curr_edge_num / 2; ++j) {

             line((int)currEdges[2 * j].x, curr_gy, (int)currEdges[2 * j + 1].x, curr_gy);//填充当前扫描线通过的两个x坐标之间的部分

             Edge* curr_edge1 = this->GetThisEdge(T, currEdges[2 * j].x, currEdges[2 * j].y_min, currEdges[2 * j].y_max);//获取当前边链表元素对应的总AET中的边存到curr_edge1边中

             curr_edge1->x += curr_edge1->deltax;//利用增量DDA算法求出下一个x坐标值

             Edge* curr_edge2 = this->GetThisEdge(T, currEdges[2 * j+1].x, currEdges[2 * j+1].y_min, currEdges[2 * j+1].y_max);

             curr_edge2->x += curr_edge2->deltax;//方法与之前相同

         }

    }

}//扫描线法绘制多边形函数

};//这个类是存放的解决方案的类,内部有解决扫描线算法的各种函数、方法、变量等

//主函数

int main()

{

    Solution solution;//创建解决方案对象

    int edges[] = { 4,18,14,30,26,22,26,10,18,15,18,2,10,2,10,10,4,6,4,18 };

    Edge* newEdges = solution.InitEdges(edges, 9);//创建ET边表对象newEdges

    ET T;//创建ET对象T

    solution.Init(T, newEdges, 9);//初始化ETnewEdges

    initgraph(800, 800, SHOWCONSOLE);//建立800×800的图窗,并保留控制台显示

    solution.Draw(T,WHITE);//扫描线算法绘制多边形

    getchar();//读取字符

    closegraph();//关闭图窗

    system("pause");

    return 0;

}

  • 运行结果

(可以自行根据需要修改代码中的顶点坐标,绘制不同的图形) 

这次实验主要实现计算机屏幕上多边形的绘制,多边形在计算机屏幕上的表示方法有两种,第一种为顶点表示法,即只绘制出多边形的顶点并连接相邻顶点绘制出多边形的边即可,内部不需要填充,使用这种方法可以绘制出多边形的轮廓,多边形形状较为清晰明确,不需要占用太多内存空间和计算空间,算法效率较高,但是其不能表示多边形内部的所有点,无法判断屏幕上各点与多边形之间的从属关系,其对应的算法为边缘填充法。另一种方法则是利用点阵进行表示和绘制,点阵指的是多边形边界和内部的所有点,这种绘制方法可以点亮多边形内部的所有像素点,也可以得到所有屏幕上像素点与多边形之间的从属关系,但是其算法过程中循环较多,需要依次判断屏幕上每一个像素点与多边形边之间的位置关系,较为复杂,算法效率较低,其对应的算法为逐点判断法。

而根据这两种方法的优缺点,可以将这两种方法进行综合,即利用顶点表示多边形的边,并根据这些边的一些属性,绘制出这些边内部的所有像素点,从而将多边形的顶点表示转化为多边形的点阵表示,这一类的方法统称为多边形的扫描转换算法,最具有代表性的就是扫描线算法。而这个转换媒介就是扫描线,即从多边形最低点向上以1为步长步进到多边形最高点的平行于x轴的直线,每一次点亮这个直线与多边形边交点之间的坐标即可实现这个转换。所以这个方法既可以点亮多边形的内部和边界所有像素点,得到屏幕上像素点与多边形的从属关系,从而改善边缘填充法的缺点,而算法中又不需要过于复杂地判断像素点是否处于多边形内部,直接利用多边形边之间的关系和扫描线的媒介作用,直接一条一条地填充像素点,循环次数和判断次数比逐点判断法少,效率比逐点判断法要高。

对于扫描线算法,其原理及C++编程实现的操作已经在算法原理部分详细阐述了,此处便不再进行赘述。

对于算法的时间复杂度,可以进行分析,对于初始化ET数组的函数Init,其包括两个独立循环,而这个函数里包含的函数时间复杂度如下:计算表项函数TableItemCount函数的时间复杂度为O(n2),其位于循环体外部,而向表项中添加边元素结点的AppendNode函数的时间复杂度为O(n),其位于循环体内部,而Init语句中本身就包含一个循环体,所以综上所述,Init函数的时间复杂度为O(n)。而对于Draw函数,其本身语句就包含三层循环,其中第一层循环里,包含顺序结构的删除AET中结点的函数DeleteNode和添加AET中结点的函数GetCurrEdge,DeleteNode函数的时间复杂度为O(n2),而GetCurrEdge函数的时间复杂度也为O(n2),还有在第二层循环里的用于获取符合条件的边的函数GetThisEdge,其时间复杂度为O(n2),所以综上所述,Draw函数的时间复杂度是O(n4),所以,对于扫描线算法,其时间复杂度为O(n4),虽然循环深度较大,但这只需要对为数不多的扫描线进行循环即可,且有一些不符合条件扫描线还不需要进入深度的循环,大大减少了循环次数,而且对于每次循环中的问题规模,免去了判断点与多边形从属关系的直接判断,且免去了对于屏幕上其他大面积范围内冗余像素点的判断,大大减少了算法的执行时间,提高了算法的可执行性,提高了算法的效率,可以说扫描转换算法是绘制出多边形点阵的效率最高的算法。虽然每一步操作变多,但是这可以说是一种以空间复杂度换取算法效率的方法。

而对于这次上机实验,其C++代码很长,所以必须偏重于对于算法原理的理解,了解扫描线每一步变化的行为,即插入结点、填充像素、删除结点、x变化、y变化这五部主要操作。在此基础上,在去理解代码中的各项操作,各项函数原理,首先,要理解在C++中对于多边形边的表示方法和整个多边形ET表的表示方法,以及AET的表示方法,多边形的边、ET、AET都是以结构体的形式建立的,而在C++中,结构体与类的区别主要就在于结构体中的成员变量都是public类型的,而类中的成员变量访问权限可以调节,类可以定义成员函数,所以结构体也可以理解为一个不完整的类,而这个程序也可以理解为面向对象的编程,多边形的边、ET、AET在进行抽象定义后,在利用相关的函数进行具体实现,这也体现了模块化编程的思路,即先进行顶层设计,将多边形的边、ET、AET这几个重要物体定义好以后,将整个操作过程分为及大块,逐一实现每一块的设计,即设计对其进行的各种操作函数的功能,接着进行下层设计,根据函数功能编程实现相关函数,在编程实现函数的过程中可能还需要编写下一层级的函数,思路都是相同的,最终完成整个操作,这就是由顶向下的程序设计,在编写复杂功能的程序时,这种思路尤其重要。我们阅读和理解代码时也要按照这种思路,将代码分成几块,逐块地去看整体的操作,明白函数在整体中的功能,再去逐一理解函数内部的语句,最终才能理解比较冗长的代码。

多边形填充在GIS中也有广泛的应用,在GIS中有很多利用面要素表示的地理实体,往往在构建面要素时都是按照顶点顺序进行绘制,需要根据绘制出的顶点将这些面要素要在计算机中表示,这时候就需要用到多边形填充算法,可能是扫描线算法或是一些其他的算法,而掌握这些算法,也就相当于可以在GIS中将地理实体表示为面要素,为后续对其进行存储和处理打下了基础,这也是当今信息化时代,测绘工程逐渐向地理信息的转化、存储、处理,遥感数据的获取和处理倾斜,作为一名测绘工程专业的学生所必须掌握的基本知识,而编写相关程序的能力也是一名测绘工程专业的学生的基本素养。可能今后会使用不同的编程语言进行相关程序的编写,但是算法的掌握和编程思维的掌握是不会随着编程语言的不同而变化的,所以掌握扫描线算法的知识无论是对于算法设计还是编程能力而言都是一种提升,是至关重要的。

注意:本上机报告适用于中国地质大学(北京)测绘工程专业计算机图形学课程,其他学校也可以参考使用。这些代码只是起到启发作用,不能完全照搬(可能会有抄作业的嫌疑哦(手动狗头)),具体细节还需要自己斟酌修改。

如有问题欢迎在评论区留言,让我们共同交流,一起进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值