作者:charlee
按:我也是在迷茫中走过来的,初学OpenGL时,略微了解了一些有关变换的基本知识,但是却不知道具体的使用方法,因此经常需要在布置场景时反复调整各种参数。当我终于有一天明白了它们的用法时,就觉得应该把这些心得体会写下来,让那些和我一样曾经迷茫过的人能够迅速地找到出路。本文的读者对象为那些初学OpenGL,了解了一些坐标系、几何变换等基本知识,但是又不知道具体应该如何运用这些操作的人。如果你对OpenGL一无所知,建议你先去学学OpenGL的基本知识。
OpenGL中使用的坐标系有两种,分别为世界坐标系和屏幕坐标系。世界坐标系即OpenGL内部处理时使用的三维坐标系,而屏幕坐标系即为在计算机屏幕上绘图时使用的坐标系。
通常,OpenGL所使用的世界坐标系为右手型,如下图所示。
将世界坐标系中的物体映射到屏幕坐标系上的方法称为投影。投影的方式包括平行投影和透视投影两种。
平行投影的投影线相互平行,投影的结果与原物体的大小相等,因此广泛地应用于工程制图等方面。
透视投影的投影线相交于一点,因此投影的结果与原物体的实际大小并不一致,而是会近大远小。因此透视投影更接近于真实世界的投影方式。
OpenGL中使用下面的函数来设置投影方式为平行投影。
glOrtho(xleft, xright, ybottom, ytop, znear, zfar);
各参数的含义如下图所示。
注意,只有位于立方体之内的物体才可见。
OpenGL中使用下面的函数来设置投影方式为透视投影。
gluPerspective(fovy, aspect, znear, zfar);
各参数的含义如下图所示。
fovy为四棱台的顶角,aspect为投影面的纵横比,znear和zfar为四棱台的顶面和底面到视点的距离(注意不是z坐标)。
注意,只有位于四棱台之内的物体才可见。
OpenGL中可以使用的几何变换有平移、旋转、缩放三种。
glTranslatef(x, y, z);
该函数可以实现平移变换,x、y、z为各坐标轴上的平移量。
glRotatef(θ, x, y, z);
该函数实现旋转变换。θ为旋转角度,x、y、z为旋转轴。旋转方向由右手法则决定(参见第一节“坐标系”)。
glScalef(x, y, z);
该函数实现缩放变换。x、y、z为各轴方向的扩大量。若为负值,则沿着坐标轴的反方向进行缩放。
实际上,几何变换并不是针对坐标系中的某个物体进行变换,而是对整个坐标系进行变换。进行绘图时,世界坐标系上的点将以如下的方式被投影到屏幕坐标系上:
其中(x y z 1)T为该点在世界坐标系中的坐标,(x' y' z' 1)T为该点在屏幕坐标系上的投影的坐标。P为投影变换矩阵,An为几何变换矩阵。在处理变换和投影时,OpenGL先把几何变换矩阵A1、A2、…、An从左侧依次与点坐标矩阵相乘,最后再将投影矩阵从左侧与经过几何变换之后的点坐标相乘,即得到该点的屏幕坐标。也就是说,在OpenGL中进行几何变换的方式为,首先通过glTranslatef、glRotatef、glScalef等函数设置好几何变换矩阵(相当于对坐标系进行了变换),然后再进行绘图,那么图形的投影坐标将受到设置好的几何变换矩阵所影响而显现出几何变换的效果;而并不是首先进行绘图然后再通过几何变换函数对已经存在的图形进行变换。
变换的一般形式如下式所示:
常见的变换矩阵如下。
(1)平移变换
(2)旋转变换
沿x轴旋转
沿y轴旋转
沿z轴旋转
(3)缩放变换
(4)平行投影(投影到xy平面的情况)
(5)透视投影(投影到xy平面的情况。投影中心为z轴上的点(0, 0, R))
在实际编程中,为了保存几何变换和投影变换的操作,OpenGL维护两个栈,即投影变换栈和几何变换栈。栈中保存的元素即为投影变换和几何变换的变换矩阵。使用下面的函数可以在两个栈之间进行切换:
切换当前操作的栈为投影变换栈:glMatrixMode(GL_PROJECTION);
切换当前操作的栈为几何变换栈:glMatrixMode(GL_MODELVIEW);
此外,下面的函数可以清除当前操作的栈中的内容,并向栈中压入一个单位矩阵:
glLoadIdentity();
两个栈之间的关系如下图所示:
一般情况下,我们在进行OpenGL初始化时都要执行下面的命令:
glMatrixMode(GL_PROJECTION); // 切换到投影变换栈
glLoadIdentity(); // 初始化投影变换栈
gluPrespective(30.0, aspect, 1.0, 50.0); // 压入透视投影矩阵
glMatrixMode(GL_MODELVIEW); // 切换到几何变换栈
另外,在调用glMatrixMode(GL_MODELVIEW)时,系统会自动将几何变换栈清空并压入单位矩阵,因此不必再调用glLoadIdentity()函数。
对于几何变换栈,还有以下两个常用的操作:
glPushMatrix(); // 保存当前坐标系
glPopMatrix(); // 恢复当前坐标系
在调用几何变换操作时,OpenGL将该几何变换操作的变换矩阵与当前栈的栈顶元素相乘,得到一个新的矩阵并将其作为几何变换栈的栈顶。
作为例子,我们来看下面的这段程序。
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30.0, aspect, 1.0, 50.0);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glTranslatef(0.0, 0.0, -20.0);
glPushMatrix();
glTranslatef(0.0, 1.0, 0.0);
myWireCylinder(1.0, 2.0, 12);
glTranslatef(0.0, 1.0, 0.0);
glRotatef(-90.0, 1.0, 0.0, 0.0);
glutWireCone(1.0, 2.0, 12, 3);
glPopMatrix();
glTranslatef(0.0, -1.0, 0.0);
myWireCylinder(1.0, 2.0, 12);
glPopMatrix();
下面我们来分析一下该程序执行过程中两个栈的变化情况。图中左侧的栈为透视变换栈,右侧的栈为几何变换栈。黄色表示当前操作栈。I表示单位矩阵,P表示投影变换矩阵,T为平移变换矩阵,R为旋转变换矩阵,S为缩放变换矩阵。
glMatrixMode(GL_PROJECTION); glLoadIdentity(); |
|
gluPerspective(30.0, aspect, 1.0, 50.0);
|
|
glMatrixMode(GL_MODELVIEW); |
|
glPushMatrix(); |
|
glTranslatef(0.0, 0.0, -20.0); |
|
glPushMatrix(); |
|
glTranslatef(0.0, 1.0, 0.0); myWireCylinder(1.0, 2.0, 12); |
|
glTranslatef(0.0, 1.0, 0.0); |
|
glRotatef(-90.0, 1.0, 0.0, 0.0); glutWireCone(1.0, 2.0, 12, 3); |
|
glPopMatrix(); |
|
glTranslatef(0.0, -1.0, 0.0); myWireCylinder(1.0, 2.0, 12); |
|
glPopMatrix(); |
|
列式存储:
具体到OpenGL的实现,OpenGL和数学中相同采用右手系,OpenGL把模型变换和视图变换合二为一,即模型视图矩阵。OpenGL和GLM的变换矩阵都是按照列优先存储在内存中,这和C++二维数组不同,其实,GLM中的4x4矩阵是由4个列向量组成的。按照上面的分析,当OpenGL的模型视图矩阵和投影矩阵均为单位阵时, 这时摄像机位于世界坐标系原点看向z负方向,向右方向沿x轴正方向,向上方向沿y正方向,由于投影矩阵为单位阵,这时为正交投影(另一种是透视投影),裁 剪面为xyz的±1,也就是说,对应到最后的显示窗口,x方向向右,y方向向上,z方向垂直屏幕向外,窗口中心对应坐标原点,窗口边缘对应±1,并且z值 小的片断遮挡z值大的片断(正好和离摄像机的远近关系反了,这是因为没有对z坐标进行变号)。对了,OpenGL除了模型视图矩阵和投影矩阵之外,还有纹理坐标变换矩阵和颜色变换矩阵。请见OpenGL官方手册文献[8]2.12和2.16。
实例代码:
#include <GL/glut.h> #include <stdlib.h> void init(void) { glClearColor (0.0, 0.0, 0.0, 0.0); glShadeModel (GL_FLAT); } void display(void) { glClear (GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0); //OGL中都是和当前矩阵相乘作为变换矩阵,所以这里要清理下; //虽然模型视图矩阵栈中调用gluLookAt会初始化单位矩阵但是屏幕大小改变时候,还是会出现错乱所以视图变换前glLoadIdentity是好习惯。 //glLoadMatrix指定的矩阵,glMultMatrix是乘以当前矩阵作为结果矩阵; //glLoadTransposeMatrix,glMultTransposeMatrix是行主序矩阵类似D3D,转置矩阵刚好把列主序矩阵变换为行主序矩阵。 glLoadIdentity (); /* clear the matrix */ /* viewing transformation */ gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); //glTranslatef( .0, .0f, -5.0f); // 变换坐标系等于相反的方向变换物体,故这里模型和视图变换放到一起 // glTranslate glRotate glScale生成恰当的矩阵,且调用了glMulteMatrix作为结果; // 这些变换矩阵放置到display list中比每帧计算更快 glRotatef(45, 0, 0, 1); // 绕原点到指定点的直线(指定轴),逆时针旋转45度 glPushMatrix(); // 若当前物体点用局部坐标系统刻画那么在局部坐标系中缩放 // 使用缩放会降低光照计算效率,因为要重新计算物体表面的法向量; 一般不缩放为0而是使用投影矩阵而不是模型视图矩阵(例如平面阴影) glScalef (1.0, 2.0, 1.0); /* modeling transformation */ // OGL的矩阵乘法,默认是列主序矩阵,M[i][j]是i列j行,矩阵乘法是左乘当前矩阵或向量 glutWireTeapot(1); glPopMatrix(); glTranslatef(-2.0, -2.0f, 0); glScalef(-1.0, 2.0, 1.0); glutWireTeapot(1); glFlush (); } void reshape (int w, int h) { // 视口变换,将[-1,-1,-1]->[1,1,1]空间变换到OGL左下角[0,0]右上角[1,1]中 glViewport (0, 0, (GLsizei) w, (GLsizei) h); // 透视投影变换到裁剪4D空间(裁剪掉视景体外的),变换到视口变换前硬件会进行透视除法/w,建筑师需要实际大小用gluOrtho2D glMatrixMode (GL_PROJECTION); glLoadIdentity (); glFrustum (-1.0, 1.0, -1.0, 1.0, 1.5, 20.0); //gluPerspective(90.0, w / h, 1, 1000.0); // 切换到模型视图变换 glMatrixMode (GL_MODELVIEW); } void keyboard(unsigned char key, int x, int y) { switch (key) { case 27: exit(0); break; } } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB); glutInitWindowSize (500, 500); glutInitWindowPosition (100, 100); glutCreateWindow (argv[0]); init (); glutDisplayFunc(display); glutReshapeFunc(reshape); glutKeyboardFunc(keyboard); glutMainLoop(); return 0; }
OpenGLによる3次元CGプログラミング,林武文、加藤清敬著,コロナ社