這是我最近在 CodePen 上制作的 WebGL 演示案例。它可以捕獲網(wǎng)絡(luò)攝像頭的數(shù)據(jù)(或在無法訪問網(wǎng)絡(luò)攝像頭時(shí),從 placekitten 獲取備用圖像),并將其實(shí)時(shí)轉(zhuǎn)換為 ASCII 圖像藝術(shù)。
為了獲得更多的復(fù)古性,我使用了 90 年代 DOS PC 中常見的 8x8 像素光柵字體(您可能會(huì)在某些 BIOS 中看到這種字體)。
要將圖像內(nèi)容映射到特定字符,我通過使用亮度圖選擇最佳匹配。我計(jì)算每個(gè) 4x4 正方形的像素。在畫板內(nèi)向下滾動(dòng)以查看亮度圖:
我還為這些字體創(chuàng)建了一個(gè)編輯器: https://terabaud.github.io/pi...
若干 WebGL 基礎(chǔ)知識(shí)
我將介紹 WebGL 的一些基礎(chǔ)知識(shí),但這里僅涉及部分問題。獲取有關(guān)詳細(xì)指導(dǎo),建議您訪問 https://webglfundamentals.org
對(duì)于 WebGL,一個(gè)常見誤解是把它當(dāng)作瀏覽器中的 3D 引擎。盡管 WebGL 技術(shù)能使我們?cè)跒g覽器中提供 GPU 加速的 3D 內(nèi)容,但 WebGL 本身不是 3D 引擎。在 WebGL 之上,有專門用于 GPU 加速的 2D 或 3D 內(nèi)容的圖形庫(kù)(例如用于 2D 的 Pixi,用于 3D 的 ThreeJS)。
WebGL 本身是很基礎(chǔ)的繪圖標(biāo)準(zhǔn)庫(kù),并且是一個(gè)以 GPU 加速的方式,將點(diǎn)、線和三角形繪制到 html <canvas> 元素上的庫(kù)。
可以通過 getContext (類似于 2D canvas API )檢索 WebGL 渲染上下文:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
一個(gè) WebGL 程序包含多個(gè)著色器組件,著色器是運(yùn)行在 GPU 上的代碼,它們不是用 JAVAScript 編寫的,而是具有自己的語言,稱為 GLSL(GL 著色器語言)。
GLSL 快速概覽
- 類似 C語言,著色器程序包含 void main()
- 變量聲明也像在 C 語言中一樣
- 原始數(shù)據(jù)類型: int , float , double
- 向量: vec2 , vec3 , vec4 , ...
- 矩陣: mat2 , mat3 , mat4 , ...
- 訪問紋理數(shù)據(jù)的類型: sampler2D
- 內(nèi)置向量、矩陣運(yùn)算
- 大量?jī)?nèi)置功能 , 例如,求取向量的長(zhǎng)度( length(v) )
著色器的類型
WebGL 程序中有兩種類型的著色器。
- 頂點(diǎn)著色器計(jì)算位置。
- 片段著色器處理柵格化。
如果您的 WebGL 程序想要在屏幕上繪制一個(gè)三角形,它會(huì)把三角形的 3 個(gè)坐標(biāo)傳遞給頂點(diǎn)著色器。然后,片段著色器的任務(wù)是用像素填充該三角形,這種逐像素處理過程非???,因?yàn)樗轻槍?duì) GPU 上的每個(gè)像素并行運(yùn)行處理的。
在我的演示案例中,我使用 4 個(gè)矢量坐標(biāo)來覆蓋適合整個(gè)屏幕的矩形,所有工作都在片段著色器中完成。
頂點(diǎn)著色器
顧名思義,頂點(diǎn)著色器存在于頂點(diǎn)。它從 JavaScript 代碼提供的緩沖區(qū)中獲取一堆數(shù)據(jù),并根據(jù)這些數(shù)據(jù)計(jì)算在畫布中的相應(yīng)位置。
以下代碼段將數(shù)據(jù)從緩沖區(qū)拉入一個(gè) 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);
}
片段著色器針對(duì)每個(gè)片段(像素)并行運(yùn)行。在上面的示例中,片段著色器從 gl_FragCoord 變量讀取當(dāng)前像素坐標(biāo),并通過 gl_FragColor 中的 sin() 計(jì)算運(yùn)行并輸出顏色。
gl_FragColor 是一個(gè) vec4 向量,其中包含(紅色,綠色,藍(lán)色,alpha),取值各為 0 .. 1。
GLSL 變量的類型
attribute
uniformvarying
上傳圖像數(shù)據(jù)
您可以使用圖像數(shù)據(jù)訪問到著色器中的 WebGLRenderingContext,并將其上傳到 紋理 中。(另請(qǐng)參見: WebGL 基礎(chǔ)知識(shí):圖像處理 )
您可以使用 texImage2D 內(nèi)部方法 WebGLRenderingContext 將圖像數(shù)據(jù)上傳到紋理中。
// 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 的圖像數(shù)據(jù),可以是 img 元素、視頻元素、ImageData 等。
由于視頻的圖像數(shù)據(jù)不斷變化,因此您必須在 requestAnimationFrame 動(dòng)畫循環(huán)內(nèi)更新紋理。以下是獲取完成的 texSubImage2D 。
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, video);
在著色器代碼中讀取紋理數(shù)據(jù)
您可以通過 texture 的 2Dglsl 函數(shù)訪問紋理的像素?cái)?shù)據(jù)。
當(dāng)紋理坐標(biāo)從(0,0)變?yōu)椋?,1)時(shí),圖像會(huì)上下顛倒。同時(shí),我正處于水平鏡像圖像中(就像用相機(jī)自拍一樣)。
uniform sampler2D texture0;
void main() {
vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
gl_FragColor = texture2D(texture1, coord);
}
訪問網(wǎng)絡(luò)攝像頭
要從網(wǎng)絡(luò)攝像頭獲取圖像數(shù)據(jù),我們可以使用 video 標(biāo)簽,并使用 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 => { ... });
要訪問網(wǎng)絡(luò)攝像頭,您可以使用 getUserMedia API 來訪問網(wǎng)絡(luò)攝像頭,如上所述。
提供后備圖像
如果用戶阻止了對(duì)網(wǎng)絡(luò)攝像頭的訪問,或者沒有可用的網(wǎng)絡(luò)攝像頭,則可以提供一個(gè)備用圖像供您使用。
我也將 new Image() 中的 onload 操作包裝成一個(gè) 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 函數(shù)放入了我創(chuàng)建的一個(gè)小助手庫(kù) GLea 中。
它初始化 WebGL 應(yīng)用上下文,編譯 WebGL 著色器代碼,并為頂點(diǎn)著色器創(chuàng)建屬性和緩沖區(qū):
默認(rèn)情況下, position 為頂點(diǎn)著色器提供一個(gè)屬性,該屬性帶有一個(gè)緩沖區(qū),該緩沖區(qū)包含 4 個(gè) 2D 坐標(biāo),覆蓋整個(gè)屏幕上的 2 個(gè)三角形。
import GLea from 'glea.js';
const frag = ` ... `; // 片段著色器代碼
const vert = ` ... `; // 頂點(diǎn)著色器代碼
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);
結(jié)論
基本上就是這樣。我希望您喜歡閱讀本文,并對(duì)自己探索 WebGL 感到好奇。我會(huì)在這里放一些資源。