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. Aperçu du contenu 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é Chronologie de WebGL et WebGPU , 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 : WebGL La version de bureau d'OpenGL fait ses débuts. Bureau OpenGL (1993) : Il s'agissait de la première version stable de WebGL, basée sur OpenGL ES 2.0, lui-même introduit en 2007. Elle offrait aux développeurs Web la possibilité d'utiliser des graphiques 3D directement dans les navigateurs, sans avoir besoin de plugins supplémentaires. WebGL 1.0 (2011) : Introduit six ans après la première version, WebGL 2.0 était basé sur OpenGL ES 3.0 (2012). Cette version a apporté un certain nombre d'améliorations et de nouvelles fonctionnalités, rendant les graphiques 3D sur le Web encore plus puissants. WebGL 2.0 (2017) 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é : : Créée par le groupe Khronos, cette API multiplateforme est le « successeur » d'OpenGL. Vulkan fournit un accès de niveau inférieur aux ressources matérielles graphiques, permettant des applications hautes performances avec un meilleur contrôle sur le matériel graphique. Vulkan (2016) : Cette API a été créée par Microsoft et est exclusivement pour Windows et Xbox. D3D12 est le successeur du D3D10/11 et offre aux développeurs un contrôle plus approfondi sur les ressources graphiques. D3D12 (2015) : Créée par Apple, Metal est une API exclusive pour les appareils Apple. Il a été conçu dans un souci de performances maximales sur le matériel Apple. Metal (2014) L'état actuel du WebGPU et ce qui va arriver 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 : : Prise en charge complète de WebGPU. Babylon JS : Support expérimental pour le moment. ThreeJS : En développement, mais avec des perspectives très prometteuses. PlayCanvas : Un support très précoce et expérimental du WebGPU a été annoncé dans la version 2023.2 alpha. Unity : Supporte officiellement WebGPU, ce qui en fait l'un des pionniers dans ce domaine. Cocos Creator 3.6.2 : actuellement pris en charge dans la version v113+ pour Windows, macOS et ChromeOS uniquement. Construct 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. Différences conceptuelles de haut niveau 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. 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. WebGL : le modèle de contexte 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 : le modèle d'appareil 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. Programmes et pipelines WebGL et WebGPU représentent différentes approches pour gérer et organiser le pipeline graphique. WebGL : programme 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 : : Le code source des shaders est écrit et compilé. Création de Shaders : Les shaders compilés sont attachés au programme puis liés. Création du programme : Le programme est activé avant le rendu. Utilisation du programme : Les données sont transmises au programme activé. Transmission de données 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 : Pipeline 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 : : le code source du shader est écrit et compilé, de la même manière que dans WebGL. Définition du shader : les shaders et autres paramètres de rendu sont combinés dans un pipeline. Création de pipeline : Le pipeline est activé avant le rendu. Utilisation du pipeline 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 : Uniformes Les variables uniformes fournissent des données constantes disponibles pour toutes les instances de shader. Uniformes dans WebGL 1 Dans WebGL de base, nous avons la possibilité de définir des variables directement via des appels API. uniform : 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 Uniformes dans WebGL 2 Avec l'arrivée de WebGL 2, nous avons désormais la possibilité de regrouper des variables 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. uniform : 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é . 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 . bindBufferRange setBindGroup Uniformes dans WebGPU WebGPU nous offre une méthode encore meilleure. Dans ce contexte, les variables individuelles ne sont plus prises en charge et le travail est effectué exclusivement via des tampons . uniform 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. Shaders 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. Langage de shader : GLSL vs WGSL 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); } Comparaison des types de données 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. Structures 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. Déclarations de fonction : 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. Fonctions intégrées 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. Conversion de nuanceur 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 : Différences de convention Textures 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. Espace de la fenêtre 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. Espaces de découpe 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 , vous pouvez utiliser ; des fonctions similaires sont disponibles pour d’autres opérations matricielles. perspective perspectiveZO 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. Trucs et astuces WebGPU Voyons maintenant quelques trucs et astuces pour travailler avec WebGPU. Réduisez le nombre de pipelines que vous utilisez. 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 des pipelines à l'avance 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 ! La promesse se réalise lorsque le pipeline est prêt à être utilisé, sans aucun blocage. create*PipelineAsync 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()]); }); Utiliser les RenderBundles 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. Résumé 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. Ressources et liens utiles : WebGPU – Tous les cœurs, aucun du canevas De WebGL à WebGPU dans Construct par Alain Galvan Tutoriel WebGPU brut par Brandon Jones Meilleures pratiques WebGPU Meetup WebGL + WebGPU - Juillet 2023 Lien