Cette série présente le WebGPU et l'infographie en général.
Regardons d'abord ce que nous allons construire,
Jeu de la vie
Rendu 3D
Rendu 3D, mais avec éclairage
Rendu du modèle 3D
Sauf les connaissances de base de JS, aucune connaissance préalable n'est nécessaire.
Le tutoriel est déjà terminé sur mon github, ainsi que le code source.
WebGPU est une API relativement nouvelle pour le GPU. Bien que nommé WebGPU, il peut en fait être considéré comme une couche au-dessus de Vulkan, DirectX 12 et Metal, OpenGL et WebGL. Il est conçu pour être une API de bas niveau et est destiné à être utilisé pour des applications hautes performances, telles que des jeux et des simulations.
Dans ce chapitre, nous allons dessiner quelque chose sur l'écran. La première partie fera référence au didacticiel Google Codelabs. Nous allons créer un jeu de vie sur l'écran.
Nous allons simplement créer un projet Vanilla JS vide rapidement avec TypeScript activé. Effacez ensuite tous les codes supplémentaires, ne laissant que le main.ts.
const main = async () => { console.log('Hello, world!') } main()
Avant le codage proprement dit, veuillez vérifier si WebGPU est activé sur votre navigateur. Vous pouvez le vérifier sur les exemples WebGPU.
Chrome est désormais activé par défaut. Sur Safari, vous devez accéder aux paramètres du développeur, signaler les paramètres et activer WebGPU.
Nous devons également activer les types pour WebGPU, installer @webgpu/types et dans les options du compilateur tsc, ajouter des "types": ["@webgpu/types"].
De plus, nous remplaçons le
Il existe de nombreux codes passe-partout pour WebGPU, voici à quoi cela ressemble.
Nous devons d’abord accéder au GPU. Dans WebGPU, cela se fait par le concept d'adaptateur, qui est un pont entre le GPU et le navigateur.
const adapter = await navigator.gpu.requestAdapter();
Ensuite, nous devons demander un appareil à l'adaptateur.
const device = await adapter.requestDevice(); console.log(device);
On dessine notre triangle sur la toile. Nous devons récupérer l'élément canvas et le configurer.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
Ici, nous utilisons getContext pour obtenir des informations relatives sur le canevas. En spécifiant webgpu, nous obtiendrons un contexte responsable du rendu avec WebGPU.
CanvasFormat est en fait le mode couleur, par exemple srgb. Nous utilisons généralement simplement le format préféré.
Enfin, nous configurons le contexte avec le périphérique et le format.
Avant de plonger plus profondément dans les détails d'ingénierie, nous devons d'abord comprendre comment le GPU gère le rendu.
Le pipeline de rendu GPU est une série d'étapes que le GPU suit pour restituer une image.
L'application exécutée sur GPU est appelée un shader. Le shader est un programme qui s'exécute sur le GPU. Le shader a un langage de programmation spécial dont nous parlerons plus tard.
Le pipeline de rendu comporte les étapes suivantes,
En fonction des primitives, la plus petite unité que le GPU peut restituer, le pipeline peut comporter différentes étapes. En règle générale, nous utilisons des triangles, ce qui signale au GPU de traiter chaque groupe de 3 sommets comme un triangle.
Render Pass est une étape du rendu GPU complet. Lorsqu'une passe de rendu est créée, le GPU commencera le rendu de la scène, et vice versa une fois le rendu terminé.
Pour créer une passe de rendu, nous devons créer un encodeur chargé de compiler la passe de rendu en codes GPU.
const main = async () => { console.log('Hello, world!') } main()Ensuite, nous créons une passe de rendu.
const adapter = await navigator.gpu.requestAdapter();Copier après la connexionCopier après la connexionCopier après la connexionIci, nous créons une passe de rendu avec une pièce jointe de couleur. La pièce jointe est un concept GPU qui représente l'image qui va être rendue. Une image peut avoir de nombreux aspects que le GPU doit traiter, et chacun d'eux est une pièce jointe.
Ici, nous n'avons qu'une seule pièce jointe, qui est la pièce jointe couleur. La vue est le panneau sur lequel le GPU effectuera le rendu, ici nous la définissons sur la texture du canevas.
loadOp est l'opération que le GPU effectuera avant la passe de rendu, clear signifie que le GPU effacera d'abord toutes les données précédentes de la dernière image, et storeOp est l'opération que le GPU effectuera après la passe de rendu, store signifie GPU stockera les données dans la texture.
loadOp peut être load, qui préserve les données de la dernière image, ou clear, qui efface les données de la dernière image. storeOp peut être store, qui stocke les données dans la texture, ou throw, qui supprime les données.
Maintenant, appelez simplement pass.end() pour terminer la passe de rendu. Désormais, la commande est enregistrée dans le tampon de commandes du GPU.
Pour obtenir la commande compilée, utilisez le code suivant,
const device = await adapter.requestDevice(); console.log(device);Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionEt enfin, soumettez la commande à la file d'attente de rendu du GPU.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionMaintenant, vous devriez voir une vilaine toile noire.
Sur la base de nos concepts stéréotypés sur la 3D, nous nous attendrions à ce que l'espace vide soit de couleur bleue. Nous pouvons le faire en définissant la couleur claire.
const encoder = device.createCommandEncoder();Copier après la connexionDessiner un triangle à l'aide d'un shader
Maintenant, nous allons dessiner un triangle sur la toile. Nous utiliserons un shader pour ce faire. Le langage du shader sera wgsl, WebGPU Shading Language.
Maintenant, supposons que nous voulions dessiner un triangle avec les coordonnées suivantes,
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store", }] });Copier après la connexionComme nous l'avons indiqué précédemment, pour compléter un pipeline de rendu, nous avons besoin d'un vertex shader et d'un fragment shader.
Shader de sommet
Utilisez le code suivant pour créer des modules de shader.
const commandBuffer = encoder.finish();Copier après la connexionl'étiquette ici est simplement un nom, destiné au débogage. le code est le code du shader réel.
Vertex shader est une fonction qui prend n'importe quel paramètre et renvoie la position du sommet. Cependant, contrairement à ce à quoi on pourrait s'attendre, le vertex shader renvoie un vecteur à quatre dimensions, et non un vecteur à trois dimensions. La quatrième dimension est la dimension w, utilisée pour la division en perspective. Nous en discuterons plus tard.
Maintenant, vous pouvez simplement considérer un vecteur à quatre dimensions (x, y, z, w) comme un vecteur à trois dimensions (x/w, y/w, z/w).
Cependant, il y a un autre problème : comment transmettre les données au shader et comment extraire les données du shader.
Pour transmettre les données au shader, nous utilisons le vertexBuffer, un tampon qui contient les données des sommets. Nous pouvons créer un tampon avec le code suivant,
const main = async () => { console.log('Hello, world!') } main()Copier après la connexionCopier après la connexionCopier après la connexionIci, nous créons un tampon d'une taille de 24 octets, 6 flottants, qui est la taille des sommets.
l'utilisation est l'utilisation du tampon, qui est VERTEX pour les données de sommet. GPUBufferUsage.COPY_DST signifie que ce tampon est valide comme destination de copie. Pour tous les tampons dont les données sont écrites par le CPU, nous devons définir cet indicateur.
La carte ici signifie mapper le tampon sur le CPU, ce qui signifie que le CPU peut lire et écrire le tampon. Le démappage signifie démapper le tampon, ce qui signifie que le CPU ne peut plus lire et écrire le tampon, et donc le contenu est disponible pour le GPU.
Maintenant, nous pouvons écrire les données dans le tampon.
const adapter = await navigator.gpu.requestAdapter();Copier après la connexionCopier après la connexionCopier après la connexionIci, nous mappons le tampon sur le CPU et écrivons les données dans le tampon. Ensuite, nous démapper le tampon.
vertexBuffer.getMappedRange() renverra la plage du tampon mappé au CPU. Nous pouvons l'utiliser pour écrire les données dans le tampon.
Cependant, ce ne sont que des données brutes, et le GPU ne sait pas comment les interpréter. Nous devons définir la disposition du tampon.
const device = await adapter.requestDevice(); console.log(device);Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionIci, arrayStride est le nombre d'octets que le GPU doit parcourir dans le tampon lorsqu'il recherche l'entrée suivante. Par exemple, si arrayStride est 8, le GPU sautera 8 octets pour obtenir l'entrée suivante.
Puisqu'ici, nous utilisons float32x2, la foulée est de 8 octets, 4 octets pour chaque flotteur et 2 flotteurs pour chaque sommet.
Maintenant, nous pouvons écrire le vertex shader.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionIci, @vertex signifie qu'il s'agit d'un vertex shader. @location(0) signifie l'emplacement de l'attribut, qui est 0, tel que défini précédemment. Veuillez noter que dans le langage shader, vous avez affaire à la disposition du tampon, donc chaque fois que vous transmettez une valeur, vous devez transmettre soit une structure dont les champs ont défini @location, soit simplement une valeur avec @location.
vec2f est un vecteur flottant à deux dimensions et vec4f est un vecteur flottant à quatre dimensions. Puisque le vertex shader est requis pour renvoyer une position vec4f, nous devons l'annoter avec @builtin(position).
Shader de fragments
Le Fragment shader, de la même manière, est quelque chose qui prend la sortie du sommet interpolé et affiche les pièces jointes, la couleur dans ce cas. L'interpolation signifie que bien que seuls certains pixels sur les sommets aient une valeur déterminée, pour un pixel sur deux, les valeurs sont interpolées, soit linéairement, moyennées ou par d'autres moyens. La couleur du fragment est un vecteur à quatre dimensions, qui est la couleur du fragment, respectivement rouge, vert, bleu et alpha.
Veuillez noter que la couleur est comprise entre 0 et 1, et non entre 0 et 255. De plus, le fragment shader définit la couleur de chaque sommet, et non la couleur du triangle. La couleur du triangle est déterminée par la couleur des sommets, par interpolation.
Comme nous ne prenons actuellement pas la peine de contrôler la couleur du fragment, nous pouvons simplement renvoyer une couleur constante.
const main = async () => { console.log('Hello, world!') } main()Copier après la connexionCopier après la connexionCopier après la connexionPipeline de rendu
Ensuite, nous définissons le pipeline de rendu personnalisé en remplaçant le vertex et le fragment shader.
const adapter = await navigator.gpu.requestAdapter();Copier après la connexionCopier après la connexionCopier après la connexionNotez que dans Fragment Shader, nous devons spécifier le format de la cible, qui est le format du canevas.
Appel au tirage au sort
Avant la fin de la passe de rendu, nous ajoutons l'appel de tirage.
const device = await adapter.requestDevice(); console.log(device);Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionIci, dans setVertexBuffer, le premier paramètre est l'index du tampon, dans le champ de définition du pipeline buffers, et le deuxième paramètre est le tampon lui-même.
Lors de l'appel de draw, le paramètre est le nombre de sommets à dessiner. Puisque nous avons 3 sommets, nous en dessinons 3.
Maintenant, vous devriez voir un triangle jaune sur la toile.
Dessiner des cellules de jeu de vie
Maintenant, nous modifions un peu nos codes - puisque nous voulons construire un jeu de vie, nous devons donc dessiner des carrés au lieu de triangles.
Un carré est en fait deux triangles, nous devons donc dessiner 6 sommets. Les changements ici sont simples et vous n'avez pas besoin d'une explication détaillée.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Copier après la connexionCopier après la connexionCopier après la connexionCopier après la connexionMaintenant, vous devriez voir un carré jaune sur la toile.
Système de coordonnées
Nous n'avons pas discuté du système de coordonnées du GPU. C'est plutôt simple. Le système de coordonnées actuel du GPU est un système de coordonnées droitier, ce qui signifie que l'axe des x pointe vers la droite, l'axe des y pointe vers le haut et l'axe z pointe hors de l'écran.
La plage du système de coordonnées est de -1 à 1. L'origine est au centre de l'écran. L'axe z va de 0 à 1, 0 est le plan proche et 1 est le plan éloigné. Cependant, l'axe z correspond à la profondeur. Lorsque vous effectuez un rendu 3D, vous ne pouvez pas simplement utiliser l'axe z pour déterminer la position de l'objet, vous devez également utiliser la division en perspective. C'est ce qu'on appelle la coordonnée normalisée de l'appareil NDC.
Par exemple, si vous souhaitez dessiner un carré dans le coin supérieur gauche de l'écran, les sommets sont (-1, 1), (-1, 0), (0, 1), (0, 0) , mais vous devez utiliser deux triangles pour le dessiner.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!