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