计算机图形学-二维图形变换 笔记总结与代码实战

文章目录

    • 1.向量基础知识
    • 2.图形坐标系
    • 3.二维图形变换原理
    • 4.二维图形几何变换
    • 5.窗口视区变换
    • 基本二维几何变换代码
    • 二维复合变换实战-五星红旗绘制

1.向量基础知识

为什么向量如此重要:在计算机图形学中主要处理三维世界中的物体对象,因此所有需要描绘的对象都有形状、位置和方向等属性。对于这样的特征,需要编写合适的计算机程序来大致描述这些对象,并描述围绕在物体周围的光线强度,计算出最终在显示器上每一个像素的值,而这些都需要使用到向量分析和图形变换。
所有点和向量都是基于某一个坐标系定义的。坐标系可以分为二维坐标系和三维坐标系,三维坐标系又可以分为左手坐标系和右手坐标系。从几何的角度看,向量是具有长度和方向的实体,但是没有位置;而点只有位置,没有长度和方向。
向量的表示:从一个点到另一个点的位移,也可以换一个角度说,向量是一个点到另一个点的平移。一个n维向量其实就是一个n元组。也可以把向量表示为矩阵形式。
向量的基本运算有加减和数乘,向量的加减可以采用平行四边形法则进行。向量还可以进行线性组合,其中两种特殊的线性组合在计算机图形学中非常重要:仿射组合是指线性组合的系数之和等于1;凸组合是特殊的仿射组合,在仿射组合的基础上要求每一个系数非负。向量的长度计算与单位向量(略)。
向量的点积得到一个标量,也就是将两个向量的分量分别相乘然后相加。点积最重要的应用就是计算两个向量的夹角。向量的叉积得到另一个三维向量,叉积也只对三维向量有意义。叉积最常用的属性是其与原来两个向量都正交。两个向量叉积所得的向量的长度等于两个向量决定的平行四边形的面积。可以使用叉积来求出平面的法向量。

2.图形坐标系

坐标系是建立图形与数之间对应联系的参考系。坐标系从维度上可以分为各种维度的坐标系,从坐标轴的空间关系看又可以分为直角坐标系、极坐标系、圆柱坐标系和球坐标系等。图形显示的过程其实就是几何模型在不同坐标系之间的映射变换。
世界坐标系:程序员可以用最适合他们手中问题的坐标系来描述对象,并且可以自动缩放和平移图形,使得其能正确地在屏幕窗口中显示,用于描述对象的空间就称为世界坐标系,即场景中物体在实际世界中的坐标;建模坐标系又称为局部坐标系,每个物体都有自己的局部中心和坐标系,建模坐标系独立于世界坐标系来定义物体的几何特性;观察坐标系是从观察者的角度来对整个世界坐标系内的对象进行重新定位和描述的坐标系,二维观察变换的一般方法就是在世界坐标系中指定一个观察坐标系统,以该系统为参考通过选定方向和位置来制定矩形裁剪窗口;设备坐标系是适合特定输出设备输出对象的坐标系,设备坐标都是整数;规范化坐标系是独立于设备,能够容易地转变为设备坐标系的中间坐标系,规范化坐标系中坐标系的取值范围是0-1。

3.二维图形变换原理

图形变换和观察是计算机图形学的基础内容之一,也是图形显示过程中不可缺少的一个环节。一个简单的图形可以通过各种变换形成一个丰富多彩的图形或者图案。
图形变换的基本原理:图形变化后原图形的连边规则没有变化,图形的变化是因为顶点位置的改变产生的。
二维仿射变换就是从二维坐标到二维坐标之间的线性变换,其需要满足平直性和平行性:平直性是指直线经过变换后仍然是直线,平行性是指平行线经过变换后仍然是平行线。变换后的横纵坐标都可以表示为变换前横纵坐标的线性组合加上一个常数的形式。
可以将二维仿射变换用矩阵的形式进行表示,使用该方法需要将原来的[x,y]表示为[x,y,1],可以理解为是在第三维为常数的平面上的一个二维向量。这种利用一个n+1维向量表示一个n维向量的方法称为齐次坐标表示法。
n维向量的变换是在n+1维的空间内进行的,变换后的n维结果是被反投回原维度的空间内而得到的。一个普通坐标可以对应多个齐次坐标,视其所加的哑坐标决定。当哑坐标为1产生的齐次坐标称为规格化坐标。使用齐次坐标给之后的矩阵运算提供了可行性和方便性。

4.二维图形几何变换

图形的几何变换是指对图形经过平移、比例、旋转等处理后产生新的图形。
①平移变换是指将指定点从一个坐标位置移动到另一个坐标位置的过程,其中平移的距离称为平移矢量。如果采用矩阵形式进行表示,则该三阶矩阵就称为平移变换矩阵。平移是一种不产生变形而移动物体的刚体变换,即每一个点都移动相同的坐标。平移变换矩阵为(Tx和Ty分别表示在横轴方向和纵轴方向的平移距离):
计算机图形学-二维图形变换 笔记总结与代码实战_第1张图片

②比例变换是指一个点相对于某一个参考点横纵坐标分别变为原来的一定倍数。比例变换所对应的矩阵就称为比例变换矩阵。对于参考点为原点时,比例变换矩阵为(Sx和Sy分别表示横轴和纵轴方向的比例系数,也就是放大或缩小多少倍):
计算机图形学-二维图形变换 笔记总结与代码实战_第2张图片

当参考点不为原点,而是指定点(x0,y0)时,则变换后点的横纵坐标为:
X’=XSx-X0(Sx-1) Y’=YSy-Y0(Sy-1)
③对称变换也称镜像变换或反射变换,指求出一个点关于某条特点直线的对称点。变换后点的坐标可以通过如下公式求出:
计算机图形学-二维图形变换 笔记总结与代码实战_第3张图片

④旋转变换是指将某个点绕着指定点旋转一定角度,规定逆时针方向为正,顺时针方向为负。旋转变换所用到的矩阵称为旋转变换矩阵,当参考点为原点时的矩阵为:
计算机图形学-二维图形变换 笔记总结与代码实战_第4张图片

对于参考点不是坐标原点,而是(x0,y0)的情况,变换后点的坐标可以通过如下公式求出(其中a表示旋转角度,用角度制表示,角度=弧度*圆周率/180):
X’=(X-X0)*cos(a)-(Y-Y0)*sin(a)+X0
Y’=(X-X0)*sin(a)+(Y-Y0)*cos(a)+Y0
⑤图形学中有时需要产生弹性物体的变形处理,这就需要使用到错切变换。错切变换最显著的特点是变换后横坐标的变化取决于变换前的纵坐标,而变换后的纵坐标取决于变换前的横坐标。横纵坐标越小,错切量越小,反之则越大。错切变换矩阵如下,其中b和c需要人工进行确定而不能直接获得:
计算机图形学-二维图形变换 笔记总结与代码实战_第5张图片

为什么在二维图形处理时需要使用齐次坐标而不是直接使用二阶矩阵呢?这是因为平移变换不能通过二阶矩阵表示,为了实现复合二维变换,就必须使用至少三阶矩阵。
复合变换是指图形作一次以上的几何变换,变换结果是每一次的变换矩阵相乘,但是矩阵相乘的顺序不能改变。从另一方面看,任何一个复杂的几何变换都可以看作基本几何变换的组合形式。
坐标系的变换可以通过两步来进行:首先对坐标系进行一次平移变换使得两个坐标系的坐标原点重合,接着对平移后的坐标系进行一次旋转就可以使得两个坐标系完全重合。
计算机图形学-二维图形变换 笔记总结与代码实战_第6张图片

其中x0和y0分别表示第二个坐标系的原点在第一个坐标系中的坐标,sita表示第一个坐标系和第二个坐标系平行所需要顺时针旋转的角度。
二维空间中某一点的变化均可以表示为点的齐次坐标和三阶的二维变换矩阵的乘积;对于直线的变换可以通过对直线两端点进行变换,从而改变直线的位置和方向;对于多边形的变换就是将变换矩阵作用到每个顶点的坐标位置,并按新的顶点坐标生成新的多边形。

5.窗口视区变换

在世界坐标系中需要显示的区域称为窗口,将世界坐标系中窗口的内容映射到设备坐标系则产生于一个视区,一个世界坐标系中的窗口可以对应多个视区。为了将窗口内的图形在视区中显示出来,必须经过窗口到视区的变换处理,这个过程称为观察变换。
当窗口变小但视区的大小不变时,就可以实现放大图形对象的某一部分,从而观察到在较大的窗口中未显示出来的细节;反之窗口变大而视区不变时则显示出来的图形就会变小。当窗口大小不变而视区大小发生变化时,就可以得到整体缩放的效果,这种缩放不改变所观察对象的内容。当把一个固定大小的窗口在一幅大图形上移动而视区不变,就会产生漫游效果。
为了完整且真实地在视区中显示窗口内的图形对象,就需要求出图形在窗口和视区间的映射关系,这时就需要根据用户定义的参数,找出窗口和视区之间的坐标对应关系。为了实现映射能够保持原有的比例关系,新坐标中的横纵坐标和原先坐标系中的横纵坐标应该满足线性关系。

基本二维几何变换代码

本章的重点是几种基本的二维图形变换,用C++代码实现这几种几何变换的代码如下:

//实验名称:五种二维图形的几何变换(平移、比例、旋转、对称、错切)
//功       能:对于系统给定的基本图形,用户输入几何变换类型,程序根据用户给定的参数绘制二维图形的几何变换,用户按任意键可以查看动态过程
//编译环境:Visual Studio 2022,EasyX_20220116
#include
#include
#include
#include
using namespace std;

const double pi = 3.1415926;//用符号常量的方式定义圆周率,比用宏定义define更加安全

//五种基本变换均不会改变基本图形自身的点坐标,因此可以使得原来点的坐标被重复使用

//平移变换函数,函数的主要参数为横纵坐标的平移距离,在绘制时用红色图形表示平移变换
void Translation(const int* x, const int* y, const unsigned& n,const int& dx,const int& dy)
{
	//首先绘制一次平移变换前的图形,以便展示区别
	initgraph(1000, 800);
	setfillcolor(RED);
	POINT* p = new POINT[n];
	for (unsigned i = 0; i < n; i++)
	{
		p[i] = { x[i],y[i] };
	}
	fillpolygon(p, n);
	_getch();
	//展示平移图形,与原图形在同一张图中
	for (unsigned i = 0; i < n; i++)
	{
		p[i].x += dx;
		p[i].y += dy;
	}
	fillpolygon(p, n);
	_getch();
	return;
}

//比例变换函数,函数的主要参数为横轴和纵轴的比例变换系数,在绘制时用黄色图形表示比例变换
void ScaleChange(const int* x,const int* y,const unsigned& n,const double& sx,const double& sy)
{
	//考虑到实际过程中参考点不一定是原点,因此让用户输入是否将坐标原点作为参考点
	int DefaultPoint;
	cout << "请确定是否采用默认参考点(0,0)(1表示使用,其他输入表示不使用):";
	cin >> DefaultPoint;
	POINT* p = new POINT[n];
	//直接使用坐标原点作为参考点的情况
	if (DefaultPoint == 1)
	{
		initgraph(1000, 800);
		setfillcolor(YELLOW);
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = x[i];
			p[i].y = y[i];
		}
		fillpolygon(p, n);
		_getch();
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x *= sx;
			p[i].y *= sy;
		}
		fillpolygon(p, n);
		_getch();
		return;
	}
	//用户自定义参考点的情况
	else
	{
		int x0, y0;
		cout << "请输入参考点的横纵坐标:";
		cin >> x0 >> y0;
		initgraph(1000, 800);
		setfillcolor(YELLOW);
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = x[i];
			p[i].y = y[i];
		}
		fillpolygon(p, n);
		_getch();
		closegraph();
		initgraph(1000, 800);
		setfillcolor(YELLOW);
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = sx * p[i].x - (sx - 1) * x0;
			p[i].y = sy * p[i].y - (sy - 1) * y0;
		}
		fillpolygon(p, n);
		_getch();
		return;
	}
}

//旋转变换函数,函数的主要参数为旋转角度,绘图时用蓝色表示旋转操作
void Rotation(const int* x, const int* y, const unsigned& n, const double& angle)
{
	//与比例变换类似,让用户自行选择是否以坐标原点为旋转中心
	int DefaultPoint;
	cout << "请选择是否采用默认参考点(0,0)(1表示使用,其他输入表示不使用):";
	cin >> DefaultPoint;
	POINT* p = new POINT[n];
	double Angle = angle * pi / 180.0;
	//以坐标原点作为旋转中心的情况
	if (DefaultPoint == 1)
	{
		initgraph(1000, 800);
		setfillcolor(BLUE);
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = x[i];
			p[i].y = y[i];
		}
		fillpolygon(p, n);
		_getch();
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = int(double(x[i]) * cos(Angle) - double(y[i]) * sin(Angle)+0.5);
			p[i].y = int(double(y[i]) * sin(Angle) + double(y[i]) * cos(Angle)+0.5);
		}
		fillpolygon(p, n);
		_getch();
		return;
	}
	//以用户自定义的点作为旋转中心的情况
	else
	{
		int x0, y0;
		cout << "请输入参考点的横纵坐标:";
		cin >> x0 >> y0;
		initgraph(1000, 800);
		setfillcolor(BLUE);
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = x[i];
			p[i].y = y[i];
		}
		fillpolygon(p, n);
		_getch();
	
		for (unsigned i = 0; i < n; i++)
		{
			p[i].x = int(double(x[i] - x0) * cos(Angle) - double(y[i] - y0) * sin(Angle) + x0+0.5);
			p[i].y = int(double(x[i] - x0) * sin(Angle) + double(y[i] - y0) * cos(Angle) + y0+0.5);
		}
		fillpolygon(p, n);
		_getch();
		return;
	}
}

//对称变换函数,函数的主要参数为对称轴的方程,用直线的一般式表示,用绿色图形来表示对称变换
void Symmetry(const int* x, const int* y, const unsigned& n, const double& A, const double& B, const double& C)
{
	initgraph(1000, 800);
	setfillcolor(GREEN);
	POINT* p = new POINT[n];
	for (unsigned i = 0; i < n; i++)
	{
		p[i].x = x[i];
		p[i].y = y[i];
	}
	fillpolygon(p, n);
	_getch();
	for (unsigned i = 0; i < n; i++)
	{
		int tempx, tempy;
	    tempx = p[i].x - int(2 * A * (A * p[i].x + B * p[i].y + C) / (A * A + B * B) + 0.5);
	    tempy = p[i].y - int(2 * B * (A * p[i].x + B * p[i].y + C) / (A * A + B * B) + 0.5);
		p[i].x = tempx;
		p[i].y = tempy;
	}
	fillpolygon(p, n);
	_getch();
	return;
}

//错切变换函数,函数的主要参数是横轴和纵轴的错切系数,用浅蓝色图形表示错切变换
void StaggeredCutting(const int* x, const int* y, const unsigned& n, const double& a, const double& b)
{
	initgraph(1000, 800);
	setfillcolor(LIGHTBLUE);
	POINT* p = new POINT[n];
	for (unsigned i = 0; i < n; i++)
	{
		p[i].x = x[i];
		p[i].y = y[i];
	}
	fillpolygon(p, n);
	_getch();
	for (unsigned i = 0; i < n; i++)
	{
		int tempx, tempy;
		tempx = p[i].x + a * p[i].y;
		tempy = p[i].y + b * p[i].x;
		p[i].x = tempx;
		p[i].y = tempy;
	}
	fillpolygon(p, n);
	_getch();
	return;
}

int main(void)
{
	//以一个等腰三角形作为基本图形,当然也可以使用其他基本图形或者让用户自定义一个基本图形,此处未作引申
	int x[3] = { 100,100,150 };
	int y[3] = { 100,300,200 };

	//根据用户的输入来判断进行的基本变换类型
	int cmd;
	cout << "请输入您想要进行的基本变换类型(1表示平移,2表示比例变换,3表示旋转,4表示对称,5表示错切):" ;
	cin >> cmd;
	if (cmd == 1)
	{
		int dx, dy;
		cout << "您选择平移操作,请分别输入横纵方向的平移距离:";
		cin >> dx >> dy;
		cout << "参数确认成功,按任意键继续程序...(画图中按任意键继续,完成后按任意键返回)" << endl;
		_getch();
		Translation(x, y, 3, dx, dy);//传参后直接调用平移函数展示绘制效果
	}
	else if (cmd == 2)
	{
		double sx, sy;
		cout << "您选择比例变换操作,请输入横轴和纵轴的变换比例:";
		cin >> sx >> sy;
		ScaleChange(x, y, 3, sx, sy);//传参后直接调用比例变换函数展示绘制效果
	}
	else if (cmd == 3)
	{
		double angle;
		cout << "请输入逆时针旋转的角度大小(用角度表示):";
		cin >> angle;
		Rotation(x, y, 3, angle);//直接调用旋转函数绘制变换效果
	}
	else if (cmd == 4)
	{
		double A, B, C;
		cout << "请输入对称轴的直线方程参数(形如Ax+By+C=0,分别输入A、B、C):";
		cin >> A >> B >> C;
		cout << "参数确认成功,按任意键继续程序...(画图完成后按任意键返回)" << endl;
		_getch();
		Symmetry(x, y, 3, A, B, C);//调用对称函数展示对称变换的变换效果
	}
	else if (cmd == 5)
	{
		double a, b;
		cout << "请分别输入横轴错切系数和纵轴错切系数:";
		cin >> a >> b;
		cout << "参数确认成功,按任意键继续程序...(画图完成后按任意键返回)" << endl;
		StaggeredCutting(x, y, 3, a, b);//调用错切变换函数展示绘制效果
	}
	else//对于其他输入,则退出程序并输出相关的提示信息
	{
		cout << "没有找到对应的绘图命令哦~" << endl;
	}
	return 0;
}

二维复合变换实战-五星红旗绘制

学习了上面的内容,那么我们就来看一下绘制我们国家五星红旗的代码吧!

//实验名称:基于二维图形的复合变换实现的五星红旗绘制(使用了平移、比例、旋转和对称)
//功       能:用户可以逐次按任意键,程序将展示五星红旗的动态绘制过程
//编译环境:Visual Studio 2022,EasyX_20220116
#include
#include
#include
const double pi = 3.141593;//用符号常量定义圆周率,方便后续使用
const double ScaleFactor = 0.381966;//用一个符号常量定义一个比例系数,这个系数表示五角星内半径和外半径之比(也就是内部五边形的半径和外接圆的半径比)

//固定使用一个五角星函数,根据给定的横纵坐标数组绘制有填充的五角星(专门定义一个函数便于统一接口)
//注意:该函数中横纵坐标数组中都各自有十个元素而不是五个元素
void FivePointsStar(const int* x, const int* y)
{
	POINT p[10];
	for (int i = 0; i < 10; i++)
	{
		p[i].x = x[i];
		p[i].y = y[i];
	}
	fillpolygon(p, 10);
	_getch();
}

//平移变换函数,对于给定的横纵坐标平移距离,会修改参数中的横纵坐标数组
void Translation(int* x, int* y,const int& dx,const int& dy)
{
	for (int i = 0; i < 10; i++)
	{
		x[i] += dx;
		y[i] += dy;
	}
}

//比例变换函数,对于给定的比例系数与参考点,会修改参数中的横纵坐标数组
void ScaleChange(int* x, int* y, const double& sx, const double& sy, const int& x0, const int& y0)
{
	for (int i = 0; i < 10; i++)
	{
		x[i] = sx * x[i] - (sx - 1) * x0;
		y[i] = sy * y[i] - (sy - 1) * y0;
	}
}

//旋转变换函数,对于给定的角度(逆时针为正)和旋转中心,修改参数中横纵坐标数组
void Rotation(int* x, int* y, const double& angle, const int& x0, const int& y0)
{
	for (int i = 0; i < 10; i++)
	{
		int tempX = int(double(x[i] - x0) * cos(angle) - double(y[i] - y0) * sin(angle) + x0 + 0.5);
		int tempY = int(double(x[i] - x0) * sin(angle) + double(y[i] - y0) * cos(angle) + y0 + 0.5);
		x[i] = tempX;
		y[i] = tempY;
	}
}

//对称变换函数,对于给定的对称轴(用一般式表示),修改参数中的横纵坐标数组
void Symmetry(int* x, int* y, const double& A, const double& B, const double& C)
{
	for (int i = 0; i < 10; i++)
	{
		int tempx, tempy;
		tempx = x[i] - int(2 * A * (A * x[i] + B * y[i] + C) / (A * A + B * B) + 0.5);
		tempy = y[i] - int(2 * B * (A * x[i] + B * y[i] + C) / (A * A + B * B) + 0.5);
		x[i] = tempx;
		y[i] = tempy;
	}
}

int main(void)
{
	int x[10], y[10];
	double angle;
	//对国旗要严肃对待,各个五角星的位置和方向都需要正确才行!
	//对大五角星的坐标进行初始化,其顶点坐标通过数学计算获得
	for (int i = 1; i < 10; i+=2)
	{
		double angle = (i * 36 + 18) * pi / 180.0;
		x[i] = 175 + 105 * cos(angle);
		y[i] = 175 + 105 * sin(angle);
	}
	int r = 105 * ScaleFactor;
	for (int i = 0; i < 10; i += 2)
	{
		double angle = (i * 36 + 18) * pi / 180;
		x[i] = 175 + r * cos(angle);
		y[i] = 175 + r * sin(angle);
	}
	initgraph(1050, 700);//国旗的长宽比例是3:2,这个得保持不变
	setbkcolor(RGB(255,0,0));//由于直接使用RED作为参数感觉颜色太暗了,所以用RGB体系自己设置背景颜色
	cleardevice();
	setfillcolor(YELLOW);
	setlinecolor(YELLOW);
	FivePointsStar(x, y);//绘制大五角星

	//通过一次平移、一次比例变换和一次旋转,作出第一颗小五角星
	Translation(x, y, 175, -105);
	ScaleChange(x, y, 0.3333, 0.3333, 350, 70);
	angle = atan(0.4);
	Rotation(x, y, angle, 350, 70);
	FivePointsStar(x, y);

	//通过一次平移、一次旋转和两次对称,作出第二颗小五角星
	Translation(x, y, 70, 70);
	angle = atan(-1 / 7.0);
	Rotation(x, y, angle, 420, 140);
	Symmetry(x, y, 1, 0, -420);
	FivePointsStar(x, y);
	Symmetry(x, y, 1, 0, -420);

	//通过一次平移和一次旋转作出第三颗小五角星
	Translation(x, y, 0, 105);
	angle = atan(-2.0 / 7);
	Rotation(x, y, angle, 420, 245);
	FivePointsStar(x, y);

	//通过一次平移和一次旋转作出第四颗小五角星
	Translation(x, y, -70, 70);
	angle = atan(-1.25);
	Rotation(x, y, angle, 350, 315);
	FivePointsStar(x, y);

	closegraph();
	return 0;
}

由于CSDN审核限制不能直接展示绘制效果,大家可以自己试运行一下代码。

你可能感兴趣的:(C++,计算机图形学,经验分享,c++)