天天看點

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的潛力了。

  1. 支援GPU Compute Shader,支援GPU通用計算

這意味着在浏覽器端可以用GPU跑計算任務了,不光可以用來繪制圖形,還可以利用GPU并行計算能力來做更多的算法,像大數排序,機器學習等任務有可能放在浏覽器端實作。

  1. 自定義的着色器語言 WGSL

WGSL(WebGPU Shading Language)是全新的一門語言,WebGPU設計這門語言時大量參考了Vulkan SPIR-V,因為版權、利益配置設定等問題,最終決定新造一門語言,一門混合Rust、TypeScript、Metal的程式設計語言,之前用WebGL的同學應該知道着色器是用GLSL編寫的,沒關系,最終隻要有工具轉為Vulkan SPIR-V 二進制程式即可。

目前WGSL還沒有定最終版本,學習成本也比GLSL要大一些。

  1. 更好的架構設計

WebGPU擺脫了狀态機機制,新增 Pipeline、Renderpass、CommandEncoder 等對象。

WebGPU對應的JavaScript對象,實際操作的就是GPU内部對象。

所有的WebGPU方法都是Promise,異步代碼會交給GPU來實作,外層不需關心。

更好的TypeScript類型支援。

  1. 更好的性能

重中之重,我們看一下benchmark

WebGPU,前端可視化的未來。

這是在維持60fps下,能畫出的最多三角形,可以看出顯示卡的潛力被釋放出來了。

還有一個babylon的例子(搬自知乎)

WebGPU,前端可視化的未來。

這個場景有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,前端可視化的未來。

怎麼樣,是不是很簡單?

WebGPU,前端可視化的未來。

當然費了這麼大工夫隻畫了個三角形,但是主要是了解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. 第1行,​

    ​stage(vertex)​

    ​是内置關鍵詞,用來聲明這是頂點着色器。
  2. 第2行,定義了名字為​

    ​main​

    ​的函數,對應上文中的​

    ​entryPoint​

    ​。

我們看一下參數,這裡用了​

​builtin(xx)​

​​來對變量進行注解,builtin的意思就是将變量關聯到内置參數中(類似GLSL中的gl_xxx),詳細參考官方文檔。變量名字為​

​VertexIndex​

​​,類型為​

​u32​

​,無符号32位整數。

​builtin(vertex_index)​

​ 表示目前頂點的下标位置

  1. 第3行,定義此函數傳回值類型

​builtin(position)​

​​類似于​

​gl_Position​

​​,即計算後頂點的最後位置。類型為​

​vec4<f32>​

​,即四元32位浮點類型。

  1. 第4行,進入函數體了,這裡定義一個名字為​

    ​pos​

    ​的數組變量,元素類型為​

    ​vec<f32>​

    ​,數組長度為3。
  2. 第5-7行分别定義數組成員,也就是三角形三個頂點位置,這裡和WebGL一樣,坐标取值在[0.0, 1.0]之間。
  3. 第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<f32>​

​類型。這是為了用下标的方式擷取定義的任意元素。

第3行,傳回了一個​

​vec4<f32>​

​類型的元素,其中第1個元素(即R分量)為1.0,即把紅色拉滿,最後一個元素(即Alpha分量)為1.0,即把不透明度為100%。

  • ​​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​​