> 文章列表 > 一起学 WebGL:感受三维世界之视图矩阵

一起学 WebGL:感受三维世界之视图矩阵

一起学 WebGL:感受三维世界之视图矩阵

大家好,我是前端西瓜哥。之前绘制的图形都是在 XY 轴所在的平面上,这次我们来加入一点深度信息 z,带你走入三维的世界。

视图矩阵

对于一个立方体来说,我们从它的正前方看,不管距离它多远,也只能看到一个二维的正方形。因此我们需要引入 视图矩阵(view matrix)。它的作用就像是一个在特定位置的摄像头。

视图矩阵需要三个信息:

  1. 视点位置;
  2. 观察点位置;
  3. 上方向;

就好比我们站在某个位置看一个模型,眼睛的位置就是观察点,目光落在的点就是视点。我们站着看,上方向 就是朝上(y 正轴方向),躺着看就是水平方向,倒立着看就是朝下(y 负半轴方向)。

实际上我们并没有一个真正的视口,我们的世界坐标的正中心永远是原点,z 负半轴指向观察者。

但我们可以利用相对运动的原理,给图形做一个相反的操作,比如我往右边走 1 个单位去看模型,其实等价于我不懂,模型向左移动 1 个单位,它们的效果是一样的。

视图矩阵的算法实现如下:

function createViewMatrix(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) {const normalize = (v) => {const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);return [v[0] / length, v[1] / length, v[2] / length];};const subtract = (v1, v2) => {return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]];};const cross = (v1, v2) => {return [v1[1] * v2[2] - v1[2] * v2[1],v1[2] * v2[0] - v1[0] * v2[2],v1[0] * v2[1] - v1[1] * v2[0]];};const zAxis = normalize(subtract([eyeX, eyeY, eyeZ], [atX, atY, atZ]));const xAxis = normalize(cross([upX, upY, upZ], zAxis));const yAxis = normalize(cross(zAxis, xAxis));return new Float32Array([xAxis[0],yAxis[0],zAxis[0],0,xAxis[1],yAxis[1],zAxis[1],0,xAxis[2],yAxis[2],zAxis[2],0,-(xAxis[0] * eyeX + xAxis[1] * eyeY + xAxis[2] * eyeZ),-(yAxis[0] * eyeX + yAxis[1] * eyeY + yAxis[2] * eyeZ),-(zAxis[0] * eyeX + zAxis[1] * eyeY + zAxis[2] * eyeZ),1]);
}

视图坐标的实现细节不讲,不重要。(顺带一提,上面的算法由 Github Copilot 生成)

通过这个方法计算出矩阵,传入到顶点着色器的矩阵变量中,和顶点位置计算即可。

const viewMatrix = createViewMatrix(0.2, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
const u_ViewMatrix = gl.getUniformLocation(gl.program, "u_ViewMatrix");
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);

其他的创建缓冲区的逻辑就不讲了,之前的文章都讲过了。

完整代码

贴一下完整代码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;
varying vec4 v_Color;
void main() {gl_Position = u_ViewMatrix * a_Position;v_Color = a_Color;
}
`;const fragmentShaderSrc = `
precision mediump float;
varying vec4 v_Color;
void main() {gl_FragColor = v_Color;
}
`;/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;// prettier-ignore
const verticesColors = new Float32Array([// 下方的红色三角形0, 0.2, -0.2, 1, 0, 0,  // 位置和颜色信息-0.2, -0.2, -0.2, 1, 0, 0,  0.2, -0.2, -0.2, 1, 0, 0,  // 上方的黄色三角形0, 0.2, 0, 1, 1, 0,  // 点 1 的位置和颜色信息-0.2, -0.2, 0, 1, 1, 0,  // 点 20.2, -0.2, 0, 1, 1, 0,  // 点 3
]);
// 每个数组元素的字节数
const SIZE = verticesColors.BYTES_PER_ELEMENT;// 创建缓存对象
const vertexColorBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, SIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);const a_Color = gl.getAttribLocation(gl.program, "a_Color");
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, SIZE * 6, SIZE * 3);
gl.enableVertexAttribArray(a_Color);/****** 视图矩阵 ****/
// prettier-ignore
// 取消下面一行注释,并注释下下一行代码,可观察没有使用视图矩阵的原始效果
// const viewMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0,0,0,1]);
const viewMatrix = createViewMatrix(0.2, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
const u_ViewMatrix = gl.getUniformLocation(gl.program, "u_ViewMatrix");
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 6);function createViewMatrix(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) {const normalize = (v) => {const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);return [v[0] / length, v[1] / length, v[2] / length];};const subtract = (v1, v2) => {return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]];};const cross = (v1, v2) => {return [v1[1] * v2[2] - v1[2] * v2[1],v1[2] * v2[0] - v1[0] * v2[2],v1[0] * v2[1] - v1[1] * v2[0]];};const zAxis = normalize(subtract([eyeX, eyeY, eyeZ], [atX, atY, atZ]));const xAxis = normalize(cross([upX, upY, upZ], zAxis));const yAxis = normalize(cross(zAxis, xAxis));return new Float32Array([xAxis[0],yAxis[0],zAxis[0],0,xAxis[1],yAxis[1],zAxis[1],0,xAxis[2],yAxis[2],zAxis[2],0,-(xAxis[0] * eyeX + xAxis[1] * eyeY + xAxis[2] * eyeZ),-(yAxis[0] * eyeX + yAxis[1] * eyeY + yAxis[2] * eyeZ),-(zAxis[0] * eyeX + zAxis[1] * eyeY + zAxis[2] * eyeZ),1]);
}

demo 地址:

https://codesandbox.io/s/ijxwu2?file=/index.js

这里我绘制了红色和黄色两个三角形,红色在更下边,z 为 -0.2,黄色在上面一点,z 为 0。

应用视图矩阵前的效果。因为两者大小相同,黄色三角形完全盖住了红色。

一起学 WebGL:感受三维世界之视图矩阵

应用视图矩阵后:

一起学 WebGL:感受三维世界之视图矩阵

结尾

我是前端西瓜哥,欢迎关注我,学习更多 WebGL 知识。

今天简单讲了下让我们指定一个位置观察模型的方法:视图矩阵。

之前我们也讲了一个叫做模型矩阵的玩意,模型矩阵就好比一个三维软件,我们将一个模型导入到场景中,移动它的位置、缩放它的尺寸,旋转一下之类的。视图矩阵就好比通过一个摄像机的视角看到的世界。

不知道你发现没有,这里的两个三角形并没有近大远小的透视效果。此外,当我们的观察点位置非常靠右或靠左的时候,三角形会缺失部分。

关于这点,我会在下节讲解 可视空间,解答这些问题。