1、作用:将物体从世界坐标系转换到相机坐标系,相当于从世界坐标系转换到相机的局部(本地)坐标系。
2、基于LookAt函数的视图矩阵:
相机位置eye:(ex,ey,ez),世界坐标系下的位置
目标位置center:(cx,cy,cz),这是相机朝向的点,也是世界坐标系下的位置
上方向up:(ux,uy,uz),用于定义相机的上方向,一般都是世界坐标系的上方向,这样相机才是正对着物体而不是倾斜的
构建视图矩阵的步骤如下:
- 计算相机的方向向量(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=∣eye−center∣eye−center - 计算相机的右方向向量(x轴方向,camera right,相机坐标系的“右方向”在世界坐标系中的表示):通过向上方向和摄像机方向的叉积,计算出相机的右方向,用于定义新的x轴
x = u p × z ∣ u p × z ∣ \mathbf{x}=\frac{\mathbf{up}×\mathbf{z}}{|\mathbf{up}×\mathbf{z}|} x=∣up×z∣up×z - 计算相机的上方向向量(y轴方向,camera up,相机坐标系的“上方向”在世界坐标系中的表示):通过相机的Z轴和X轴的叉积,得到新的Y轴方向
y = z × x \mathbf{y}=\mathbf{z}×\mathbf{x} y=z×x - 构建视图矩阵:根据上面计算的向量,视图矩阵可以写成如下形式:
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= xxyxzx0xyyyzy0xzyzzz0−x⋅eye−y⋅eye−z⋅eye1
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>