一起學 WebGL:可視空間之正射投影
嗨,大家好呀,我是你們的前端西瓜哥啊。上一節我們學習了 視圖矩陣,通過它我們可以像一個自由的攝像機一樣,可以在三維世界的任意位置觀察目標模型,但也遇到了一些問題。
其中一個問題就是,在超過某個臨界值時。三角形會出現殘缺的現象,甚至直接不見了,見下圖。這是為什么呢?
對應源碼:
https://codesandbox.io/s/j86roh?file=/index.js。
可視空間
上面殘缺的情況會在 z 變得比較大時出現。所謂視圖矩陣,其實利用的是一種相對關系。
我們沒有移動,坐標系的原點還是在畫布的中心位置,但視圖矩陣可以計算出模型新的位置,給一個更小的 z,其實就是這個模型的所有頂點在遠離我們。
但是,部分像素超出了某個范圍,就被 WebGL 直接忽略掉了。
這個范圍是多少呢?是 [-1, 1] 區間(大于等于 -1,小于等于 1),x、y、z 維都是這個范圍。超出這個范圍的像素,一律不畫。
利用這個性質,我們可以將一些不需要出現在畫面中的物體刪除。比如一些遙遠的遠景,或是跑到視口外的模型,不繪制它們能提高繪制效率。
正視投影
長方體可視空間是一種常用的可視空間,它由 正射投影(Orthographic Projection) 產生。此外還有更常用的 透視投影,我們后面再聊。
正視投影的作用是:將一個指定尺寸的盒子,映射壓縮成 [-1, 1] 范圍的盒子,使其中的像素點全部可以繪制出來,之外的像素點則被拋棄。
正射投影的矩陣公式為:
其中 r、l 等值,對應 left、right、top、bottom、near、far 的縮寫。
JavaScript 代碼實現為:
function createOrthoMatrix(left, right, bottom, top, near, far) {
const width = right - left;
const height = top - bottom;
const depth = far - near;
// prettier-ignore
return new Float32Array([
2 / width, 0, 0, 0,
0, 2 / height, 0, 0,
0, 0, -2 / depth, 0,
-(right + left) / width, -(top + bottom) / height, -(far + near) / depth, 1
]);
}
這里不得不感謝 Github Colipot 提供的支持了。
實驗
我們來寫個 demo,來體驗一下透視矩陣的效果。
為了不引入過多的復雜度,我們只繪制兩個三角形,且不引入視圖矩陣。然后可以通過上下方向鍵改變可視空間的 near 和 far 值。此時我們就能看到三角形消失的現象,因為它跑出我們的可視空間了。
兩個三角形都平行于 xy 屏幕,只是 z 不同,分別為 0.2 和 -0.2。紅色在上邊,黃色在下邊。
兩個三角形的緩沖區數據為:
const verticesColors = new Float32Array([
// 底下的黃色三角形
0, 0.5, -0.1, 1, 1, 0, // 點 1 的位置和顏色信息
-0.5, -0.5, -0.1, 1, 1, 0, // 點 2
0.5, -0.5, -0.1, 1, 1, 0, // 點 3
// 上邊的紅色三角形
-0.5, 0.25, 0.1, 1, 0, 0,
0.5, 0.25, 0.1, 1, 0, 0,
0, -0.75, 0.1, 1, 0, 0,
]);
效果如下:
這些點都在 -1 到 1 的范圍內。所以能全部渲染出來。
頂點著色器要加一個保存正射投影矩陣的 mat4 矩陣類型變量,因為不需要改變,所以使用 uniform 關鍵字。
const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_OrthoMatrix; // 正射投影矩陣
varying vec4 v_Color;
void main() {
gl_Position = u_OrthoMatrix * a_Position;
v_Color = a_Color;
}
`;
然后綁定鍵盤按下事件:
window.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowUp':
near += 0.1;
far += 0.1;
break;
case 'ArrowDown':
near -= 0.1;
far -= 0.1;
break;
}
near = Math.round(near * 10) / 10; // 消除浮點數誤差
far = Math.round(far * 10) / 10;
render();
});
渲染函數為:
function render() {
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, SIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, SIZE * 6, SIZE * 3);
gl.enableVertexAttribArray(a_Color);
/****** 正射投影 ******/
// prettier-ignore
const orthoMatrix = createOrthoMatrix(
// 左右上下近遠
-1, 1, -1, 1, near, far
)
gl.uniformMatrix4fv(u_OrthoMatrix, false, orthoMatrix);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
不斷地按下方向鍵,near 和 far 越來越小,即可視空間離我們越來越遠。
可以看到,當 near 從 0.1 變成 0 后,紅色三角形不見了,黃色的還在。因為我們一開始給紅色三角形設置的 z 為 0.1,已經不在可視范圍內了。
同理,當 far 越來越大,從 -0.1 變成 0 后,黃色三角形則不見了。
源碼實現
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
const infoDiv = document.createElement("div");
document.body.appendChild(infoDiv);
const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_OrthoMatrix; // 正射投影矩陣
varying vec4 v_Color;
void main() {
gl_Position = u_OrthoMatrix * a_Position;
v_Color = a_Color;
}
`;
const fragmentShaderSrc = `
precision highp 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.5, -0.1, 1, 1, 0, // 點 1 的位置和顏色信息
-0.5, -0.5, -0.1, 1, 1, 0, // 點 2
0.5, -0.5, -0.1, 1, 1, 0, // 點 3
// 上邊的紅色三角形
-0.5, 0.25, 0.1, 1, 0, 0,
0.5, 0.25, 0.1, 1, 0, 0,
0, -0.75, 0.1, 1, 0, 0,
]);
// 每個數組元素的字節數
const SIZE = verticesColors.BYTES_PER_ELEMENT;
const orthoMatrix = createOrthoMatrix(-1, 1, -1, 1, 1.05, -1);
// 創建緩存對象
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");
const a_Color = gl.getAttribLocation(gl.program, "a_Color");
const u_OrthoMatrix = gl.getUniformLocation(gl.program, "u_OrthoMatrix");
let near = 1;
let far = -1;
// 初次渲染
render();
window.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowUp":
near += 0.1;
far += 0.1;
break;
case "ArrowDown":
near -= 0.1;
far -= 0.1;
break;
}
near = Math.round(near * 10) / 10; // 消除浮點數誤差
far = Math.round(far * 10) / 10;
render();
});
function render() {
infoDiv.innerHTML = `Near: ${near}, Far: ${far}`;
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, SIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, SIZE * 6, SIZE * 3);
gl.enableVertexAttribArray(a_Color);
/****** 正射投影 ******/
// prettier-ignore
const orthoMatrix = createOrthoMatrix(
// 左右上下近遠
-1, 1, -1, 1, near, far
)
gl.uniformMatrix4fv(u_OrthoMatrix, false, orthoMatrix);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪制三角形
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
/********* 構造正射投影 *********/
function createOrthoMatrix(left, right, bottom, top, near, far) {
const width = right - left;
const height = top - bottom;
const depth = far - near;
// prettier-ignore
return new Float32Array([
2 / width, 0, 0, 0,
0, 2 / height, 0, 0,
0, 0, -2 / depth, 0,
-(right + left) / width, -(top + bottom) / height, -(far + near) / depth, 1
]);
}
線上體驗 demo:
https://codesandbox.io/s/f1q0ct?file=/index.js。
回到開頭
回到開頭,我們在使用視圖矩陣的時候,修改了模型的頂點坐標,這個坐標容易跑到可視空間外,這就是上面三角形出現殘缺的問題。
我們來看看到底發生了什么事。
從表現上看,根據前面我們學到的知識,應該是左下角的點超出了 -1 到 1 的范圍。
我們確認一下,用計算出來的視圖矩陣乘以左下角點 (-0.2, -0.2, 0, 1):
矩陣運算示意圖用 Matrix Calculator 網頁工具生成。
我們只要加個正視投影矩陣,并將 near 設置為大于 1.04946 的值即可繪制一個完整的三角形啦。
const orthoMatrix = createOrthoMatrix(
-1, 1, -1, 1, 1.05, -1
)
這個 near 再小一丟丟就會導致殘缺,讀者可以去 demo 中修改代碼驗證。
線上體驗 demo:
https://codesandbox.io/s/6i9n2y。