第7章 二维几何变换
应用于对象几何描述并改变它的位置、方向或大小的操作称为几何变换(geometric transformation)。
几何变换有时也称为建模变换(modeling transformation),但有些图形系统将两者区分开来。建模变换一般用于构造场景或给出由多个部分组合而成的复杂对象的层次式描述等。
基本的二维几何变换
平移、旋转和缩放是所有图形软件包中都包含的几何变换函数。可能包括在图形软件包中的其他变换函数有反射和错切操作。
二维平移
通过将位移量加到一个点的坐标上来生成一个新的坐标位置,可以实现一次平移(translation)。实际上,我们将该点从原始位置沿一直线路径移动到新位置。
将平移距离(translation distance)和加到原始坐标上获得一个新的坐标位置,可以实现一个二维位置的平移。
一对平移距离称为平移向量(translation vector)或位移向量(shift vector)。
如果用矩阵表示
这样就可以使用矩阵形式来表示二维平移方程:
下面的程序演示了平移操作。输入的平移向量用来将一个多边形的n个顶点从世界坐标系的一个位置移动到另一个位置,而OpenGL子程序用来重新生成平移后的多边形。
class wcPt2D(
public: GLfloat x,y;
};
void translatePolygon (wcPt2D"verts, GLint nVerts, GLfloat tx, GLfloat ty){
GLint k;
for (k=0;k<nVerts:k++){
verts [k].x = verts [k].x + tx;
verts [k].y =verts [k].y + ty;
}
g1Begin (GL_POLYGON);
for (k=0;k< nVerts;k++)
g1Vertex2f (verts [k].x, verts [k].y);
g1End();
}
二维旋转
通过指定一个旋转轴(rotation axis)和一个旋转角度(rotation angle),可以进行一次旋转(rotation)变换。在将对象的所有顶点按指定角度绕指定旋转轴旋转后,该对象的所有点都旋转到新位置。
对象的二维旋转通过在平面上沿圆路径将对象重定位来实现。此时,我们将对象绕与平面垂直的旋转轴(与轴平行)旋转。二维旋转的参数有旋转角和称为旋转点(rotation point 或pivot point)的位置,对象绕该点旋转。基准点是旋转轴与平面的交点。正角度定义绕基准点的逆时针旋转,而负角度将对象沿顺时针方向旋转。
为了简化模型,我们先假设基准点为原点。
因此旋转后用角度和表示为
在极坐标系中,点的原始坐标为
所以可以得到:
如果用矩阵的形式表示:
其中,旋转矩阵为:
但现在OpenGL、Java、PHIGS和GKS都按标准列向量方式表示。
任意的旋转位置旋转点的变换方程:
线段的旋转可以通过用于每个线段端点,并重新绘制新端点间的线段而得到。多边形的旋转则是将每个顶点旋转指定的旋转角,并使用新的顶点来生成多边形而实现旋转。曲线的旋转通过重新定位定义的点并重新绘制曲线而完成。例如圆或椭圆,可以通过将中心位置沿指定旋转角对着的弧移动而绕非中心轴旋转。椭圆可通过旋转其长轴和短轴来实现绕其中心位置的旋转。
class wcPt2D {}
public:
GLfloat x,y;
};
void rotatePolygon (wcPt2D* verts, GLint nVerts, wePt2D pivPt, GLdouble theta)
{
wcPt2D* vertsRot;
GLint k;
for (k=0;k< nVerta:k++){
vertsRot [k].x=pivPt.x+(verts [k].x-pivPt.x) * cos (theta)-(verts [k].y-pivpt.y)* sin (theta);
vertsRot [k].y=pivPt.y+(verts [k],x-pivPt.x )* sin (theta)+(verts [k].y-pivPt.y)* cos (theta):
]
g1Begin { GL_POLYGON}:
for(k=0;k<nVerts;k++)
g1Vertex2f (verteRot [k].x, verteRot [k].y):
g1End();
二维缩放
改变一个对象的大小,可使用缩放(scaling)变换。一个简单的二维缩放操作可通过将缩放系数(scaling factor)和,与对象坐标位置相乘而得:
矩阵形式如下
或者是
当和相同的值的时候,就会产生保持对象相对比例一致的一致缩放(uniform scaling)。当两个值不相等的时候就是差值缩放(differential scaling)。
当缩放系数的绝对值小于1时,缩放后的对象向原点靠近;而缩放系数绝对值大于1时,缩放后的坐标位置远离原点。
我们可以选择一个在缩放变换后不改变位置的点,称为固定点(fixed point),以控制缩放后对象的位置。固定点的坐标可以选择对象的中点等位置或任何其他空间位置。这样,多边形通过缩放每个顶点到固定点的距离而相对于固定点进行缩放。对于坐标为的顶点,缩放后的坐标可计算为
可以得到
其中和都是常数。
class wePt2D {
public:
GLfloat x,y;
};
void scalePolygon (wcPt2D· verts, GLint nVerts, wcpt2D fixedPt, GLfloat sx, GLfloat sy)
{
wcPt2D vertsNew;
GLint k;
for (k-0;k<nVerts;k++){
vertsNew[k].x=verts [k].x* sx+ fixedPt.x*(1-sx);
vertaNew[k].y=verts [k].y*sy+fixedPt.y*(1: sy):
}
g1Begin (GL POLYGON];
for (k-0;k<nVerte;k++)
g1Vertex2f (vertsNew [k].x, vertsNew [k].y);
g1End():
矩阵表示和齐次坐标
每个基本变换(平移、旋转和缩放)都可以表示为普通矩阵形式
和都是坐标的列向量。矩阵是一个包含乘法系数的的矩阵,是包含平移项的两元素列矩阵。
对于平移,是单位矩阵。对于旋转或缩放,包含与基准点或缩放固定点相关的平移项。
齐次坐标
如果将矩阵表达式扩充为矩阵,就可以把二维儿何变换的乘法和平移组合成单一矩阵表示。这时将变换矩阵的第三列用于平移项,而所有的变换公式可表达为矩阵乘法。但为了这样操作,必须解释二维坐标位置到三元列向量的矩阵表示。标准的实现技术是将二维坐标位置表示扩充到三维表示,称为齐次坐标(homogeneous coordinate),这里的齐次参数(homogeneous parameter)h是一个非零值,因此
普通的二维齐次坐标表示可写为。最简单的。因此每个二维位置都可以用齐次坐标来表示来表示。
二维平移矩阵
使用齐次坐标方法,坐标位置的二维平移可表示为下面的矩阵乘法:
该平移操作可简写为
二维旋转矩阵
二维的旋转变换公式为
其中旋转变换操作的旋转参数
二维缩矩阵
相对于坐标原点的缩放变换可以表示为
逆变换
对于平移变换,我们通过对平移距离取负值而得到逆矩阵。因此,如果二维平移距离是和,则其逆平移矩阵是
逆旋转通过用旋转角度的负角取代该旋转角来实现。例如,绕坐标系原点的角度为0的二维旋转有如下的逆变换矩阵
将缩放系数用其倒数取代就得到了缩放变换的逆矩阵。对以坐标系原点为中心、缩放参数为和的二维缩放,其逆变换矩阵为
二维复合变换
利用矩阵表达式,可以通过计算单个变换的矩阵乘积,将任意的变换序列组成复合变换矩阵(composite transformation matrix)。形成变换矩阵的乘积经常称为矩阵的合并(concatenation) 或复合(composition)。由于一个坐标位置用齐次列矩阵表示,我们必须用表达任意变换顺序的矩阵来前乘该列矩阵。由于场景中许多位置用相同的顺序变换,先将所有变换矩阵相乘形成一个复合矩阵将是高效率的方法。因此,如果我们要对点位置P进行两次变换,变换后的位置将用下式计算:
复合二维平移
假如将两个连续的平移向量和用于坐标位置P,那么最后的变换位置可以计算为
这表示两个连续平移是相加的。
复合二维旋转
并且
复合二维缩放
或者是
通用二维基准点旋转
当图形软件包仅提供绕坐标系原点的旋转函数时,我们可通过完成下列平移-旋转-平移操作序列来实现绕任意选定的基准点的旋转。
- 平移对象使基准点位置移动到坐标原点;
- 绕坐标原点旋转;
- 平移对象使基准点回到其原始位置。
该等式可以使用下列形式表示:
其中。
通用二维基准点缩放
在只有相对于坐标原点缩放的缩放函数时,缩放的变换序列:
- 平移对象使固定点与坐标原点重合;
- 对于坐标原点进行缩放;
- 使用步骤1的反向平移将对象返回到原始位置。
即
通用二维定向缩放
参数和沿x和y方向缩放对象,可以通过在应用缩放变换之前,将对象所希望的缩放方向旋转到与坐标轴一致来实现在其他方向上缩放对象。
首先完成旋转操作,使和的方向分别与x和y轴重合。然后应用缩放变换,再进行反向旋转回到其原始位置。从这三个变换的乘积得到的复合矩阵为
矩阵合并特性
矩阵相乘符合结合律,对于任何三个矩阵,和。矩阵积可以写成
因此,依靠变换的描述顺序,我们既可以使用从左到右(前乘),也可以使用从右到左(后乘)的结合分组来求矩阵乘积。有些图形软件包要求变换按应用的顺序描述。
二维刚体变换
如果一个变换矩阵仅包含平移和旋转参数,则它是一个刚体变换矩阵(rigid-body transformation matrix)。二维刚体变换矩阵的一般形式为
其中,4个元素是多重旋转项,元素和是平移项。坐标位置的刚体变换有时也称为刚体运动(rigid motion)。变换后的坐标位置间的所有角度和距离都不变化。
因此上述左上角的矩阵是一个正交矩阵(orthogonal matrix)的特性。说明,如果将子矩阵的每一行(或每一列)作为向量,那么两个行向量和形成单位向量的正交组。这样的一组向量也称为正交向量组。每个向量具有单位长度。
并且向量相互垂直
二维复合矩阵编程
其他二维变换
反射
产生对象镜像的变换称为反射(reflection)。对于二维反射而言,其反射镜像通过将对象绕反射轴旋转而生成。我们选择的反射轴(axis of reflection)可以是在平面内的一条直线或者是垂直平面的一条直线。当反射轴是平面内的一条直线时,绕这个轴的旋转路径在垂直于平面的平面中;而对于垂直于平面的反射轴,旋转路径在平面内。下面举出一些普通的反射例子。
关于直线(轴)的反射,可以由下列的变换矩阵完成。
对于(y轴)的反射,翻动的坐标而保持坐标不变,这种变换的矩阵是
关于的平面内任意直线
通常,我们先平移直线使其经过原点。然后将直线旋转到坐标轴之一,并进行相对于坐标轴的反射。最后利用逆旋转和逆平移变换将直线还原到原来位置。
错切
错切(shear) 是一种使对象形状发生变化的变换,经过错切的对象好像是由已经相互滑动的内部夹层组成。两种常用的错切变换是移动坐标值的错切和移动坐标值的错切。
相对于轴的方向错切由下列变换矩阵产生:
该矩阵将坐标位置转换成
可以将任意实数赋给错切参数.。然后将坐标位置水平地移动与其到轴的距离(值)成正比的量。
错切操作可以表示为基本变换的序列。
几何变换的光栅方法
光栅系统的特殊功能为特定的二维变换提供了另一种方法。光栅系统将图像信息作为颜色图案存储在帧缓冲器中。控制矩形像素数组的光栅功能通常称为光栅操作(raster operation),将一块像素从一个位置移动到另一个位置的过程也称为像素值的块移动(block transfer,bitblt或pixblt)。图形软件中通常包含完成某些光栅操作的子程序。
90°倍数的旋转可以很容易地利用重新安排像素矩阵的元素而实现。通过首先将阵列的每一行的像素值颠倒,然后交换其行和列来将对象逆时针旋转90°;通过颠倒阵列的每一行中元素的顺序,然后将行的顺序颠倒来得到180°的旋转。
像素块的光栅缩放采用类似方法实现。我们用指定的和值对原始块中的像素区域进行缩放,并将缩放的矩形映射到一组目标像素上,然后按照其与缩放像素区域的重叠区域,设置每个目标像素的亮度。
OpenGL光栅变换
像素颜色值的矩形数组从一个缓存到另一个的平移可以作为如下的OpenGL复制操作来完成:
g1CopyPixe1s (xmin, ymin, width, height. GL_COLOR):
前面4个参数给出了像素块的位置和尺寸。而OpenGL符号常量GL_COLOR指定要复制的颜色值。该像素数组复制到刷新缓存中由当前光栅位置指定的左下角的一个矩形区域内。像素颜色值依赖于颜色模式的当前设定,按RGBA或颜色表进行复制。提供复制的区域(源)和复制目标区域均应位于屏幕坐标边界内。该平移可作用于任何刷新缓存或不同缓存之间。g1CopyPixels函数的源缓存用g1ReadBuffer 子程序选择,而目标缓存用g1DrawBuffer子程序选择。
缓存中的一个RGB颜色块可以用下列函数存入一个数组:
g1ReadPixels (xmin, ymin, width. height, GL_RGB.GL_UNSIGNED_BYTE, colorArray):
如果颜色表索引存于像素位置,则将GL_COLOR_INDEX取代GL_RGB。为了旋转颜色值,必须如前一节所述重新安排颜色数组的行与列。然后使用下列语句将旋转后的数组放回缓存:
g1DrawPixe1s (width,height,GL_RGB,GL_UNSIGNED_BYTE, colorArray);
该数组的左下角放到当前光栅位置。我们用g1ReadBuffer选择包含原来的像素值块的源缓存,用g1DrawBuffer指定目标缓存。
二维缩放变换通过指定缩放因子然后引用g1Copypixe1s或g1Drawpixe1s按OpenGL中的光栅操作来完成。对于光栅操作,使用下列函数来设定缩放因子:
g1Pixe1zoom(sx,sy):
这里,参数sx和sy可赋以任何非零浮点值。大于1.0的正值增太源数组元素的尺对,而小于1.0的正值减少元素尺寸。sx或sy中有负值或两个都为负值则生成该数组元素的反射及缩放。因此,如果sx=sy=-3.0,则源数组相对于当前光栅位置反射且数组的每一颜色元素映射到目标缓存中的3×3像素块。如果目标像素的中心位于一数组缩放颜色元素的矩形区域,则用该数组元素给它赋值。中心在缩放数组元素左边界或上边界的目标像素也赋以该元素的颜色。sx和sy的默认值均为1.0。
二维坐标系间的变换
非笛卡儿系统的例子有极坐标系统、球面坐标系统、椭圆坐标系统和抛物线坐标系统。
给出了一个在笛卡儿坐标系中用坐标原点及方向角指定的笛卡儿坐标系。为了将对象描述从坐标变换到坐标,必须建立把轴叠加到轴的变换,这需要分两步进行:
- 将系统的坐标原点平移到系统的原点
- 将轴旋转到轴上
坐标原点的平移可以使用下列矩阵操作表示:
为了将两个系统的轴重合,可以顺时针旋转:
把这两个变换矩阵合并起来,就给出了将对象描述从系统转换到系统的完整复合矩阵。
OpenGL二维几何变换函数
在OpenGL的核心库中,每一种基本的几何变换都有一个独立的函数。由于OpenGL是作为三维图形应用编程接口(APl)来设计的,所有变换都在三维空间中定义。在内部,所有坐标均使用4元素列向量表示,而所有变换均使用4×4矩阵表示。因此,二维变换可以通过在OpenGL中选择使第三维(z)不改变的z值来实现。
基本的OpenGL几何变换
平移矩阵用下列子程序构造
g1Translate*(tx,ty,tz)
平移参数tx、ty和tz可赋予任意的实数值,附加于该函数的单个后缀码或者是f(浮点)或者是d。
旋转矩阵下列函数生成:
glRotate*(theta,vx,vy,vz)
向量的分量可以有任意的浮点数值。该向量用于定义通过坐标原点的旋转轴的方向。
用下列函数可得到相对于坐标原点的4×4缩放矩阵:
glScale*(sx, sy, sz)
OpenGL矩阵操作
将该矩阵看做建模观察矩阵(modelview matrix),它用于存储和组合几何变换,也用于将几何变换与向观察坐标系的变换进行组合。建模观察模式用下列语句指定:
g1MatrixMode (GL_MODELVIEW)
该语句指定一个4×4建模观察矩阵作为当前矩阵(current matrix)。
在这个调用后的OpenGL变换子程序用来修改建模观察矩阵,而后该矩阵用来变换场景中的坐标位置。用g1MatrixMode函数还可以设定另外两个模式:纹理模式(texture mode)和颜色模式(color mode)。纹理模式用于映射表面的纹理图案,而颜色模式用于从一个颜色模型转换到另一个。后面几章将讨论观察、投影、纹理和颜色变换。
建立建模观察模式(或任何其他模式)后,调用变换子程序所生成的矩阵要与该模式的当前矩阵相乘。另外,我们可以对当前矩阵的元素赋值,OpenGL库中有两个函数可用于此目的。使用下列函数可设定当前矩阵为单位矩阵:
glLoadIdentity():
也可以为当前矩阵的元素赋其他值:
g1LoadMatrix*(elements16):
参数elements16指定了一个单下标、16元素的浮点值数组,而后缀f或d用来指定数据类型。该数组的元素必须按列优先顺序指定。即先列出第一列的4个元数,接着列出第二列的4个元素,然后是第三列,而最后是第四列。
也可以将指定的矩阵与当前矩阵合并:
glMultMatrix*(otherElements16):
实例
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import math
winWidth, winHeight = 600, 600
xwcMin, xwcMax = 0.0, 300.0
ywcMin, ywcMax = 0.0, 300.0
pi = 3.14159
matComposite = [[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0]]
def init():
glClearColor(1.0, 1.0, 1.0, 0.0)
def get3x3Matfloat():
temp = [[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0]]
return temp
# 矩阵初始化
def matrix3x3SetIndentity(matIdent3x3):
for row in range(3):
for col in range(3):
matIdent3x3[row][col] = (row == col)
# 显示三角形
def triangle(verts):
glBegin(GL_TRIANGLES)
for k in range(3):
glVertex2f(verts[k][0], verts[k][1])
glEnd()
def matrix3x3PreMultiply(m1, m2):
mattemp = get3x3Matfloat()
for row in range(3):
for col in range(3):
mattemp[row][col] = m1[row][0] * m2[0][col] + m1[row][1] * m2[1][col] + m1[row][2] * m2[2][col]
for row in range(3):
for col in range(3):
m2[row][col] = mattemp[row][col]
def rotate2D(pivotPt, theta):
global matComposite
matRot = get3x3Matfloat()
matrix3x3SetIndentity(matRot)
matRot[0][0] = math.cos(theta)
matRot[0][1] = -math.sin(theta)
matRot[0][2] = pivotPt[0] * (1-math.cos(theta)) + pivotPt[1] * math.sin(theta)
matRot[1][0] = math.sin(theta)
matRot[1][1] = math.cos(theta)
matRot[1][2] = pivotPt[0] * (1-math.cos(theta)) - pivotPt[1]*math.sin(theta)
matrix3x3PreMultiply(matRot, matComposite)
def scale2D(sx, sy, fixedPt):
global matComposite
matScale = get3x3Matfloat()
matrix3x3SetIndentity(matScale)
matScale[0][0] = sx
matScale[0][2] = (1-sx)*fixedPt[0]
matScale[1][1] = sy
matScale[1][2] = (1-sy) * fixedPt[1]
matrix3x3PreMultiply(matScale, matComposite)
def translate2D(tx, ty):
global matComposite
matTrans1 = get3x3Matfloat()
matrix3x3SetIndentity(matTrans1)
matTrans1[0][2] = tx
matTrans1[1][2] = ty
matrix3x3PreMultiply(matTrans1, matComposite)
def transformVerts2D(nVerts, verts):
global matComposite
for k in range(nVerts):
temp = matComposite[0][0] * verts[k][0] + matComposite[0][1] * verts[k][1] + matComposite[0][2]
verts[k][1] = matComposite[1][0] * verts[k][0] + matComposite[1][1] * verts[k][1] + matComposite[1][2]
verts[k][0] = temp
def dispalyFcn():
global pi,matComposite
nVerts = 3
verts = [[50.0, 25.0],[150.0, 25.0],[100.0, 100.0]]
xsum , ysum = 0,0
for k in range(nVerts):
xsum += verts[k][0]
ysum += verts[k][1]
centroidPt = [0,0]
centroidPt[0] = xsum/nVerts
centroidPt[1] = ysum/nVerts
pivPt = centroidPt[:]
fixedPt = centroidPt[:]
tx = 0.0
ty = 100.0
sx = 0.5
sy = 0.5
theta = pi/2.0
glClear(GL_COLOR_BUFFER_BIT)
glColor3f(0.0, 0.0, 1.0)
triangle(verts)
matrix3x3SetIndentity(matComposite)
scale2D(sx, sy, fixedPt)
rotate2D(pivPt, theta)
translate2D(tx, ty)
transformVerts2D(nVerts, verts)
glColor3f(1.0, 0.0, 0.0)
triangle(verts)
glFlush()
def winReshapeFcn(newWidth, newHeight):
global xwcMax, xwcMin, ywcMax, ywcMin
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(xwcMin, xwcMax, ywcMin, ywcMax)
glClear(GL_COLOR_BUFFER_BIT)
if __name__ == '__main__':
glutInit()
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB)
glutInitWindowPosition(50, 50)
glutInitWindowSize(winWidth, winHeight)
glutCreateWindow("几何变换".encode('gbk'))
init()
glutDisplayFunc(dispalyFcn)
glutReshapeFunc(winReshapeFcn)
glutMainLoop()