一、3D笛卡尔坐标系
3D笛卡尔坐标系与我们常见2D笛卡尔坐标系相同,但是增加了一条Z轴。这三条轴互相垂直,坐标系有3个自由度。要定义3D空间中的一点P,需要三个坐标:x,y,z,或简写P(x,y,z)。另外,这三条轴构成了三个平面:x-y平面,x-z平面和y-z平面,每个平面将空间分成两个区域(半空间),这三个平面最终将整个空间分成8个子空间,也就是8个卦限。
新增的z轴可以指向两个不同的方向,因此产生了两种不同的3D笛卡尔坐标系:右手坐标系和左手坐标系。之所以将它们命名为左手坐标系和右手坐标系,是因为用左手或者右手握住z轴时,如果其它手指从x轴环绕到y轴,则大拇指将指向z轴的正方向,如下图所示:
2D的笛卡尔坐标,无论如何选择x轴和y轴的方向,总能通过旋转使x轴向右为正,y轴向上为正。从某种意义上来说,所有的2D坐标都是“等价”的。但是在3D笛卡尔空间中,右手坐标系和左手坐标系,无论如何进行旋转旋转,都不可能重合。所以它们不是等价的。
二、OpenGL右手坐标系
OpenGL使用的是右手坐标系,坐标的中心点在屏幕中间,X轴朝向屏幕的右边,Y轴朝向屏幕的的上边,Z轴朝向屏幕的外面,如图所示:
三、使用索引数组绘制三棱锥
之前使用glDrawArrays函数绘制图元时,需要为每个图元提供所有顶点坐标。如果按照之前的方式,绘制一个有4个三角面的三棱锥,就需要12个顶点坐标。这样做的缺点很明显,如果多个图元共享一个顶点,那么这个顶点将会在顶点数组中多次出现,造成顶点属性数据的大小会翻倍增加,导致占用更多的内存和内存带宽,影响渲染效率。
为了提高效率,实现顶点共享,我们可以索引数组的方式绘图。首先定义三棱锥的顶点坐标,顺便为每个顶点指定顶点颜色,如下:
// 三棱锥的4个顶点坐标 Vector3f vVertices[] = { Vector3f( 0.5f, -0.4f, -0.5f), //V0点坐标 Vector3f(-0.5f, -0.4f, -0.5f), //V1点坐标 Vector3f( 0.0f, -0.4f, 0.5f), //V2点坐标 Vector3f( 0.0f, 0.5f, 0.0f), //V3点坐标 }; // 三棱锥4个顶点的颜色 Vector3f vColor[] = { Vector3f(1.0f, 0.0f, 0.0f), //V0点的颜色 Vector3f(0.0f, 1.0f, 0.0f), //V1点的颜色 Vector3f(1.0f, 1.0f, 0.0f), //V2点的颜色 Vector3f(0.0f, 0.0f, 1.0f), //V3点的颜色 };
将顶点属性数据发送给OpenGL后,还需要额外提供一个索引数组,在数组中指定每一个图元的所有顶点索引。然后调用glDrawElements函数,使索引数组一次就可以绘制出所有图元:
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices)
mode — 指定绘制图元的模式,可选的值有:
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
count — 索引的数量
type — 索引数组中元素的数据类型。可选的值有:
GL_UNSIGNED_BYTE
GL_UNSIGNED_SHORT
GL_UNSIGNED_INT
indices — 索引数组的首地址
使用顶点坐标在顶点数组中的索引编号,定义三棱锥的四个三角面,如下:
//图元的顶点索引 GLubyte indexes[] = { 0, 1, 2, //第1个三角形: V0-V1-V2 1, 3, 0, //第2个三角形: V1-V3-V0 2, 3, 0, //第3个三角形: V2-V3-V0 2, 3, 1, //第4个三角形: V2-V3-V1 };
现在,就可以使用上面定义的索引数组,绘制三棱锥了:
// 用顶点索引的方式绘制图形 GL30::glDrawElements(GL30::GL_TRIANGLES, //图元的绘制方式 4*3, //每个三角形3个索引,共4个三角形 GL30::GL_UNSIGNED_BYTE, //索引中的数据类型为无符号byte indexes); //索引数组首地址
使用索引方式绘制模型时,OpenGL将通过索引数组在顶点数组中去找每一个图元对应的顶点属性数据,组成图元进行渲染。例如,下图中的顶点数组和索引数组,将会生成V0V1V2,V1V3V0,V2V3V0,V2V3V1四个三角形。
四、透视投影
将三维空间中的物体绘制到二维屏幕上,为了获得接近真实三维物体的视觉效果,需要对三维空间中的坐标点进行透视投影变换。使最终产生的渲染图,具有消失感、距离感等一系列的透视特性,能逼真地反映物体的空间形象。
透视投影变换需要定义一个视景体,视景体由观察点以及包含4个侧平面的无限远的金字塔对象构成。其中无限远金字塔对象被两个附加的平面截取,即远裁剪面和近裁剪面。如下图所示:
透视投影变换的视景体有两个用途,首先它决定了物体从3D空间中如何被映射到2D屏幕上,其次,视景体定义了那些物体(或物体的一部分)被裁剪到最终的图像之外。
下面Perspective函数针对参数所定义的视景体,生成了一个透视投影的矩阵。fovy是yz平面上视野的角度值,它的值必须在[0,π]范围内。asepct是这个视景体纵横比,也就是它的宽度除以高度。near和far的值分别是观察点与近裁剪平面及远裁剪平面的距离(沿z轴负方向),这两个值都必须为正。
//生成透视投影矩阵 template<typename N> Matrix4x4<N> Matrix4x4<N>::Perspective(N fovy, N aspect, N near, N far) { N radians = fovy / 2; N sine = Math<N>::Sin(radians); N cotangent = Math<N>::Cos(radians) / sine; Matrix4x4<N> prjMarix; prjMarix.m11 = cotangent/aspect; prjMarix.m22 = cotangent; prjMarix.m33 = -(far + near)/(far - near); prjMarix.m34 = -1; prjMarix.m43 = -2*near*far/(far - near); prjMarix.m44 = 0; return prjMarix; }
该函数定义了一个观察点位于原点,视线沿z轴负方向,同时对称于x轴和y轴的视景体。从视景体生成透视投影矩阵的过程比较复杂,这里直接给出函数实现,不做推导,有兴趣的可以看《OpenGL透视投影矩阵的推导》。
使用Prespective函数时,需要挑选正确的视野值,否则图像看上去会变形。为了获得完美的视野,可以推测自己眼睛在正常情况下距离屏幕有多远以及窗口有多大,并根据距离和大小计算出视野的角度。
当窗口的大小发生变化时,投影矩阵都必须重新生成。所以,需要在窗口类的ReSizeGLScene函数中,调用Prepspective函数:
//生成投影矩阵 m_prjMat = Matrix4x4f::Perspective(Mathf::Radians(45.0f), //视野角度 ((float)width)/((float)height), //宽高比 1.0f, //近裁剪面距离 100.0f); //远裁剪面距离
在顶点着色器中,使用投影矩阵对模型的顶点进行变换,对应的顶点着色器源码如下:
//输入变量 attribute vec4 vPosition; attribute vec3 vColor; //投影矩阵 uniform mat4 matProject; //输出变量 varying vec3 varColor; void main() { varColor = vColor; gl_Position = matProject * vPosition; }
最后,在窗口类的场景绘制函数中,将投影矩阵传递给顶点着色器:
// 设置透视投影矩阵 GL30::glUniformMatrix4fv(m_prjLoc, //统一变量的索引 1, //只有一个矩阵 GL_FALSE, //矩阵无需转置 &m_prjMat.mat[0]); //矩阵数组的指针
使用投影变换之后,必须对三棱锥模型进行平移,使它位于视景体之内。否则它就会被裁剪掉,无法在窗口中渲染出来。这里将三棱锥向z轴的负方向平移2个单位(即所有顶点的z坐标减2),我们就能看到渲染成功后,具有立体效果的三棱锥模型。
五、配套代码
源码下载:TriangularPyramid