Web 端的下一代三維圖形

檻外畸人發表於2019-02-28

今天,蘋果 WebKit 團隊提議在 W3C 成立一個新的社群群組(Community Group)來討論 Web 端三維圖形的未來和開發一款支援現代 GPU 特性(包括底層影像處理和通用計算)的標準 API。W3C 社群允許大家自由參與進來,而且我們也誠邀瀏覽器開發商、GPU 硬體提供商、軟體開發者和 Web 社群加入我們

權當拋磚引玉,我們分享了一個 API 提案和一個針對 WebKit 開源專案的 API 原型。我們希望這是一個有益的開始,並期待隨著社群討論的進行 API 會不斷髮展進化。

更新:現在有一個實現和演示 WebGPU 的 demo

讓我們來看看我們成立這個社群群組的前因後果,以及這個新組與現有 Web 圖形 API(如 WebGL)的關係。

首先談點歷史問題

有一段時間,基於 Web 標準的技術可以生成具有靜態內容的頁面,而其中唯一的圖形則是嵌入的圖片。不久之後,Web 開始增加更多開發人員可以通過 JavaScript 訪問的功能。最終,我們需要一個完全可程式設計的圖形 API,以使指令碼可以實時建立影像。因此,“canvas” 元素及其相關的 2D 渲染 API 誕生於 WebKit,隨後迅速普及到其他瀏覽器引擎中,並且很快標準化了。

隨著時間的推移,Web 應用程式和內容漸趨豐富和複雜,並開始觸及平臺的瓶頸。以遊戲為例,其效能和視覺質量至關重要。在瀏覽器中開發遊戲的需求是有的,但大多數遊戲使用的是 GPU 提供的 3D 圖形 API。Mozilla 和 Opera 公佈了一些從 “canvas” 元素中暴露出 3D 渲染上下文的實驗,其結果非常具有吸引力,因此社群決定一起將大家都可以實現的內容進行標準化。

所有的瀏覽器引擎協作建立了 WebGL,這是在 Web 上渲染 3D 圖形的標準。它基於 OpenGL ES —— 一種面向嵌入式系統的跨平臺圖形 API。這個起點是正確的,因為它可以輕鬆地在所有瀏覽器中實現相同的 API,而且大多數瀏覽器引擎都在支援 OpenGL 的系統上執行。即使系統沒有直接支援 OpenGL,像 ANGLE 這樣的專案也可以在其他技術之上進行模擬,畢竟這種 API 的抽象級別是很高的。隨著 OpenGL 的發展,WebGL 也可以跟著發展。

WebGL 已經在開放平臺上賦予了開發人員圖形處理器的功能,所有主流瀏覽器都支援 WebGL 1,使得可以在 Web 上開發出高質量的遊戲(console-quality games),也促進了 three.js 等第三方庫的蓬勃發展。此後,該標準發展成為 WebGL 2,包括 WebKit 在內的所有主流瀏覽器引擎都承諾對它提供支援。

接下來呢?

在 WebGL 發展的同時,GPU 技術也在發展進步,而且已經建立了新的軟體 API,能夠更好地反映現代 GPU 的設計特性。這些新 API 的抽象級別比較低,並且由於其降低了開銷,通常來說比 OpenGL 的效能更好。該領域的主要技術平臺有微軟的 Direct3D 12、蘋果的 Metal 和 Khronos Group 的 Vulkan。雖然這些技術的設計理念都是相似的,但可惜的是沒有一項技術是跨平臺可用的。

那麼這對 Web 意味著什麼呢?從充分利用 GPU 的角度來講,這些新技術無疑是未來的發展方向。Web 平臺想要成功必須定義一種允許多個系統上實現的通用標準,而現在已經有幾個在架構上稍有差別的圖形 API 了。要開發一款可以加速圖形和計算的現代化底層技術,必須設計一個可以在多種系統(包括上面提到的那些系統)上實現的 API。隨著圖形技術的蓬勃發展,繼續遵循像 OpenGL 這樣的某個特定 API 標準顯然是不可行的。

相反,我們需要評估和設計一個新的 Web 標準:它能夠提供一組核心功能,以及一個支援多種系統圖形技術和平臺的 API,此外還要保障 Web 所要求的保密性和安全性。

再者,我們還需要考慮如何在圖形處理之外使用 GPU,以及新標準如何與其他 Web 技術協同工作。該標準應該暴露現代 GPU 的通用計算功能。其設計架構應符合 Web 的既定模式以便開發和使用。它需要能夠與其他重要的新興 Web 標準(如 WebAssembly 和 WebVR)協同工作。最重要的是,這個標準的制定應該是一個開放的過程,允許行業專家和更廣泛的網路社群參與。

W3C 為這種情況提供了社群群組平臺。“Web 端的 GPU” 社群群組現已開放會員註冊。

WebKit 的初始 API 提案

幾年前我們就預估了下一代圖形 API 的發展情況,並著手在 WebKit 中設計原型以驗證我們可以將非常低階別的 GPU API 暴露給 Web 同時還可以獲得有價值的效能提升。我們得到了一些非常鼓舞人心的實驗結果,所以我們將原型分享給了 W3C 社群群組。我們也準備將程式碼部署到 WebKit 中,所以你很快就可以自己去嘗試了。我們並不奢望這一 API 本身能成為最後的標準,社群也有可能根本就不會從它入手,但是我們認為編寫程式碼的工作本身是很有價值的。其他瀏覽器引擎也已經開發了類似的原型。與社群合作併為計算機圖形提出一個偉大的新技術想必是一件十分令人激動的事情。

下文將詳細闡述我們的實驗,我們將它稱為 “WebGPU”。

獲取渲染上下文(Rendering Context)和渲染管道(Rendering Pipeline)

不出意料,WebGPU 的介面是通過 “canvas” 元素來訪問的。

let canvas = document.querySelector("canvas");
let gpu = canvas.getContext("webgpu");複製程式碼

WebGPU 比 WebGL 要更加物件導向化,事實上這也是效能提升的緣由之一。WebGPU 允許你建立和儲存表示狀態的物件和可以處理一組命令的物件,而無需在每次繪製操作之前設定狀態。這樣,我們可以在狀態建立時就執行一些驗證工作,從而減少繪圖時的工作量。

WebGPU 上下文暴露了圖形命令和平行計算命令。假設需要繪製一些圖形,這需要用到圖形管道。圖形管道中最重要的元素是著色器(shaders),它們是在 GPU 上執行用以處理幾何資料併為每個畫素的繪製提供顏色的程式。著色器通常用專門用於圖形的程式語言進行編寫。

決定 Web API 使用何種著色語言是件有趣的事情,因為有很多因素需要考慮。我們需要一種功能強大的語言,要求程式設計儘量簡單、能序列化為可高效傳輸的格式,並要求可以由瀏覽器進行驗證以確保著色器的安全性。業內有部分人傾向於使用可以從許多源格式生成的著色器表示,這有點類似於組合語言。同時,在“檢視原始碼”方面 Web 可謂發展迅速,對人而言程式碼的可讀性還是很重要的。我們期望關於著色語言的討論成為標準化過程中最有趣的部分之一,我們也十分願意聽取社群的意見。

就 WebGPU 原型而言,我們決定暫不考慮著色語言的問題,而是直接採用一種現存的語言。因為我們當時的工作是建立在蘋果的平臺上的,所以我們選擇了Metal Shading Language。那接下來的問題就是如何將著色器載入到 WebGPU 了。

let library = gpu.createLibrary( /* 原始碼 */ );

let vertexFunction = library.functionWithName("vertex_main");
let fragmentFunction = library.functionWithName("fragment_main");複製程式碼

我們使用 gpu 物件從原始碼載入並編譯著色器,生成一個 WebGPULibrary。著色器程式碼本身並不重要 —— 其實就是一個非常簡單的頂點(vertex)和片段(fragment)的組合。一個 WebGPULibrary 可以容納多個著色器函式,因此我們通過函式名稱取出將要在管道中用到的相應函式。

現在我們就可以建立管道了。

// 管道的一些細節。
let pipelineDescriptor = new WebGPURenderPipelineDescriptor();
pipelineDescriptor.vertexFunction = vertexFunction;
pipelineDescriptor.fragmentFunction = fragmentFunction;
pipelineDescriptor.colorAttachments[0].pixelFormat = "BGRA8Unorm";

let pipelineState = gpu.createRenderPipelineState(pipelineDescriptor);複製程式碼

傳入所需描述資訊(包括使用的頂點、片段著色器以及影像格式)即可從上下文中得到一個新的 WebGPURenderPipelineState 物件。

緩衝區(Buffers)

繪圖操作要求使用緩衝區向渲染管道提供資料,例如幾何座標、顏色、法向量等等,而 WebGPUBuffer 則是容納這些資料的物件。

let vertexData = new Float32Array([ /* some data */ ]);
let vertexBuffer = gpu.createBuffer(vertexData);複製程式碼

此例中,我們有一個 Float32Array,它包含了需要在幾何圖形中繪製的每個頂點的資料。我們從 Float32Array 建立一個 WebGPUBuffer,該緩衝區會在之後的繪圖操作中用到。

諸如此類的頂點資料很少發生變化,但也有些資料是幾乎每次繪製時都會發生變化的。像這種不變的資料被稱為 uniforms。表示相機位置的當前變換矩陣即是 uniform 的一個很常見的例子。WebGPUBuffer 也可用於 uniform,但此處我們希望在建立之後將其寫入緩衝區。

// 將 "buffer" 看作是一個之前分配好的 WebGPUBuffer。
// buffer.contents 暴露一個 ArrayBufferView,我們將其
// 解析為一個 32 位的浮點數陣列。
let uniforms = new Float32Array(buffer.contents);

// 設定所需 uniform。
uniforms[42] = Math.PI;複製程式碼

這樣做的好處之一是 JavaScript 開發人員可以將 ArrayBufferView 封裝在帶有自定義 getter 和 setter 的類或代理物件(Proxy object)中,這樣外部介面看起來像典型的 JavasScript 物件一樣。然後,包裝器物件會更新緩衝區正在使用的底層陣列中的相應部分。

繪圖(Drawing)

在通知 WebGPU 上下文繪圖之前還需要設定一些狀態,這包括渲染的目標位置(最終將在 canvas 中顯示的 WebGPUTexture)以及紋理(texture)初始化和使用情況的描述資訊。這些狀態儲存在 WebGPURenderPassDescriptor 中。

// 從上下文獲取下一幀所期望的紋理資訊。
let drawable = gpu.nextDrawable();

let passDescriptor = new WebGPURenderPassDescriptor();
passDescriptor.colorAttachments[0].loadAction = "clear";
passDescriptor.colorAttachments[0].storeAction = "store";
passDescriptor.colorAttachments[0].clearColor = [0.8, 0.8, 0.8, 1.0];
passDescriptor.colorAttachments[0].texture = drawable.texture;複製程式碼

首先,我們向 WebGPU 上下文請求一個表示下一可繪幀的物件,此物件最終會被複制到 canvas 元素中去。完成繪圖程式碼後,我們要通知 WebGPU 以便其顯示繪圖結果並準備下一個可繪幀。

從初始化 WebGPURenderPassDescriptor 的程式碼中可以看出,我們不會在繪圖操作正在進行的時候從紋理中讀取資訊(因為 loadAction 的值是 clear),而是在繪圖操作完成之後才使用該紋理(因為 storeAction 的值是 store),此外程式碼還指定了紋理的填充顏色。

接下來,我們建立用於儲存實際繪製操作的物件。一個 WebGPUCommandQueue 有一組 WebGPUCommandBuffers。我們使用 WebGPUCommandEncoder 將操作推送到 WebGPUCommandBuffer 中去。

let commandQueue = gpu.createCommandQueue();
let commandBuffer = commandQueue.createCommandBuffer();

// 使用之前建立的描述符。
let commandEncoder = commandBuffer.createRenderCommandEncoderWithDescriptor(
                        passDescriptor);

// 告知編碼器使用何種狀態(例如:著色器)。
commandEncoder.setRenderPipelineState(pipelineState);

// 最後,編碼器還需要知道使用哪個緩衝區。
commandEncoder.setVertexBuffer(vertexBuffer, 0, 0);複製程式碼

至此,我們已經設定好了一個渲染管道,其中包含若干著色器、一個用於儲存幾何資訊的緩衝區、一個用於儲存繪製操作的佇列以及一個可以提交到該佇列的編碼器。現在只需將實際繪圖命令推入編碼器即可。

// 我們知道我們的緩衝區有 3 個頂點,
// 我們希望繪製出一個填充的三角形。
commandEncoder.drawPrimitives("triangle", 0, 3);
commandEncoder.endEncoding();

// 所有繪圖命令已經提交。通知 WebGPU
// 一旦佇列處理完畢即刻顯示 canvas 中的繪圖結果。
commandBuffer.presentDrawable(drawable);
commandBuffer.commit();複製程式碼

像大多數 3D 圖形的示例程式碼一樣,繪製一個簡單的形狀看起來要寫很多程式碼,但其實並非如此。這些現代 API 有一個優點 —— 其大部分程式碼都是在建立可以重用以繪製其他內容的物件。例如,一般渲染上下文只需要一個 WebGPUCommandQueue 例項,又者可以為不同的著色器提前建立多個 WebGPURenderPipelineState 物件。此外,瀏覽器還可以在前期進行很多驗證工作,從而減少繪圖操作過程中的開銷。

希望本文可以讓你對 WebGPU 提案有一個大致瞭解。儘管由 W3C 社群群組最終確定的 API 可能同此提案有很大不同,但我們相信很多一般的設計原則都是通用的。

公開邀請

蘋果的 WebKit 團隊已經建議為 Web 端 GPU 建立一個 W3C 社群群組作為工作論壇,同時也請你加入我們一起定義 GPU 的下一代標準。我們的建議得到了其他瀏覽器引擎開發商、GPU 供應商、框架開發人員等業內同仁的積極回應。在行業的支援下,我們誠邀所有對 Web GPU 感興趣或有專長的人加入社群群組。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章