计算机图形学各种算法VC++版本.docx
《计算机图形学各种算法VC++版本.docx》由会员分享,可在线阅读,更多相关《计算机图形学各种算法VC++版本.docx(51页珍藏版)》请在冰豆网上搜索。
![计算机图形学各种算法VC++版本.docx](https://file1.bdocx.com/fileroot1/2022-11/23/e62a18da-b45e-4574-8e24-447a99d59042/e62a18da-b45e-4574-8e24-447a99d590421.gif)
计算机图形学各种算法VC++版本
基本图形的生成
注:
本文档主要解释了构建图形的主要操作步骤以及操作重点。
1.1直线
数学上,理想的直线是由无数个点构成的集合,没有宽度。
计算机绘制直线是在显示器所给定的有限个像素组成的矩阵中,确定最佳逼近该直线的一组像素,并且按扫描线顺序,对这些像素进行写操作,实现显示器绘制直线,即通常所说的直线的扫描转换,或称直线光栅化。
由于一图形中可能包含成千上万条直线,所以要求绘制直线的算法应尽可能地快。
本节介绍一个像素宽直线的常用算法:
数值微分法(DDA)、中点画线法、Bresenham算法。
1.1.1DDA(数值微分)算法
DDA算法原理:
如图1-1所示,已知过端点
的直线段
;直线斜率为
,从
的左端点
开始,向
右端点步进画线,步长=1(个像素),计算相应的
坐标
;取像素点 [
round(y)] 作为当前点的坐标。
计算
,当
,即当x每递增1,y递增k(即直线斜率)。
注意:
上述分析的算法仅适用于k1的情形。
在这种情况下,x每增加1,y最多增加1。
当
时,必须把x,y地位互换,y每增加1,x相应增加1/k(请参阅后面的VisualC++程序)。
1.1.2生成直线的中点画线法
中点画线法的基本原理如图1-2所示。
在画直线段的过程中,当前像素点为P,下一个像素点有两种选择,点P1或P2。
M为P1与P2中点,Q为理想直线与X=Xp+1垂线的交点。
当M在Q的下方时,则P2应为下一个像素点;当M在Q的上方时,应取P1为下一点。
中点画线法的实现:
令直线段为L[ p0(x0,y0),p1(x1,y1)],其方程式F(x,y)=ax+by+c=0。
其中,a=y0–y1,b=x1–x0,c=x0y1–x1y0;点与L的关系如下。
在直线上,F(x,y)=0;
在直线上方,F(x,y)>0;
在直线下方,F(x,y)<0。
把M代入F(x,y),判断F的符号,可知Q点在中点M的上方还是下方。
为此构造判别式d=F(M)=F(xp+1,yp+0.5)=a(xp+1)+b(yp+0.5)+c。
当d < 0,L(Q点)在M上方,取P2为下一个像素。
当d > 0,L(Q点)在M下方,取P1为下一个像素。
当d=0,选P1或P2均可,取P1为下一个像素。
其中d是xp,yp的线性函数。
1.1.3Bresenham算法
Bresenham算法是计算机图形学领域使用最广泛的直线扫描转换算法。
由误差项符号决定下一个像素取右边点还是右上方点。
图1-3第一象限直线光栅化
Bresenham算法
设直线从起点(x1,y1)到终点(x2,y2)。
直线可表示为方程y = mx+b,其中b=y1–mx1,m = (y2–y1)/(x2–x1)=dy/dx;此处的讨论直线方向限于第一象限,如图1-3所示,当直线光栅化时,x每次都增加1个单元,设x像素为(xi,yi)。
下一个像素的列坐标为xi+1,行坐标为yi或者递增1为yi+1,由y与yi及yi+1的距离d1及d2的大小而定。
计算公式为
y=m(xi+1)+b(1.1)
d1=y–yi(1.2)
d2=yi+1–y(1.3)
如果d1–d2>0,则yi+1=yi+1,否则yi+1=yi。
式(1.1)、(1.2)、(1.3)代入d1–d2,再用dx乘等式两边,并以Pi=(d1–d2),dx代入上述等式,得
Pi=2xidy–2yidx+2dy+(2b–1)dx(1.4)
d1–d2是用以判断符号的误差。
由于在第一象限,dx总大于0,所以Pi仍旧可以用做判断符号的误差。
Pi+1为
Pi+1=Pi+2dy–2(yi+1–yi)dx(1.5)
求误差的初值P1,可将x1、y1和b代入式(1.4)中的xi、yi,而得到
P1=2dy–dx
综述上面的推导,第一象限内的直线Bresenham算法思想如下:
(1)画点(x1,y1),dx=x2–x1,dy=y2–y1,计算误差初值P1=2dy–dx,i=1。
(2)求直线的下一点位置xi+1=xi+1,如果Pi>0,则yi+1=yi+1,否则yi+1=yi。
(3)画点(xi+1,yi+1)。
(4)求下一个误差Pi+1,如果Pi>0,则Pi+1=Pi+2dy–2dx,否则Pi+1=Pi+2dy。
(5)i=i+1;如果i(2);否则结束操作。
1.1.4程序设计
1.程序设计功能说明
为编程实现上述算法,本程序利用最基本的绘制元素(如点、直线等),绘制图形。
如图1-4所示,为程序运行主界面,通过选择菜单及下拉菜单的各功能项分别完成各种对应算法的图形绘制。
图1-4基本图形生成的程序运行界面
2.创建工程名称为“基本图形的生成”单文档应用程序框架
(1)启动VC,选择“文件”|“新建”菜单命令,并在弹出的新建对话框中单击“工程”标签。
(2)选择MFCAppWizard(exe),在“工程名称”编辑框中输入“基本图形的生成”作为工程名称,单击“确定”按钮,出现Step1对话框。
(3)选择“单个文档”选项,单击“下一个”按钮,出现Step2对话框。
(4)接受默认选项,单击“下一个”按钮,在出现的Step3~Step5对话框中,接受默认选项,单击“下一个”按钮。
(5)在Step6对话框中单击“完成”按钮,即完成“基本图形的生成”应用程序的所有选项,随后出现工程信息对话框(记录以上步骤各选项选择情况),如图1-5所示,单击“确定”按钮,完成应用程序框架的创建。
图1-5信息程序基本
3.编辑菜单资源
设计如图1-4所示的菜单项。
在工作区的ResourceView标签中,单击Menu项左边“+”,然后双击其子项IDR_MAINFRAME,并根据表1-1中的定义编辑菜单资源。
此时VC已自动建好程序框架,如图1-5所示。
表1-1菜单资源表
菜单标题
菜单项标题
标示符ID
直线
DDA算法生成直线
ID_DDALINE
Bresenham算法生成直线
ID_BRESENHAMLINE
中点算法生成直线
ID_MIDPOINTLINE
4.添加消息处理函数
利用ClassWizard(建立类向导)为应用程序添加与菜单项相关的消息处理函数,ClassName栏中选择CMyView,根据表1-2建立如下的消息映射函数,ClassWizard会自动完成有关的函数声明。
表1-2菜单项的消息处理函数
菜单项ID
消息
消息处理函数
ID_DDALINE
CONMMAN
OnDdaline
ID_MIDPOINTLINE
CONMMAN
OnMidpointline
ID_BRESENHAMLINE
CONMMAN
OnBresenhamline
5.程序结构代码,在CMyView.cpp文件中相应位置添加如下代码:
//DDA算法生成直线
voidCMyView:
:
OnDdaline()
{
CDC*pDC=GetDC();//获得设备指针
intxa=100,ya=300,xb=300,yb=200,c=RGB(255,0,0);//定义直线的两端点,直线颜色
intx,y;
floatdx,dy,k;
dx=(float)(xb-xa),dy=(float)(yb-ya);
k=dy/dx,y=ya;
if(abs(k)<1)
{
for(x=xa;x<=xb;x++)
{pDC->SetPixel(x,int(y+0.5),c);
y=y+k;}
}
if(abs(k)>=1)
{
for(y=ya;y<=yb;y++)
{pDC->SetPixel(int(x+0.5),y,c);
x=x+1/k;}
}
ReleaseDC(pDC);
}
说明:
(1)以上代码理论上通过定义直线的两端点,可得到任意端点之间的一直线,但由于一般屏幕坐标采用右手系坐标,屏幕上只有正的x,y值,屏幕坐标与窗口坐标之间转换知识请参考第3章。
(2)注意上述程序考虑到当k£1的情形x每增加1,y最多增加1;当k>1时,y每增加1,x相应增加1/k。
在这个算法中,y与k用浮点数表示,而且每一步都要对y进行四舍五入后取整。
//中点算法生成直线
voidCMyView:
:
OnMidpointline()
{
CDC*pDC=GetDC();
intxa=300,ya=200,xb=450,yb=300,c=RGB(0,255,0);
floata,b,d1,d2,d,x,y;
a=ya-yb,b=xb-xa,d=2*a+b;
d1=2*a,d2=2*(a+b);
x=xa,y=ya;
pDC->SetPixel(x,y,c);
while(x{if(d<0){x++,y++,d+=d2;}
else{x++,d+=d1;}
pDC->SetPixel(x,y,c);
}
ReleaseDC(pDC);
}
说明:
(1)其中d是xp,yp的线性函数。
为了提高运算效率,程序中采用增量计算。
具体算法如下:
若当前像素处于d>0情况,则取正右方像素P1(xp+1,yp),判断下一个像素点的位置,应计算d1=F(xp+2,yp+0.5)=a(xp+2)+b(yp+0.5)=d+a;,其中增量为a。
若d<0时,则取右上方像素P2(xp+1,yp+1)。
再判断下一像素,则要计算d2 =F(xp+2,yp+1.5)=a(xp+2)+b(yp+1.5) + c=d+a+b,增量为a+b。
(2)画线从(x0,y0)开始,d的初值d0=F(x0+1,y0+0.5)=F(x0,y0)+a+0.5b,因F(x0,y0)=0,则d0=a+0.5b。
(3)程序中只利用d的符号,d的增量都是整数,只是初始值包含小数,用2d代替d,使程序中仅包含整数的运算。
//Bresenham算法生成直线
voidCMyView:
:
OnBresenhamline()
{
CDC*pDC=GetDC();
intx1=100,y1=200,x2=350,y2=100,c=RGB(0,0,255);
inti,s1,s2,interchange;
floatx,y,deltax,deltay,f,temp;
x=x1;
y=y1;
deltax=abs(x2-x1);
deltay=abs(y2-y1);
if(x2-x1>=0)s1=1;elses1=-1;
if(y2-y1>=0)s2=1;elses2=-1;
if(deltay>deltax){
temp=deltax;
deltax=deltay;
deltay=temp;
interchange=1;
}
elseinterchange=0;
f=2*deltay-deltax;
pDC->SetPixel(x,y,c);
for(i=1;i<=deltax;i++){
if(f>=0){
if(interchange==1)x+=s1;
elsey+=s2;
pDC->SetPixel(x,y,c);
f=f-2*deltax;
}
else{
if(interchange==1)y+=s2;
elsex+=s1;
f=f+2*deltay;
}
}
}
说明:
(1)以上程序已经考虑到所有象限直线的生成。
(2)Bresenham算法的优点如下:
①不必计算直线的斜率,因此不做除法。
②不用浮点数,只用整数。
③只做整数加减运算和乘2运算,而乘2运算可以用移位操作实现。
④Bresenham算法的运算速度很快。
1.2圆
给出圆心坐标(xc,yc)和半径r,逐点画出一个圆周的公式有下列两种。
1.2.1直角坐标法
直角坐标系的圆的方程为
由上式导出:
图1-7确定y的位置
图1-6中点画圆法示意图
当x–xc从–r到r做加1递增时,就可以求出对应的圆周点的y坐标。
但是这样求出的圆周上的点是不均匀的,| x–xc | 越大,对应生成圆周点之间的圆周距离也就越长。
因此,所生成的圆不美观。
1.2.2中点画圆法
如图1-6所示,函数为F(x,y)=x2+y2–R2的构造圆,圆上的点为F(x,y)=0,圆外的点F(x,y)>0,圆内的点F(x,y)<0,构造判别式:
d=F(M)=F(xp+1,yp–0.5)=(xp+1)2+(yp–0.5)2
若d<0,则应取P1为下一像素,而且下一像素的判别式为
d=F(xp+2,yp–0.5)=(xp+2)2+(yp–0.5)2–R2=d+2xp+3
若d≥0,则应取P2为下一像素,而且下一像素的判别式为
d=F(xp+2,yp–1.5)=(xp+2)2+(yp–1.5)2–R2=d+2(xp–yp)+5
我们讨论按顺时针方向生成第二个八分圆,则第一个像素是(0,R),判别式d的初始值为
d0=F(1,R–0.5)=1.25–R
1.2.3圆的Bresenham算法
设圆的半径为r,先考虑圆心在(0,0),从x=0、y=r开始的顺时针方向的1/8圆周的生成过程。
在这种情况下,x每步增加1,从x=0开始,到x=y结束,即有xi+1=xi+1;相应的,yi+1则在两种可能中选择:
yi+1=yi或者yi+1=yi – 1。
选择的原则是考察精确值y是靠近yi还是靠近yi–1(见图1-7),计算式为
y2=r2–(xi+1)2
d1=yi2–y2=yi2–r2+(xi+1)2
d2=y2–(yi – 1)2=r2–(xi+1)2–(yi – 1)2
令pi=d1–d2,并代入d1、d2,则有
pi=2(xi+1)2+yi2+(yi – 1)2–2r2(1.6)
pi称为误差。
如果pi<0,则yi+1=yi,否则yi+1=yi – 1。
pi的递归式为
pi+1=pi+4xi+6+2(yi2+1–yi2)–2(yi+1–yi)(1.7)
pi的初值由式(1.6)代入xi=0,yi=r,得
p1=3–2r(1.8)
根据上面的推导,圆周生成算法思想如下:
(1)求误差初值,p1=3–2r,i=1,画点(0,r)。
(2)求下一个光栅位置,其中xi+1=xi+1,如果pi<0则yi+1=yi,否则yi+1=yi – 1。
(3)画点(xi+1,yi+1)。
(4)计算下一个误差,如果pi<0则pi+1=pi+4xi+6,否则pi+1=pi+4(xi – yi)+10。
(5)i=i+1,如果x=y则结束,否则返回步骤
(2)。
程序设计步骤如下。
(1)创建应用程序框架,以上面建立的单文档程序框架为基础。
(2)编辑菜单资源。
在工作区的ResourceView标签中,单击Menu项左边“+”,然后双击其子项IDR_MAINFRAME,并根据表1-3中的定义添加编辑菜单资源。
此时建好的菜单如图1-8所示。
图1-8程序主菜单
表1-3菜单资源表
菜单标题
菜单项标题
标示符ID
圆
中点画圆
ID_MIDPOINTCIRCLE
Bresenham画圆
ID_BRESENHAMCIRCLE
(3)添加消息处理函数。
利用ClassWizard(建立类向导)为应用程序添加与菜单项相关的消息处理函数,ClassName栏中选择CMyView,根据表1-4建立如下的消息映射函数,ClassWizard会自动完成有关的函数声明。
表1-4菜单项的消息处理函数
菜单项ID
消息
消息处理函数
ID_MIDPOINTCIRCLE
CONMMAN
OnMidpointcircle
ID_BRESENHAMCIRCLE
CONMMAN
OnBresenhamcircle
(4)程序结构代码,在CMyView.cpp文件中的相应位置添加如下代码。
voidCMyView:
:
OnMidpointcircle()//中点算法绘制圆,如图1-9所示
{
图1-9中点算法绘制圆
//TODO:
Addyourcommandhandlercodehere
CDC*pDC=GetDC();
intxc=300,yc=300,r=50,c=0;
intx,y;
floatd;
x=0;y=r;d=1.25-r;
pDC->SetPixel((xc+x),(yc+y),c);
pDC->SetPixel((xc-x),(yc+y),c);
pDC->SetPixel((xc+x),(yc-y),c);
pDC->SetPixel((xc-x),(yc-y),c);
pDC->SetPixel((xc+y),(yc+x),c);
pDC->SetPixel((xc-y),(yc+x),c);
pDC->SetPixel((xc+y),(yc-x),c);
pDC->SetPixel((xc-y),(yc-x),c);
while(x<=y)
{if(d<0)d+=2*x+3;
else{d+=2*(x-y)+5;y--;}
x++;
pDC->SetPixel((xc+x),(yc+y),c);
pDC->SetPixel((xc-x),(yc+y),c);
pDC->SetPixel((xc+x),(yc-y),c);
pDC->SetPixel((xc-x),(yc-y),c);
pDC->SetPixel((xc+y),(yc+x),c);
pDC->SetPixel((xc-y),(yc+x),c);
pDC->SetPixel((xc+y),(yc-x),c);
pDC->SetPixel((xc-y),(yc-x),c);
}
}
voidCMyView:
:
OnBresenhamcircle()////Bresenham算法绘制圆,如图1-10所示
{
CDC*pDC=GetDC();
intxc=100,yc=100,radius=50,c=0;
图1-10Bresenham算法绘制圆
intx=0,y=radius,p=3-2*radius;
while(x{
pDC->SetPixel(xc+x,yc+y,c);
pDC->SetPixel(xc-x,yc+y,c);
pDC->SetPixel(xc+x,yc-y,c);
pDC->SetPixel(xc-x,yc-y,c);
pDC->SetPixel(xc+y,yc+x,c);
pDC->SetPixel(xc-y,yc+x,c);
pDC->SetPixel(xc+y,yc-x,c);
pDC->SetPixel(xc-y,yc-x,c);
if(p<0)
p=p+4*x+6;
else
{
p=p+4*(x-y)+10;
y-=1;
}
x+=1;
}
if(x==y)
pDC->SetPixel(xc+x,yc+y,c);
pDC->SetPixel(xc-x,yc+y,c);
pDC->SetPixel(xc+x,yc-y,c);
pDC->SetPixel(xc-x,yc-y,c);
pDC->SetPixel(xc+y,yc+x,c);
pDC->SetPixel(xc-y,yc+x,c);
pDC->SetPixel(xc+y,yc-x,c);
pDC->SetPixel(xc-y,yc-x,c);
}
1.3椭圆扫描转换中点算法
下面讨论椭圆的扫描转换中点算法,设椭圆为中心在坐标原点的标准椭圆,其方程为
图1-11第一象限的椭圆弧
F(x,y)=b2x2+a2y2–a2b2=0
(1)对于椭圆上的点,有F(x,y)=0;
(2)对于椭圆外的点,F(x,y)>0;
(3)对于椭圆内的点,F(x,y)<0。
以弧上斜率为–1的点作为分界将第一象限椭圆弧分为上下两部分(如图1-11所示)。
法向量:
而在下一个点,不等号改变方向,则说明椭圆弧从上部分转入下部分。
与中点绘制圆算法类似,一个像素确定后,在下面两个候选像素点的中点计算一个判别式的值,再根据判别式符号确定离椭圆最近的点。
先看椭圆弧的上半部分,具体算法如下:
假设横坐标为xp的像素中与椭圆最近点为(xp,yp),下一对候选像素的中点应为(xp+1,yp–0.5),判别式为
,表明中点在椭圆内,应取正右方像素点,判别式变为
若
,表明中点在椭圆外,应取右下方像素点,判别式变为
判别式
的初始条件确定。
椭圆弧起点为(0,b),第一个中点为(1,b – 0.5),对应判别式为
在扫描转换椭圆的上半部分时,在每步迭代中需要比较法向量的两个分量来确定核实从上部分转到下半部分。
在下半部分算法有些不同,要从正上方和右下方两个像素中选择下一个像素。
在从上半部分转到下半部分时,还需要对下半部分的中点判别式进行初始化。
即若上半部分所选择的最后一个像素点为(xp,yp),则下半部分中点判别式应在(xp+0.5,yp–1)的点上计算。
其在正下方与右下方的增量计算同上半部分。
具体算法的实现请参考下面的程序设计。
程序设计步骤如下。
(1)创建应用程序框架,以上面建立的单文档程序框架为基础。
(2)编辑菜单资源。
在工作区的ResourceView标签中,单击Menu项左边“+”,然后双击其子项IDR_MAINFRAME,并根据表1-5中的定义添加编辑菜单资源。
此时建好的菜单如图1-12所示。
表1-5菜单资源表
菜单标题
菜单项标题
标示符ID
椭圆
中点画椭圆
ID_MIDPOINTELLISPE
图1-12程序主菜单
(3)添加消息处理函数。
利用ClassWizard(建立类向导)为应用程序添加与菜单项相关的消息处理函数,ClassName栏中选择CMyView,根据表1-6建立如下的消息映射函数,ClassWizard会自动完成有关的函数声明。
表1-6菜单项的消息处理函数
菜单项ID
消息
消息处理函数
ID_MIDPOINTELLISPE
CONMMAN
OnMidpointellispe
(4)程序结构代码如下:
图1-13中点算法绘制椭圆
voidCMyView:
:
OnMidpointellispe()//中点算法绘制椭圆,如图1-13所示
{
CDC*pDC=GetDC();
inta=200,b=100,xc=300,yc=200,c=0;
intx