WebGPU ist eine globale Technologie, die verspricht, modernste GPU-Computing-Funktionen ins Web zu bringen, wovon alle Verbraucherplattformen profitieren, die eine gemeinsame Codebasis nutzen.
Obwohl sein Vorgänger WebGL leistungsstark ist, mangelt es ihm erheblich an Compute-Shader-Fähigkeiten, was seinen Anwendungsbereich einschränkt.
WGSL (WebGPU Shader/Compute Language) stützt sich auf Best Practices aus Bereichen wie Rust und GLSL.
Als ich den Umgang mit WebGPU lernte, stieß ich auf einige Lücken in der Dokumentation: Ich hatte gehofft, einen einfachen Ausgangspunkt für die Verwendung von Compute-Shadern zum Berechnen von Daten für Vertex- und Fragment-Shader zu finden.
Das Einzeldatei-HTML für den gesamten Code in diesem Tutorial finden Sie unter https://www.php.cn/link/2e5281ee978b78d6f5728aad8f28fedb – lesen Sie weiter für eine detaillierte Aufschlüsselung.
Hier ist eine Ein-Klick-Demonstration dieses HTML, das auf meiner Domain ausgeführt wird: https://www.php.cn/link/bed827b4857bf056d05980661990ccdc Ein WebGPU-basierter Browser wie Chrome oder Edge https://www.php.cn/link/bae00fb8b4115786ba5dbbb67b9b177a).
Dies ist eine Partikelsimulation – sie geschieht in Zeitschritten über die Zeit.
Die Zeit wird auf JS/CPU verfolgt und als (Float-)Einheit an die GPU übergeben.
Partikeldaten werden vollständig auf der GPU verwaltet – interagieren jedoch weiterhin mit der CPU, sodass Speicher zugewiesen und Anfangswerte festgelegt werden können. Es ist auch möglich, die Daten in die CPU zurückzulesen, darauf wird in diesem Tutorial jedoch verzichtet.
Der Zauber dieses Setups besteht darin, dass jedes Partikel parallel zu allen anderen Partikeln aktualisiert wird, was unglaubliche Berechnungs- und Rendering-Geschwindigkeiten im Browser ermöglicht (Parallelisierung maximiert die Anzahl der Kerne auf der GPU; wir können die Anzahl der Partikel durch dividieren die Anzahl der Kerne, um die tatsächliche Anzahl der Zyklen pro Aktualisierungsschritt pro Kern zu erhalten).
Der Mechanismus, den WebGPU für den Datenaustausch zwischen CPU und GPU verwendet, ist bindend – JS-Arrays (wie Float32Array) können mithilfe von WebGPU-Puffer an Speicherorte in WGSL „gebunden“ werden. WGSL-Speicherorte werden durch zwei Ganzzahlen identifiziert: die Gruppennummer und die Bindungsnummer.
In unserem Fall basieren sowohl der Compute-Shader als auch der Vertex-Shader auf zwei Datenbindungen: Zeit und Partikelposition.
Einheitliche Definitionen gibt es in Compute-Shadern (https://www.php.cn/link/2e5281ee978b78d6f5728aad8f28fedb#L43) und Vertex-Shadern (https://www.php.cn/link/2e5281ee978b78d6f5728aad8f28fedb#L69) Mittel – Berechnen Sie die Shader-Aktualisierungsposition, der Vertex-Shader aktualisiert die Farbe basierend auf der Zeit.
Werfen wir einen Blick auf die Bindungseinrichtung in JS und WGSL, beginnend mit Compute-Shadern.
<code>const computeBindGroup = device.createBindGroup({ /* 参见 computePipeline 定义,网址为 https://www.php.cn/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}) }) https://www.php.cn/link/2e5281ee978b78d6f5728aad8f28fedb#L129 */ buffer: timeBuffer } }, { // 粒子位置数据在绑定号 1(仍在组 0) binding: 1, resource: { buffer: particleBuffer } }] });</code>
und die entsprechende Deklaration im Compute-Shader
<code>// 来自计算着色器 - 顶点着色器中也有类似的声明 @group(0) @binding(0) var<uniform> t: f32; @group(0) @binding(1) var<storage read_write=""> particles : array<particle>; </particle></storage></uniform></code>
Wichtig ist, dass wir den timeBuffer auf der JS-Seite an WGSL binden, indem wir die Gruppennummer und die Bindungsnummer in JS und WGSL abgleichen.
Dadurch können wir den Wert der Variablen von JS aus steuern:
<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>
Wir speichern und aktualisieren Partikelpositionen direkt im GPU-zugänglichen Speicher – so können wir sie parallel aktualisieren, indem wir die Vorteile der massiven Multi-Core-Architektur der GPU nutzen.
Parallelisierung wird mit Hilfe der Arbeitsgruppengröße koordiniert, die im Compute-Shader angegeben wird:
<code>@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { // ... } </u32></code>
@builtin(global_invocation_id) global_id : vec3
Per Definition ist global_invocation_id = workgroup_id * workgroup_size local_invocation_id – das bedeutet, dass es als Partikelindex verwendet werden kann.
Wenn wir beispielsweise 10.000 Partikel haben und die Arbeitsgruppengröße 64 beträgt, müssen wir Math.ceil(10000/64)-Arbeitsgruppen planen. Jedes Mal, wenn ein Rechendurchlauf von JS ausgelöst wird, weisen wir die GPU explizit an, diesen Arbeitsaufwand auszuführen:
<code>computePass.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / WORKGROUP_SIZE));</code>
Wenn PARTICLE_COUNT == 10000 und WORKGROUP_SIZE == 64, starten wir 157 Arbeitsgruppen (10000/64 = 156,25), und der berechnete Bereich der local_invocation_id jeder Arbeitsgruppe beträgt 0 bis 63 (während der Bereich der workgroup_id 0 bis 157 beträgt). ). Da 157 * 64 = 1048, werden wir am Ende etwas mehr Berechnungen in einer Arbeitsgruppe durchführen. Wir behandeln Überläufe, indem wir redundante Aufrufe verwerfen.
Hier ist das Endergebnis der Berechnung des Shaders unter Berücksichtigung dieser Faktoren:
<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>
Diese Werte bleiben über alle Berechnungsdurchläufe hinweg bestehen, da Partikel als Speichervariablen definiert sind.
Um die Partikelpositionen im Vertex-Shader vom Compute-Shader zu lesen, benötigen wir eine schreibgeschützte Ansicht, da nur der Compute-Shader in den Speicher schreiben kann.
Das Folgende ist eine Aussage von 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>
Der Versuch, denselben Lese-/Schreibstil in einem Computer-Shader wiederzuverwenden, führt zu folgender Fehlermeldung:
<code>var with 'storage' address space and 'read_write' access mode cannot be used by vertex pipeline stage</code>
Beachten Sie, dass die Bindungsnummern im Vertex-Shader nicht mit den Bindungsnummern des Compute-Shaders übereinstimmen müssen – sie müssen nur mit der Bindungsgruppendeklaration des Vertex-Shaders übereinstimmen:
<code>const renderBindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: timeBuffer } }, { binding: 1, resource: { buffer: particleBuffer } }] });</code>
Ich habe binding:2 im GitHub-Beispielcode ausgewählt https://www.php.cn/link/2e5281ee978b78d6f5728aad8f28fedb#L70 – nur um die Grenzen der durch WebGPU auferlegten Einschränkungen zu erkunden
Wenn alle Einstellungen vorhanden sind, werden die Aktualisierungs- und Renderschleifen in JS koordiniert:
<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 entfesselt die Leistung des massiv parallelen GPU-Computings im Browser.
Es läuft in Durchgängen – bei jedem Durchlauf werden lokale Variablen über eine Pipeline mit Speicherbindung (Überbrückung von CPU-Speicher und GPU-Speicher) aktiviert.
Compute Delivery ermöglicht die Koordination paralleler Arbeitslasten durch Arbeitsgruppen.
Obwohl es eine aufwändige Einrichtung erfordert, denke ich, dass der lokale Bindungs-/Statusstil eine enorme Verbesserung gegenüber dem globalen Statusmodell von WebGL darstellt – er macht die Verwendung einfacher und bringt gleichzeitig endlich die Leistung des GPU-Computing in Entered the Web.
Das obige ist der detaillierte Inhalt vonWebGPU-Tutorial: Compute-, Vertex- und Fragment-Shader im Web. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!