目录
透视投影
透视投影可视空间
可视空间构造效果图
Matrix4.setPerspective()
三角形与可视化空间的相对位置
示例代码
代码详解
示例效果
投影矩阵的作用
透视投影矩阵对物体进行了两次变换
透视投影变换示意图
透视投影
在透视投影下,产生的三维场景看上去更是有深度感,更加自然,因为我们平时观察真实世界用的也是透视投影。在大多数情况下,比如三维射击类游戏中,我们都应当采用透视投影。
在下图的场景中,道路两边都有成排的树木。树应该都是差不多高的,但是在照片上,越远的树看上去越矮。同样,道路尽头的建筑看上去比近处的树矮,但实际上那座建筑比树高很多。这种“远处的东西看上去小”的效果赋予了照片深度感,或称透视感。我们的眼睛就是这样观察世界的。有趣的是,孩童的绘画往往会忽视这一点。
在正射投影的可视空间中,不管三角形与视点的距离是远是近,它有多大,那么画出来就有多大。为了打破这条限制,我们可以使用透视投影可视空间,它将使场景具有图上图那样的深度感。
本例PerspectiveView使用了一个透视投影可视空间,视点在(0,0,5),视线沿着Z轴负方向。下图显示了程序的运行效果,以及程序的场景中各三角形的位置。
如上图(右)所示,沿着Z轴负半轴(也就是视线方向),在轴的左右侧各依次排列着3个相同大小的三角形,场景与本例第一张图中的道路和树木有一点相似。在使用透视投影矩阵后,WebGL就能够自动将距离远的物体缩小显示,从而产生上图(左)中的深度感。
透视投影可视空间
透视投影可视空间如下图所示。就像盒状可视空间那样,透视投影可视空间也有视点、视线、近裁剪面和远裁剪面,这样可视空间内的物体才会被显示,可视空间外的物体则不会显示。那些跨越可视空间边界的物体则只会显示其在可视空间内的部分。
可视空间构造效果图
不论是透视投影可视空间还是盒状可视空间,我们都用投影矩阵来表示它,但是定义矩阵的参数不同。Matrix4对象的setPerspective()方法可用来定义透视投影可视空间。(WebGL矩阵变换库_山楂树の的博客-CSDN博客)
Matrix4.setPerspective()
定义了透视投影可视空间的矩阵被称为透视投影(perspective projection matrix)。
注意,第2个参数aspect是近裁剪面的宽高比,而不是水平视角(第1个参数是垂直视角)。比如说,如果近裁剪面的高度是100而宽度是200,那么宽高比就是2。
在本例中,各个三角形与可视空间的相对位置如下图所示。我们指定了near=1.0,far=100,aspect=1.0(宽度等于高度,与画面相同),以及fov=30.0。
三角形与可视化空间的相对位置
示例代码
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'uniform mat4 u_ViewMatrix;\n' +
'uniform mat4 u_ProjMatrix;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n' +
' v_Color = a_Color;\n' +
'}\n';
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) return;
// 设置顶点坐标和颜色,蓝色三角形在最前面
var n = initVertexBuffers(gl);
gl.clearColor(0, 0, 0, 1);
var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
var viewMatrix = new Matrix4(); // 视图矩阵
var projMatrix = new Matrix4(); // 模型矩阵
viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0); // 视点、被观察点、正方向
projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100); // 视角顶底面夹角、近裁面宽高比、near、far
// 将视图和投影矩阵传递给u_ViewMatrix、u_ProjectMatrix
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
// 顶点坐标、颜色
0.75, 1.0, -4.0, 0.4, 1.0, 0.4, // 后面的绿色
0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
1.25, -1.0, -4.0, 1.0, 0.4, 0.4,
0.75, 1.0, -2.0, 1.0, 1.0, 0.4, // 中间的黄色
0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
1.25, -1.0, -2.0, 1.0, 0.4, 0.4,
0.75, 1.0, 0.0, 0.4, 0.4, 1.0, // 前面的蓝色
0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
1.25, -1.0, 0.0, 1.0, 0.4, 0.4,
// 左侧有三个三角形
-0.75, 1.0, -4.0, 0.4, 1.0, 0.4, // 后面的绿色
-1.25, -1.0, -4.0, 0.4, 1.0, 0.4,
-0.25, -1.0, -4.0, 1.0, 0.4, 0.4,
-0.75, 1.0, -2.0, 1.0, 1.0, 0.4, // 中间的黄色
-1.25, -1.0, -2.0, 1.0, 1.0, 0.4,
-0.25, -1.0, -2.0, 1.0, 0.4, 0.4,
-0.75, 1.0, 0.0, 0.4, 0.4, 1.0, // 前面的蓝色
-1.25, -1.0, 0.0, 0.4, 0.4, 1.0,
-0.25, -1.0, 0.0, 1.0, 0.4, 0.4,
]);
var n = 18; // 六个三角形18个顶点
var vertexColorbuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
var FSIZE = verticesColors.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);
return n;
}
代码详解
main()函数中首先调用initVertexBuffers()函数,向缓冲区对象中写入这6个三角形的顶点坐标和颜色数据(第26行),在这6个三角形中,右侧3个的数据从第44行开始,左侧3个的数据从第54行开始。而且,需要绘制的顶点的个数为18(第75行,6个三角形,3×6=18)。
接着,我们获取了着色器中视图矩阵和透视投影矩阵uniform变量的存储地址(第28行和第29行),并创建了两个对应的矩阵对象(第30和第31行)。
然后,我们计算了视图矩阵(第32行),视点设置在(0,0,5),视线为Z轴负方向,上方向为Y轴正方向。最后,我们按照金字塔状的可视空间建立了透视投影矩阵(第33行)。
其中,第2个参数aspect宽高比(近裁剪面的宽度与高度的比值)应当与<canvas>保持一致,我们根据<canvas>的width和height属性来计算出该参数,这样如果<canvas>的大小发生变化,也不会导致显示出来的图形变形。
接下来,将准备好的视图矩阵和透视投影矩阵传给着色器中对应的uniform变量(第35和第36行)。最后将三角形绘制出来(第38行),就获得了如下图所示的效果。
示例效果
到目前为止,还有一个重要的问题没有完全解释,那就是矩阵为什么可以用来定义可视空间。接下来,我们尽量避开其中复杂的数学过程,稍做一些探讨。
投影矩阵的作用
首先看一下本例的运行效果如上图,可以看到:运用透视投影矩阵后,场景中的三角形有了两处变化
透视投影矩阵对物体进行了两次变换
首先,距离较远的三角形看上去变小了;其次,三角形被不同程度地平移以贴近中心线(即视线),使得它们看上去在视线的左右排成了两列。实际上,如下图(左)所示,这些三角形的大小是完全相同的,透视投影矩阵对三角形进行了两次变换:(1)根据三角形与视点的距离,按比例对三角形进行了缩小变换;(2)对三角形进行平移变换,使其贴近视线,如下图右所示。经过了这两次变换之后,就产生了上图0那张照片中的深度效果。
透视投影变换示意图
这表明,可视空间的规范(对透视投影可视空间来说,就是近、远裁剪面,垂直视角,宽高比)可以用一系列基本变换(如缩放、平移)来定义。Matrix4对象的setPerspective()方法自动地根据上述可视空间的参数计算出对应的变换矩阵。
换一个角度来看,透视投影矩阵实际上将金字塔状的可视空间变换为了盒状的可视空间,这个盒状的可视空间又称规范立方体(Canonical View Volume),如上图(右)所示。
注意,正射投影矩阵不能产生深度感。正射投影矩阵的工作仅仅是将顶点从盒状的可视空间映射到规范立方体中。顶点着色器输出的顶点都必须在规范立方体中,这样才会显示在屏幕上。