1. WebGpu的前輩WebGL
要說起webGPU就不得不提起一下他的前輩webgl.這樣才能更好知道WebGPU 取代 WebGL 為什么是大勢所趨钉蒲。
?說到 WebGL执解,就不得不說說 OpenGL。在早期的個人電腦中,使用最廣泛的 3D 圖形渲染技術是 Direct3D 和 OpenGL。Direct3D 是微軟 DirectX 技術的一部分荣挨,主要用于 Windows 平臺。 OpenGL 作為一種開源的跨平臺技術朴摊,贏得了眾多開發(fā)者的青睞默垄。
? 后來一個特殊的版本——OpenGL ES,它專為嵌入式計算機甚纲、智能手機口锭、家用游戲機和其他設備而設計。它從 OpenGL 中刪除了許多舊的和無用的功能,同時添加了新功能鹃操。例如去掉了矩形等多余的多邊形韭寸,只保留點、線荆隘、三角形等基本圖形恩伺。這使它在保持輕巧的同時仍然足夠強大以渲染精美的 3D 圖形。
? 而 WebGL 是從 OpenGL ES 派生出來的椰拒,它專注于 Web 的 3D 圖形渲染晶渠。
下圖展示了它們之間的關系:
image.png
WebGL 歷史
image.png
?從上圖可以看出,WebGL 已經很老了燃观。不僅因為它存在已久褒脯,還因為它的標準是從 OpenGL 繼承而來的。OpenGL 的設計理念可以追溯到 1992 年仪壮,而這些古老的理念其實與今天 GPU 的工作原理非常不符。
? 對于瀏覽器開發(fā)者來說胳徽,需要適配 GPU 的不同特性积锅,這給他們帶來了很多不便。雖然這些對于上層開發(fā)人員來說是看不到的养盗。
?從上圖可以看出缚陷,2014 年蘋果發(fā)布了 Metal。 Steve Jobs 是 OpenGL ES 的支持者往核,他認為這是行業(yè)的未來箫爷。所以當時蘋果設備上的游戲都依賴 OpenGL ES(比如憤怒的小鳥,水果忍者)聂儒,都是很經典的游戲虎锚。
?但喬布斯去世后,蘋果放棄了 OpenGL ES衩婚,開發(fā)了新的圖形框架 Metal窜护。
?微軟在 2015 年也發(fā)布了自己的 D3D12【Direct3D 12】圖形框架。緊隨其后的是 Khronos Group非春,圖形界的國際組織柱徙,類似于前端圈子里的 W3C、TC39奇昙。而 WebGL 是它的標準护侮。甚至也逐漸淡化了 WebGL,轉而支持現在的 Vulkan储耐。
?迄今為止羊初,Metal、D3D12 [Direct3D 12] 和 Vulkan 并列為現代三大圖形框架什湘。這些框架充分釋放了 GPU 的可編程能力凳忙,讓開發(fā)者可以最大限度的自由控制 GPU业踏。
?同樣重要的是要注意,當今的主流操作系統(tǒng)不再支持 OpenGL 作為主要支持涧卵。這意味著今天編寫的每一行 WebGL 代碼都有 90% 的機會不被 OpenGL 繪制勤家。它在 Windows 計算機上使用 DirectX 繪制,在 Mac 計算機上使用 Metal 繪制柳恐。
?可見 OpenGL 已經過期了伐脖。但這并不意味著它會消失。繼續(xù)在嵌入式乐设、科學研究等特殊領域發(fā)揮作用讼庇。
?WebGL 也是如此,大量的適配工作使得推進困難重重近尚,于是推出了 WebGPU蠕啄。
2. WebGPU介紹
WebGPU 是由 W3C GPU for the Web 社區(qū)組所發(fā)布的規(guī)范,目標是允許網頁代碼以高性能且安全可靠的方式訪問 GPU 功能戈锻。
- WebGPU API 使 web 開發(fā)人員能夠使用底層系統(tǒng)的 GPU(圖形處理器)進行高性能計算并繪制可在瀏覽器中渲染的復雜圖形歼跟。
- WebGPU 是 WebGL 的繼任者,為現代 GPU 提供更好的兼容格遭、支持更通用的 GPU 計算哈街、更快的操作以及能夠訪問到更高級的 GPU 特性。
- 封裝了現代圖形API(Dx12拒迅、Vulkan骚秦、Metal),提供給Web 3D程序員璧微,為 Web釋放了更多的GPU 硬件的功能作箍。
3. WebGPU架構原理
WebGPU 使用Adapter來實現從操作系統(tǒng)的本機圖形 API 到 WebGPU 的轉換層。對于使用者是通過LogicalDevice來實現對GPU的抽象前硫,由于瀏覽器是可以運行多個 Web 應用程序的單一 OS 級應用程序蒙揣,因此需要多路復用,以便每個 Web 應用程序感覺就像它擁有對 GPU 的唯一控制權开瞭。這就是邏輯設備抽象的作用懒震。
4、著色器
使用過WebGL的都知道嗤详,我們要告訴GPU需要繪制什么是通過著色器和片段著色器个扰,需要你將數據緩沖區(qū)上傳到 GPU,并告訴它如何將該數據解釋為一系列三角形葱色。每個頂點占據該數據緩沖區(qū)的一塊递宅,描述該頂點在 3D 空間中的位置,但可能還包括顏色、紋理 ID办龄、法線和其他內容等輔助數據烘绽。列表中的每個頂點都由 GPU 在頂點階段處理,在每個頂點上運行頂點著色器俐填,這將應用平移安接、旋轉或透視變形。
著色器: “著色器”這個詞曾經讓我感到困惑英融,因為你可以做的不僅僅是著色盏檐。但在過去(即 1980 年代后期!)驶悟,這個術語是恰當的:它是在 GPU 上運行的一小段代碼胡野,用于決定每個像素應該是什么顏色,這樣你就可以對正在渲染的對象進行著色痕鳍,實現燈光和陰影的錯覺硫豆。如今,著色器泛指在 GPU 上運行的任何程序笼呆。
GPU 現在對三角形進行光柵化熊响,這意味著 GPU 會計算出每個三角形在屏幕上覆蓋的像素。然后每個像素由片段著色器處理抄邀,它可以訪問像素坐標耘眨,也可以訪問輔助數據來決定該像素應該是哪種顏色昼榛。如果使用得當境肾,可以使用此過程創(chuàng)建令人驚嘆的 3D 圖形。
這種將數據傳遞到頂點著色器胆屿,然后到片段著色器奥喻,然后將其直接輸出到屏幕上的系統(tǒng)稱為管道,在 WebGPU 中非迹,你必須明確定義管道环鲤。
5. 管線
管線(pipeline)是一個邏輯結構,其包含在你完成程序工作的可編程階段憎兽。WebGPU 目前能夠處理兩種類型的管線:
-
渲染管線用于渲染圖形冷离,通常渲染到
<canvas>
元素中,但它也可以在畫面之外的地方渲染圖形纯命。它有兩個主要階段:- 頂點著色階段:在該階段中西剥,頂點著色器(vertex shader)接受 GPU 輸入的位置數據并使用像旋轉、平移或透視等特定的效果將頂點在 3D 空間中定位亿汞。然后瞭空,這些頂點會被組裝成基本的渲染圖元,例如三角形等,然后通過 GPU 進行光柵化咆畏,計算出每個頂點應該覆蓋在 canvas 上的哪些像素南捂。
- 片元著色階段:在該階段中,片元著色器(fragment shader)計算由頂點著色器生成的基本圖元所覆蓋的每個像素的顏色旧找。這些計算通常使用輸入溺健,如圖像(以紋理的方式)提供表面細節(jié)以及虛擬化光源的位置和顏色。
計算管線用于通用計算钦讳。計算管線包含單獨的計算階段矿瘦,在該階段中,計算著色器(compute shader)接受通用的數據愿卒,在指定數量的工作組之間并行處理數據缚去,然后將結果返回到一個或者多個緩沖區(qū)。這些緩沖區(qū)可以包含任意類型的數據琼开。
6.命令編碼器
命令編碼器(mmand encoder),的意思是說你可以把你想讓 GPU 執(zhí)行所有的命令都壓到命令編碼器里面易结,讓命令編碼器把所有的這些命令編在一起,然后存成命令緩存柜候,然后再發(fā)送到 GPU 里面去執(zhí)行搞动。這樣設計的好處就是把數據編碼這種cpu擅長的事情交給cpu處理,這樣可以減少和GPU的通信開銷渣刷。提高效率鹦肿。
7.WebGPU作流程和代碼實踐
有了上面這些知識點,就可以來看看WebGPU的一個工作流程辅柴。
根據這個流程箩溃,我們就可以上手來寫一個簡單的demo,通過編碼來體會它的工作機制碌嘀。直接上代碼涣旨。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas></canvas>
<script type="module">
const triangleVert = `
@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);
}
`;
const redFrag = `
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
`;
// initialize webgpu device & config canvas context
async function initWebGPU(canvas) {
if (!navigator.gpu) throw new Error("Not Support WebGPU");
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
// powerPreference: 'low-power'
});
if (!adapter) throw new Error("No Adapter Found");
const device = await adapter.requestDevice({
requiredFeatures: ["texture-compression-bc"],
requiredLimits: {
maxStorageBufferBindingSize:
adapter.limits.maxStorageBufferBindingSize,
},
});
// 獲取canvas
const context = canvas.getContext("webgpu");
// 獲取瀏覽器默認的顏色格式
const format = navigator.gpu.getPreferredCanvasFormat();
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const size = { width: canvas.width, height: canvas.height };
context.configure({
// json specific format when key and value are the same
device,
format,
// prevent chrome warning
alphaMode: "opaque",
});
return { device, context, format, size };
}
// create a simple pipiline
async function initPipeline(device, format) {
const descriptor = {
layout: "auto",
vertex: {
module: device.createShaderModule({
code: triangleVert,
}),
entryPoint: "main",
},
primitive: {
topology: "triangle-list", // try point-list, line-list, line-strip, triangle-strip?
},
fragment: {
module: device.createShaderModule({
code: redFrag,
}),
entryPoint: "main",
targets: [
{
format: format,
},
],
},
};
return await device.createRenderPipelineAsync(descriptor);
}
// create & submit device commands
function draw(device, context, pipeline) {
const commandEncoder = device.createCommandEncoder();
const view = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [
{
view: view,
clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
loadOp: "clear", // clear/load
storeOp: "store", // store/discard
},
],
};
const passEncoder =
commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
// 3 vertex form a triangle
passEncoder.draw(3);
passEncoder.end();
// webgpu run in a separate process, all the commands will be executed after submit
device.queue.submit([commandEncoder.finish()]);
}
async function run() {
const canvas = document.querySelector("canvas");
if (!canvas) throw new Error("No Canvas");
const { device, context, format } = await initWebGPU(canvas);
const pipeline = await initPipeline(device, format);
// start draw
draw(device, context, pipeline);
// re-configure context on resize
window.addEventListener("resize", () => {
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
// don't need to recall context.configure() after v104
draw(device, context, pipeline);
});
}
run();
</script>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
background: #000;
color: #fff;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
}
canvas {
width: 100%;
height: 100%;
}
</style>
</body>
</html>
參考
- [1] WebGPU深入探索
- [2] WebGPU API
- [3] webgpu入門