Bootstrap

2、顶点着色器之视图矩阵

1、作用:将物体从世界坐标系转换到相机坐标系,相当于从世界坐标系转换到相机的局部(本地)坐标系。

2、基于LookAt函数的视图矩阵:

相机位置eye:(ex,ey,ez),世界坐标系下的位置

目标位置center:(cx,cy,cz),这是相机朝向的点,也是世界坐标系下的位置

上方向up:(ux,uy,uz),用于定义相机的上方向,一般都是世界坐标系的上方向,这样相机才是正对着物体而不是倾斜的

构建视图矩阵的步骤如下:

  1. 计算相机的方向向量(z轴方向,camera direction,相机坐标系的“视线方向”在世界坐标系中的表示):从相机位置到目标位置的反方向,用于定义新的Z轴
    z = e y e − c e n t e r ∣ e y e − c e n t e r ∣ \mathbf{z}=\frac{\mathbf{eye}-\mathbf{center}}{|\mathbf{eye}-\mathbf{center}|} z=eyecentereyecenter
  2. 计算相机的右方向向量(x轴方向,camera right,相机坐标系的“右方向”在世界坐标系中的表示):通过向上方向和摄像机方向的叉积,计算出相机的右方向,用于定义新的x轴
    x = u p × z ∣ u p × z ∣ \mathbf{x}=\frac{\mathbf{up}×\mathbf{z}}{|\mathbf{up}×\mathbf{z}|} x=up×zup×z
  3. 计算相机的上方向向量(y轴方向,camera up,相机坐标系的“上方向”在世界坐标系中的表示):通过相机的Z轴和X轴的叉积,得到新的Y轴方向
    y = z × x \mathbf{y}=\mathbf{z}×\mathbf{x} y=z×x
  4. 构建视图矩阵:根据上面计算的向量,视图矩阵可以写成如下形式:
    V i e w M a t r i x = [ x x x y x z − x ⋅ e y e y x y y y z − y ⋅ e y e z x z y z z − z ⋅ e y e 0 0 0 1 ] \mathbf{ViewMatrix}=\begin{bmatrix} x_x & x_y & x_z & −\mathbf{x⋅eye}\\ y_x & y_y & y_z & −\mathbf{y⋅eye}\\ z_x & z_y & z_z & −\mathbf{z⋅eye}\\ 0&0&0&1\end{bmatrix} ViewMatrix= xxyxzx0xyyyzy0xzyzzz0xeyeyeyezeye1
    3、示例代码:
// matrix.js
const regPos = /^-?\d+(\.\d+)?$/; // 支持整数和浮点数,支持负号
function isVector3D(vector) {
  if (!Array.isArray(vector)) return false;
  if (vector.length != 3) return false;
  return (
    regPos.test(vector[0]) && regPos.test(vector[1]) && regPos.test(vector[2])
  );
}

function normalized(vector) {
  if (!isVector3D(vector)) return null;

  const vectorLength = Math.sqrt(
    Math.pow(vector[0], 2) + Math.pow(vector[1], 2) + Math.pow(vector[2], 2)
  );
  return vector.map((item) => {
    return item / vectorLength;
  });
}

function cross(v1, v2) {
  if (!isVector3D(v1)) return null;
  if (!isVector3D(v2)) return null;
  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],
  ];
}

function dot(v1, v2) {
  if (!isVector3D(v1)) return null;
  if (!isVector3D(v2)) return null;
  return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
}

function lookAt(eye, target, up = [0, 1, 0]) {
  if (!isVector3D(eye)) return null;
  if (!isVector3D(target)) return null;
  if (!isVector3D(up)) return null;

  const eyeMinusTarget = eye.map((item, index) => item - target[index]);
  const z = normalized(eyeMinusTarget); // Z轴
  const x = normalized(cross(up, z)); // X轴
  const y = cross(z, x); // Y轴

  // glsl中的mat4类型是列主序的,这里也要改为列主序
  return new Float32Array([
    x[0],
    y[0],
    z[0],
    0,
    x[1],
    y[1],
    z[1],
    0,
    x[2],
    y[2],
    z[2],
    0,
    -dot(x, eye),
    -dot(y, eye),
    -dot(z, eye),
    1,
  ]);
}

export { isVector3D, normalized, dot, cross, lookAt };
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL2 视图矩阵示例</title>
    <style>
        html,
        body {
            margin: 0;
            overflow: hidden;
        }

        canvas {
            position: fixed;
            top: 0;
            left: 0;
            outline: none;
            width: 100%;
            height: 100%;
        }
    </style>
</head>

<body>
    <canvas id="webgl-canvas"></canvas>
    <div style="display: flex;position: fixed;left: 10px;top: 10px;">
        <button id="front">从正面看</button>
        <button id="back">从背面看</button>

    </div>
    <script type="module">
        import { lookAt } from './matrix.js'
        const canvas = document.getElementById("webgl-canvas");
        const gl = canvas.getContext("webgl2");

        if (!gl) {
            console.log("WebGL2 not supported, falling back on WebGL");
        }

        const vertexShaderSource = `#version 300 es
        in vec4 aPosition;
        uniform mat4 uViewMatrix;
        void main() {
            gl_Position = uViewMatrix * aPosition;
        }`;

        const fragmentShaderSource = `#version 300 es
        precision highp float;
        out vec4 outColor;
        void main() {
            outColor = vec4(1.0, 0.0, 0.0, 1.0);  // Red color
        }`;

        function createShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error("Shader compile failed:", gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        function createProgram(gl, vertexShader, fragmentShader) {
            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error("Program link failed:", gl.getProgramInfoLog(program));
                gl.deleteProgram(program);
                return null;
            }
            return program;
        }

        const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
        const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
        const program = createProgram(gl, vertexShader, fragmentShader);

        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        const size = 0.5;
        const positions = new Float32Array([
            -size, -size, size,
            size, -size, size,
            -size, size, size,
            size, -size, size,
            size, size, size,
            -size, size, size,
        ]);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

        const vao = gl.createVertexArray();
        gl.bindVertexArray(vao);

        const aPosition = gl.getAttribLocation(program, "aPosition");
        gl.enableVertexAttribArray(aPosition);
        gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);

        gl.useProgram(program);

        // 根据设备的像素比率调整 canvas 尺寸,否则很模糊
        const pixelRatio = window.devicePixelRatio || 1;
        canvas.width = canvas.clientWidth * pixelRatio;
        canvas.height = canvas.clientHeight * pixelRatio;
        gl.viewport(0, 0, canvas.width, canvas.height);

        // 设置视图矩阵
        const uViewMatrix = gl.getUniformLocation(program, "uViewMatrix");
        let viewMatrix = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ])
        gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix);

        document.getElementById("back").onclick = (e) => {
            // 相机在红色矩形的后面,由于启用了背面剔除,所以看不到
            viewMatrix = lookAt([0, 0, -1], [0, 0, 0], [0, 1, 0])
            gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix);
            alert("相机在(0,0,-1)处看向(0,0,0)处,相机在红色矩形的后面,由于启用了背面剔除,所以看不到")
        }
        document.getElementById("front").onclick = (e) => {
            // 相机在红色矩形的前面,可以看到
            viewMatrix = lookAt([0, 0, 1], [0, 0, 0], [0, 1, 0])
            gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix);
            alert("相机在(0,0,-1)处看向(0,0,0)处,相机在红色矩形的前面,所以可以看到")
        }

        function render() {
            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);

            gl.enable(gl.DEPTH_TEST);       // 开启深度测试,防止面重叠
            gl.enable(gl.CULL_FACE);        // 开启背面剔除
            gl.cullFace(gl.BACK);           // 剔除背面

            gl.bindVertexArray(vao);
            gl.drawArrays(gl.TRIANGLES, 0, 6);

            requestAnimationFrame(render);
        }

        render();
    </script>
</body>

</html>

在这里插入图片描述

在这里插入图片描述

;