這是我最近在 CodePen 上製作的 WebGL 演示案例。它可以捕獲網絡攝像頭的數據(或在無法訪問網絡攝像頭時,從 placekitten 獲取備用圖像),並將其實時轉換為 ASCII 圖像藝術。
為了獲得更多的復古性,我使用了 90 年代 DOS PC 中常見的 8x8 像素光柵字體(您可能會在某些 BIOS 中看到這種字體)。
要將圖像內容映射到特定字符,我通過使用亮度圖選擇最佳匹配。我計算每個 4x4 正方形的像素。在畫板內向下滾動以查看亮度圖:
我還為這些字體創建了一個編輯器: https://terabaud.github.io/pi...
我將介紹 WebGL 的一些基礎知識,但這裡僅涉及部分問題。獲取有關詳細指導,建議您訪問 https://webglfundamentals.org
對於 WebGL,一個常見誤解是把它當作瀏覽器中的 3D 引擎。儘管 WebGL 技術能使我們在瀏覽器中提供 GPU 加速的 3D 內容,但 WebGL 本身不是 3D 引擎。在 WebGL 之上,有專門用於 GPU 加速的 2D 或 3D 內容的圖形庫(例如用於 2D 的 Pixi,用於 3D 的 ThreeJS)。
WebGL 本身是很基礎的繪圖標準庫,並且是一個以 GPU 加速的方式,將點、線和三角形繪製到 html <canvas> 元素上的庫。
可以通過 getContext (類似於 2D canvas API )檢索 WebGL 渲染上下文:
const canvas = document.querySelector(&39;);const gl = canvas.getContext(&39;) || canvas.getContext(&39;);
一個 WebGL 程序包含多個著色器組件,著色器是運行在 GPU 上的代碼,它們不是用 JavaScript 編寫的,而是具有自己的語言,稱為 GLSL(GL 著色器語言)。
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。
attributeuniformvarying
您可以使用圖像數據訪問到著色器中的 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 webglfundamentalsgl.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(&39;, true); video.onloadedmetadata = (e) => { video.play(); resolve(video); } }).catch(err => { reject(err); }); } );}// 使用說明:// const video = await accessWebcam(document.querySelector(&39;));// or via promises:// accessWebcam(document.querySelector(&39;)).then(video => { ... });
要訪問網絡攝像頭,您可以使用 getUserMedia API 來訪問網絡攝像頭,如上所述。
如果用戶阻止了對網絡攝像頭的訪問,或者沒有可用的網絡攝像頭,則可以提供一個備用圖像供您使用。
我也將 new Image() 中的 onload 操作包裝成一個 promise 。
function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = &39;; img.src = url; img.onload = () => { resolve(img); }; img.onerror = () => { reject(img); }; });}
為了使事情變得容易一些,我將常用的 WebGL 函數放入了我創建的一個小助手庫 GLea 中。
它初始化 WebGL 應用上下文,編譯 WebGL 著色器代碼,並為頂點著色器創建屬性和緩衝區:
默認情況下, position 為頂點著色器提供一個屬性,該屬性帶有一個緩衝區,該緩衝區包含 4 個 2D 坐標,覆蓋整個屏幕上的 2 個三角形。
import GLea from &39;;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(&39;, [width, height]); glea.uni(&39;, time * 1e-3); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(loop);}window.addEventListener(&39;, () => { glea.resize();});loop(0);
基本上就是這樣。我希望您喜歡閱讀本文,並對自己探索 WebGL 感到好奇。我會在這裡放一些資源。