一起學(xué) WebGL:感受三維世界之視圖矩陣
大家好,我是前端西瓜哥。之前繪制的圖形都是在 XY 軸所在的平面上,這次我們來加入一點(diǎn)深度信息 z,帶你走入三維的世界。
視圖矩陣
對于一個(gè)立方體來說,我們從它的正前方看,不管距離它多遠(yuǎn),也只能看到一個(gè)二維的正方形。因此我們需要引入 視圖矩陣(view matrix)。它的作用就像是一個(gè)在特定位置的攝像頭。
視圖矩陣需要三個(gè)信息:
- 視點(diǎn)位置;
- 觀察點(diǎn)位置;
- 上方向;
就好比我們站在某個(gè)位置看一個(gè)模型,眼睛的位置就是觀察點(diǎn),目光落在的點(diǎn)就是視點(diǎn)。我們站著看,上方向 就是朝上(y 正軸方向),躺著看就是水平方向,倒立著看就是朝下(y 負(fù)半軸方向)。
實(shí)際上我們并沒有一個(gè)真正的視口,我們的世界坐標(biāo)的正中心永遠(yuǎn)是原點(diǎn),z 負(fù)半軸指向觀察者。
但我們可以利用相對運(yùn)動(dòng)的原理,給圖形做一個(gè)相反的操作,比如我往右邊走 1 個(gè)單位去看模型,其實(shí)等價(jià)于我不懂,模型向左移動(dòng) 1 個(gè)單位,它們的效果是一樣的。
視圖矩陣的算法實(shí)現(xiàn)如下:
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
]);
}
視圖坐標(biāo)的實(shí)現(xiàn)細(xì)節(jié)不講,不重要。(順帶一提,上面的算法由 Github Copilot 生成)
通過這個(gè)方法計(jì)算出矩陣,傳入到頂點(diǎn)著色器的矩陣變量中,和頂點(diǎn)位置計(jì)算即可。
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);
其他的創(chuàng)建緩沖區(qū)的邏輯就不講了,之前的文章都講過了。
完整代碼
貼一下完整代碼:
/** @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;
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點(diǎn)渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?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, // 點(diǎn) 1 的位置和顏色信息
-0.2, -0.2, 0, 1, 1, 0, // 點(diǎn) 2
0.2, -0.2, 0, 1, 1, 0, // 點(diǎn) 3
]);
// 每個(gè)數(shù)組元素的字節(jié)數(shù)
const SIZE = verticesColors.BYTES_PER_ELEMENT;
// 創(chuàng)建緩存對象
const vertexColorBuffer = gl.createBuffer();
// 綁定緩存對象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向緩存區(qū)寫入數(shù)據(jù)
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。
這里我繪制了紅色和黃色兩個(gè)三角形,紅色在更下邊,z 為 -0.2,黃色在上面一點(diǎn),z 為 0。
應(yīng)用視圖矩陣前的效果。因?yàn)閮烧叽笮∠嗤S色三角形完全蓋住了紅色。
應(yīng)用視圖矩陣后:
結(jié)尾
今天簡單講了下讓我們指定一個(gè)位置觀察模型的方法:視圖矩陣。
之前我們也講了一個(gè)叫做模型矩陣的玩意,模型矩陣就好比一個(gè)三維軟件,我們將一個(gè)模型導(dǎo)入到場景中,移動(dòng)它的位置、縮放它的尺寸,旋轉(zhuǎn)一下之類的。視圖矩陣就好比通過一個(gè)攝像機(jī)的視角看到的世界。
不知道你發(fā)現(xiàn)沒有,這里的兩個(gè)三角形并沒有近大遠(yuǎn)小的透視效果。此外,當(dāng)我們的觀察點(diǎn)位置非??坑一蚩孔蟮臅r(shí)候,三角形會(huì)缺失部分。
關(guān)于這點(diǎn),我會(huì)在下節(jié)講解 可視空間,解答這些問題。