Der Wechsel zur kommenden WebGPU bedeutet mehr als nur den Wechsel der Grafik-APIs. Es ist auch ein Schritt in die Zukunft der Webgrafik. Aber diese Migration wird mit Vorbereitung und Verständnis besser gelingen – und dieser Artikel wird Sie darauf vorbereiten.
Hallo zusammen, mein Name ist Dmitrii Ivashchenko und ich bin Softwareentwickler bei MY.GAMES. In diesem Artikel besprechen wir die Unterschiede zwischen WebGL und der kommenden WebGPU und erläutern, wie Sie Ihr Projekt auf die Migration vorbereiten.
Zeitleiste von WebGL und WebGPU
Der aktuelle Stand von WebGPU und was noch kommt
Konzeptionelle Unterschiede auf hoher Ebene
Initialisierung
• WebGL: Das Kontextmodell
• WebGPU: Das Gerätemodell
Programme und Pipelines
• WebGL: Programm
• WebGPU: Pipeline
Uniformen
• Uniformen in WebGL 1
• Uniformen in WebGL 2
• Uniformen in WebGPU
Shader
• Shader-Sprache: GLSL vs. WGSL
• Vergleich von Datentypen
• Strukturen
• Funktionsdeklarationen
• Integrierte Funktionen
• Shader-Konvertierung
Konventionsunterschiede
Texturen
• Ansichtsfensterbereich
• Clip-Leerzeichen
WebGPU-Tipps und Tricks
• Minimieren Sie die Anzahl der verwendeten Pipelines.
• Erstellen Sie Pipelines im Voraus
• Verwenden Sie RenderBundles
Zusammenfassung
WebGL hat wie viele andere Webtechnologien Wurzeln, die weit in die Vergangenheit zurückreichen. Um die Dynamik und Motivation hinter der Umstellung auf WebGPU zu verstehen, ist es hilfreich, zunächst einen kurzen Blick auf die Geschichte der WebGL-Entwicklung zu werfen:
In den letzten Jahren ist das Interesse an neuen Grafik-APIs gestiegen, die Entwicklern mehr Kontrolle und Flexibilität bieten:
Heute ist WebGPU auf mehreren Plattformen wie Windows, Mac und ChromeOS über die Browser Google Chrome und Microsoft Edge verfügbar, beginnend mit Version 113. Unterstützung für Linux und Android wird in naher Zukunft erwartet.
Hier sind einige der Engines, die WebGPU bereits unterstützen (oder experimentelle Unterstützung anbieten):
Vor diesem Hintergrund scheint der Übergang zu WebGPU oder zumindest die Vorbereitung von Projekten auf einen solchen Übergang ein zeitnaher Schritt in naher Zukunft zu sein.
Lassen Sie uns herauszoomen und einen Blick auf einige der allgemeinen konzeptionellen Unterschiede zwischen WebGL und WebGPU werfen, beginnend mit der Initialisierung.
Wenn Sie beginnen, mit Grafik-APIs zu arbeiten, besteht einer der ersten Schritte darin, das Hauptobjekt für die Interaktion zu initialisieren. Dieser Prozess unterscheidet sich zwischen WebGL und WebGPU, mit einigen Besonderheiten für beide Systeme.
In WebGL wird dieses Objekt als „Kontext“ bezeichnet, der im Wesentlichen eine Schnittstelle zum Zeichnen auf einem HTML5-Canvas-Element darstellt. Diesen Kontext zu erhalten ist ganz einfach:
const gl = canvas.getContext('webgl');
Der Kontext von WebGL ist tatsächlich an eine bestimmte Leinwand gebunden. Das bedeutet, dass Sie mehrere Kontexte benötigen, wenn Sie auf mehreren Leinwänden rendern müssen.
WebGPU führt ein neues Konzept namens „Gerät“ ein. Dieses Gerät stellt eine GPU-Abstraktion dar, mit der Sie interagieren. Der Initialisierungsprozess ist etwas komplexer als in WebGL, bietet aber mehr Flexibilität:
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
Einer der Vorteile dieses Modells besteht darin, dass ein Gerät auf mehreren Leinwänden oder sogar auf keiner rendern kann. Dies bietet zusätzliche Flexibilität; Beispielsweise kann ein Gerät das Rendern in mehreren Fenstern oder Kontexten steuern.
WebGL und WebGPU stellen unterschiedliche Ansätze zur Verwaltung und Organisation der Grafikpipeline dar.
Bei WebGL liegt der Schwerpunkt auf dem Shader-Programm. Das Programm kombiniert Vertex- und Fragment-Shader und definiert, wie Vertices transformiert und wie jedes Pixel gefärbt werden soll.
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
Schritte zum Erstellen eines Programms in WebGL:
Dieses Verfahren ermöglicht eine flexible Grafiksteuerung, kann jedoch insbesondere bei großen und komplexen Projekten auch komplex und fehleranfällig sein.
WebGPU führt das Konzept einer „Pipeline“ anstelle eines separaten Programms ein. Diese Pipeline vereint nicht nur Shader, sondern auch andere Informationen, die in WebGL als Zustände festgelegt werden. Das Erstellen einer Pipeline in WebGPU sieht also komplexer aus:
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });
Schritte zum Erstellen einer Pipeline in WebGPU:
Während WebGL jeden Aspekt des Renderings trennt, versucht WebGPU, mehr Aspekte in einem einzigen Objekt zu kapseln, wodurch das System modularer und flexibler wird. Anstatt Shader und Rendering-Zustände separat zu verwalten, wie es in WebGL der Fall ist, kombiniert WebGPU alles in einem Pipeline-Objekt. Dadurch wird der Prozess vorhersehbarer und weniger fehleranfällig:
Einheitliche Variablen stellen konstante Daten bereit, die allen Shader-Instanzen zur Verfügung stehen.
Im einfachen WebGL haben wir die Möglichkeit, uniform
Variablen direkt über API-Aufrufe festzulegen.
GLSL :
uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;
JavaScript :
const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);
Diese Methode ist einfach, erfordert jedoch mehrere API-Aufrufe für jede uniform
Variable.
Mit der Einführung von WebGL 2 haben wir nun die Möglichkeit, uniform
Variablen in Puffern zu gruppieren. Obwohl Sie immer noch separate Uniform-Shader verwenden können, ist es eine bessere Option, verschiedene Uniformen mithilfe von Uniform-Puffer in einer größeren Struktur zu gruppieren. Dann senden Sie alle diese einheitlichen Daten auf einmal an die GPU, ähnlich wie Sie einen Vertex-Puffer in WebGL 1 laden können. Dies hat mehrere Leistungsvorteile, wie z. B. die Reduzierung von API-Aufrufen und eine Annäherung an die Funktionsweise moderner GPUs.
GLSL :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
JavaScript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
Um Teilmengen eines großen einheitlichen Puffers in WebGL 2 zu binden, können Sie einen speziellen API-Aufruf namens bindBufferRange
verwenden. In WebGPU gibt es etwas Ähnliches namens Dynamic Uniform Buffer Offsets, bei dem Sie beim Aufruf der setBindGroup
API eine Liste von Offsets übergeben können.
WebGPU bietet uns eine noch bessere Methode. In diesem Zusammenhang werden einzelne uniform
Variablen nicht mehr unterstützt und die Arbeit erfolgt ausschließlich über uniform
Puffer.
WGSL :
[[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;
JavaScript :
const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });
Moderne GPUs bevorzugen das Laden von Daten in einem großen Block statt in vielen kleinen. Anstatt kleine Puffer jedes Mal neu zu erstellen und neu zu binden, sollten Sie erwägen, einen großen Puffer zu erstellen und verschiedene Teile davon für verschiedene Zeichenaufrufe zu verwenden. Dieser Ansatz kann die Leistung erheblich steigern.
WebGL ist wichtiger, da es bei jedem Aufruf den globalen Status zurücksetzt und versucht, so einfach wie möglich zu sein. WebGPU hingegen zielt darauf ab, objektorientierter zu sein und sich auf die Wiederverwendung von Ressourcen zu konzentrieren, was zu Effizienz führt.
Der Übergang von WebGL zu WebGPU kann aufgrund unterschiedlicher Methoden schwierig erscheinen. Allerdings kann der Übergang zu WebGL 2 als Zwischenschritt Ihr Leben vereinfachen.
Die Migration von WebGL zu WebGPU erfordert Änderungen nicht nur an der API, sondern auch an den Shadern. Die WGSL-Spezifikation soll diesen Übergang reibungslos und intuitiv gestalten und gleichzeitig die Effizienz und Leistung moderner GPUs aufrechterhalten.
WGSL ist als Brücke zwischen WebGPU und nativen Grafik-APIs konzipiert. Im Vergleich zu GLSL sieht WGSL etwas ausführlicher aus, aber die Struktur bleibt vertraut.
Hier ist ein Beispiel-Shader für Textur:
GLSL :
sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }
WGSL :
[[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); }
Die folgende Tabelle zeigt einen Vergleich der Basis- und Matrixdatentypen in GLSL und WGSL:
Der Übergang von GLSL zu WGSL zeigt den Wunsch nach strengerer Typisierung und expliziter Definition von Datengrößen, was die Lesbarkeit des Codes verbessern und die Fehlerwahrscheinlichkeit verringern kann.
Auch die Syntax zur Deklaration von Strukturen hat sich geändert:
GLSL:
struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };
WGSL:
struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };
Die Einführung einer expliziten Syntax für die Deklaration von Feldern in WGSL-Strukturen unterstreicht den Wunsch nach mehr Klarheit und vereinfacht das Verständnis von Datenstrukturen in Shadern.
GLSL :
float saturate(float x) { return clamp(x, 0.0, 1.0); }
WGSL :
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
Die Änderung der Syntax von Funktionen in WGSL spiegelt die Vereinheitlichung des Ansatzes für Deklarationen und Rückgabewerte wider und macht den Code konsistenter und vorhersehbarer.
In WGSL wurden viele integrierte GLSL-Funktionen umbenannt oder ersetzt. Zum Beispiel:
Das Umbenennen integrierter Funktionen in WGSL vereinfacht nicht nur ihre Namen, sondern macht sie auch intuitiver, was den Umstellungsprozess für Entwickler, die mit anderen Grafik-APIs vertraut sind, erleichtern kann.
Für diejenigen, die planen, ihre Projekte von WebGL auf WebGPU umzustellen, ist es wichtig zu wissen, dass es Tools zur automatischen Konvertierung von GLSL in WGSL gibt, wie zum Beispiel **[Naga](https://github.com/gfx-rs/naga /)**, eine Rust-Bibliothek zum Konvertieren von GLSL in WGSL. Mit Hilfe von WebAssembly kann es sogar direkt in Ihrem Browser funktionieren.
Hier sind von Naga unterstützte Endpunkte:
Nach der Migration kann es sein, dass Sie auf eine Überraschung in Form von umgedrehten Bildern stoßen. Wer schon einmal Anwendungen von OpenGL auf Direct3D (oder umgekehrt) portiert hat, stand bereits vor diesem klassischen Problem.
Im Kontext von OpenGL und WebGL werden Texturen üblicherweise so geladen, dass das Startpixel der unteren linken Ecke entspricht. In der Praxis laden viele Entwickler jedoch Bilder beginnend in der oberen linken Ecke, was zu dem Fehler „umgedrehte Bilder“ führt. Dieser Fehler kann jedoch durch andere Faktoren kompensiert werden, wodurch das Problem letztendlich ausgeglichen wird.
Im Gegensatz zu OpenGL verwenden Systeme wie Direct3D und Metal traditionell die obere linke Ecke als Ausgangspunkt für Texturen. Da dieser Ansatz für viele Entwickler am intuitivsten zu sein scheint, haben sich die Entwickler von WebGPU für diese Vorgehensweise entschieden.
Wenn Ihr WebGL-Code Pixel aus dem Bildpuffer auswählt, müssen Sie darauf vorbereitet sein, dass WebGPU ein anderes Koordinatensystem verwendet. Möglicherweise müssen Sie eine einfache Operation „y = 1,0 – y“ anwenden, um die Koordinaten zu korrigieren.
Wenn ein Entwickler auf ein Problem stößt, bei dem Objekte abgeschnitten werden oder früher als erwartet verschwinden, hängt dies oft mit Unterschieden in der Tiefendomäne zusammen. Es gibt einen Unterschied zwischen WebGL und WebGPU darin, wie sie den Tiefenbereich des Clip-Bereichs definieren. Während WebGL einen Bereich von -1 bis 1 verwendet, verwendet WebGPU einen Bereich von 0 bis 1, ähnlich wie andere Grafik-APIs wie Direct3D, Metal und Vulkan. Diese Entscheidung wurde aufgrund mehrerer Vorteile der Verwendung eines Bereichs von 0 bis 1 getroffen, die bei der Arbeit mit anderen Grafik-APIs identifiziert wurden.
Die Hauptverantwortung für die Umwandlung der Positionen Ihres Modells in Clip-Bereich liegt bei der Projektionsmatrix. Der einfachste Weg, Ihren Code anzupassen, besteht darin, sicherzustellen, dass die Ergebnisse Ihrer Projektionsmatrix im Bereich von 0 bis 1 ausgegeben werden. Für diejenigen, die Bibliotheken wie gl-matrix verwenden, gibt es eine einfache Lösung: Anstatt die perspective
zu verwenden, können Sie diese verwenden perspectiveZO
; Ähnliche Funktionen stehen für andere Matrixoperationen zur Verfügung.
if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }
Manchmal verfügen Sie jedoch möglicherweise über eine vorhandene Projektionsmatrix und können deren Quelle nicht ändern. Um ihn in diesem Fall in einen Bereich von 0 bis 1 umzuwandeln, können Sie Ihre Projektionsmatrix vorab mit einer anderen Matrix multiplizieren, die den Tiefenbereich korrigiert.
Lassen Sie uns nun einige Tipps und Tricks für die Arbeit mit WebGPU besprechen.
Je mehr Pipelines Sie verwenden, desto mehr Statuswechsel haben Sie und desto geringer ist die Leistung. Dies ist möglicherweise nicht trivial, je nachdem, woher Ihr Vermögen stammt.
Das Erstellen und sofortige Verwenden einer Pipeline kann funktionieren, wird jedoch nicht empfohlen. Erstellen Sie stattdessen Funktionen, die sofort zurückkehren, und beginnen Sie mit der Arbeit an einem anderen Thread. Wenn Sie die Pipeline verwenden, muss die Ausführungswarteschlange warten, bis die ausstehenden Pipeline-Erstellungen abgeschlossen sind. Dies kann zu erheblichen Leistungsproblemen führen. Um dies zu vermeiden, stellen Sie sicher, dass zwischen der Erstellung der Pipeline und der ersten Verwendung etwas Zeit vergeht.
Oder, noch besser, verwenden Sie die create*PipelineAsync
Varianten! Das Versprechen wird aufgelöst, wenn die Pipeline ohne Verzögerung einsatzbereit ist.
device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });
Render-Bundles sind vorab aufgezeichnete, teilweise wiederverwendbare Render-Durchgänge. Sie können die meisten Renderbefehle enthalten (mit Ausnahme von Dingen wie dem Festlegen des Ansichtsfensters) und können später als Teil eines tatsächlichen Renderdurchgangs „wiedergegeben“ werden.
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
Render-Bundles können neben regulären Render-Pass-Befehlen ausgeführt werden. Der Render-Pass-Status wird vor und nach jeder Bundle-Ausführung auf die Standardwerte zurückgesetzt. Dies geschieht in erster Linie, um den JavaScript-Aufwand beim Zeichnen zu reduzieren. Die GPU-Leistung bleibt unabhängig vom Ansatz gleich.
Der Übergang zu WebGPU bedeutet mehr als nur den Wechsel der Grafik-APIs. Es ist auch ein Schritt in die Zukunft der Webgrafik, da erfolgreiche Funktionen und Praktiken verschiedener Grafik-APIs kombiniert werden. Diese Migration erfordert ein gründliches Verständnis der technischen und philosophischen Änderungen, aber die Vorteile sind erheblich.