WebGPU 作為全球性技術,可望將尖端的 GPU 運算能力帶入 Web 領域,惠及所有使用共享程式碼庫的消費平台。
其前身 WebGL 雖然功能強大,但卻嚴重缺乏計算著色器功能,限制了應用範圍。
WGSL(WebGPU 著色器/計算語言)汲取了 Rust 和 GLSL 等領域的最佳實踐。
在我學習使用 WebGPU 的過程中,發現文件中存在一些空白:我希望找到一個簡單的起點,使用計算著色器來計算頂點和片段著色器的相關資料。
本教學中所有程式碼的單一檔案 HTML 可在 //m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb 找到-請繼續閱讀以了解詳細分解。
以下是該 HTML 在我的網域上運行的單次點擊演示://m.sbmmt.com/link/bed827b4857bf056d05980661990ccdc WebGPU 的瀏覽器,例如 Chrome 或 Edge //m.sbmmt.com/link/bae00fb8b4115786ba5dbbb67b9b177a)。
這是一個粒子模擬-它會隨著時間推移,按時間步驟進行。
時間在 JS/CPU 上跟踪,作為 (float) uniform 傳遞給 GPU。
粒子資料完全在 GPU 上管理——儘管仍然與 CPU 交互,允許分配記憶體並設定初始值。也可以將資料讀回 CPU,但這在本教程中省略了。
此設定的神奇之處在於,每個粒子都與所有其他粒子並行更新,從而在瀏覽器中實現了令人難以置信的計算和渲染速度(並行化最大限度地達到GPU 的核心數量;我們可以將粒子數量除以核心數量以獲得每個核心每次更新步驟的真實循環次數)。
WebGPU 用於 CPU 與 GPU 之間資料交換的機制是綁定-JS 陣列(如 Float32Array)可以使用 WebGPU 緩衝區「綁定」到 WGSL 中的記憶體位置。 WGSL 記憶體位置以兩個整數標示:組號和綁定號。
在我們的例子中,計算著色器和頂點著色器都依賴兩個資料綁定:時間和粒子位置。
uniform 定義存在於計算著色器 (//m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb#L43) 和頂點著色器(//m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb#L69) 中-計算著色器更新位置,頂點著色器依時間更新顏色。
讓我們來看看 JS 和 WGSL 中的綁定設置,從計算著色器開始。
<code>const computeBindGroup = device.createBindGroup({ /* 参见 computePipeline 定义,网址为 //m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb#L102 它允许将 JS 字符串与 WGSL 代码链接到 WebGPU */ layout: computePipeline.getBindGroupLayout(0), // 组号 0 entries: [{ // 时间绑定在绑定号 0 binding: 0, resource: { /* 作为参考,缓冲区声明为: const timeBuffer = device.createBuffer({ size: Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST}) }) //m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb#L129 */ buffer: timeBuffer } }, { // 粒子位置数据在绑定号 1(仍在组 0) binding: 1, resource: { buffer: particleBuffer } }] });</code>
以及計算著色器中對應的聲明
<code>// 来自计算着色器 - 顶点着色器中也有类似的声明 @group(0) @binding(0) var<uniform> t: f32; @group(0) @binding(1) var<storage read_write=""> particles : array<particle>; </particle></storage></uniform></code>
重要的是,我們透過匹配 JS 和 WGSL 中的組號和綁定號,將 JS 端的 timeBuffer 綁定到 WGSL。
這使我們能夠從 JS 控制變數的值:
<code>/* 数组中只需要 1 个元素,因为时间是单个浮点值 */ const timeJs = new Float32Array(1) let t = 5.3 /* 纯 JS,只需设置值 */ timeJs.set([t], 0) /* 将数据从 CPU/JS 传递到 GPU/WGSL */ device.queue.writeBuffer(timeBuffer, 0, timeJs);</code>
我們將粒子位置直接儲存和更新在 GPU 可存取的記憶體中——允許我們利用 GPU 的大規模多核心架構並行更新它們。
並行化是在工作組大小的幫助下協調的,在計算著色器中聲明:
<code>@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { // ... } </u32></code>
@builtin(global_invocation_id) global_id : vec3
根據定義,global_invocation_id = workgroup_id * workgroup_size local_invocation_id-這表示它可以用作粒子索引。
例如,如果我們有 10k 個粒子,且 workgroup_size 為 64,我們需要調度 Math.ceil(10000/64) 個工作組。每次從 JS 觸發計算傳遞時,我們將明確地告訴 GPU 執行該數量的工作:
<code>computePass.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / WORKGROUP_SIZE));</code>
如果PARTICLE_COUNT == 10000 且WORKGROUP_SIZE == 64,我們將啟動157 個工作組(10000/64 = 156.25),每個工作組的計算範圍local_invocation_id 為0 163(而work_id 到)。由於 157 * 64 = 1048,我們將最終在一個工作組中進行稍微更多的計算。我們透過丟棄多餘的呼叫來處理溢出。
以下是考慮這些因素後計算著色器的最終結果:
<code>@compute @workgroup_size(${WORKGROUP_SIZE}) fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { let index = global_id.x; // 由于工作组网格未对齐,因此丢弃额外的计算 if (index >= arrayLength(&particles)) { return; } /* 将整数索引转换为浮点数,以便我们可以根据索引(和时间)计算位置更新 */ let fi = f32(index); particles[index].position = vec2<f32>( /* 公式背后没有宏伟的意图 - 只不过是用时间+索引的例子 */ cos(fi * 0.11) * 0.8 + sin((t + fi)/100)/10, sin(fi * 0.11) * 0.8 + cos((t + fi)/100)/10 ); } </f32></u32></code>
這些值將在計算傳遞中持續存在,因為粒子被定義為儲存變數。
為了從計算著色器中在頂點著色器中讀取粒子位置,我們需要一個唯讀視圖,因為只有計算著色器才能寫入儲存。
以下是 WGSL 的聲明:
<code>@group(0) @binding(0) var<uniform> t: f32; @group(0) @binding(1) var<storage> particles : array<vec2>>; /* 或等效: @group(0) @binding(1) var<storage read=""> particles : array<vec2>>; */ </vec2></storage></vec2></storage></uniform></code>
嘗試重新使用計算著色器中相同的 read_write 樣式只會出錯:
<code>var with 'storage' address space and 'read_write' access mode cannot be used by vertex pipeline stage</code>
請注意,頂點著色器中的綁定號碼不必與計算著色器綁定號碼相符-它們只需要與頂點著色器的綁定組宣告相符:
<code>const renderBindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: timeBuffer } }, { binding: 1, resource: { buffer: particleBuffer } }] });</code>
我在 GitHub 範例程式碼 //m.sbmmt.com/link/2e5281ee978b78d6f5728aad8f28fedb#L70 中選擇綁定:2——只是為了探索 WebGPU 強加的約束的邊界
所有設定就位後,更新和渲染循環在 JS 中協調:
<code>/* 从 t = 0 开始模拟 */ let t = 0 function frame() { /* 为简单起见,使用恒定整数时间步 - 无论帧速率如何,都会一致渲染。 */ t += 1 timeJs.set([t], 0) device.queue.writeBuffer(timeBuffer, 0, timeJs); // 计算传递以更新粒子位置 const computePassEncoder = device.createCommandEncoder(); const computePass = computePassEncoder.beginComputePass(); computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); // 重要的是要调度正确数量的工作组以处理所有粒子 computePass.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / WORKGROUP_SIZE)); computePass.end(); device.queue.submit([computePassEncoder.finish()]); // 渲染传递 const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, loadOp: 'clear', storeOp: 'store', }] }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, renderBindGroup); passEncoder.draw(PARTICLE_COUNT); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); requestAnimationFrame(frame); } frame();</code>
WebGPU 在瀏覽器中釋放了大規模平行 GPU 運算的強大功能。
它以傳遞方式運行——每個傳遞都有局部變量,透過具有記憶體綁定的管道啟用(橋接 CPU 記憶體和 GPU 記憶體)。
計算傳遞允許透過工作小組協調並行工作負載。
雖然它確實需要一些繁重的設置,但我認為局部綁定/狀態樣式比WebGL 的全局狀態模型有了巨大的改進——使其更容易使用,同時也最終將GPU 計算的強大功能帶入了Web。
以上是WebGPU 教學:網路上的運算、頂點和片段著色器的詳細內容。更多資訊請關注PHP中文網其他相關文章!