应用于对象几何描述并改变它的位置、方向或大小的操作称为几何变换。
几何变换有时也称为建模变换。建模变换一般用于构造场景或给出由多个部分组合而成的复杂对象的层次式描述等。另一方面,几何变换能用来描述动画序列中对象在场景中可以怎样移动或简单地从另一角度来观察它们。
平移、旋转和缩放是所有图形软件包中都包含的几何变换函数。可能包括在图形软件包中的其他变换函数有反射和错切操作。
通过将位移量加到一个点的坐标上来生成一个新的坐标位置,可以实现一次平移。
将平移距离tx和ty加到原始坐标(x,y)上获得一个新的坐标位置可以实现一个二维位置的平移,
一队平移距离(tx,ty)称为平移向量或位移向量。
可以使用矩阵形式来表示二维平移方程:
下面的程序实现了平移操作:
可以使用同样的方法来平移其他对象。为了改变圆或椭圆的位置,可以平移中心坐标并在新的中心位置上重画图形。对于一个样条曲线,通过平移定义该曲线路径上的点,然后使用平移过的坐标位置来重构曲线。
通过指定一个旋转轴和一个旋转角度,可以进行一次旋转变换。在将对象的所有顶点按指定角度绕指定旋转轴旋转后,该对象的所有点都旋转到新位置。
对象的二维旋转通过在xy平面上沿圆路径将对象重定位来实现。此时,我们将对象绕与xy平面垂直的旋转轴(与z轴平行)旋转。二维旋转的参数有旋转角和称为旋转点的位置,对象绕该点旋转。
确定当基准点为坐标原点时点位置P进行旋转的变换方程。
点的原始坐标为:
代入上式中,就得到了相对于原点、将位置(x,y)的点旋转指定角度的变换方程:
旋转方程的矩阵形式为:
其中,旋转矩阵为:
多边形的旋转则是将每个顶点旋转指定的旋转角,并使用新的顶点来生成多边形而实现旋转。曲线的旋转通过重新定位定义的点并重新绘制曲线而完成。
下列程序中,一个多边形绕指定的世界坐标系中的基准点旋转。
改变一个对象的大小,可使用缩放变换。一个简单的二维缩放操作可通过将缩放系数sx和sy与对象坐标位置(x,y)相乘得到:
基本二维缩放公式的矩阵形式:
当赋给sx和sy相同的值时,就会保持对象相对比例的一致缩放。sx和sy值不等时将产生设计应用中常见的差值缩放。
可以选择一个在缩放后不改变位置的点,称为固定点,以控制缩放后对象的位置。固定点的坐标(xf,yf)可以选择对象的中点等位置或任何其他空间位置。这样,多边形通过缩放每个顶点到固定点的距离而相对于固定点进行缩放。对于坐标为(x,y)的顶点,缩放后的坐标可计算为:
多边形的缩放可以通过变换方程应用于每个顶点,然后利用变换后的顶点重新生成多边形而实现。
下列程序给出了对一个多边形缩放而进行计算的例子。
每个基本变换(平移、旋转和缩放)都可以表示为普通矩阵形式
为了利用这个公式产生先缩放、再旋转、后平移这样的变换顺序,必须一步一步地计算变换的坐标。更有效的方法是将变换组合,从而直接从初始坐标得到最后的坐标位置,这样就消除了中间坐标值的计算。
如果将 2 x 2 矩阵表达式扩充为 3 x 3矩阵,就可以把二维集合变换的乘法和平移组合成单一矩阵表示。标准的实现技术是将二维坐标位置表示(x,y)扩充到三维表示(xh,yh,h),称为齐次坐标,这里的齐次参数h是一个非零值,因此:
利用齐次坐标表示位置,使我们可以用矩阵相乘的形式来表示所有的几何变换公式,而这是图形系统中使用的标准方法。
由于场景中有许多位置用相同的顺序变换,先将所有变换矩阵相乘形成一个复合矩阵将是高效率的方法。
当图形软件包仅提供绕坐标原点的旋转函数时,可通过完成下列平移——旋转——平移操作序列来实现绕任意选定的基准点(xr,yr)的旋转。
1.平移对象使基准点位置移动到坐标原点;
2.绕坐标原点旋转;
3.平移对象使基准点回到其原始位置。
关于任意选择的基准位置(xf,yf)缩放的变换序列:
1.平移对象使固定点与坐标原点重合;
2.对于坐标原点进行缩放;
3.使用步骤1的反向平移将对象返回原始位置。
为了完成这种缩放而不改变对象方向,首先完成旋转操作,使s1和s2的方向分别与x和y轴重合。然后应用缩放变换S(s1,s2),再进行反向旋转回到其原始位置。这三个变换的乘积得到的复合矩阵为:
矩阵相乘符合结合律。变换积一般不可交换。对于变换序列中每一个类型都相同的特殊情况,变换矩阵的多重相乘是可交换的。
表示平移、旋转和缩放组合的通用二维变换可以表示为
一旦把单个矩阵连接起来计算出复合矩阵的元素值,这就是任何变换序列所需计算的最大数目。
变换操作的有效实现是先形成变换矩阵,合并所有变换序列,然后计算变换坐标。
由于旋转计算需要对每个变换点进行三角求值和多次乘法,因而在旋转变换中的计算效率就成为十分重要的问题。在动画及其他包含许多重复变换和小旋转角的应用中,可用近似和循环计算来减少复合变换方程中的计算量。对于足够小的角度(小于10°),cos近似为1,而sin近似于该角度的弧度值。
其中,只要旋转角不变化,sin对所有步长只需求值一次。在很多步之后,积累的误差也会变得很大。必须在误差积累变得太大时重新设置对象位置。
如果一个变换矩阵仅包含平移和旋转参数,则它是一个刚体变换矩阵。二维刚体变换矩阵的一般形式为:
坐标位置的刚体变化有时也成为刚体运动变换。变换后坐标位置间所有角度和距离都不变化。此外矩阵(7.45)具有其左上角2x2是一个正交矩阵的特性。
因此,假如这些单位向量通过旋转子矩阵进行变换,那么(rxx,rxy)就转换成沿x轴的单位向量,(ryx,ryy)转换成沿坐标系统y轴的单位向量:
作为一个例子,下列刚体变换先将对象对于基准点(xr,yr)旋转角度,然后平移:
在只直到对象的最后方向而不知道将对象放到这个位置所需的旋转角度时,旋转矩阵的正交特性可用于构造矩阵。
该方向信息可以根据与场景中某一对象对齐或由场景中所选定的位置来确定。
该三角形先对其中心位置进行缩放,然后绕其中心旋转,最后进行平移。
#include
#include
#include
GLsizei winWidth = 600, winHeight = 600;
GLfloat xwcMin = 0.0, xwcMax = 225.0;
GLfloat ywcMin = 0.0, ywcMax = 225.0;
class wcPt2D
{
public:
GLfloat x, y;
};
typedef GLfloat Matrix3x3[3][3];
Matrix3x3 matComposite;
const GLdouble pi = 3.14159;
void Init()
{
glClearColor(1.0, 1.0, 1.0, 0.0);
}
void Matrix3x3SetIdentity(Matrix3x3 matIdent3x3)
{
GLint row, col;
for (row = 0; row < 3; row++)
{
for (col = 0; col < 3; col++)
{
matIdent3x3[row][col] = (row == col);
}
}
}
void Matrix3x3PreMultiply(Matrix3x3 m1, Matrix3x3 m2)
{
GLint row, col;
Matrix3x3 matTemp;
for (row = 0; row < 3; row++)
{
for (col = 0; col < 3; col++)
{
matTemp[row][col] =
m1[row][0]* m2[0][col] +
m1[row][1]* m2[1][col]+
m1[row][2]* m2[2][col];
}
}
for (row = 0; row < 3; row++)
{
for (col = 0; col < 3; col++)
{
m2[row][col] = matTemp[row][col];
}
}
}
void Translate2D(GLfloat tx, GLfloat ty)
{
Matrix3x3 matTransl;
Matrix3x3SetIdentity(matTransl);
matTransl[0][2] = tx;
matTransl[1][2] = ty;
Matrix3x3PreMultiply(matTransl, matComposite);
}
void Rotate2D(wcPt2D pivotPt, GLfloat theta)
{
Matrix3x3 matRot;
Matrix3x3SetIdentity(matRot);
matRot[0][0] = cos(theta);
matRot[0][1] = -sin(theta);
matRot[0][2] = pivotPt.x * (1-cos(theta)) +
pivotPt.y * sin(theta);
matRot[1][0] = sin(theta);
matRot[1][1] = cos(theta);
matRot[1][2] = pivotPt.y * (1 - cos(theta)) -
pivotPt.x * sin(theta);
Matrix3x3PreMultiply(matRot, matComposite);
}
void Scale2D(GLfloat sx, GLfloat sy, wcPt2D fixedPt)
{
Matrix3x3 matScale;
Matrix3x3SetIdentity(matScale);
matScale[0][0] = sx;
matScale[0][2] = (1 - sx) * fixedPt.x;
matScale[1][1] = sy;
matScale[1][2] = (1 - sy) * fixedPt.y;
Matrix3x3PreMultiply(matScale, matComposite);
}
void TransformVerts2D(GLint nVerts, wcPt2D * verts)
{
GLint k;
GLfloat temp;
for (k = 0; k < nVerts; k++)
{
temp = matComposite[0][0] * verts[k].x +
matComposite[0][1] * verts[k].y +
matComposite[0][2];
verts[k].y = matComposite[1][0] * verts[k].x +
matComposite[1][1] * verts[k].y +
matComposite[1][2];
verts[k].x = temp;
}
}
void Trangle(wcPt2D * verts)
{
GLint k;
glBegin(GL_TRIANGLES);
for (k = 0; k < 3; k++)
{
glVertex2f(verts[k].x, verts[k].y);
}
glEnd();
}
void WinReshapFcn(GLint newWidth, GLint newHeight)
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(xwcMin, xwcMax, ywcMin, ywcMax);
glClear(GL_COLOR_BUFFER_BIT);
}
void DisplayFcn()
{
GLint nVerts = 3;
wcPt2D verts[3] = { {50,25},{150,25 },{100,100} };
wcPt2D centroidPt;
GLint k, xSum = 0, ySum = 0;
for (size_t k = 0; k < nVerts; k++)
{
xSum += verts[k].x;
ySum += verts[k].y;
}
centroidPt.x = GLfloat(xSum) / GLfloat(nVerts);
centroidPt.y = GLfloat(ySum) / GLfloat(nVerts);
wcPt2D pivPt, fixedPt;
pivPt = centroidPt;
fixedPt = centroidPt;
GLfloat tx = 0, ty = 100;
GLfloat sx = 0.5, sy = 0.5;
GLdouble theta = pi / 2;
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0, 0, 1);
Trangle(verts);
Matrix3x3SetIdentity(matComposite);
Scale2D(sx, sy, fixedPt);
Rotate2D(pivPt, theta);
Translate2D(tx, ty);
TransformVerts2D(nVerts, verts);
glColor3f(1, 0, 0);
Trangle(verts);
glFlush();
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowPosition(50, 50);
glutInitWindowSize(winWidth, winHeight);
glutCreateWindow("Geometric Transformation Sequence");
Init();
glutDisplayFunc(DisplayFcn);
glutReshapeFunc(WinReshapFcn);
glutMainLoop();
return 0;
}
产生对象镜像的变换称为反射。
关于 y = 0 (x轴)的反射,可以由下列变换矩阵来完成:
对于x = 0 (y轴)的反射,翻动x的坐标而保持y坐标不变,这种变换的矩阵是:
可以通过一系列的平移、旋转和坐标轴反射矩阵合并来完成关于任意直线的反射变换。
错切是一种使对象形状发生变化的变换,经过错切的对象好像是由已经相互滑动的内部夹层组成。两种常用的错切变换是移动x坐标值的错切和移动y坐标值的错切。
相对于x轴的x方向错切由下列变换矩阵产生:
可以使用下列矩阵生成相对于其他参考线的x方向错切:
使用下列变换矩阵生成相对于线 x = x ref的y方向错切:
错切操作可以表示为基本变换的序列。
光栅系统将图像信息作为颜色图案存储在帧缓冲器中。因此,一些简单的变换可以通过操作存储的像素值的矩形数组而快速地执行。由于只需要很少地算术操作,因此像素变换特别有效。
控制矩形像素数组的光栅功能通常称为光栅操作,将一块像素从一个位置移到另一位置的过程也成为像素值的块移动。
作为光栅缓存区域的块移动而完成的二维平移实现方式是,所有在矩形区域显示的位作为一个块而复制到光栅的另一部分,通过使用背景亮度填充该块的矩形区域来删除原始对象。
通用的阵列旋转,将每个目标区域映射到旋转的网格中,并计算其与旋转的像素区域的重叠值。通过对覆盖的源像素亮度求得平均值,并通过区域重叠的百分比加权来计算目标像素的亮度。或使用像反走样那样的近似方法来确定目标像素的颜色。
像素块的光栅缩放采用类似方法实现,用指定的sx和sy值对原始块中的像素区域进行缩放,并将缩放的矩形映射到一组目标像素上,然后按照其与缩放像素区域的重叠区域,设置每个目标像素的亮度。
使用在像素块中颠倒行或列的变换,并与平移结合,可完成光栅对象的反射。沿行或列移动像素位置则可实现错切。
像素颜色值的矩形数组从一个缓存到另一个的平移可以作为如下的OpenGL复制操作来完成:
该平移可作为任何刷新缓存或不同缓存之间。glCopyPixels函数的源缓存用glReadBuffer子程序选择,而目标缓存用glDrawBuffer子程序选择。
通过先将块存储于一个数组,再重新安排数组的元素并将其放回刷新缓存,可实现像素块的90°倍数的旋转。缓存中的一个RGB颜色块可以用下列函数存入一个数组:
如果颜色表索引存于像素位置,则将GL_COLOR_INDEX取代GL_RGB。使用下列语句将旋转后的数组放回缓存:
用glReadBuffer选择包含原来的像素值块的源缓存,用glDrawBuffer指定目标缓存。
对于光栅操作,使用下列函数来设定缩放因子:
为了将对象描述从xy坐标转换到x‘y'坐标,必须建立把x’y‘轴叠加到xy轴的变换,这需要分两步进行:
1.将x’y’系统的坐标原点(x0,y0)平移到xy系统的原点(0,0);
2.将x‘轴旋转搭到x轴上。
坐标原点的平移可以用下列矩阵操作表示:
旋转矩阵为:
将这两个矩阵合并,得到复合矩阵:
另一种方法是,指定一个表明正y’轴方向的向量V。将向量V指定为xy参照系中相对于xy坐标系原点的一个点。那么,在y‘方向上的单位向量可以计算为:
通过将v顺时针旋转90°,可以得到沿x’轴的单位向量u:
任何旋转矩阵的元素可以表示为一组正交单位向量的元素。因此,将x‘y’系统旋转到与xy系统重合的矩阵可以写为:
例如,如果V = (-1,0),那么旋转变换矩阵为:
在交互应用中,为V选择相对于位置P0的方位,要比相对于xy坐标原点指定其方向更方便。此时v的分量计算为:
二维变换可以通过OpenGL中选择使第三维(z)不改变的z值来实现。要完成一次平移,需引用平移子程序且设定三维平移向量。在旋转函数中,需指定经过坐标系原点的旋转轴的角度和方向。而缩放函数用来设定相对于坐标系原点的三个坐标缩放系数。
必须指出,OpenGL内部使用复合矩阵来支持变换。这导致变换是可以累积的,即如果先指定一次平移再指定一次旋转,那么此后进行位置描述的对象要获得两次变换。如果不希望这样做,必须去除前述变换的效果。
使用子程序glMatrixMode用来设定投影模式,即指定将用于投影变换的矩阵。同样的子程序可用来设定几何变换矩阵。但此时将该矩阵看作建模观察矩阵,它用于存储和组合几何变换,也用于将几何变换与观察坐标系的变换组合。建模观察模式用下列语句指定:
该语句指定一个4x4 建模观察矩阵作为当前矩阵。必须在进行几何变换前使用它来修改建模观察矩阵。
此函数还可以设定另外两个模式:纹理模式和颜色模式。纹理模式用于映射表面的纹理图案,而颜色模式用于从一个颜色模型转换到另一个。
使用下列函数可设定当前矩阵为单位矩阵:
可以将指定的矩阵与当前矩阵合并:
在这个序列中,最先应用的变换是程序中最后指定的。
OpenGL按列有限顺序存储矩阵同样重要。
OpenGL实际上为使用glMatrixMode子程序在四种模式中选择的每一种模式提供一个栈。
基本的几何变换是平移、旋转和缩放。平移将一个对象从一个位置沿直线路径移动到另一位置。旋转将一个对象从一个位置绕指定旋转轴沿圆周路径移动到另一位置。缩放变换改变相对于固定点的对象的尺寸。
可以用3x3的矩阵操作表示二维变换,从而使得一系列变换可合并成一个复合矩阵。
对于一个二维系统,二维坐标系统之间的变换通过一组使两个系统变成一致的平移——旋转变换来实现。
OpenGL基本库包含三个函数用于对坐标位置进行单独的平移、旋转和缩放变换。几何变换必须按照逆向顺序指定。
OpenGL中有若干操作可用来完成光栅变换。
OpenGL几何变换函数和矩阵子程序如下: