概述
OpenGL管线中,在光栅化操作之前,包括顶点位置与法线向量的几何数据经顶点操作与图元装配操作进行变换。
模型坐标
它是模型对象的局部坐标系,同时也是任何变换之前模型对象的初始位置与朝向。为了变换模型对象,可以使用glRotatef()、glTranslatef()、glScalef()。
观察坐标
它由模型坐标乘以GL_MODELVIEW矩阵产生。在OpenGL中,可以使用GL_MODELVIEW矩阵将模型对象从模型空间转换到观察空间。GL_MODELVIEW矩阵由模型矩阵与视图矩阵相结合组成(Mview*Mmodel)。模型变换从模型空间变换到世界空间。视图变换从世界空间变换到观察空间。
注意,OpenGL中并没有独立的摄像机(视图)矩阵。因此,为了模仿摄像机变换或视图变换,场景(3D对象与光照)必须按视图变换的逆方向变换。换句话所,OpenGL定义定义摄像机总是位于观察空间坐标的(0,0,0)点且朝向Z轴负方向,且不能够发生变换。关于GL_MODELVIEW矩阵的更详细信息,请参考视图变换矩阵。
为了计算光照,法线向量也将从模型坐标变换到观察坐标。注意,法线向量的变换与顶点变换不同。它将法线向量与GL_MODELVIEW矩阵的逆矩阵的转置矩阵相乘。更详细信息,请参考法线向量变换。
裁剪坐标
此时,观察坐标乘以GL_PROJECTION矩阵变换为裁剪坐标。GL_PROJECTION矩阵定义视锥体(平头截面),定义顶点数据如何投影到屏幕(透视或正视)。之所以被称为裁剪坐标,是因为变换后的顶点(x,y,z)通过与±w比较进行裁剪。关于GL_PROJECTION矩阵的更详细信息,请参考投影矩阵。
规范化设备坐标(MDC)
它由裁剪坐标除以w产生。这被称为透视除法。它更接近窗口(屏幕)坐标,不过它并没有经过移动与按屏幕像素缩放。此时,值得范围在3个坐标轴上都规范化为-1到1。
窗口坐标(屏幕坐标)
它由将规范化的设备坐标(NDC)进行视口变换产生。NDC被缩放与平移以适应渲染屏幕。窗口坐标最后经过OpenGL管线的光栅化过程变为片段。glViewport()用于定义最终图像映射的渲染区域矩形。glDepthRange()用于决定窗口坐标的z值。窗口坐标由上述两个函数的指定参数计算而来:
glViewport(x,y,w,h);
glDepthRange(n,f);
视口变换公式需要再NDC与窗口坐标之间进行线性关系映射:
OpenGL变换矩阵
OpenGL使用4×4矩阵。注意,矩阵中的16个元素以列优先顺序存储在1维数组中。如果你希望将它转换为标准习惯的行优先格式,你需要进行转置操作。
OpenGL具有4种不同类别的矩阵:GL_MODELVIEW、GL_PROJECTION、GL_TEXTIRE与GL_COLOR。你可以在代码中通过使用glMatrixMode()切换当前类型。例如,为了选择GL_MODELVIRE矩阵,需要使用glMatrixMode(GL_MODELVIEW)。
模型视图矩阵(GL_MODELVIEW)
GL_MODELVIEW矩阵将视图矩阵与模型矩阵组合成一个矩阵。为了变换视图矩阵,需要以逆变换方式移动整个场景。gluLookAt()主要用于设置视图变换。
最右面3列矩阵元素(m12,m13,m14)为平移变换,glTranslatef()。元素m15为其次坐标。它主要用于投影变换。
3元素集合(m0,m1,m2)、(m4,m5,m6)与(m8,m9,m10)是便于欧几里得与仿射变换,如旋转glRotatef()与缩放glScalef()。注意,这3个集合实际表示3个正交坐标轴:
- (m0,m1,m2):+X轴,左向量,默认(1,0,0)
- (m4,m5,m6):+Y轴,上向量,默认(0,1,0)
- (m8,m9,m10):+Z轴,前向量,默认(0,0,1)
我们可以直接通过角度或观察向量构建GL_MODELVIEW矩阵,而不是变换函数。下面是构造GL_MODELVIEW矩阵的有用代码:
注意,当多个变换作用于一个顶点上时,OpenGL以相反顺序执行矩阵乘法。例如,当顶点首先通过MA进行变换,其次通过MB进行变换,OpenGL先执行MB×MA,然后再乘以该顶点。因此,在代码中,后一次变换先出现,第一次变换后出现。
// 注意,模型对象将先平移再旋转
glRotatef(angle, 1, 0, 0); // 绕X轴旋转模型对象angle角度
glTranslatef(x, y, z); // 移动模型对象到(x,y,z
drawObject();
投影矩阵(GL_PROJECTION)
GL_PROJECTION矩阵用于定义视锥体。视锥体决定哪些模型对象或部分模型对象将被裁剪掉。同时,它也决定3D场景如何投影到屏幕。(请阅读更详尽的如何构造投影矩阵。)
OpenGL提供两个GL_PROJECTION变换函数。glFrustum()用于产生一个透视投影,glOrtho()用于产生一个正交(平行)投影。这两个函数都需要6个参数以指定6个裁剪面:左裁剪面、右裁剪面、下裁剪面、上裁剪面、近裁剪面与远裁剪面。视锥体的8个顶点如下列图片。
OpenGL透视椎体
远(后)平面的顶点可以简单滴通过简单三角形比率计算出来,如,左远平面为:
OpenGL正交椎体
对于正交投影,该比率为1,因此远平面的左、右、下与上顶点值与*面上的对应值相同。
你也可以使用更少参数的gluPerspective()与gluOrtho2D()函数。gluPerspective()仅仅需要4个参数:垂直视场(FOV)、宽高纵横比与远近裁剪面的距离。从gluPerspective()到glFrustum()的等价变换描述如下的代码:
// 创建对称截面体
// 从给定4个参数(fovy, aspect, near, far)
// 转换到glFrustum()的六个参数(l, r, b, t, n, f)
void makeFrustum(double fovY, double aspectRatio, double front, double back)
{
const double DEG2RAD = 3.14159265 / 180; double tangent = tan(fovY/2 * DEG2RAD); // 半fovY的正切值
double height = front * tangent; // *面半高度
double width = height * aspectRatio; // *面半宽度 // 参数: left, right, bottom, top, near, far
glFrustum(-width, width, -height, height, front, back);
}
对称视锥体示例
然而,如果你需要建立一个非对称视锥体,你必须直接使用glFrustum()。例如,如果你希望渲染宽场景到2个邻接屏幕上,你可以将截面体分成两个对称截面体(左与右)。然后,使用每一个截面体渲染场景。
纹理矩阵(GL_TEXTURE)
纹理坐标(s,t,r,q)在纹理映射之前乘以GL_TEXTURE矩阵。默认情况下,该纹理矩阵为单位矩阵,因此纹理会完全按照你赋值纹理坐标的方式映射到模型对象。通过改变GL_TEXTURE矩阵,你可以滑动、旋转、伸展与收缩纹理。
// 绕X轴旋转纹理
glMatrixMode(GL_TEXTURE);
glRotatef(angle, 1, 0, 0);
颜色矩阵(GL_COLOR)
颜色分量(r,g,b,a)与GL_COLOR矩阵相乘。这可用于颜色空间转换与颜色分量交换。GL_COLOR矩阵并不常用且需要GL_ARB_imaging扩展。
其他矩阵函数
glPushMatrix():将当前矩阵压入当前矩阵栈
glPopMatrix():将当前矩阵弹出当前矩阵栈
glLoadMatrix():设置当前矩阵为单位矩阵
glLoadMatrix{fd}(m):用矩阵m替换当前矩阵
glLoadTransposeMatrix{fd}(m):用行优先矩阵m替换当前矩阵
glMultMatrix{fd}(m):将当前矩阵乘以矩阵m,并更新当前矩阵值
glMultTransposeMatrix{fd}(m):将当前矩阵乘以行优先矩阵m,并更新当前矩阵值
glGetFloatv(GL_MODELVIEW_MATRIX, m):返回GL_MODELVIEW矩阵的16个值到m
实例:模型视图矩阵
这个例子程序展示如何使用glTranslatef()与glRotatef()操作GL_MODELVIEW矩阵。
下载源程序与二进制程序:matrixModelView.zip
注意,所有OpenGL函数调用在ModelGL.h与ModelGL.cpp中实现。
为了制定模型与相机变换,该实例程序使用自定义的4×4矩阵类与默认OpenGL矩阵函数。在ModelGL.cpp中定义3个矩阵对象:matrixModel、matrixView与matrixModelView。每个矩阵保存先前计算的矩阵,并且使用glLoadMatrix()传递这些矩阵元素。实际绘制程序像这样:
...
glPushMatrix(); // 为相机变换设置视图矩阵
glLoadMatrixf(matrixView.getTranspose()); // 在模型变换之前绘制网格
drawGrid(); // 为模型与视图变换设置模型视图矩阵
// 从模型对象空间变换到观察空间
glLoadMatrixf(matrixModelView.getTranspose()); // 在视图与模型变换之后绘制一个茶壶
drawTeapot(); glPopMatrix();
...
使用模型OpenGL矩阵函数的等价代码为:
...
glPushMatrix(); //初始化模型视图矩阵
glLoadIdentity(); // 首先,变换相机(视图矩阵):从世界空间到观察空间
// 注意所有变量都为负,因为我们将整个场景超相机变换相反方向移动
glRotatef(-cameraAngle[2], 0, 0, 1); // roll
glRotatef(-cameraAngle[1], 0, 1, 0); // heading
glRotatef(-cameraAngle[0], 1, 0, 0); // pitch
glTranslatef(-cameraPosition[0], -cameraPosition[1], -cameraPosition[2]); // 在模型变换之前在原点绘制网格
drawGrid(); // 变换模型(模型矩阵)
// GL_MODELVIEW矩阵的结果为:ModelView_M = View_M * Model_M
glTranslatef(modelPosition[0], modelPosition[1], modelPosition[2]);
glRotatef(modelAngle[0], 1, 0, 0);
glRotatef(modelAngle[1], 0, 1, 0);
glRotatef(modelAngle[2], 0, 0, 1); // 使用模型与视图变换绘制一个茶壶
drawTeapot(); glPopMatrix();
...
实例:投影矩阵
这个例子程序展示如何使用glFrustum()与glOrtho()操作投影变换。
下载源程序与二进制程序:matrixProjection.zip
同样,ModelGL.h与ModelGL.cpp是OpenGL函数调用放置的地方。
ModelGL类使用自定义矩阵对象,matrixProjection与2个成员函数:setFrustum()与setOrthoFrustum(),这两个函数与glFrustum()与glOrtho()等价。
///////////////////////////////////////////////////////////////////////////
// 使用与glFrustum()相同的6个参数(left,right,bottom,top,near,far)设置透视截面体// 注意: 此处为行优先标记法。OpenGL需要对它进行转置
///////////////////////////////////////////////////////////////////////////
void ModelGL::setFrustum(float l, float r, float b, float t, float n, float f)
{
matrixProjection.identity();
matrixProjection[0] = 2 * n / (r - l);
matrixProjection[2] = (r + l) / (r - l);
matrixProjection[5] = 2 * n / (t - b);
matrixProjection[6] = (t + b) / (t - b);
matrixProjection[10] = -(f + n) / (f - n);
matrixProjection[11] = -(2 * f * n) / (f - n);
matrixProjection[14] = -1;
matrixProjection[15] = 0;
} //////////////////////////////////////////////////////////////////////////////
// 使用与glOrtho()相类似的6个参数(left, right, bottom, top, near, far)设置正交投影
// 注意: 此处为行优先标记法。OpenGL需要对它进行转置
//////////////////////////////////////////////////////////////////////////////
void ModelGL::setOrthoFrustum(float l, float r, float b, float t, float n,
float f)
{
matrixProjection.identity();
matrixProjection[0] = 2 / (r - l);
matrixProjection[3] = -(r + l) / (r - l);
matrixProjection[5] = 2 / (t - b);
matrixProjection[7] = -(t + b) / (t - b);
matrixProjection[10] = -2 / (f - n);
matrixProjection[11] = -(f + n) / (f - n);
}
... // pass projection matrx to OpenGL before draw
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(matrixProjection.getTranspose());
...