paint-brush
Migration von WebGL zu WebGPUvon@dmitrii
5,305 Lesungen
5,305 Lesungen

Migration von WebGL zu WebGPU

von Dmitrii Ivashchenko13m2023/12/20
Read on Terminal Reader

Zu lang; Lesen

Dieser Leitfaden erläutert den Übergang von WebGL zu WebGPU und behandelt die wichtigsten Unterschiede, allgemeine Konzepte und praktische Tipps. Da sich WebGPU als die Zukunft der Webgrafik herauskristallisiert, bietet dieser Artikel sowohl für Softwareentwickler als auch für Projektmanager unschätzbare Einblicke.
featured image - Migration von WebGL zu WebGPU
Dmitrii Ivashchenko HackerNoon profile picture

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.


Inhaltsübersicht

  1. Zeitleiste von WebGL und WebGPU

  2. Der aktuelle Stand von WebGPU und was noch kommt

  3. Konzeptionelle Unterschiede auf hoher Ebene

  4. Initialisierung

    • WebGL: Das Kontextmodell

    • WebGPU: Das Gerätemodell

  5. Programme und Pipelines

    • WebGL: Programm

    • WebGPU: Pipeline

  6. Uniformen

    • Uniformen in WebGL 1

    • Uniformen in WebGL 2

    • Uniformen in WebGPU

  7. Shader

    • Shader-Sprache: GLSL vs. WGSL

    • Vergleich von Datentypen

    • Strukturen

    • Funktionsdeklarationen

    • Integrierte Funktionen

    • Shader-Konvertierung

  8. Konventionsunterschiede

  9. Texturen

    • Ansichtsfensterbereich

    • Clip-Leerzeichen

  10. WebGPU-Tipps und Tricks

    • Minimieren Sie die Anzahl der verwendeten Pipelines.

    • Erstellen Sie Pipelines im Voraus

    • Verwenden Sie RenderBundles

  11. Zusammenfassung


Zeitleiste von WebGL und WebGPU

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:


  • OpenGL-Desktop (1993) Die Desktop-Version von OpenGL kommt auf den Markt.
  • WebGL 1.0 (2011) : Dies war die erste stabile Version von WebGL, basierend auf OpenGL ES 2.0, das 2007 eingeführt wurde. Es bot Webentwicklern die Möglichkeit, 3D-Grafiken direkt in Browsern zu verwenden, ohne dass zusätzliche Plugins erforderlich waren.
  • WebGL 2.0 (2017) : WebGL 2.0 wurde sechs Jahre nach der ersten Version eingeführt und basierte auf OpenGL ES 3.0 (2012). Diese Version brachte eine Reihe von Verbesserungen und neuen Funktionen mit sich, die 3D-Grafiken im Web noch leistungsfähiger machten.


In den letzten Jahren ist das Interesse an neuen Grafik-APIs gestiegen, die Entwicklern mehr Kontrolle und Flexibilität bieten:


  • Vulkan (2016) : Diese von der Khronos-Gruppe entwickelte plattformübergreifende API ist der „Nachfolger“ von OpenGL. Vulkan bietet Zugriff auf Grafikhardwareressourcen auf niedrigerer Ebene und ermöglicht so Hochleistungsanwendungen mit besserer Kontrolle über die Grafikhardware.
  • D3D12 (2015) : Diese API wurde von Microsoft erstellt und ist ausschließlich für Windows und Xbox. D3D12 ist der Nachfolger von D3D10/11 und bietet Entwicklern eine umfassendere Kontrolle über Grafikressourcen.
  • Metal (2014) : Metal wurde von Apple entwickelt und ist eine exklusive API für Apple-Geräte. Es wurde mit Blick auf maximale Leistung auf Apple-Hardware entwickelt.


Der aktuelle Stand von WebGPU und was noch kommt

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):


  • Babylon JS : Volle Unterstützung für WebGPU.
  • ThreeJS : Derzeit experimentelle Unterstützung.
  • PlayCanvas : In der Entwicklung, aber mit vielversprechenden Aussichten.
  • Unity : In Version 2023.2 Alpha wurde eine sehr frühe und experimentelle WebGPU-Unterstützung angekündigt.
  • Cocos Creator 3.6.2 : Unterstützt offiziell WebGPU und ist damit einer der Pioniere in diesem Bereich.
  • Construct : wird derzeit nur in v113+ für Windows, macOS und ChromeOS unterstützt.



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.


Konzeptionelle Unterschiede auf hoher Ebene

Lassen Sie uns herauszoomen und einen Blick auf einige der allgemeinen konzeptionellen Unterschiede zwischen WebGL und WebGPU werfen, beginnend mit der Initialisierung.

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.

WebGL: Das Kontextmodell

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: Das Gerätemodell

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.



Programme und Pipelines

WebGL und WebGPU stellen unterschiedliche Ansätze zur Verwaltung und Organisation der Grafikpipeline dar.

WebGL: Programm

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:


  1. Shader erstellen : Der Quellcode für Shader wird geschrieben und kompiliert.
  2. Programm erstellen : Kompilierte Shader werden an das Programm angehängt und dann verknüpft.
  3. Programm verwenden : Das Programm wird vor dem Rendern aktiviert.
  4. Datenübertragung : Daten werden an das aktivierte Programm übertragen.


Dieses Verfahren ermöglicht eine flexible Grafiksteuerung, kann jedoch insbesondere bei großen und komplexen Projekten auch komplex und fehleranfällig sein.

WebGPU: Pipeline

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:


  1. Shader-Definition : Der Shader-Quellcode wird ähnlich wie in WebGL geschrieben und kompiliert.
  2. Pipeline-Erstellung : Shader und andere Rendering-Parameter werden in einer Pipeline zusammengefasst.
  3. Pipeline-Nutzung : Die Pipeline wird vor dem Rendern aktiviert.


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:



Uniformen

Einheitliche Variablen stellen konstante Daten bereit, die allen Shader-Instanzen zur Verfügung stehen.

Uniformen in WebGL 1

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.

Uniformen in WebGL 2

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.



Uniformen in WebGPU

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.



Shader

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.

Shader-Sprache: GLSL vs. WGSL

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); } 



Vergleich von Datentypen

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.



Strukturen

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.



Funktionsdeklarationen

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.



Integrierte Funktionen

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.



Shader-Konvertierung

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:



Konventionsunterschiede

Texturen

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.

Ansichtsfensterbereich

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.



Clip-Leerzeichen

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.



WebGPU-Tipps und Tricks

Lassen Sie uns nun einige Tipps und Tricks für die Arbeit mit WebGPU besprechen.

Minimieren Sie die Anzahl der verwendeten Pipelines.

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.

Erstellen Sie Pipelines im Voraus

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()]); });

Verwenden Sie RenderBundles

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.

Zusammenfassung

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.

Nützliche Ressourcen und Links: