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