Web编程技术营地
研究、演示、创新

投影、视图、模型变换

撰写时间:2023-08-19

修订时间:2026-04-17

在本章中,我们加入投影变换投影变换其实就是确定一个如下图所示的视锥体viewing volumn)的过程。

Viewing volumn

视锥体范围(透明部分)内的所有物体将得到渲染,而视锥体范围之外的所有物体则不会被渲染。并且,视锥体范围内的众多物体存在近大远小、被遮挡的关系。

着色器

顶点着色器代码如下:

之前只有一个视图矩阵或一个模型矩阵参加与aPosition的乘法运算,而这次运算还加入了投影矩阵

这里涉及到矩阵向量相乘、矩阵之间相乘的运算顺序与规则。

实际真实物理世界的顺序为变换顺序应是:

矩阵与向量的相乘

当一个矩阵乘以一个向量时,要求矩阵的列数必须等于向量的维数。而根据向量的种类,算法分为2种。

矩阵左乘列向量

\[ \begin{bmatrix} a & b & c & d \\ e & f & g & h \\ i & j & k & l \\ m & n & o & p \\ \end{bmatrix} \begin{pmatrix} x \\ y \\ z \\ w \\ \end{pmatrix} = \begin{pmatrix} ax + by + cz + dw \\ ex + fy + gz + hw \\ ix + jy + kz + lw \\ mx + ny + oz + pw \\ \end{pmatrix} \]

向量以列的方式排列时,称为列向量

此时,矩阵应放在左边,向量应放在右边。因矩阵位于左边,故称矩阵左乘列向量

一个简化的2阶例子:

\[ \begin{bmatrix} 8 & 6 \\ 2 & 4 \end{bmatrix} \begin{pmatrix} 3 \\ 5 \end{pmatrix} = \begin{pmatrix} 8 \times 3 + 6 \times 5 \\ 2 \times 3 + 4 \times 5 \end{pmatrix} = \begin{pmatrix} 24 + 30 \\ 6 + 20 \end{pmatrix} = \begin{pmatrix} 54 \\ 26 \end{pmatrix} \]

矩阵右乘行向量

\[ \begin{pmatrix} x & y & z & w \end{pmatrix} \begin{bmatrix} a & b & c & d \\ e & f & g & h \\ i & j & k & l \\ m & n & o & p \\ \end{bmatrix} = \begin{pmatrix} xa + ye + zi + wm, & xb + yf + zj + wn, & xc + yg + zk + wo, & xd + yh + zl + wp \end{pmatrix} \]

向量以行的方式排列时,称为行向量

此时,矩阵应放在右边,向量应放在左边。因矩阵位于右边,故称矩阵右乘行向量

一个简化的2阶例子:

\[ \begin{pmatrix} 3 & 5 \end{pmatrix} \begin{bmatrix} 8 & 6 \\ 2 & 4 \end{bmatrix} = \begin{pmatrix} 3 \times 8 + 5 \times 2, & 3 \times 6 + 5 \times 4 \end{pmatrix} = \begin{pmatrix} 24 + 10, & 18 + 20 \end{pmatrix} = \begin{pmatrix} 34, & 38 \end{pmatrix} \]

列主序约定

从上面的简单例子可见,矩阵左乘列向量的结果,不等于矩阵右乘行向量的结果。即:

`M * v \ne v * M`

WebGLGLSL均约定,向量列主序column majored),即向量应采用列向量的方式。因此在WebGL中,当一个矩阵需与一个向量相乘时,应采用矩阵左乘列向量的方式,即:

`M * v`

因此,之前我们将一个模型向量gl_Position相乘时,其对应的代码为:

相对应的,glMatrix也遵循了列主序的约定。下面调用glMatrix相应方法来计算上面简单的矩阵向量相乘的结果。

mat2是专用于计算2阶矩阵的模块,调用该模块的fromValues函数来初始化一个2阶矩阵。该函数的4个参数中,采用了列主序的顺序,即先指定第0列中的2个元素,再指定第1列中的2个元素。但在直接打印变量matrix时,却发现其在内存中按列主序参数顺序直接存储为一维数组。此时,如果按一维数组的元素顺序拆分为用以表示矩阵的二维数组,则矩阵将变成行主序矩阵!因此,下面将通过一个函数来专门处理此问题。

接着,调用vec2fromValues函数来创建了一个值为(3, 5)向量

再次,调用vec2transformMat2函数,将matrixvector相乘,将其值存储于变量resultVector,并打印其值。

从打印结果来看,与上面所说的矩阵左乘列向量的结果一致,因此可确定,glMatrix也遵循了列主序的约定。

函数printColMajoredMatrix用于格式化输出一个代表列主序矩阵Float32Array实例。在函数内,先调用mat2transpose函数,将Float32Array的数据调整为行主序后,依数组元素顺序拆分为二维数组,再分别打印其内存状态及其代表的矩阵

第一版

projection-view-model.htmlJavaScript代码如下:

import { WebGLUtils } from './js/esm/WebGLUtils-v8.js'; import { GLColors, WireframeMesh, FaceMesh, mat4, glMatrix } from './js/esm/index-independent.js'; let glu, gl, program; let axisMesh; let mesh1, mesh2; init(); function init() { initContext(); initProjectionMatrix(); initViewMatrix(); initMeshes(); render(); } function initContext() { glu = new WebGLUtils({ uniforms: ['uProjectionMatrix', 'uViewMatrix', 'uModelMatrix'], vertexAttribs: ['aPosition', 'aColor'], renderFunc: render }); gl = glu.gl; program = glu.program; gl.enable(gl.DEPTH_TEST); gl.clearColor(0.0, 0.0, 0.0, 1.0); } function initProjectionMatrix() { let projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, glMatrix.toRadian(30), gl.drawingBufferWidth / gl.drawingBufferHeight, 0.01, null); gl.uniformMatrix4fv(program['uProjectionMatrix'], false, projectionMatrix); } function initViewMatrix() { let viewMatrix = mat4.create(); mat4.lookAt( viewMatrix, [ 3.0, 3.0, 5.0], [-0.5, -0.5, -1.0], [ 0.0, 1.0, 0.0] ); gl.uniformMatrix4fv(program['uViewMatrix'], false, viewMatrix); } function initMeshes() { let vertices = [ -1.0, 0.0, 0.0, // V0 1.0, 0.0, 0.0, // V1 0.95, 0.025, 0.0, // V2 0.95, -0.025, 0.0, // V3 0.0, 1.0, 0.0, // V4 0.0, -1.0, 0.0, // V5 -0.025, 0.95, 0.0, // V6 0.025, 0.95, 0.0, // V7 0.0, 0.0, 1.0, // V8 0.0, 0.0, -1.0, // V9 -0.025, 0.0, 0.95, // V10 0.025, 0.0, 0.95 // V11 ]; let wireframeIndices = [ 0, 1, 1, 2, 1, 3, 4, 5, 4, 6, 4, 7, 8, 9, 8, 10, 8, 11 ]; axisMesh = new WireframeMesh(vertices, wireframeIndices); axisMesh.modelMatrix = mat4.create(); mesh1 = new FaceMesh([ 0.0, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5 ], null, GLColors.STEELBLUE); mesh1.modelMatrix = mat4.create(); mesh2 = new FaceMesh([ 0.0, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5 ], null, GLColors.SEAGREEN); mesh2.modelMatrix = mat4.create(); mat4.rotateZ(mesh2.modelMatrix, mesh2.modelMatrix, glMatrix.toRadian(180)); } function render() { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.uniformMatrix4fv(program['uModelMatrix'], false, axisMesh.modelMatrix); axisMesh.render(); gl.uniformMatrix4fv(program['uModelMatrix'], false, mesh1.modelMatrix); mesh1.render(); gl.uniformMatrix4fv(program['uModelMatrix'], false, mesh2.modelMatrix); mesh2.render(); }

运行网页

这一版的代码,主要在于学习与掌握当我们需要同时应用投影变换、视图变换及模型变换时,最基本的程序结构及运行顺序。

第二版

第一版存在的问题

主要有2个问题。一是视口与投影区域的大小不一致。

我们按原来的代码运行的先后顺序来分析此问题。第一步,在WebGLUtilsgetContext方法中,调用了updateViewport方法:

getContext(canvasId) { ... this.updateViewport(); }

第二步,该方法的代码如下:

updateViewport() { ... let squareLen = Math.min(gl.drawingBufferWidth, gl.drawingBufferHeight); gl.viewport((gl.drawingBufferWidth - squareLen) / 2, (gl.drawingBufferHeight - squareLen) / 2, squareLen, squareLen); }

上面的viewport只取长方形的宽度与高度中最小值为视口。在宽屏显示器中,往往造成左右两边留白。

第三步,在projection-view-model.htmlinitProjectionMatrix函数中:

function initProjectionMatrix() { projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, glMatrix.toRadian(30), gl.drawingBufferWidth / gl.drawingBufferHeight, 0.01, null); gl.uniformMatrix4fv(program['uProjectionMatrix'], false, projectionMatrix); }

perspective方法却是使用了渲染缓冲区的整个区域。该方法的特点是,根据宽高比,在方法内部按渲染图像的比例来映射到整个渲染缓冲区。也即说,只要传入正确的宽高比,该方法能自动保留原有图像的比例,确保不变形。

也即说,将若按原来尺寸不变形的图像,投影至宽度缩小的区域,则会导致投影图像反而变形。

第二个问题,当canvas大小改变时,投影区域未作变动。

原来的事件代码为:

initWebEvents() { window.addEventListener('resize', () => { this.updateViewport(); this.renderFunc(); }); ... }

上面的代码,当canvas大小改变时,只更新了视口,但投影区域未作更新。显然,在此也需要一个方法来重新设置projectionMatrix的值。

第二版的修改

第二版projection-view-model-1.html主要有3个地方的修改。

一是导入WebGLUtils-v9.js文件。

import { WebGLUtils } from './js/esm/WebGLUtils-v9.js';

二是主干程序删除了initProjectionMatrix方法。

function init() { initContext(); initViewMatrix(); initMeshes(); render(); }

该功能将移至WebGLUtils类中集中实现。

三是initContext函数中,在WebGLUtils类的构造方法中,传入表示视域的变量fov

function initContext() { glu = new WebGLUtils({ uniforms: ['uProjectionMatrix', 'uViewMatrix', 'uModelMatrix'], vertexAttribs: ['aPosition', 'aColor'], fov: 30, renderFunc: render }); ... }

改动较大的地方从而集中到WebGLUtils类中。

首先,导入glMatrix的模块。

import { CanvasUtils, Mesh, WebGLBufferUtils as BufUtils, mat4, glMatrix } from './index-independent.js';

其次,修改构造方法。

constructor({ canvasId = 'webgl-canvas', shaderIds = ['vShader', 'fShader'], uniforms, vertexAttribs, fov = 30, renderFunc }){ this.canvas = null; this.gl = null; this.program = null; this.renderFunc = renderFunc; this.initWebEvents(); this.getContext(canvasId); this.initProgram(shaderIds, vertexAttribs, uniforms); this.initProjectionMatrix(fov); ... }

第三,initWebEvents方法:

initWebEvents() { window.addEventListener('resize', () => { this.updateViewport(); this.updateProjectionMatrix(); this.renderFunc(); }); ... }

第四,修改updateViewport方法如下:

updateViewport() { ... if (canvas.width !== canvasClientWidth || canvas.height !== canvasClientHeight) { canvas.width = canvasClientWidth; canvas.height = canvasClientHeight; } gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); }

第五,添加initProjectionMatrixupdateProjectionMatrix方法如下:

initProjectionMatrix(fov) { let gl = this.gl; this.fov = fov; this.projectionMatrix = mat4.create(); mat4.perspective(this.projectionMatrix, glMatrix.toRadian(this.fov), gl.drawingBufferWidth / gl.drawingBufferHeight, 0.01, null); gl.uniformMatrix4fv(this.program['uProjectionMatrix'], false, this.projectionMatrix); } updateProjectionMatrix() { let gl = this.gl; mat4.perspective(this.projectionMatrix, glMatrix.toRadian(this.fov), gl.drawingBufferWidth / gl.drawingBufferHeight, 0.01, null); gl.uniformMatrix4fv(this.program['uProjectionMatrix'], false, this.projectionMatrix); }

上面五步的环节可用两句话来概述:开始时,根据fov来新建一个投影矩阵。在resize事件中,在更新视口后,同时更新投影矩阵。

运行网页。经修改后,无论如何改变浏览器的大小,视口及投影区域均能使用canvas的全部区域,且图像不会变形。

发现重构的需求

在实现第二版的过程中,我们发现视口、投影矩阵总是与canvas的尺寸紧紧地绑在一起,因此,有必要引入一个新类,专门集中处理视口、投影变换及视图变换的问题。

在下一章,我们将在本章所掌握的知识的基础上,实现多视口应用程序。

参考资源

Specifications

  1. WebGL 2.0 Specification

glMatrix

  1. mat2.fromValues