您现在的位置是:首页 >技术杂谈 >一起学 WebGL:感受三维世界之视图矩阵网站首页技术杂谈

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

前端西瓜哥 2023-06-03 08:00:03
简介一起学 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,  // 点 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。

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

应用视图矩阵后:

结尾

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

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

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

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

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

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。