使用 WebGL 進行實時視訊影象處理
這是我最近在 CodePen 上製作的 WebGL 演示案例。它可以捕獲網路攝像頭的資料(或在無法訪問網路攝像頭時,從placekitten獲取備用影象),並將其實時轉換為 ASCII 影象藝術。
為了獲得更多的復古性,我使用了 90 年代 DOS PC 中常見的 8x8 畫素光柵字型(您可能會在某些 BIOS 中看到這種字型)。
要將影象內容對映到特定字元,我通過使用亮度圖選擇最佳匹配。我計算每個 4x4 正方形的畫素。在畫板內向下滾動以檢視亮度圖:
我還為這些字型建立了一個編輯器:https://terabaud.github.io/
辦公資源網址導航 https://www.wode007.com
若干 WebGL 基礎知識
我將介紹 WebGL 的一些基礎知識,但這裡僅涉及部分問題。獲取有關詳細指導,建議您訪問https://webglfundamentals.org
對於 WebGL,一個常見誤解是把它當作瀏覽器中的 3D 引擎。儘管 WebGL技術能使我們在瀏覽器中提供 GPU 加速的 3D 內容,但 WebGL 本身不是 3D 引擎。在 WebGL 之上,有專門用於 GPU 加速的 2D 或 3D 內容的圖形庫(例如用於 2D 的 Pixi,用於 3D 的 Threejs)。
WebGL 本身是很基礎的繪圖標準庫,並且是一個以 GPU 加速的方式,將點、線和三角形繪製到
可以通過 getContext(類似於 2D canvas API)檢索 WebGL 渲染上下文:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
一個 WebGL 程式包含多個著色器元件,著色器是執行在 GPU 上的程式碼,它們不是用JavaScript編寫的,而是具有自己的語言,稱為 GLSL(GL 著色器語言)。
GLSL 快速概覽
- 類似 C語言,著色器程式包含 void main()
- 變數宣告也像在 C 語言中一樣
- 原始資料型別:int, float, double
- 向量:vec2, vec3, vec4, ...
- 矩陣:mat2, mat3, mat4, ...
- 訪問紋理資料的型別:sampler2D
- 內建向量、矩陣運算
- 大量內建功能, 例如,求取向量的長度(length(v))
著色器的型別
WebGL 程式中有兩種型別的著色器。
- 頂點著色器計算位置。
- 片段著色器處理柵格化。
如果您的 WebGL 程式想要在螢幕上繪製一個三角形,它會把三角形的 3 個座標傳遞給頂點著色器。然後,片段著色器的任務是用畫素填充該三角形,這種逐畫素處理過程非常快,因為它是針對 GPU 上的每個畫素並行執行處理的。
在我的演示案例中,我使用 4 個向量座標來覆蓋適合整個螢幕的矩形,所有工作都在片段著色器中完成。
頂點著色器
顧名思義,頂點著色器存在於頂點。它從JavaScript程式碼提供的緩衝區中獲取一堆資料,並根據這些資料計算在畫布中的相應位置。
以下程式碼段將資料從緩衝區拉入一個 attribute 變數,並將其傳遞給該 gl_Position 變數:
attribute vec3 position;
void main() {
gl_Position = vec4(position, 1.0);
}
片段著色器
precision highp float;
void main() {
vec2 p = gl_FragCoord.xy;
gl_FragColor = vec4(
1.0, .5 + .5 * sin(p.y), .5 + .5 * sin(p.x), 1.0);
}
片段著色器針對每個片段(畫素)並行執行。在上面的示例中,片段著色器從 gl_FragCoord 變數讀取當前畫素座標,並通過 gl_FragColor 中的 sin() 計算執行並輸出顏色。
gl_FragColor 是一個 vec4 向量,其中包含(紅色,綠色,藍色,alpha),取值各為 0 .. 1。
GLSL 變數的型別
- attribute: 頂點著色器從緩衝區中提取一個值並將其儲存在屬性變數中。
- uniform: 從js端設定統一變數。例如,您可以統一使用諸如當前滑鼠、輕敲位置之類的內容傳遞給著色器。您還可以使用統一變數來訪問從 JavaScript 上傳的紋理資料。
- varying: 將值從頂點傳遞到片段著色器並進行插值。
上傳影象資料
您可以使用影象資料訪問到著色器中的 WebGLRenderingContext,並將其上傳到紋理中。(另請參見:WebGL 基礎知識:影象處理)
您可以使用 texImage2D內部方法 WebGLRenderingContext 將影象資料上傳到紋理中。
// gl is the WebGLRenderingContext
const texture = gl.createTexture()
gl.activeTexture(gl.TEXTURE0 + textureIndex);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
// more info about these parameters in the webglfundamentals
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
您傳遞給 texImage2D 的影象資料,可以是 img 元素、視訊元素、ImageData 等。
由於視訊的影象資料不斷變化,因此您必須在 requestAnimationFrame 動畫迴圈內更新紋理。以下是獲取完成的 texSubImage2D。
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, video);
讀取紋理資料
在著色器程式碼中讀取紋理資料,您可以通過 texture 的 2Dglsl函式訪問紋理的畫素資料。
當紋理座標從(0,0)變為(1,1)時,影象會上下顛倒。同時,我正處於水平映象影象中(就像用相機自拍一樣)。
uniform sampler2D texture0;
void main() {
vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
gl_FragColor = texture2D(texture1, coord);
}
訪問網路攝像頭
要從網路攝像頭獲取影象資料,我們可以使用 video 標籤,並使用 getUserMediaAPI:
function accessWebcam(video) {
return new Promise((resolve, reject) => {
const mediaConstraints = { audio: false, video: {
width: 1280,
height: 720,
brightness: {ideal: 2}
}
};
navigator.mediaDevices.getUserMedia(
mediaConstraints).then(mediaStream => {
video.srcObject = mediaStream;
video.setAttribute('playsinline', true);
video.onloadedmetadata = (e) => {
video.play();
resolve(video);
}
}).catch(err => {
reject(err);
});
}
);
}
// 使用說明:
// const video = await accessWebcam(document.querySelector('video'));
// or via promises:
// accessWebcam(document.querySelector('video')).then(video => { ... });
要訪問網路攝像頭,您可以使用 getUserMedia API 來訪問網路攝像頭,如上所述。
提供後備影象
如果使用者阻止了對網路攝像頭的訪問,或者沒有可用的網路攝像頭,則可以提供一個備用影象供您使用。
我也將 new Image() 中的 onload 操作包裝成一個 promise。
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = url;
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(img);
};
});
}
合併全部操作
為了使事情變得容易一些,我將常用的 WebGL函式放入了我建立的一個小助手庫GLea中。
它初始化 WebGL 應用上下文,編譯 WebGL 著色器程式碼,併為頂點著色器建立屬性和緩衝區:
預設情況下,position 為頂點著色器提供一個屬性,該屬性帶有一個緩衝區,該緩衝區包含 4 個 2D 座標,覆蓋整個螢幕上的 2 個三角形。
import GLea from 'glea.js';
const frag = ` ... `; // 片段著色器程式碼
const vert = ` ... `; // 頂點著色器程式碼
const glea = new GLea({
shaders: [
GLea.fragmentShader(frag),
GLea.vertexShader(vert)
]
}).create();
function loop(time = 0) {
const { gl, width, height } = glea;
glea.clear();
glea.uniV('resolution', [width, height]);
glea.uni('time', time * 1e-3);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(loop);
}
window.addEventListener('resize', () => {
glea.resize();
});
loop(0);
結論
基本上就是這樣。我希望您喜歡閱讀本文,並對自己探索 WebGL 感到好奇。我會在這裡放一些資源。
如果我還有沒介紹到的內容,請隨時發表補充評論 =)