欧阳鸿荣 161220096
(南京大学 计算机科学与技术系 南京 210093)
【摘要】:本绘图系统采用了面向对象设计,以 C++为基础,使用 Qt的GUI框架,完成了图形学实验中要求的所有功能。实现了直线、曲线、圆、椭圆和多边形的输入,编辑和平移、旋转缩放等变换功能,同时实现了直线的裁剪、任意区域的填充以及二维图形的存储功能,能够将绘制出来的图形保存为图像。并且支持三维模型的显示,能够载入并显示一个OFF格式的三维模型。
《计算机图形学》系统技术报告1.综述1.1 基本功能:1.2 扩展功能:1.3 演示2.算法介绍2.1 直线绘制算法2.11 算法一:DDA直线算法(1)基本原理(2)理论绘制过程(3)算法的C++实现2.11 算法二:Bresenham直线算法(1)基本原理(2)理论绘制过程(3)算法的C++实现2.2 曲线绘制算法贝塞尔曲线算法(1)基本原理(2)理论绘制过程(3)绘制的代码2.3 圆绘制算法2.31 算法一: 中点圆算法(1)基本原理(2)优点(3)理论绘制过程(4)算法的C++实现2.32 算法二: Bresenham 画圆算法(1)基本原理(2)理论绘制过程(3)算法的C++实现2.4 椭圆绘制算法中点椭圆算法(1)基本原理(2)理论绘制过程(3)算法的C++实现2.5 平移变换算法 (1)基本原理(2)算法实现(3)算法的C++实现2.6 旋转变换算法 (1)基本原理(2)算法实现(3) C++实现实现2.7 放缩变换算法 (1)基本原理(2)算法实现(3)算法的C++实现2.8 泛滥区域填充算法(1)基本定义(2)填充算法(3)C++代码实现2.8 直线裁剪算法梁友栋-Barsky参数裁剪算法(1)基本原理(2)优点——与Cohen-Sutherland算法的比较(3)算法过程(4)算法的C++实现3.系统介绍3.1 实验环境3.2 系统组织3.2.1 系统结构和类关系图3.2.2程序基本流程4.性能测试4.1 直线测试4.2 曲线测试4.3 圆测试4.4 椭圆测试4.5 多边形测试4.6 填充测试5.总结6.致谢7.参考文献
系统名 | 语言和框架 | IDE | 编译器 |
---|---|---|---|
PaintYoung | C++和Qt 5.11.2 | Qt Creator | MinGW 5.3.0 |
该系统按照最初预期实现了所有要求实现的功能
直线、曲线、圆、椭圆、多边形的输入实现
填充区域的输入
直线、曲线、圆,椭圆,多边形的编辑
实现直线的裁剪
裁剪窗口可用鼠标点击拖动输入
裁剪后的图形仍然可以编辑
直线、曲线、圆、椭圆、多边形的平移
直线、曲线、圆、椭圆、多边形的旋转
任意角度旋转
直线、圆的旋转实现了精度控制
直线、曲线、圆、椭圆、多边形的缩放
对变换后的图形仍然可以编辑
数值差分分析DDA(digital differential analyzer)算法:直接利用或的线段扫描转换算法,利用光栅特性(屏幕单位网格表示像素列阵),使用或方向单位增量(用或)来离散取样,并逐步计算沿线路径各像素位置。在一个坐标轴上以单位间隔对线段离散取样,确定另一个坐标轴上最靠近线段路径的对应整数值。
1.具有正斜率时
1.1 从左到右生成线段。
若斜率 ,在x方向以单位间隔取样,以增量形式顺序计算每个y值。下标k取整数值从k=1开始递增。为0与1间的任意实数,计算出的坐标值必须取整。
1.2 从右端点到左端点生成线段,取
1.3 若斜率m>1,从左到右生成线段。在y方向以单位间隔取样,顺序计算每个x值。下标取整数值从1开始递增,直至最后端点。计算出的坐标值必须取整。
1.4 若从右到左生成线段,取
2.具有负斜率时
2.1 假如斜率的绝对值小于1时,
2.2 假如斜率的绝对值大于1时,
xvoid LineController::MyDrawLineDDA(QPainter *painter, QPoint &start, QPoint &end)
{
qDebug()<<"MyDrawLine DDA"<<endl;
int x1 = start.x();
int y1 = start.y();
int x2 = end.x();
int y2 = end.y();
double dx=x2-x1;
double dy=y2-y1;
double e=(fabs(dx)>fabs(dy))?fabs(dx):fabs(dy);
double x=x1;
double y=y1;
dx/=e;
dy/=e;
for(int i=1;i<=e;i++){
QPoint temPt((int)(x+0.5), (int)(y+0.5));
painter->drawPoint(temPt);
x+=dx;
y+=dy;
}
}
Bresenham直线算法是Bresenham提出的一种算法:采用整数增量运算,精确而有效的光栅设备线生成算法。它可用于其它曲线显示。根据光栅扫描原理(逐个像素和逐条扫描线显示图形),线段离散过程中的每一放样位置上只可能有两个像素更接近于线段路径。Bresenham算法引入一个整型参量来衡量“两候选像素与实际线路径点间的偏移关系”。Bresenham算法通过对整型参量值符号的检测,选择候选像素中离实际线路径近的像素作为线的一个离散点。
假设直线由+1个点组成,且起点到终点的坐标分别为:,第步的决策参数为,则有纵坐标和决策参数的递推公式如下:
1.若斜率 ,则有
2.若斜率 ,则有
2.若斜率 ,则有
xxxxxxxxxx
void LineController::MyDrawLineBresenham(QPainter *painter, QPoint &start, QPoint &end)
{
//首先先在这里实现我的直线算法
qDebug()<<"MyDrawLine Bresenham"<<endl;
int x1 = start.x();
int y1 = start.y();
int x2 = end.x();
int y2 = end.y();
int x,y,dx,dy,p;
x=x1;
y=y1;
dx=x2-x1;
dy=y2-y1;
p=2*dy-dx;
for(;x<=x2;x++)
{
QPoint temPt(x,y);
painter->drawPoint(temPt);
if(p>=0)
{ y++;
p+=2*(dy-dx);
}
else
{
p+=2*dy;
}
}
}
贝塞尔曲线是通过一组多边折线的各个顶点唯一定义出来的。曲线的形状趋近于多边折线的形状。贝塞尔曲线可以拟合任何数目的控制顶点。一般通过基函数多项式来描述贝赛尔曲线。
则可以通过递归定义得到贝塞尔曲线上点的坐标
其中,,
这里我主要简述德卡斯特里奥(de Casteljau)算法:该算法描述从参数计算次曲线贝塞尔型值点的过程。对于某一个特定的参数,有计算公式如下:
则当时,计算结果为顶点本身。而曲线上的型值点为
如上图是三次贝塞尔曲线在某个值下的计算过程(的变化生成多个离散型值点:)
由此,只需令在0到1之间变化,则可以根据该原理绘制贝塞尔曲线。
由于曲线的代码量较大,因此这里没有直接给出。总的来说,用户从鼠标输入曲线的各个控制点并且选择曲线的绘制后,则对参数,使得的,递增步长为的初始情况下,对每个的控制点使用德卡斯特里奥算法,对个控制点则通过轮的递归,从而找到型值点。然后将各个型值点之间用直线连接做近似得到曲线。(修改的递增步长,曲线会更为精准,但是与之相对的是速度更加缓慢)。效率与精度,这是一个值得思考的问题。
中点圆算法通过决策参数,在候选像素中选择接近圆周的像素。避免平方根运算,直接采用像素与圆距离的平方作为判决依据。通过检验两候选像素中点与圆周边界的相对位置关系(圆周边界的内或外)来选择像素。
适应性强:易应用于其它圆锥曲线。误差可控:对于整数圆半径,生成与Bresenham算法相同的像素位置。且所确定像素位置误差限制在半个像素以内。
根据圆的对称性,只绘制了八分之一圆,其余部分通过对称性即可得到坐标。使用经过改良的中点圆算法,使用递推,减少了计算量,并且避免了浮点运算。算法过程如下:
输入圆半径r和圆心,并得到圆心在原点的圆周上的第一点为
计算圆周点的初始决策参数值为:
从k=0开始每个取样位置位置处完成下列检测
确定在其它七个八分圆中的对称点
将计算出的像素位置移动到中心在的圆路径上,即:对像素位置进行平移
重复步骤3到5,直到
xxxxxxxxxx
//中点圆算法
void CycleController::MyDrawCycle(QPainter *painter, QPoint &start, QPoint &end)
{
//首先先在这里实现我的画圆算法
qDebug()<<"MyDrawCycle "<<endl;
int x0 = start.x();
int y0 = start.y();
double R = this->getLength(start,end);
int x,y,p;
x=0;
y=R;
p=3-2*R;
for(;x<=y;x++)
{
this->drawEighthCycle(painter,x0,y0,x,y);
if(p>=0){
p+=4*(x-y)+10;
y--;
}else{
p+=4*x+6;
}
}
}
//由于中点圆算法只对八分之一圆进行绘制,因此用该函数将八分之一圆投射成完整的圆
void CycleController::drawEighthCycle(QPainter *painter, int x0, int y0, int x, int y)
{
QPoint temPt1(x0+x,y0+y);QPoint temPt2(x0+y,y0+x);
QPoint temPt3(x0+x,y0-y);QPoint temPt4(x0+y,y0-x);
QPoint temPt5(x0-x,y0-y);QPoint temPt6(x0-y,y0-x);
QPoint temPt7(x0-x,y0+y);QPoint temPt8(x0-y,y0+x);
painter->drawPoint(temPt1);painter->drawPoint(temPt2);
painter->drawPoint(temPt3);painter->drawPoint(temPt4);
painter->drawPoint(temPt5);painter->drawPoint(temPt6);
painter->drawPoint(temPt7);painter->drawPoint(temPt8);
}
利用圆的对称性,我们只需要对一个八分圆进行扫描转换。如下图,如下图,在 的 1/8 圆周上,像素坐标 值单调增加, 值单调减少。设第步已确定是要画圆上的象素点,看第步象素点应如何确定。下一个象素点只能是或中的一个
有判别式如下:
则根据判别式,可以得到如下结果:
若精确圆弧为③,则和。
若精确圆弧是①或②,显然是应选择点,而此时,必有。
若精确圆弧是④或⑤,显然是应选择点,而此时,必有。
根据递推公式有:
因此我们可以得到如下常规画法: 做判别量
当时,选点为下一个象素点。
此时
当时,选点为下一个象素点。
此时
xxxxxxxxxx
void CycleController::MyDrawCycleBresenham(QPainter *painter, QPoint &start, QPoint &end){
//首先先在这里实现我的画圆算法
qDebug()<<"MyDrawCycleBresenham "<<endl;
int x0 = start.x();
int y0 = start.y();
double R = this->getLength(start,end);
int x,y,p;
x=0;
y=R;
p=3-2*R;
for(;x<=y;x++)
{
this->drawEighthCycle(painter,x0,y0,x,y);
if(p>=0){
p+=4*(x-y)+10;
y--;
}else{
p+=4*x+6;
}
}
}
考虑椭圆沿长轴和短轴不同而修改圆生成算法来实现椭圆生成。
给定长短轴参数(假设)和椭圆中心位置,利用平移:先确定中心在原点的标准位置的椭圆点;然后将点变换为圆心在的点。再利用对称性:生成第一象限内的椭圆弧,再利用对称性求出其它三个象限的对应点。
根据椭圆的对称性,只绘制四分之一椭圆,其余部分通过对称性即可得到坐标。算法过程如下
输入和中心,得到中心在原点的椭圆上的第一个点:
计算区域1中决策参数的初值为:
在区域1中每个位置处,从k=0开始,完成下列测试
使用区域1中最后点计算区域2参数初值为
在区域2的每个位置处,从k=0开始,完成下列检测
确定其它三个像限中对称的点。
将每个计算出的像素位置的椭圆轨迹上,并且按照坐标之绘点
xxxxxxxxxx
//由于中点椭圆算法只对四分之一椭圆进行绘制,因此用该函数将四分之一椭圆投射成完整的椭圆
void EllipseController::drawQuarterEllipse(QPainter *painter, int x0, int y0, int x, int y)
{
QPoint temPt1(x0+x,y0+y);
QPoint temPt2(x0+x,y0-y);
QPoint temPt3(x0-x,y0+y);
QPoint temPt4(x0-x,y0-y);
painter->drawPoint(temPt1);
painter->drawPoint(temPt2);
painter->drawPoint(temPt3);
painter->drawPoint(temPt4);
}
//中点椭圆算法
void EllipseController::MyDrawEllipse(QPainter *painter, QPoint &start, QPoint &end)
{
//首先先在这里实现我的椭圆算法
qDebug()<<"MyDrawEllipse "<<endl;
int x0 = start.x(); //椭圆中心
int y0 = start.y();
int rx = abs(end.x()-x0); //椭圆长短轴
int ry = abs(end.y()-y0);
double rx_2 = rx*rx;
double ry_2 = ry*ry;
double pl = ry_2 - rx_2*ry + rx_2/4; //区域1中决策参数
int x = 0;
int y = ry;
drawQuarterEllipse(painter,x0, y0, x, y);//第一个点
//区域一 切线斜率k<=1
while (ry_2*x <= rx_2*y){
if (pl < 0){
pl += 2*ry_2*x +3*ry_2;
}else{
pl += 2*ry_2*x - 2*rx_2*y + 2*rx_2+ 3*ry_2;
y--;
}
x++;
drawQuarterEllipse(painter,x0, y0, x, y);
}
//区域二 切线斜率k > 1
//使用区域1中最后点(x0,y0)来计算区域2中参数初值
pl = ry_2*(x+1/2)*(x+1/2)+rx_2*(y-1)*(y-1)-rx_2*ry_2;
while (y > 0){
if (pl < 0){
pl += 2*ry_2*x - 2*rx_2*y + 2*ry_2 + 3*rx_2;
x++;
}
else{
pl += 3*rx_2 - 2*rx_2*y;
}
y--;
drawQuarterEllipse(painter,x0, y0, x, y);
}
}
平移是将物体沿着直线路径从一个坐标位置到另一个坐标位置重定位。对于原始位置平移和 到新位置 的移动满足
给每一个图形都定义一个中心点(一般是对称中心或外接矩形的中心),绘制时加粗显示辅助,用户通过鼠标拖动中心点得到偏移量dx和dy。对于不同图形的平移,采取不同的实现方法:
这里以直线的平移代码做例子,曲线、圆、椭圆和多边形原理相同。
用户实际使用时通过拖动图形的中心点进行平移。鼠标移动时,计算中心点横坐标和纵坐标的变化量,从而将图形中各个点都平移相同的偏差值,由此实现整个图形的平移。
xxxxxxxxxx
void LineController::moveToPoint(Point point)
{
int offsetX = point.getX() - curLine->centerPoint.getX(); //得到dx
int offsetY = point.getY() - curLine->centerPoint.getY(); //得到dy
curLine->setStartPoint( //对起点平移dx,dy
Point(curLine->startPoint.getX()+offsetX,curLine->startPoint.getY()+offsetY));
curLine->setEndPoint( //对终点平移dx,dy
Point(curLine->endPoint.getX()+offsetX,curLine->endPoint.getY()+offsetY));
MyDrawLineDDA(painter,curLine->startPoint.point,curLine->endPoint.point);//重新绘制
drawHandle(painter,pen);
}
旋转是沿平面内圆弧路径重定位。指定旋转基准点位置旋转角,对任意基准位置的旋转
给每一个图形都定义一个旋转点,绘制时加粗显示辅助,用户通过鼠标拖动中心点得到旋转角
对于不同图形的旋转,采取不同的实现方法:
直线:直线的旋转点定义为四等分点。
圆:圆的旋转点定义为在的位置。(圆的旋转没必要就是了)
椭圆:椭圆的旋转点相对于其他图形略有不同。
由于椭圆的中点椭圆算法只能实现对称轴垂直或者平行坐标轴的椭圆,因此在不引入其他算法的情况下,椭圆的旋转我定义了一个“偏向角”来付诸实现。
多边形和曲线:多边形和曲线的旋转点定义与椭圆类似,但是也有区别
这里主要介绍旋转时用到的一些辅助函数
(1) 角度函数:对于点A绕着点CENTER旋转到点B的情况,用余弦定理得到角
xxxxxxxxxx
double FigureController::getRotaryAngle(Point center, Point a, Point b)
{
double ab = a.distanceToPoint(b.getQPoint());
double ac = a.distanceToPoint(center.getQPoint());
double bc = b.distanceToPoint(center.getQPoint());
qreal cosC = (bc*bc+ac*ac-ab*ab)/(2*bc*ac);
double theta = qAcos(cosC);
return theta; //弧度制
}
(2) 顺逆时针判断函数:这并不是真正的旋转角,故通过判断顺逆时针来对进行修正得到角(通过线性规划)
xxxxxxxxxx
bool FigureController::clockWise(Point center, Point a, Point b)
{
//k=0
if(center.getY()==a.getY()){
if(a.getX()>center.getX())// -------->型向量
{
if(b.getY() > a.getY()){
return true;
}else{
return false;
}
}
else// <--------型向量
{
if(b.getY() < a.getY()){
return true;
}else{
return false;
}
}
}
//k不存在
if(center.getX()==a.getX()){
if(a.getY()<center.getY())//竖直向上
{
if(b.getX()>a.getX()){
return true;
}else{
return false;
}
}
else //竖直向下
{
if(b.getX()<a.getX()){
return true;
}else{
return false;
}
}
}
//斜率存在切不为零
double x0 = 0;
double y0 = 0;
double x1 = a.getQPoint().x() - center.getQPoint().x(); //把中点当原点
double y1 = a.getQPoint().y() - center.getQPoint().y(); //把中点当原点
double k = (y1-y0)/(x1-x0);
double x2 = b.getX() - center.getX(); //把中点当原点(要用坐标直接比较还得标准化)
double y2 = b.getY() - center.getY(); //把中点当原点
if(a.getX()>center.getX())//方向向右
{
if(y2>(k*x2)){ //y > kx
return true;
}else{
return false;
}
}
else //方向朝左
{
if(y2<(k*x2)){ //y > kx
return true;
}else{
return false;
}
}
}
根据旋转方向对处理得到旋转角,便可以对点进行旋转
(3) 旋转误差修正函数:
这是在实现圆的旋转后(虽然圆的旋转并无意义,但是起码放大了精度损失这个问题)发现的,由于旋转具有一定的精度损失(像素点都是整数的坐标),因此当精度累积后,可能会导致旋转失真甚至图形不断放大或者缩小。因此我采取了3个措施来实现旋转误差的修正:
(1)旋转时,在double转int时加上修正参数0.5
(2)其中ROTATE_ACCURACY是误差的容许值,ridus是旋转前圆的半径,x,y分别是旋转后的点。由于绕着圆心旋转,因此就寻找以的附近在点集的范围内到圆心距离与原半径误差最小的点。这样确保了误差不会太大
xxxxxxxxxx
Point CycleController::getTheAccurayRotatePoint(qreal ridus, int x, int y)
{
int resX = x-ROTATE_ACCURACY;
int resY = y-ROTATE_ACCURACY;
double minDiff =100;
for(int i=x-ROTATE_ACCURACY;i<=x+ROTATE_ACCURACY;i++){
for(int j=y-ROTATE_ACCURACY;j<=y+ROTATE_ACCURACY;j++){
Point tmp(i,j);
double diffTmp = fabs(this->cycle->centerPoint.distanceToPoint(tmp.getQPoint())-ridus);
if(diffTmp<minDiff){
resX=i;
resY=j;
minDiff = diffTmp;
}
}
}
return Point(resX,resY);
}
(3)由于半径的计算也有误差,因此在一次旋转中,对每次求得的半径取平均,以求达到稳定。
基于上述算法,后期对于直线的旋转也进行了相应的校正处理
我实现的是相对于原点的缩放。将每个顶点坐标乘以缩放系数到得到新坐标 的移动满足
这里采用直线的做例子,其中ZOOM_IN
和ZOOM_OUT
是缩放参数,在constparam.h中定义,两者互为倒数。这里对于自定义的Point类重载了其乘法,得以直接对坐标进行变换
xxxxxxxxxx
void LineController::setBigger(QPainter* painter, QMouseEvent *e, QPen pen){
this->curLine->setStartPoint(this->curLine->startPoint*ZOOM_IN); //乘以放大系数
this->curLine->setEndPoint(this->curLine->endPoint*ZOOM_IN); //乘以放大系数
MyDrawLineDDA(painter,curLine->startPoint.point,curLine->endPoint.point);
drawHandle(painter,pen);
}
void LineController::setSmaller(QPainter* painter, QMouseEvent *e, QPen pen){
this->curLine->setStartPoint(this->curLine->startPoint*ZOOM_OUT); //乘以缩小系数
this->curLine->setEndPoint(this->curLine->endPoint*ZOOM_OUT); //乘以缩小系数
MyDrawLineDDA(painter,curLine->startPoint.point,curLine->endPoint.point);
drawHandle(painter,pen);
}
泛滥填充算法:区域内部用单一颜色定义的区域填充。通过替换指定的内部颜色来对这个区域着色(填充)。
从种子点开始,按像素连通定义,递归检测和扩展区域内部像素,并将填充颜色赋给这些像素,直到所有内部点均被着色。
泛滥填充算法实际上思路很简单,我目前实现是类似windows画图程序的油漆桶功能,即鼠标点击一个点,则按照4连通定义,递归检测,对其连通区域中与颜色相同的像素点赋予用户选择颜色。
但是泛滥填充算法的效率和对于栈的要求比较高,因此我对其进行了一些优化
(1)定义bool processed[]
数组,大小为整个区域,表示是否处理过,这样对于处理过的点可以不在判断
(2) 改递归为循环,用一个栈stack存储待处理的点,处理后将未处理过的且四连通的的颜色与底色相同的点入栈,每次对出栈的点处理,一直填充直到栈为空
(3) 将栈stack定义在堆区,减少对系统栈的使用负担
xxxxxxxxxx
stack->clear();
stack->push(QPoint(cx,cy));
while(!stack->empty()){
QPoint p = stack->pop();
int x = p.x();
int y = p.y();
if(x<=0 || x>=pix->width()-2) continue; //超限
if(y<=0 || y>=pix->height()-2) continue; //超限
if(img->pixelColor(x,y) != backcolor) continue; //边界
if(processed[x][y] == true) continue; //上色过了
processed[x][y] = true; //标记,代表处理过了
painter->drawPoint(x,y);
if(!processed[x][y+1]){
stack->push(QPoint(x,y+1));
}
if(!processed[x][y-1]){
stack->push(QPoint(x,y-1));
}
if(!processed[x-1][y]){
stack->push(QPoint(x-1,y));
}
if(!processed[x+1][y]){
stack->push(QPoint(x+1,y));
}
}
stack->clear();
对于直线段,可以用参数方程形式表示
若有位于由所构成的裁剪窗口内,则有下式成立
这四个不等式可以表达为
其中,被定义为
则有以下结论:
平行于窗口某边界的直线,其,值对应于相应的边界(对应于左、右、下、上边界)。
当时
当时
当时,可以计算出参数u的值,它对应于无限延伸的直线与延伸的窗口边界k的交点
对于每条直线,可以计算出参数和,该值定义了位于窗口内的线段部分:
与Cohen-Sutherland算法相比,梁友栋-Barsky算法减少了交点计算次数:
(1)梁友栋-Barsky
(2)Cohen-Sutherland算法
参数初始化:线段交点初始参数分别为:。
定义判断函数
用来判断:是舍弃线段?还是改变交点参数r
更新后的判断
求解交点参数
反复执行上述过程,计算各裁剪边界的值进行判断。
xxxxxxxxxx
bool LineController::cutLineLiangBsrsky(QPoint cutStart, QPoint cutEnd, QPainter *painter, QPen pen)
{
//在这里进行判断
//对裁剪窗口预处理
double xmin = std::min(cutStart.x(),cutEnd.x());
double ymin = std::min(cutStart.y(),cutEnd.y());
double xmax = std::max(cutStart.x(),cutEnd.x());
double ymax = std::max(cutStart.y(),cutEnd.y());
//得到待裁剪直线的各个端点
double x1 = curLine->startPoint.getX();
double y1 = curLine->startPoint.getY();
double x2 = curLine->endPoint.getX();
double y2 = curLine->endPoint.getY();
//各个参数的定义
double dx = x2-x1; //△x
double dy = y2-y1; //△y
double p[4] = {-dx,dx,-dy,dy};
double q[4] = {x1-xmin,xmax-x1,y1-ymin,ymax-y1};
double u1 = 0;
double u2 = 1;
//梁友栋裁剪算法,对p和q进行判断
for(int i=0;i<4;i++){
if(fabs(p[i])<1e-6 && q[i]<0){ //p=0且q<0时,舍弃该线段
this->clearState();
return false;
}
double r = q[i]/p[i];
if(p[i]<0){
u1 = r>u1?r:u1; //u1取0和各个r值之中的最大值
}else{
u2 = r<u2?r:u2; //u2取1和各个r值之中的最小值
}
if(u1>u2){ //如果u1>u2,则线段完全落在裁剪窗口之外,应当被舍弃
this->clearState();
return false;
}
}
curLine->setStartPoint(Point(x1+int(u1*dx+0.5), y1+int(u1*dy+0.5)));
curLine->setEndPoint(Point(x1+int(u2*dx+0.5), y1+int(u2*dy+0.5)));
MyDrawLineDDA(painter,curLine->startPoint.point,curLine->endPoint.point);
drawHandle(painter,pen);
return true;
}
操作系统(运行平台) | Windows 10 |
---|---|
开发语言 | C++ |
开发环境 | Qt Creator |
编译环境 | MinGW 5.3.0 |
本系统使用Qt提供图形界面,通过Qt集成的QOpenGLWidget类来提供画布,实验主要分为以下类(PDF版本中表格在下一页,可能有些影响阅读体验,还请见谅)
类名 | 继承于 | 类功能 |
---|---|---|
MainWindow | QMainWindow | 提供整个系统的框架,并提供基础功能,如选择绘图类型(目前是直线,圆,椭圆),撤销,清屏,新建图像,保存图像等选项,是交互的窗口。 |
Canvas_GL | QOpenGLWidget | 每个Canvas_GL类都是一个绘图的画布,用户在上面绘图 |
Canvas_3DGL | Canvas_GL | 每个Canvas_3DGL类提供3D图形的显示和控制功能 |
ColorPanel | QWidget | 尚未完全实现,用于提供更为便捷的功能选择 |
FigureController | 图形控制的接口,用于定义绘图行为(目前只有绘制,后期预计在此接口上实现更多功能) | |
LineController | FigureController | 控制直线的输入和编辑,实现了DDA直线算法 |
CycleController | FigureController | 控制圆的输入和编辑,实现了中点圆和Bresenham画圆法 |
EllipseController | FigureController | 控制椭圆的输入和编辑,实现了中点点椭圆绘制算法 |
PolygonController | FigureController | 控制了多边形输入和编辑 |
CurveController | FigureController | 控制了曲线的输入和编辑 |
Figure | 图形类的基类,用于记录画过的图形,便于后期操作 | |
Line | Figure | 直线,记录了直线的起点和终点 |
Cycle | Figure | 圆,记录了圆心和半径(实际上是圆周的任意一点) |
Ellipse | Figure | 椭圆,记录了代表长短轴的矩形(其实是中心和矩形顶点) |
Polygon | Figure | 多边形,记录了多边形的各个顶点坐标 |
Curve | Figure | 曲线,记录了曲线的各个顶点坐标 |
Point | 点,集成了QPoint,对于关于点的一些常用操作进行抽象集成 | |
PointD | 和Point类相似,区别在于其坐标为浮点,用于精度的控制 |
为了实现对图形的编辑,我定义了FigureController类和Figure类,并由这两个类继承衍生出具体图形和其控制器。Figure用于图元的存储,FigureController用于图元的编辑等一系列操作。
这里给出FigureController的定义,用了许多虚函数,这样利于在Canvas中统一操作,优化代码结构
xxxxxxxxxx
class FigureController
{
public:
FigureController();
//抽象函数
virtual void mousePressEvent(QPainter* painter, QMouseEvent *e, QPen pen) =0;
virtual void mouseMoveEvent(QPainter* painter, QMouseEvent *e, QPen pen) =0;
virtual void mouseReleaseEvent(QPainter* painter,QMouseEvent *e, QPen pen) =0;
virtual bool isOperationing(QMouseEvent *e,QPoint &start,QPoint &end)=0; //判断是否有在对图形进行绘制操作
virtual void setStartPoint(Point point) =0; //设置起始点
virtual void setEndPoint(Point point) =0; //设置终点
virtual void moveToPoint(Point point) =0; //平移
virtual void rotateToPoint(Point point) =0; //旋转
virtual void setState(DRAW_STATE *state) =0; //设置状态
virtual void drawHandle(QPainter* painter, QPen pen) =0; //描绘辅助信息
virtual void clearState() =0; //情况状态
virtual void getStartAndEnd(QPoint &start,QPoint &end) =0;
virtual void setBigger(QPainter* painter, QMouseEvent *e, QPen pen) =0; //放大
virtual void setSmaller(QPainter* painter, QMouseEvent *e, QPen pen) =0; //缩小
//非抽象函数
double getRotaryAngle(Point center,Point a,Point b); //得到夹角(返回值是弧度)
bool clockWise(Point center,Point a,Point b); //判断是否顺时针
double getLength(QPoint &start,QPoint &end); //得到两点间距离
void drawOutlineToDebug(QPainter* painter,QPoint &start,QPoint &end); //描绘轮廓
void drawOutlineToDebug(QPainter* painter,QPoint a,QPoint b,QPoint c,QPoint d); //描绘轮廓
void printCtrlDebugMessage(QString msg){
qDebug()<<msg<<endl;
}
//protected:
QPainter *painter; //画板
QPen pen; //画笔
DRAW_STATE *state; //绘画状态
};
系统主题框架类:MainWindow,内集成一个画布类Canvas_GL的数组
每次创建新画布,就显示新创建的画布
画布接受事件输入
对于工具栏的输入,由最外层的MainWindow向当前活跃状态的Canvas_GL传递相应消息。
通过接受鼠标事件,对基本图形输入,编辑
共性通过对应图形的Controller的对应函数来处理,这里用到了动态绑定来精简代码
对图形的特殊性质特别处理
对于画笔和笔刷,将每次move的点通过直线连起来
填充功能直接对鼠标点击区域填充
裁剪目前只对直线有效,鼠标输入裁剪框,点击裁剪按钮即可裁剪
3D画布
为了更好地展现测试效果,我采用GIF图的形式。但是PDF不能显示GIF,因此还麻烦助教能够打开我提供的markdown版本或者html版本查看更为详细的报告。如有不便,十分抱歉。
对于性能测试,我主要对基本图形和填充功能进行测试。
直线的输入测试:可以输入任何斜率,任意大小的直线:
直线的编辑测试:可以编辑直线的顶点
直线的变换测试:可以对直线进行平移、旋转与缩放
直线的裁剪测试:针对任意斜率的直线与任意裁剪窗口,都可以正常裁剪并且继续编辑
曲线的输入测试:可以输入任意顶点数目的贝塞尔曲线:
曲线的编辑测试:可以编辑曲线的各个控制顶点
曲线的变换测试:可以对曲线进行平移、旋转与缩放
圆的输入测试:可以输入圆心任意位置,半径任意大小的圆:
圆的编辑测试:可以编辑圆的半径
圆的变换测试:可以对圆进行平移、旋转与缩放
椭圆的输入测试:可以输入中心任意位置,任意大小的椭圆:
椭圆的编辑测试:可以编辑椭圆的大小
椭圆的变换测试:可以对椭圆进行平移、旋转与缩放
多边形的输入测试:可以输入任意顶点数目的多边形:
多边形的编辑测试:可以编辑多边形的各个顶点
多边形的变换测试:可以对多边形进行平移、旋转与缩放
本绘图系统名为YoungPaint,基于C++和Qt 5.11.2,于Qt Creator上开发,编译环境为MinGW 5.3.0
在9-10月关于图形学的学习中,基于我在课上所学的理论知识,以及课外对于Qt的交互、界面设计的学习,在截止10月底的系统中,我实现了二维图形中直线,圆以及椭圆的输入,并且实现创建多个窗口,画笔颜色的选择,绘画的撤销以及图像的保存功能。
在11月的学习中,我增加了多边形的输入,增加了直线、圆、椭圆的编辑、平移、旋转、放缩功能,实现了填充和直线的裁剪。完善了图片打开的接口,并且优化了系统结构和UI设计,并增加了一些说明。
这次实验是我第一次写具有图形交互的的实验,感觉十分有趣。把图形学课上的理论同实践相结合并且不断探索,不断阅读各种文档资料学习新知识的感觉也不错。尤其是双缓冲绘图的实现,起初我为了实现类似画图程序的动态效果而自己实现了一个,后来听同学说这就是双缓冲技术,独立探索出了这样的技巧让我感觉我的确是有在学习东西的,这也让我对于该实验有着更大的兴趣。尽管由于其他原因,10月份的程序不能说尽善尽美,但是基于我对于程序的理解,一遍上着《高级程序设计》课学习C++各种高级性质,我尽可能把我的知识和设计体现在代码上。11月份中,基于《高级程序设计》课上的知识,对系统的结构进行了优化,定义了一些继承和抽象关系,让程序更加面向对象,也相对更好维护一些。同时,Figure类的定义,对后期保存所有图形并对其进行处理提供了可能。
12月中,主要增加了曲线的绘制和3D图形功能,同时针对之前系统的各种不稳定状态做了微型调整,使得系统的鲁棒性和可用性更高。整个实验过程中,各种技术和知识的学习贯穿其中。绘制算法的学习和编程语言和框架的学习,我尽可能将我所学用在了我的程序中,这个大作业也算是我的知识的结晶了。
感谢孙正兴老师和张岩老师的辛勤教学,感谢各位助教在百忙之中解答我的各种疑惑,感谢我的舍友和系友们在关于大作业的各种问题给我的指导和帮助。
[1] 孙正兴,周良,郑洪源,谢强.计算机图形学教程.北京:机械工业出版社,2006.
[2] 陈家骏,郑滔.程序设计教程用 C++语言编程(第 3 版).北京:机械工业出版社,2015.
(附其他参考资料)
[3] Qt学习社区上的《Qt基础教程之Qt学习之路》
[4] Qt官方文档
[5] Qt5.9.4利用QOpenGLWidget类进行opengl绘图 https://blog.csdn.net/cpwwhsu/article/details/79773235
[6] Qt学习之路-简易画板3(双缓冲绘图) https://blog.csdn.net/u012891055/article/details/41727391?utm_source=blogxgwz0
[7]c++实现图像旋转任意角度 https://blog.csdn.net/wonengguwozai/article/details/52049092
[8]openGL-读取off、stl、obj文件https://blog.csdn.net/OOFFrankDura/article/details/80203664
[9]DeCasteljau's 算法http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html