大家好,我是前端西瓜哥。
今天我們來入門 WebGPU,來寫一個圖形版本的 Hello World,即繪制一個三角形。
WebGPU 是一個正在開發中的潛在 Web 標準和 JavaScript API,目標是提供 “現代化的 3D 圖形和計算能力”。
簡單來說,WebGPU 提供一個更現代的 Web 上的圖形渲染標準。
WebGPU 的出現就是為了取代 WebGL 的,因為后者的 API 實在有些過時,無法利用好現代 GPU 的一些高級特性,本身的 API 設計也較難使用。
相比 WebGL,WebGPU 有更好的性能表現,API 更底層更靈活,并支持更高級的現代特性,比如計算著色器。
毫無疑問,WebGPU 是前端圖形渲染的未來,值得去學習一下。
像是以性能著稱的前端圖形庫 PixiJS,也開始進行支持 WebGPU 的工作,并在最近發布了預覽版本,聲稱性能將是 WebGL 的 2.5 倍。
不過目前 WebGPU 還不夠成熟,仍有許多工作要做,且只有少數瀏覽器的最新版本直接支持或通過設置開啟。
即使之后所有瀏覽器都支持了,舊版本瀏覽器還是不支持的,離大范圍使用還有相當長的一段路要走。
只能說未來可期。
但生產中,我們可以做一個回退機制:如果瀏覽器支持 WebGPU,我們用 WebGPU 去渲染,如果不支持就回滾到 WebGL。
只要在底層渲染方案上封裝一層渲染器 renderer,就像 PixiJS 現在做的事情一樣,個人還是比較期待它在性能上的提升的。
OK,我們開始用 WebGPU 繪制一個三角形。
確保你的瀏覽器支持 WebGPU,建議用 Chrome,并更新到最新版本。
這里我們創建一個寬高各為 300 的 canvas 元素,用于繪制圖形。
<canvas width="300" height="300"></canvas>
初始化 WebGPU 相關的一些對象。
創建一個適配器對象 adapter,適配器是一個 GPU 物理硬件設備的抽象。
const adapter = await navigator.gpu.requestAdapter();
requestAdapter() 方法會查看系統上所有可用的 GPU 設備,并選擇其中合適的適配器。該方法可以傳一些參數,去按條件匹配。比如 { powerPreference: 'low-power' } 表示優先使用低能耗的 GPU。
此外,這個方法返回的是一個 Promise,即它是 異步的,需要用 await 的方式去等待異步的結果。
然后基于 adapter,調用 requestDevice 方法拿到設備對象 device。
device 可以理解為 adapter 的一個會話。做個比喻的話 adapter 是一個公司,device 是一個具體干活的人。
const device = await adapter.requestDevice();
requestDevice() 方法也可以傳入配置項,去開啟一些高級特性,或是指定一些硬件限制,比如最大紋理尺寸。
類似 canvas 2d 和 webgl,我們需要通過 canvas 元素拿到上下文。
const canvas = document.querySelector('canvas');const ctx = canvas.getContext('webgpu');
接著是調用 ctx.configure() 方法配置剛剛聲明的 device 對象和像素格式。
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();// 給上下文配置 device 對象和ctx.configure({ device, format: canvasFormat,});
navigator.gpu.getPreferredCanvasFormat() 會返回當前環境合適的像素格式的字符串標識,通常是 'bgra8unorm',表示用 8 位無符號整數來表示藍色、綠色、紅色和透明度四個分量。
創建命令編碼器 GPUCommandEncoder 實例,它用于編碼需要提交給 GPU 的命令。
const encoder = device.createCommandEncoder();
開啟一個新的渲染通道(Render Pass),這里清空顏色緩沖區時填充了一個淺藍色背景。
和 WebGL 一樣,使用 RGBA 的格式,每個分量為 0 到 1 的范圍,比如 { r: 1, g: 0, b: 0, a: 1 } 表示紅色,或者你可以用數組的形式 [1, 0, 0, 1]。
const pass = encoder.beginRenderPass({ // 顏色附件,一個用于存儲渲染輸出顏色數據的紋理 colorAttachments: [ { // 要渲染到的目標 view: ctx.getCurrentTexture().createView(), // 渲染前清空顏色緩沖區 loadOp: 'clear', // 清除顏色為淺藍色,不設置會默認使用黑色 clearValue: { r: 0.6, g: 0.8, b: 0.9, a: 1 }, // 渲染結果會被保留在紋理中,后序好繪制到 canvas 上 storeOp: 'store', }, ],});
我們先不繪制三角形,看看背景的渲染效果,為此我們提前執行下面代碼:
// 這里是繪制三角形的代碼,之后會實現pass.end(); // 完成指令隊列的記錄const commandBuffer = encoder.finish(); // 結束編碼device.queue.submit([commandBuffer]); // 提交給 GPU 命令隊列
遠峰藍。
先說說 WebGPU 的坐標系,它和 WebGL 一樣,原點在畫布中心,x 軸向右,y 軸向上,取值范圍都是 -1 到 1。
聲明頂點數據。這些頂點為組成三角形的三個坐標。
const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, 0.5, 0.5,]);
然后創建頂點緩沖區:
const vertexBuffer = device.createBuffer({ // 標識,字符串隨意寫,報錯時會通過它定位 label: 'Triangle Vertices', // 緩沖區大小,這里是 24 字節。6 個 4 字節(即 32 位)的浮點數 size: vertices.byteLength, // 標識緩沖區用途(1)用于頂點著色器(2)可以從 CPU 復制數據到緩沖區 usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,});
label 方便我們定位錯誤位置:
接著是將頂點數據復制到緩沖區:
device.queue.writeBuffer(vertexBuffer, /* bufferOffset */ 0, vertices);
參數 bufferOffset 表示緩沖區偏移多少字節數的位置寫入數據。
設置緩沖區的讀取方式。
const vertexBufferLayout = { // 每組讀 8 個字節。一個坐標為兩個浮點數(2 * 4字節) arrayStride: 2 * 4, attributes: [ { // 指定數據格式,這樣 WebGPU 才知道該如何解析,格式為 2 個 32位浮點數 format: 'float32x2', offset: 0, // 從每組的第一個數字開始 shaderLocation: 0, // 頂點著色器中的位置 }, ],};
attributes 是一個數組,這里我們只有頂點要讀,所以只有一個數組元素。如果引入了顏色值并和頂點放在一起,我們就要多聲明一個數組元素,并將 offset 指定到顏色的位置。
這個對象此時還沒用到,后面設置渲染流水線時會用到。
聲明 WebGPU 的著色器,創建著色器模塊(GPUShaderModule)。
WebGPU 使用特有的 WGSL 著色器語言,頂點著色器和片元著色器可以寫在一起的。
// 創建著色器模塊const vertexShaderModule = device.createShaderModule({ label: 'Vertex Shader', code: ` @vertex fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f { return vec4f(pos, 0, 1); } @fragment fn fragmentMain() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); } `,});
頂點著色器函數。
@vertex fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f { return vec4f(pos, 0, 1);}
片元著色器。
@fragmentfn fragmentMain() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); // 紅色}
創建渲染流水線,也就是把之前的設置組合起來,用哪個著色器的哪個函數作為入口、如何讀取緩沖區等。
const pipeline = device.createRenderPipeline({ label: 'pipeline', // 標識,定位錯誤用 layout: 'auto', // 自動流水線布局 vertex: { module: vertexShaderModule, // 著色器模塊 entryPoint: 'vertexMain', // 入口函數為 vertexMain buffers: [vertexBufferLayout], // 讀取緩沖區的方式 }, fragment: { module: vertexShaderModule, entryPoint: 'fragmentMain', targets: [ { format: canvasFormat, // 輸出到 canvas 畫布上 }, ], },});
將渲染流水線設置到 pass 上。
pass.setPipeline(pipeline);
將緩沖區綁定到管線的第一個頂點緩沖槽(slot)。
pass.setVertexBuffer(0, vertexBuffer);
繪制圖元,這里要設置繪制幾組,一組是兩個點,所以要處以 2。
pass.draw(vertices.length / 2);
然后就是前面講過的收尾代碼。
pass.end(); // 完成指令隊列的記錄const commandBuffer = encoder.finish(); // 結束編碼device.queue.submit([commandBuffer]); // 提交給 GPU 命令隊列
至此,一個三角形就畫好了。
線上 demo 演示:
https://codesandbox.io/s/lg4w27?file=/src/index.mjs。
完整代碼:
const render = async () => { const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('webgpu'); const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); ctx.configure({ device, format: canvasFormat, }); const encoder = device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx.getCurrentTexture().createView(), loadOp: 'clear', clearValue: { r: 0.6, g: 0.8, b: 0.9, a: 1 }, storeOp: 'store', }, ], }); // 創建頂點數據 // prettier-ignore const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, ]); // 緩沖區 const vertexBuffer = device.createBuffer({ // 標識,字符串隨意寫,報錯時會通過它定位, label: 'Triangle Vertices', // 緩沖區大小,這里是 24 字節。6 個 4 字節(即 32 位)的浮點數 size: vertices.byteLength, // 標識緩沖區用途(1)用于頂點著色器(2)可以從 CPU 復制數據到緩沖區 // eslint-disable-next-line no-undef usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); // 將頂點數據復制到緩沖區 device.queue.writeBuffer(vertexBuffer, /* bufferOffset */ 0, vertices); // GPU 應該如何讀取緩沖區中的數據 const vertexBufferLayout = { arrayStride: 2 * 4, // 每一組的字節數,每組有兩個數字(2 * 4字節) attributes: [ { format: 'float32x2', // 每個數字是32位浮點數 offset: 0, // 從每組的第一個數字開始 shaderLocation: 0, // 頂點著色器中的位置 }, ], }; // 著色器用的是 WGSL 著色器語言 const vertexShaderModule = device.createShaderModule({ label: 'Vertex Shader', code: ` @vertex fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f { return vec4f(pos, 0, 1); } @fragment fn fragmentMain() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); } `, }); // 渲染流水線 const pipeline = device.createRenderPipeline({ label: 'pipeline', layout: 'auto', vertex: { module: vertexShaderModule, entryPoint: 'vertexMain', buffers: [vertexBufferLayout], }, fragment: { module: vertexShaderModule, entryPoint: 'fragmentMain', targets: [ { format: canvasFormat, }, ], }, }); pass.setPipeline(pipeline); pass.setVertexBuffer(0, vertexBuffer); pass.draw(vertices.length / 2); pass.end(); const commandBuffer = encoder.finish(); device.queue.submit([commandBuffer]);};render();
本文講解了如何用 WebGPU 繪制一個三角形。可以看到它和 WebGL 的邏輯有很多共同之處的,都要創建緩沖區、著色器、定義讀取方式。
本文鏈接:http://www.tebozhan.com/showinfo-26-16287-0.htmlWebGPU 入門:繪制一個三角形
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: 轉轉Flutter實踐之路