大家好,我是前端西瓜哥。之前绘制的图形都是在 XY 轴所在的平面上,这次我们来加入一点深度信息 z,带你走入三维的世界。
视图矩阵
对于一个立方体来说,我们从它的正前方看,不管距离它多远,也只能看到一个二维的正方形。因此我们需要引入 视图矩阵(view matrix)。它的作用就像是一个在特定位置的摄像头。
视图矩阵需要三个信息:
- 视点位置;
- 观察点位置;
- 上方向;
就好比我们站在某个位置看一个模型,眼睛的位置就是观察点,目光落在的点就是视点。我们站着看,上方向 就是朝上(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, // 点 2
0.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。
应用视图矩阵前的效果。因为两者大小相同,黄色三角形完全盖住了红色。
应用视图矩阵后:
结尾
今天简单讲了下让我们指定一个位置观察模型的方法:视图矩阵。
之前我们也讲了一个叫做模型矩阵的玩意,模型矩阵就好比一个三维软件,我们将一个模型导入到场景中,移动它的位置、缩放它的尺寸,旋转一下之类的。视图矩阵就好比通过一个摄像机的视角看到的世界。
不知道你发现没有,这里的两个三角形并没有近大远小的透视效果。此外,当我们的观察点位置非常靠右或靠左的时候,三角形会缺失部分。
关于这点,我会在下节讲解 可视空间,解答这些问题。