Passer au prochain WebGPU signifie plus que simplement changer d’API graphique. C'est aussi un pas vers l'avenir du graphisme Web. Mais cette migration se déroulera mieux avec de la préparation et de la compréhension – et cet article vous préparera.
Bonjour à tous, je m'appelle Dmitrii Ivashchenko et je suis ingénieur logiciel chez MY.GAMES. Dans cet article, nous discuterons des différences entre WebGL et le prochain WebGPU, et nous expliquerons comment préparer votre projet pour la migration.
Chronologie de WebGL et WebGPU
L'état actuel du WebGPU et ce qui va arriver
Différences conceptuelles de haut niveau
Initialisation
• WebGL : le modèle de contexte
• WebGPU : le modèle de périphérique
Programmes et pipelines
• WebGL : programme
• WebGPU : pipeline
Uniformes
• Uniformes dans WebGL 1
• Uniformes dans WebGL 2
• Uniformes dans WebGPU
Shaders
• Langage Shader : GLSL vs WGSL
• Comparaison des types de données
• Structures
• Déclarations de fonctions
• Fonctions intégrées
• Conversion de shader
Différences de convention
Textures
• Espace de visualisation
• Espaces de découpe
Trucs et astuces WebGPU
• Réduisez le nombre de pipelines que vous utilisez.
• Créer des pipelines à l'avance
• Utiliser les RenderBundles
Résumé
WebGL , comme beaucoup d'autres technologies Web, a des racines qui remontent assez loin dans le passé. Pour comprendre la dynamique et la motivation derrière l'évolution vers WebGPU, il est utile de jeter d'abord un rapide coup d'œil à l'histoire du développement de WebGL :
Ces dernières années, on a assisté à un regain d'intérêt pour les nouvelles API graphiques qui offrent aux développeurs plus de contrôle et de flexibilité :
Aujourd'hui, WebGPU est disponible sur plusieurs plates-formes telles que Windows, Mac et ChromeOS via les navigateurs Google Chrome et Microsoft Edge, à partir de la version 113. La prise en charge de Linux et Android est attendue dans un avenir proche.
Voici quelques-uns des moteurs qui prennent déjà en charge (ou offrent un support expérimental) pour WebGPU :
Compte tenu de cela, la transition vers WebGPU ou au moins la préparation de projets pour une telle transition semble être une étape opportune dans un avenir proche.
Faisons un zoom arrière et examinons certaines des différences conceptuelles de haut niveau entre WebGL et WebGPU, en commençant par l'initialisation.
Lorsque vous commencez à travailler avec des API graphiques, l'une des premières étapes consiste à initialiser l'objet principal pour l'interaction. Ce processus diffère entre WebGL et WebGPU, avec quelques particularités pour les deux systèmes.
En WebGL, cet objet est appelé « contexte », qui représente essentiellement une interface permettant de dessiner sur un élément de canevas HTML5. Obtenir ce contexte est assez simple :
const gl = canvas.getContext('webgl');
Le contexte de WebGL est en fait lié à un canevas spécifique. Cela signifie que si vous devez effectuer un rendu sur plusieurs canevas, vous aurez besoin de plusieurs contextes.
WebGPU introduit un nouveau concept appelé « appareil ». Cet appareil représente une abstraction GPU avec laquelle vous allez interagir. Le processus d'initialisation est un peu plus complexe qu'en WebGL, mais il offre plus de flexibilité :
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
L'un des avantages de ce modèle est qu'un seul appareil peut effectuer le rendu sur plusieurs toiles, voire aucune. Cela offre une flexibilité supplémentaire ; par exemple, un appareil peut contrôler le rendu dans plusieurs fenêtres ou contextes.
WebGL et WebGPU représentent différentes approches pour gérer et organiser le pipeline graphique.
En WebGL, l'accent est mis principalement sur le programme de shader. Le programme combine des shaders de sommets et de fragments, définissant comment les sommets doivent être transformés et comment chaque pixel doit être coloré.
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
Étapes pour créer un programme en WebGL :
Ce processus permet un contrôle graphique flexible, mais peut également être complexe et sujet à des erreurs, en particulier pour les projets volumineux et complexes.
WebGPU introduit le concept de « pipeline » au lieu d'un programme distinct. Ce pipeline combine non seulement des shaders mais également d'autres informations qui, dans WebGL, sont établies sous forme d'états. Ainsi, créer un pipeline dans WebGPU semble plus complexe :
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, }], }, });
Étapes pour créer un pipeline dans WebGPU :
Alors que WebGL sépare chaque aspect du rendu, WebGPU tente d'encapsuler davantage d'aspects dans un seul objet, rendant le système plus modulaire et flexible. Au lieu de gérer séparément les shaders et les états de rendu, comme cela se fait dans WebGL, WebGPU combine tout en un seul objet pipeline. Cela rend le processus plus prévisible et moins sujet aux erreurs :
Les variables uniformes fournissent des données constantes disponibles pour toutes les instances de shader.
Dans WebGL de base, nous avons la possibilité de définir des variables uniform
directement via des appels API.
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]);
Cette méthode est simple, mais nécessite plusieurs appels API pour chaque variable uniform
.
Avec l'arrivée de WebGL 2, nous avons désormais la possibilité de regrouper des variables uniform
dans des tampons. Bien que vous puissiez toujours utiliser des shaders uniformes distincts, une meilleure option consiste à regrouper différents uniformes dans une structure plus grande à l’aide de tampons uniformes. Ensuite, vous envoyez toutes ces données uniformes au GPU en même temps, de la même manière que vous pouvez charger un tampon de vertex dans WebGL 1. Cela présente plusieurs avantages en termes de performances, tels que la réduction des appels d'API et le fait d'être plus proche du fonctionnement des GPU modernes.
GLSL :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
Javascript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
Pour lier des sous-ensembles d'un grand tampon uniforme dans WebGL 2, vous pouvez utiliser un appel d'API spécial appelé bindBufferRange
. Dans WebGPU, il existe quelque chose de similaire appelé décalages de tampon uniformes dynamiques où vous pouvez transmettre une liste de décalages lors de l'appel de l'API setBindGroup
.
WebGPU nous offre une méthode encore meilleure. Dans ce contexte, les variables uniform
individuelles ne sont plus prises en charge et le travail est effectué exclusivement via des tampons uniform
.
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 });
Les GPU modernes préfèrent que les données soient chargées dans un seul gros bloc plutôt que dans plusieurs petits. Au lieu de recréer et de relier de petits tampons à chaque fois, envisagez de créer un grand tampon et d'en utiliser différentes parties pour différents appels de tirage. Cette approche peut augmenter considérablement les performances.
WebGL est plus impératif, réinitialisant l'état global à chaque appel et s'efforçant d'être aussi simple que possible. WebGPU, quant à lui, vise à être plus orienté objet et axé sur la réutilisation des ressources, ce qui conduit à l'efficacité.
La transition de WebGL vers WebGPU peut sembler difficile en raison des différences de méthodes. Cependant, commencer par une transition vers WebGL 2 comme étape intermédiaire peut vous simplifier la vie.
La migration de WebGL vers WebGPU nécessite des modifications non seulement dans l'API, mais également dans les shaders. La spécification WGSL est conçue pour rendre cette transition fluide et intuitive, tout en conservant l'efficacité et les performances des GPU modernes.
WGSL est conçu pour être un pont entre WebGPU et les API graphiques natives. Comparé à GLSL, WGSL semble un peu plus verbeux, mais la structure reste familière.
Voici un exemple de shader pour la texture :
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); }
Le tableau ci-dessous montre une comparaison des types de données de base et matricielles dans GLSL et WGSL :
La transition de GLSL vers WGSL démontre le désir d'un typage plus strict et d'une définition explicite de la taille des données, ce qui peut améliorer la lisibilité du code et réduire le risque d'erreurs.
La syntaxe de déclaration des structures a également changé :
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, };
L'introduction d'une syntaxe explicite pour déclarer les champs dans les structures WGSL souligne le désir d'une plus grande clarté et simplifie la compréhension des structures de données dans les shaders.
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); }
La modification de la syntaxe des fonctions dans WGSL reflète l'unification de l'approche des déclarations et des valeurs de retour, rendant le code plus cohérent et prévisible.
Dans WGSL, de nombreuses fonctions GLSL intégrées ont été renommées ou remplacées. Par exemple:
Renommer les fonctions intégrées dans WGSL simplifie non seulement leurs noms, mais les rend également plus intuitives, ce qui peut faciliter le processus de transition pour les développeurs familiarisés avec d'autres API graphiques.
Pour ceux qui envisagent de convertir leurs projets de WebGL vers WebGPU, il est important de savoir qu'il existe des outils pour convertir automatiquement GLSL en WGSL, tels que **[Naga](https://github.com/gfx-rs/naga /)**, qui est une bibliothèque Rust pour convertir GLSL en WGSL. Il peut même fonctionner directement dans votre navigateur avec l'aide de WebAssembly.
Voici les points de terminaison pris en charge par Naga :
Après la migration, vous pourriez rencontrer une surprise sous la forme d'images inversées. Ceux qui ont déjà porté des applications d'OpenGL vers Direct3D (ou vice versa) ont déjà été confrontés à ce problème classique.
Dans le contexte d'OpenGL et WebGL, les textures sont généralement chargées de telle manière que le pixel de départ corresponde au coin inférieur gauche. Cependant, dans la pratique, de nombreux développeurs chargent les images en commençant par le coin supérieur gauche, ce qui entraîne une erreur d'image inversée. Néanmoins, cette erreur peut être compensée par d’autres facteurs, nivelant finalement le problème.
Contrairement à OpenGL, des systèmes tels que Direct3D et Metal utilisent traditionnellement le coin supérieur gauche comme point de départ pour les textures. Considérant que cette approche semble la plus intuitive pour de nombreux développeurs, les créateurs de WebGPU ont décidé de suivre cette pratique.
Si votre code WebGL sélectionne des pixels dans le tampon de trame, préparez-vous au fait que WebGPU utilise un système de coordonnées différent. Vous devrez peut-être appliquer une simple opération "y = 1,0 - y" pour corriger les coordonnées.
Lorsqu'un développeur est confronté à un problème où des objets sont tronqués ou disparaissent plus tôt que prévu, cela est souvent lié à des différences dans le domaine de la profondeur. Il existe une différence entre WebGL et WebGPU dans la manière dont ils définissent la plage de profondeur de l'espace de découpage. Alors que WebGL utilise une plage de -1 à 1, WebGPU utilise une plage de 0 à 1, similaire à d'autres API graphiques telles que Direct3D, Metal et Vulkan. Cette décision a été prise en raison de plusieurs avantages liés à l'utilisation d'une plage de 0 à 1, identifiés lors de l'utilisation d'autres API graphiques.
La principale responsabilité de la transformation des positions de votre modèle en espace de clip incombe à la matrice de projection. Le moyen le plus simple d'adapter votre code est de vous assurer que les résultats de votre matrice de projection sont compris entre 0 et 1. Pour ceux qui utilisent des bibliothèques telles que gl-matrix, il existe une solution simple : au lieu d'utiliser la fonction perspective
, vous pouvez utiliser perspectiveZO
; des fonctions similaires sont disponibles pour d’autres opérations matricielles.
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, …); }
Cependant, il arrive parfois que vous disposiez d'une matrice de projection existante et que vous ne puissiez pas modifier sa source. Dans ce cas, pour la transformer en une plage de 0 à 1, vous pouvez pré-multiplier votre matrice de projection par une autre matrice qui corrige la plage de profondeur.
Voyons maintenant quelques trucs et astuces pour travailler avec WebGPU.
Plus vous utilisez de pipelines, plus vous avez de changements d’état et moins de performances ; cela n’est peut-être pas anodin, selon la provenance de vos actifs.
Créer un pipeline et l'utiliser immédiatement peut fonctionner, mais cela n'est pas recommandé. Au lieu de cela, créez des fonctions qui reviennent immédiatement et commencez à travailler sur un autre thread. Lorsque vous utilisez le pipeline, la file d'attente d'exécution doit attendre la fin des créations de pipeline en attente. Cela peut entraîner des problèmes de performances importants. Pour éviter cela, veillez à laisser un certain temps entre la création du pipeline et sa première utilisation.
Ou, mieux encore, utilisez les variantes create*PipelineAsync
! La promesse se réalise lorsque le pipeline est prêt à être utilisé, sans aucun blocage.
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()]); });
Les bundles de rendu sont des passes de rendu préenregistrées, partielles et réutilisables. Ils peuvent contenir la plupart des commandes de rendu (à l'exception de choses comme la configuration de la fenêtre d'affichage) et peuvent être "rejoués" ultérieurement dans le cadre d'une passe de rendu réelle.
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
Les bundles de rendu peuvent être exécutés parallèlement aux commandes de passe de rendu habituelles. L’état de réussite du rendu est réinitialisé aux valeurs par défaut avant et après chaque exécution du bundle. Ceci est principalement fait pour réduire la surcharge JavaScript du dessin. Les performances du GPU restent les mêmes quelle que soit l’approche.
La transition vers WebGPU signifie plus que simplement changer d’API graphique. C'est également une étape vers l'avenir du graphisme Web, combinant les fonctionnalités et les pratiques réussies de diverses API graphiques. Cette migration nécessite une compréhension approfondie des évolutions techniques et philosophiques, mais les bénéfices sont significatifs.