La texture de profondeur | WebGPU

Les textures de profondeur sont pratiquement nécessaires pour rendre des scènes avec plusieurs objets. Nous apprendrons comment les mettre en œuvre facilement, tout en voyant certaines erreurs que j'ai commises pendant son implémentation.

Keywords: WebGPU, javascript, rendu 3D

By Carmen Cincotti  

❗ Cet article fait partie du projet WebGPU Cloth Simulation. Pour voir ce code, voyez ce repo dans GitHub 🧵

Un bug m’avait tourmenté cette semaine. Étant équipé du nouveau chargeur de fichiers .OBJs, j’étais prêt à rendre un lapin de Stanford 🐇. Malheureusement, j’ai aussi rencontré de mauvais bugs 🪲 causés par le manque de tests de profondeur.

Au cours de ce post, j’aimerais explorer ce bug tout en répondant à la question : que sont-ils les textures de profondeur ?

Le bug du texture de profondeur 🪲

Voici l’image du lapin de Stanford que j’ai d’abord rendu :

Bad rendering of a bunny

Il m’a semblé pas correct ! Le rendu des oreilles était bizarre…

Je commençais à douter du fichier du lapin : peut-être n’était-il pas correct ? Je l’ai réexporté depuis Blender. Malheureusement, le problème est resté encore.

Puis, dans un moment de clarté, j’ai décidé de rendre un modèle avec moins de complexité. Un cube 🧊 - et le résultat était pareil par rapport au lapin :

Bad rendering of cube

Le processus de régler cette erreur a été une grosse perte du temps. Je pensais à tort que le problème était mon chargeur de fichiers .OBJ. Cependant, le vrai problème était lié aux tests de profondeur (et au fait que je les ai négligés).

Le problème et des solutions possibles

Le problème en termes clairs est l’ordre de rendu des fragments. Le dernier triangle rendu sera visible même s’il y a un triangle derrière. On a donc deux choix pour résoudre ce problème :

  1. 🔘 Trier - Triez tous les triangles en fonction de la profondeur avant de les rendre.
  2. 🔘 Utiliser un tampon de profondeur - Implémentez un tampon de profondeur et laissez-le faire le travail acharné (les tests de profondeur et la résolution finale de l’image).

Comme je suis paresseux, mon choix est naturellement le numéro deux et c’est exactement le choix que j’ai choisi. 🦥

⚠️ Cette solution marche pour moi parce que mes besoins ne sont pas complexes. Si vous rencontrez des problèmes de transparence, vous devez trouver une solution.

Repassons aux tests de profondeur

Il y a plusieurs semaines, j’ai écrit un article sur les tampons de profondeur et également sur les tests de profondeur. N’oubliez pas que les tampons de profondeur emmagasinent les coordonnées z d’une image.

Voici une image qui illustre le tampon de couleur (en haut) et le tampon de profondeur (en bas) :

Color and z-buffer images

Les tests de profondeur sont utilisés pour résoudre la visibilité de toutes les primitives sur l’image finale. Cela dit, s’il y a une primitive cachée derrière une autre, nous voudrions éviter de la rendre sur l’image finale.

Le GPU gère cela pour nous. Il compare les valeurs de profondeur calculées (la valeur z) de tous les pixels associés à différentes primitives à un emplacement donné sur l’écran. Voici un exemple du processus :

Z-buffer example

Dans ce cas -la valeur z et la couleur du pixel le plus proche sont stockées dans le framebuffer pour être rendues. Ce type de test est appelé test de profondeur.

Sinon, ces valeurs du pixel ne sont donc pas écrasées.

La solution du bug en WebGPU 🥳

Supposons que j’utilise le code du triangle que nous avons créé avec WebGPU il y a plusieurs mois. On doit d’abord configurer notre renderDescriptor en utilisant le champ depthStencilAttachment comme suit :

const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ ... ], depthStencilAttachment: { view: depthTexture.createView(), depthClearValue: 1.0, depthLoadOp: "clear", depthStoreOp: "store", }, };

depthTexture est initialisé comme suit :

const depthTexture = this.device.createTexture({ size: this.presentationSize, format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, });

Rappelons que lors de la dernière partie, on a vu une image qui représente une texture de profondeur (en noir et blanc) :

Z-buffer image black and white

En regardant cette image, on voit que les objets les plus proches sont noirs. On met le champ depthStencilAttachment.depthClearValue avec une valeur de 1.0 pour signifier ‘aussi loin que possible’ alors qu’une valeur de 0 signifie ‘aussi proche que possible’.

L’initialisation de la depthTexture est assez simple pour nous grâce à nos besoins pas complexes :

  • Taille - Il doit être de la même taille (size) que le tampon de couleur.
  • Précision - Il doit être une précision suffisante, qui est généralement 24 bits au minimum.

La configuration finale se trouve dans l’appel à GPUDevice.createRenderPipeline :

this.device.createRenderPipeline({ primitive: { ... }, depthStencil: { depthWriteEnabled: true, depthCompare: "less", format: "depth24plus", }, ...shader, });

Cette configuration est assez simple à comprendre. La chose la plus pertinente est qu’on doit exiger que le GPU effectue le test de profondeur en comparant les valeurs de profondeur et en sauvegardant les fragments avec le plus bas. On configure ce comportement en définissant la valeur less pour depthCompare.

Voilà le résultat :

Good bunny

Bonus - une explosion de mémoire et comment je l’ai résolu

En développant le système de rendu, j’ai d’abord mal implémenté la texture de profondeur. Cependant, le problème n’était pas si évident à première vue.

Chrome Error message

Google Chrome se plantait au bout d’une minute en exécutant le code de rendu avec l’erreur :

Uncaught (in promise) Error: WebGPU cannot be initialized - Device has been lost - GPU connection lost.

Après l’actualisation de la page, une nouvelle erreur est apparue :

WebGPU cannot be initialized - Adapter not found.

Quoi ?! 😧

Dans ma tête, je pensais ‘Oh, cela doit être un bug dans Chrome’ - mais non… c’était un bug à moi !

Comment est-ce que je le savais ? J’ai ouvert mes outils de profiling GPU sur mon ordinateur et j’ai vu :

Radeon GPU dashboard

La mémoire explosait lors de l’exécution du programme !

Le problème s’est avéré être que je créais une texture de profondeur sur le GPU à chaque frame ! Voici le code commenté juste en dessous de const frame = () => { :

draw(drawCb: (drawHelper: GPURenderPassEncoder) => void) { // ~~ Define render loop ~~ const depthTexture = this.device.createTexture({ size: this.presentationSize, format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); const frame = () => { // WRONG // const depthTexture = this.device.createTexture({ // size: this.presentationSize, // format: "depth24plus", // usage: GPUTextureUsage.RENDER_ATTACHMENT, // }); const commandEncoder = this.device.createCommandEncoder();

Cela ajoute au GPU une texture de profondeur supplémentaire à chaque frame ! Après environ une minute, le GPU se tuerait pour se sauver. Quelle tragédie !

Des ressources (en français et anglais)


Comments for La texture de profondeur | WebGPU



Written by Carmen Cincotti, computer graphics enthusiast, language learner, and improv actor currently living in San Francisco, CA.  Follow @CarmenCincotti

Contribute

Interested in contributing to Carmen's Graphics Blog? Click here for details!