这一章节可能会比较枯燥,当然示例代码将会非常有趣。枯燥的是将会有很多线性代数的知识夹杂其中。在本章中我们将对3D图形进行绘制。我们会介绍 顶点变换的过程,以及法线向量与绘制顺序。
这里将首先介绍一下一个三维空间上的顶点被映射到二维屏幕坐标的步骤,请各位务必牢记:
物体顶点坐标(顶点)=》模型视图变换 -> 视觉坐标=》投影变换 -> 裁剪坐标=》透视划分 -> 规格化设备坐标=》视口变换 -> 窗口坐标
上图中,=》表示经过某种变换;-> 表示得到某个坐标。
下面我们将根据这个顺序来依次介绍各个变 换。这里将主要介绍模型视图变换和投影变换。
先介绍OpenGL中物体顶点坐标的表示。
OpenGL中,物体的顶点坐标是由一个四维的行向量来表示的,表示为:(x, y, z, w)。其中,x, y, z分别对应x轴、y轴和z轴,而w用于辅助做线性变换。我们会理所当然地认为,三维坐标系只要三个分量就够了,w到底有何用呢?
我们这里首先要说 明一下,在OpenGL中,线性变换矩阵的变换都是用矩阵的乘法,没有加减法,因此所有线性变换都是通过矩阵的乘法来完成的。所以,比如说我们要把一个顶 点:(1, 1, 1)沿x轴向右平移5个单位,那要用三维变换矩阵表示的话就要是:M = [6, 0, 0; 0, 1, 0; 0, 0, 1]。那么v\' = v * M = (6, 1, 1)。
但很多场合下使用三维空间做变换将会使计算变得非常复杂,甚至很难进行变换,因此引入 四维空间使得变换更加灵活。
比如,顶点v = (1, 1, 1, 1)沿x轴向右平移5个单位,那么变换矩阵M = [1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0; 5, 0, 0, 1]。
我们将看到这个4x4的变换矩阵与3x3的相比就显得简单很多。因 为对于平移来说,本身就是加法,而三维空间中就需要使用乘法,从而加大了计算负荷。
在OpenGL中,物体的顶点坐标在进行计算时总是以行向量的 形式表示,并且计算时总是在最左边。
下面将正式介绍视图各种变换。
先贴代码:
- - (void)prepareOpenGL
- {
- glShadeModel(GL_SMOOTH);
- glClearColor(0.0, 0.0, 0.0, 0.0);
- glViewport(0, 0, 320, 320);
- glMatrixMode(GL_PROJECTION);
- glLoadIdentity();
- glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, 5.0);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glTranslatef(0.3f, 0.2f, 0.0f);
- glRotatef(-30.0f, 0.0f, 0.0f, 1.0f);
- glFrontFace(GL_CCW);
- glEnable(GL_CULL_FACE | GL_DEPTH_TEST);
- }
- - (void)drawRect:(NSRect)dirtyRect {
- static struct
- {
- GLubyte colours[4];
- GLfloat vertices[3];
- }vertexInfoList[] = {
- { {255, 0, 0, 255}, {-0.5f, 0.5f, -1.0f} },
- { {0, 255, 0, 255}, {-0.5f, -0.5f, -1.0f} },
- { {0, 0, 255, 255}, {0.5f, 0.5f, -2.0f} },
- { {255, 0, 255, 255}, {0.5f, -0.5f, -2.0f} }
- };
- // Drawing code here.
- glClear(GL_COLOR_BUFFER_BIT);
- glInterleavedArrays(GL_C4UB_V3F, 0, vertexInfoList);
- glCullFace(GL_BACK);
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- glFlush();
- }
下面介绍视图模型变换。
视图模型变换是将三维物体的顶点做平移、旋转等操作。我们可以这么理解,我们把摄像机镜头固定好,然后观察物体的移动行 为。
在上述代码的第12行,glMatrixMode(GL_MODELVIEW);用于将当前矩阵变换的操作作用到视图模型变换栈上。 glLoadIdentity();作用是将当前的矩阵作为单位矩阵,即:[1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1]。其实际效果就是:C = I;其中,C表示本矩阵模式的当前矩阵,I表示单位矩阵。
glTranslatef(0.3f, 0.2f, 0.0f);是平移变换,函数原型为:
|
其中,x表示沿x轴平移多少单位;y表示沿y轴平移多少单 位;z表示沿z轴平移多少单位。而,其对应的平移变换矩阵就是:[1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0; x, y, z, 1]。
那么glTranslatef(0.3f, 0.2f, 0.0f);实际效果就是:C\' = T * C = T * I。因为我们刚才调用了glLoadIdentity,因此在调用此函数时的当前矩阵就是I。
而下面的glRotatef(-30.0f, 0.0f, 0.0f, 1.0f);则是将所要绘制的物体的每个顶点坐标绕z轴,顺时针旋转30度。下面给出glRotatef的原型:
|
其中,angle表示角度制的角度;x、y和z表示绕着由x、 y和z构成的向量进行旋转。如果angle大于0,则做逆时针旋转,小于0做顺时针旋转,等于0不动。
由于glRotate所对应的变换矩阵比较 复杂,各位可以参考OpenGL官网上的手册。
如果将旋转变换矩阵表示为R的话,那么这里调用R的效果其实就是:C\' = R * C。那么将上面所有的串起来就是:C\' = R * (T * I)。如果顶点单纯地只经过视图模型变换,那么对于物体其中一个顶点v而言,变换后的顶点v\' = v * (R * (T * I))。
我们这 里实际上是先对物体做绕z轴旋转,然后再平移。但是调用的时候必须先调平移函数,再调旋转函数。
下面在介绍投影变换前再介绍几个对矩阵操作的函数,首先是glLoadMatrix,下面先给出函数原型:
|
该函数表示在当前变换矩阵模式下将矩阵m取代当前的变换矩阵。 比如,glLoadIdentity可以用一下代码替换:
|
然后是再介绍一下glMultMatrix函数,请参 看函数原型:
|
这个函数就很直白地表示用矩阵m与当前矩阵相乘,然后将结果再 给回当前矩阵。即:C\' = M * C。
我们可以用一下代码替换glTranslatef:
|
将上述代码替换掉3楼的第14行,效果不变。
下面介绍投影变换。
投影变换就好像为照相机选择镜头。我们可以认为这种变换的目的就是确定视野(或视景体)。并且确定哪些物体位于视野之内以及它 们能够被看到的程度。
投影变换主要有两种,一种是透视投影(Perspective Projection)。这种投影效果就像是玩第一人称游戏或是第一人称的赛车类游戏。你向前看具有透视效果,远处的物体显得小,近处的物体显得大。
还 有一种投影是正交投影(Orthographic Projection)。这种投影是将顶点以平行于视野的角度垂直地映射到屏幕上。物体不管离你多远,其大小都是一样的。
透视投影对应的函数 为:glFrustum,函数原型如下:
|
其中,left表示x轴坐标最左端的值,right表示x坐标 最右端的值,bottom表示y轴坐标最下端的值,top表示y轴最上端的值,near表示与摄像机或你的眼镜最近的距离,far表示摄像机或离你的眼镜 最远的距离。near和far都大于0方有效。
而正交投影的定义与透视投影的类似,下面给出函数原型:
void glOrtho( GLdouble left,
GLdouble right,
GLdouble bottom,
GLdouble top,
GLdouble nearVal,
GLdouble farVal); // OpenGL
void glOrthof( GLfloat left,
GLfloat right,
GLfloat bottom,
GLfloat top,
GLfloat near,
GLfloat far); // OpenGL ES
void glOrthox( GLfixed left,
GLfixed right,
GLfixed bottom,
GLfixed top,
GLfixed near,
GLfixed far); // OpenGL ES
首先,各位可以先用glFrustum来替换glOrtho,看看效果,呵呵。
然后我们看3楼代码29到32行,为什么这里z坐标为-1 和-2?
因为我们调用了glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, 5.0);也就是说你的视野最近看到的距离为1,最远是5。由于你的视野是正对屏幕的,也就是朝z轴的负方向。因此这里必须把物体沿z轴移到你的视野范围 内才能看到物体。如果我们把物体顶点的z轴值改为大于-1或小于-5,那偶们就看不见该顶点了,呵呵。
好。那么我们现在结合模型视图变换 和投影变换。那么它们的调用顺序是何如的呢?
我们在1楼知道,视图的变换步骤是先经过视图模型变换,然后再经过投影变换。因此对于一个顶点v来 说,若视图模型变换矩阵是M,而投影变换矩阵为P,那么变换后的顶点v\' = v * (M * P)。
我们不妨可以做个实验。为了能证明这个步 骤,我们将投影变换改为透视投影,即,将glOrtho改为glFrustum,观察图形。然后我们做以下修改:
复制代码
|
我 们会发现这个形状与调用glFrustum的形状完全一样,看上去像个直角梯形。
而如果我们将上面15到20行的代码搬到第24行,那么我们将会看到一个接近于等腰梯形的图形,显然与原来的不符,呵呵。
而上述代码中:
|
其实就等价于调用:
|
上面那个是与下面glFrustum调用相对应的变换矩阵。
好, 接下去我们准备开始话3D图形。
上面忘记说了,投影矩阵与模型视图矩阵是分开来的。也就是说glMatrixMode(GL_PROJECTION);所指定指定的投影变换矩阵栈与 glMatrixMode(GL_MODELVIEW);所指定的模型视图变换矩阵栈是相互独立的。
而投影变换矩阵变换的最终结果我们设为P,而 模型视图变换矩阵的最终得出的变换矩阵我们设为M。
所以上述公式中的P和M是这么得到的。
我们在后序章节中将会介绍对矩阵栈的操作。
好。我们下面将谈谈面的朝向。这个对于裁剪背面以及光照都有用。
我们知道一张纸有正反两个面。在OpenGL中,一个平面可以想像为一张纸,也有 两个面。如果你没有指定要裁剪掉看不见的反面,那么反面也会被绘制。那么我们如何确定一个面是朝向哪里呢?
这取决于我们的初始设定以及绘制顶点的 顺序。
在默认情况下,顶点以逆时针方向绘制出来的方向为正面,顺时针方向绘制出来的是反面。我们也可以用右手拇指定律来确定面的朝向。
函 数glFrontFace用于指定是顺时针方向为正面还是逆时针方向为正面。默认情况下,逆时针方向为正面。函数原型如下:
|
mode只有两个值,要么是GL_CW,指定绘制顺序为顺时针 时表示正面;要么是GL_CCW,指定绘制顺序为逆时针方向时为正面。默认为GL_CCW。
下面我们将举个绘制立方体的例子。
- - (void)prepareOpenGL
- {
- glShadeModel(GL_SMOOTH);
- glEnable(GL_CULL_FACE);
- glEnable(GL_DEPTH_TEST);
- glEnable(GL_NORMALIZE);
- glClearColor(0.4, 0.4, 0.4, 1.0);
- glViewport(0, 0, 320, 320);
- glMatrixMode(GL_PROJECTION);
- glLoadIdentity();
- glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, 5.0);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glTranslatef(0.0f, 0.0f, -3.0f);
- glRotatef(10.0f, 1.0f, 1.0f, 1.0f);
- glFrontFace(GL_CCW);
- }
- - (void)drawRect:(NSRect)dirtyRect {
- static const struct
- {
- GLubyte colours[4];
- GLfloat vertices[3];
- }vertexInfoList[] = {
- { {255, 0, 0, 255}, {-0.5f, -0.5f, -0.5f} }, // v0: left-bottom-back
- { {0, 255, 0, 255}, {0.5f, -0.5f, -0.5f} }, // v1: right-bottom-back
- { {0, 0, 255, 255}, {0.5f, 0.5f, -0.5f} }, // v2: right-top-back
- { {255, 0, 255, 255}, {-0.5f, 0.5f, -0.5f} }, // v3: left-top-back
- { {255, 255, 0, 255}, {-0.5f, -0.5f, 0.5f} }, // v4: left-bottom-front
- { {0, 255, 255, 255}, {0.5f, -0.5f, 0.5f} }, // v5: right-bottom-front
- { {20, 20, 20, 255}, {0.5f, 0.5f, 0.5f} }, // v6: right-top-front
- { {255, 255, 255, 255}, {-0.5f, 0.5f, 0.5f} }, // v7: left-top-front
- };
- static const GLubyte __attribute__((aligned(4))) indices[] = {
- 0, 3, 1, 1, 3, 2, // back face
- 0, 4, 3, 3, 4, 7, // left face
- 4, 5, 7, 7, 5, 6, // front face
- 5, 1, 6, 6, 1, 2, // right face
- 7, 6, 3, 3, 6, 2, // top face
- 0, 1, 4, 4, 1, 5, // bottom face
- };
- glRotatef(-10.0f, 1.0f, 1.0f, 1.0f);
- // Drawing code here.
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- glInterleavedArrays(GL_C4UB_V3F, 0, vertexInfoList);
- glCullFace(GL_BACK);
- #if 0
- glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(indices[0]), GL_UNSIGNED_BYTE, indices);
- #else
- static const GLfloat normals[] = {
- 0.0f, 0.0f, -1.0f, // back face normal
- -1.0f, 0.0f, 0.0f, // left face normal
- 0.0f, 0.0f, 1.0f, // front face normal
- 1.0f, 0.0f, 0.0f, // right face normal
- 0.0f, 1.0f, 0.0f, // top face normal
- 0.0f, -1.0f, 0.0f, // bottom face normal
- };
- for(int i=0; i < 6; i++)
- {
- glNormal3f(normals[i * 3 + 0], normals[i * 3 + 1], normals[i * 3 + 2]);
- glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, &indices[i * 6]);
- }
- #endif
- glFlush();
- }
- - (void)mouseUp:(NSEvent *)theEvent
- {
- [self setNeedsDisplay:YES];
- }
-
- 取自:http://www.cocoachina.com/gamedev/opengl/2010/0511/1389.html