淺入淺出WebGPU,你懂了嗎?
一、什么是WebGPU
1.1 WebGL的恩怨情仇
先跟大家分享一波科技圈的八卦,感受一下WebGL是多么的不容易吧。
OpenGL由Khronos Group組織在1992年的時(shí)候推出,距離現(xiàn)在已經(jīng)30年了。
OpenGL ES 是由Khronos Group在2003年針對(duì)手機(jī)、PDA和游戲主機(jī)等嵌入式設(shè)備設(shè)計(jì)的。
OpenGL ES 2.0 誕生于2007年3月,3.0版本則誕生于2012年8月,3.1版本是2014年3月,最后一個(gè)正式版 3.2 則是2015年8月。之后將會(huì)以擴(kuò)展的形式添加新功能,相對(duì)應(yīng)的,OpenGL 的絕唱 4.6版本 發(fā)布于2017年7月。
2009年,Khronos成立了WebGL工作組,成員包括Apple、Google、Mozilla、Opera等。
2011年的時(shí)候,WebGL 1.0版本正式推出,它是基于OpenGL ES 2.0版本發(fā)布的。
2013年的時(shí)候,WebGL工作組開(kāi)始著手定制WebGL 2.0規(guī)范,但是直到2017年2月,2.0標(biāo)準(zhǔn)才正式被發(fā)布并被Google/Mozilla支持。WebGL 2.0 基于 OpenGL ES 3.0版本。
這之后,又有一些 OpenGL ES 3.1 特性被引入到WebGL 2.0版本中,作為extension形式由各個(gè)瀏覽器自行實(shí)現(xiàn)。
2021年9月,距離標(biāo)準(zhǔn)發(fā)布已經(jīng)過(guò)去了四年半,Apple才官方宣布支持WebGL 2.0版本。
Apple曾經(jīng)的掌門人Steve Jobs曾經(jīng)力挺OpenGL ES,認(rèn)為開(kāi)放即未來(lái),對(duì)Flash嗤之以鼻,誰(shuí)知道老爺子走了以后,Apple采用了自研的圖形框架Metal,從開(kāi)放走向閉環(huán)。
提到Metal,當(dāng)代呈現(xiàn)出圖形框架三足鼎立的局勢(shì),即Apple的「Metal」、Khronos的「Vulkan」(沒(méi)錯(cuò),新開(kāi)個(gè)了個(gè)號(hào))、Windows的「DirectX 12」,全面釋放了GPU的可編程能力**。**也就是這么幾年的時(shí)間,計(jì)算機(jī)圖形學(xué)發(fā)生了翻天覆地的變化,OpenGL的思想越來(lái)越跟不上時(shí)代了。
另外根據(jù)貝殼大佬在GMTC上的分享,Chrome運(yùn)行的WebGL并沒(méi)有用OpenGL引擎,而是由Angle(https://github.com/google/angle)這個(gè)庫(kù)轉(zhuǎn)化為本地的圖形編程接口,比如Windows轉(zhuǎn)化為DirectX,Apple轉(zhuǎn)化為Metal來(lái)繪制的。
不過(guò)OpenGL仍然沒(méi)有完全過(guò)時(shí),雖然3A級(jí)別游戲大作不太可能繼續(xù)采用OpenGL構(gòu)建,但是簡(jiǎn)單場(chǎng)景、嵌入式圖形領(lǐng)域,科研行業(yè)等等,OpenGL仍然是最舒服的選擇。
1.2 WebGPU PK WebGLNext
2016年6月,Google 產(chǎn)生了使用新API來(lái)代替WebGL的想法,稱之為 WebGL Next。
2017年1月,Khronos Group 舉辦了WebGL Next研討會(huì),Chromium一馬當(dāng)先,展示了可以基于OpenGL和Metal獨(dú)立運(yùn)行的新圖形系統(tǒng)原型,同時(shí)Apple和Mozilla也分別展示了自己的原型,三者都非常類似于Metal Api。
次月,Apple就向W3C提交了一個(gè)名為 WebGPU 的技術(shù)概念驗(yàn)證方案,基于Metal圖形開(kāi)放接口,最終W3C采納了 WebGPU 這個(gè)名字作為下一代標(biāo)準(zhǔn),Apple的提案進(jìn)入了正式的小組提案中。
3月,Mozilla向Khronos Group提交了基于Vulkan的名為WebGL Next提案。
2018年6月,Chrome團(tuán)隊(duì)宣布著手實(shí)現(xiàn)WebGPU,這意味著Khronos的失敗,WebGPU勝出,大家以后還是團(tuán)結(jié)在W3C的周圍。
按照預(yù)期,工作組希望在2021年底發(fā)布WebGPU 1.0 標(biāo)準(zhǔn),不過(guò)目前只有草案。
WebGPU 1.0 草案:https://www.w3.org/standards/types#WD
1.3 WebGPU 的特性
1.直接和Vulkan、Metal、Direct3D 12等高性能的本地圖形標(biāo)準(zhǔn)庫(kù)對(duì)標(biāo)
這意味著WebGPU將會(huì)是一個(gè)對(duì)高性能GPU的橋接層,只要按照這套標(biāo)準(zhǔn)就可以實(shí)現(xiàn)一個(gè)利用GPU的工具庫(kù),它的著色器是一套符合Vulkan SPIR-V 的二進(jìn)制規(guī)范,只要是按照這個(gè)規(guī)范的產(chǎn)物,加上一個(gè)支持GPU的運(yùn)行時(shí),這會(huì)有相當(dāng)大的潛力。
像WebAssembly當(dāng)初也是被設(shè)計(jì)為瀏覽器可執(zhí)行的二進(jìn)制格式,但是隨后在Server端獲取了更廣泛的應(yīng)用,已經(jīng)具備替代Docker的潛力了。
2.支持GPU Compute Shader,支持GPU通用計(jì)算
這意味著在瀏覽器端可以用GPU跑計(jì)算任務(wù)了,不光可以用來(lái)繪制圖形,還可以利用GPU并行計(jì)算能力來(lái)做更多的算法,像大數(shù)排序,機(jī)器學(xué)習(xí)等任務(wù)有可能放在瀏覽器端實(shí)現(xiàn)。
3.自定義的著色器語(yǔ)言 WGSL
WGSL(WebGPU Shading Language)是全新的一門語(yǔ)言,WebGPU設(shè)計(jì)這門語(yǔ)言時(shí)大量參考了Vulkan SPIR-V,因?yàn)榘鏅?quán)、利益分配等問(wèn)題,最終決定新造一門語(yǔ)言,一門混合Rust、TypeScript、Metal的編程語(yǔ)言,之前用WebGL的同學(xué)應(yīng)該知道著色器是用GLSL編寫的,沒(méi)關(guān)系,最終只要有工具轉(zhuǎn)為Vulkan SPIR-V 二進(jìn)制程序即可。
目前WGSL還沒(méi)有定最終版本,學(xué)習(xí)成本也比GLSL要大一些。
4.更好的架構(gòu)設(shè)計(jì)
WebGPU擺脫了狀態(tài)機(jī)機(jī)制,新增 Pipeline、Renderpass、CommandEncoder 等對(duì)象。
WebGPU對(duì)應(yīng)的JavaScript對(duì)象,實(shí)際操作的就是GPU內(nèi)部對(duì)象。
所有的WebGPU方法都是Promise,異步代碼會(huì)交給GPU來(lái)實(shí)現(xiàn),外層不需關(guān)心。
更好的TypeScript類型支持。
5.更好的性能
重中之重,我們看一下benchmark
這是在維持60fps下,能畫出的最多三角形,可以看出顯卡的潛力被釋放出來(lái)了。
還有一個(gè)babylon的例子(搬自知乎)
這個(gè)場(chǎng)景有1000多個(gè)沒(méi)有實(shí)例化的樹(shù),每一顆樹(shù)都有一次drawcall,使用WebGL,CPU成為巨大的瓶頸,每一幀需要花費(fèi)81ms,而使用WebGPU,CPU一幀只需要花費(fèi)0.18ms,減少CPU耗時(shí)意味能給GPU留出更多的運(yùn)行時(shí)間,這是WebGPU強(qiáng)大的一點(diǎn)。
1.4 體驗(yàn)WebGPU
目前Chrome正式版沒(méi)有開(kāi)啟WebGPU,我們需要下載金絲雀版本:https://www.google.com/chrome/canary/
然后輸入 chrome://flags/,找到#enable-unsafe-webgpu并打開(kāi)
目前three.js和babylon等主流Web庫(kù)都已支持WebGPU,可以查看一下Demo:
- ThreeJS: https://threejs.org/examples/?q=webgpu#webgpu_compute
- BabylonJS: https://playground.babylonjs.com/ 右上角選擇 webgpu
- 學(xué)習(xí)實(shí)例:https://austin-eng.com/webgpu-samples/samples/helloTriangle
- 文章搜集:https://github.com/mikbry/awesome-webgpu
二、動(dòng)手寫一個(gè)WebGPU程序
由于目前WebGPU尚不穩(wěn)定,所以我們目前還沒(méi)有必要花特別多的精力來(lái)學(xué)習(xí),我們基于webgpu-samples來(lái)做一些簡(jiǎn)單的學(xué)習(xí)。源代碼參考:https://github.com/austinEng/webgpu-samples/
2.1 初始化
相比于WebGL畫圖至少要10多個(gè)API調(diào)用,WebGPU的使用八股文還是少了很多。
- 首先創(chuàng)建一個(gè)adapter
- const adapter = await navigator.gpu.requestAdapter(option);
注意如果不支持WebGPU的瀏覽器,gpu對(duì)像是undefined,需要做好異常處理。
這里的adapter就是顯示適配器的意思,通俗來(lái)說(shuō)就叫顯卡,每個(gè)適配器標(biāo)志著一個(gè)硬件加速器(例如 GPU 或 CPU)實(shí)例和一個(gè)瀏覽器在該硬件加速器之上對(duì) WebGPU 的實(shí)現(xiàn)。
這個(gè)方法接受一個(gè)option,目前如下:
- powerPreference: 'low-power' | 'high-performance'
powerPreference表示需要采用哪一種耗電類型的顯卡,low-power一般是自帶的集成顯卡,它性能較差但是更加省電,而high-performance表示采用更高性能的獨(dú)立顯卡。WebGPU推薦開(kāi)發(fā)者盡量使用低耗電的GPU,除非絕對(duì)需要再使用獨(dú)顯。
- 接下來(lái),我們拿到具體設(shè)備
- const device = await adapter.requestDevice();
這個(gè)設(shè)備是一個(gè)實(shí)例化的對(duì)象,同一個(gè)adapter可以共享device實(shí)例,設(shè)備可以創(chuàng)建緩存,紋理,渲染管線,著色器模塊等等。
創(chuàng)建一個(gè)WebGPU Canvas Context實(shí)例
- const context = canvas.getContext('webgpu');
然后我們需要拿到canvas能繪制的最精細(xì)的像素
- const size = [
- canvas.clientWidth * devicePixelRatio,
- canvas.clientHeight * devicePixelRatio
- ]
然后需要聲明圖像色彩格式,比如brga8unorm,即用8位無(wú)符號(hào)整數(shù)和rgba來(lái)表示顏色,從adapter中也能直接獲取
- const format = context.getPreferredFormat(adapter);
將參數(shù)配置化寫入context中
- context.configure({
- device,
- format,
- size,
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
- })
在 WebGL 中,我們擁有一個(gè)默認(rèn)的幀緩沖(Default Frame Buffer),如果不做任何其他操作,那么當(dāng)我們執(zhí)行繪制命令(draw call)的時(shí)候,所有繪制的內(nèi)容都會(huì)填充到默認(rèn)幀緩沖中,而顯卡會(huì)把這個(gè)默認(rèn)的幀緩沖直接提交給顯示器,並顯示在顯示器中。
這會(huì)帶來(lái)兩個(gè)問(wèn)題:
如果渲染過(guò)慢,顯示器會(huì)取走未完成的圖像,渲染出隔離的圖像
如果渲染過(guò)快,GPU在等待顯示器取圖,造成性能浪費(fèi)。
參考:https://gavinkg.github.io/ILearnVulkanFromScratch-CN/mdroot/%E6%A6%82%E5%BF%B5%E6%B1%87%E6%80%BB/%E4%BA%A4%E6%8D%A2%E9%93%BE.html
解決第一個(gè)問(wèn)題辦法是應(yīng)用雙緩沖區(qū)技術(shù),即用一個(gè)緩沖區(qū)緩存上次渲染好的內(nèi)容,極其類似React Fiber的雙緩存,看來(lái)技術(shù)都是相通的。解決第二個(gè)問(wèn)題可以繼續(xù)應(yīng)用三重緩沖,充分榨干顯卡性能。
這個(gè)configure的作用主要是關(guān)聯(lián)context和device實(shí)例,內(nèi)部會(huì)做緩沖區(qū)實(shí)現(xiàn)(因?yàn)橐@示器做交互嘛),size是繪制圖像的大小,usage是圖像用途,一般是固定搭配,表示需要向外輸出圖像。
2.2 指令編碼器
創(chuàng)建一個(gè)指令編碼器 CommandEncoder
- const cmdEncoder = device.createCommandEncoder();
指令編碼器,它的作用是把你需要讓 GPU 執(zhí)行的指令寫入到 GPU 的指令緩沖區(qū)(Command Buffer)中,例如我們要在渲染通道中輸入頂點(diǎn)數(shù)據(jù)、設(shè)置背景顏色、繪制(draw call)等等。
創(chuàng)建一個(gè)渲染通道 RenderPass
- const renderPassDescriptor = {
- colorAttachments: [
- {
- view: context.getCurrentTexture().createView(),
- loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
- storeOp: 'store',
- },
- ],
- };
colorAttachments是必填字段,用于儲(chǔ)存(或者臨時(shí)儲(chǔ)存)圖像信息,我們通常只會(huì)把渲染通道的結(jié)果存成一份,也就是只渲染到一個(gè)目標(biāo)中,但是在某些高級(jí)渲染技巧中,我們需要把渲染結(jié)果儲(chǔ)存成多份,也就是渲染到多個(gè)目標(biāo)上,因此類型是一個(gè)數(shù)組。
下面的view,表示在哪里儲(chǔ)存當(dāng)前通道渲染的圖像數(shù)據(jù),我們指定使用context創(chuàng)建一個(gè)二進(jìn)制數(shù)組來(lái)表示。loadValue可以理解為背景顏色,storeOp表示儲(chǔ)存時(shí)的操作,可選為'store'儲(chǔ)存 或者 'clear' 清除數(shù)據(jù),默認(rèn)就用store。
還有一個(gè)可選字段depthStencilAttachment表示附加在當(dāng)前渲染通道用于儲(chǔ)存渲染通道的深度信息和模板信息的附件,因?yàn)槲覀冎焕L制二維圖形,所以不需要處理深度、遮擋、混合這些事情。
讓指令編碼器開(kāi)啟渲染管道
- const renderPassEncoder = cmdEncoder.beginRenderPass(renderPassDescriptor);
這里讓cmd和renderpass產(chǎn)生了關(guān)聯(lián),接下來(lái)就可以運(yùn)行pipeline了
2.3 渲染管線
創(chuàng)建渲染管線(pipeline)是最復(fù)雜的一個(gè)步驟,在這里會(huì)應(yīng)用我們的著色器程序。
著色器分為「頂點(diǎn)著色器」和「片元著色器」,對(duì)于不了解的同學(xué)可以簡(jiǎn)單解釋下**。**
頂點(diǎn)著色器是對(duì)傳入的圖形的頂點(diǎn)進(jìn)行計(jì)算,比如我們要畫一個(gè)三角形,我們就要把三角形三個(gè)頂點(diǎn)通過(guò)著色器代碼計(jì)算出來(lái)。
片元著色器是對(duì)頂點(diǎn)計(jì)算出來(lái)的面進(jìn)行著色,比如我們要畫一個(gè)紅色的三角形,那片元著色器就應(yīng)該輸出紅色。
我們可以先不用理解著色器是如何編寫的,下面會(huì)做一些解釋,先看JS API。
最簡(jiǎn)單的場(chǎng)景下,我們只需要配置如下
- const pipeline = device.createRenderPipeline({
- vertex: {
- module: device.createShaderModule({
- code: triangleVertWGSL, // 頂點(diǎn)著色器代碼
- }),
- entryPoint: 'main', // 入口函數(shù)
- },
- fragment: {
- module: device.createShaderModule({
- code: redFragWGSL, // 片元著色器代碼
- }),
- entryPoint: 'main', // 入口函數(shù)
- targets: [
- {
- format: format, // 即上文的最終渲染色彩格式
- },
- ],
- },
- primitive: { // 繪制模式
- topology: 'triangle-list', // 按照三角形繪制
- },
- });
其中著色器部分會(huì)在之后講解,繪制模式支持繪制為點(diǎn)、線、重復(fù)連線、三角形、重復(fù)三角形,大部分情況下我們只使用triangle-list就可以了。
- 將pipeline和passencoder產(chǎn)生關(guān)聯(lián)
- renderPassEncoder.setPipeline(pipeline);
- 開(kāi)始繪制
- renderPassEncoder.draw(3, 1, 0, 0);
這里四個(gè)參數(shù)分別解釋如下:
第一個(gè):需要繪制的頂點(diǎn)數(shù)量,三角形當(dāng)然是3個(gè)頂點(diǎn)
第二個(gè):需要繪制幾個(gè)實(shí)例,我們繪制一個(gè)就好
第三個(gè):起始頂點(diǎn)位置
第四個(gè):先繪制第幾個(gè)實(shí)例
- 宣布繪制結(jié)束
- renderPassEncoder.endPass();
這行代碼表示當(dāng)前的渲染通道已經(jīng)結(jié)束了,不再向 GPU 發(fā)送指令。
結(jié)束指令編碼器并提交數(shù)據(jù)
- device.queue.submit([commandEncoder.finish()])
這行代碼結(jié)束當(dāng)前指令編碼器,并將所有指令提交給GPU設(shè)備的默認(rèn)隊(duì)列。
完畢了,一切順利的話,我們終于繪制出了一個(gè)三角形
怎么樣,是不是很簡(jiǎn)單?
當(dāng)然費(fèi)了這么大工夫只畫了個(gè)三角形,但是主要是理解WebGPU的設(shè)計(jì)理念,舉一反三。相比下來(lái)WebGL的繪制比它還要更復(fù)雜一點(diǎn)。
三、著色器 WGSL 入門
完整的語(yǔ)法說(shuō)明可以參考官方文檔:https://gpuweb.github.io/gpuweb/wgsl
這里只針對(duì)上面的例子進(jìn)行簡(jiǎn)要的解釋
3.1 頂點(diǎn)著色器
我們先看一下代碼
- [[stage(vertex)]]
- fn main([[builtin(vertex_index)]] VertexIndex : u32)
- -> [[builtin(position)]] vec4<f32> {
- var pos = array<vec2<f32>, 3>(
- vec2<f32>(0.0, 0.5),
- vec2<f32>(-0.5, -0.5),
- vec2<f32>(0.5, -0.5));
- return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
- }
這里的雙中括號(hào),對(duì)應(yīng)于WGSL的Attribute概念,用來(lái)進(jìn)行對(duì)屬性進(jìn)行注解。
- 第1行,stage(vertex)是內(nèi)置關(guān)鍵詞,用來(lái)聲明這是頂點(diǎn)著色器。
- 第2行,定義了名字為main的函數(shù),對(duì)應(yīng)上文中的entryPoint。
我們看一下參數(shù),這里用了builtin(xx)來(lái)對(duì)變量進(jìn)行注解,builtin的意思就是將變量關(guān)聯(lián)到內(nèi)置參數(shù)中(類似GLSL中的gl_xxx),詳細(xì)參考官方文檔。變量名字為VertexIndex,類型為u32,無(wú)符號(hào)32位整數(shù)。
builtin(vertex_index) 表示當(dāng)前頂點(diǎn)的下標(biāo)位置
- 第3行,定義此函數(shù)返回值類型
builtin(position)類似于gl_Position,即計(jì)算后頂點(diǎn)的最后位置。類型為vec4
- 第4行,進(jìn)入函數(shù)體了,這里定義一個(gè)名字為pos的數(shù)組變量,元素類型為vec
,數(shù)組長(zhǎng)度為3。 - 第5-7行分別定義數(shù)組成員,也就是三角形三個(gè)頂點(diǎn)位置,這里和WebGL一樣,坐標(biāo)取值在[0.0, 1.0]之間。
- 第9行,根據(jù)傳入的下標(biāo)VertexIndex,找到剛才定義數(shù)組具體值并返回,之前draw函數(shù)指定有3個(gè)頂點(diǎn),這個(gè)頂點(diǎn)著色器就會(huì)運(yùn)行3次,就能獲取三個(gè)不同頂點(diǎn)了。
3.2 片元著色器
先直接上代碼
- [[stage(fragment)]]
- fn main() -> [[location(0)]] vec4<f32> {
- return vec4<f32>(1.0, 0.0, 0.0, 1.0);
- }
第1行,類似地,應(yīng)用了stage(fragment)來(lái)聲明這是片元著色器。
第2行,定義了入口main函數(shù),因?yàn)槲覀冎讳秩疽粋€(gè)最基本的紅色,不需要任何參數(shù)。
返回類型中,需要顯式使用[[location(0)]]表示第一個(gè)返回的元素是vec4
第3行,返回了一個(gè)vec4
其本質(zhì)與GLSL并沒(méi)有太大的區(qū)別,只是語(yǔ)法略顯拗口,上手難度較高。
好了,我們終于把WGSL的大致用法說(shuō)完了,我們還沒(méi)有涉及到更復(fù)雜的應(yīng)用,比如頂點(diǎn)著色器向片元著色器傳值,內(nèi)置函數(shù),UV映射,復(fù)雜的數(shù)據(jù)綁定,內(nèi)外的數(shù)據(jù)傳遞,后處理等等,這些等著WGSL語(yǔ)法成熟以后,我會(huì)慢慢再寫一篇文章總結(jié)。
參考資料:
- https://mp.weixin.qq.com/s/4LfaNHP77s9n9SghucYoaA
- https://github.com/hjlld/LearningWebGPU
- https://gpuweb.github.io/gpuweb/wgsl/#attributes
- https://gpuweb.github.io/gpuweb/wgsl/#builtin-variables