WBOIT en OpenGL: transparence sans tri

Nous parlerons de «Transparence pondérée indépendante de l'ordre mélangé» (ci-après WBOIT) - la technique décrite dans JCGT en 2013 ( lien ).

Lorsque plusieurs objets transparents apparaissent à l'écran, la couleur du pixel dépend de celui qui est le plus proche de l'observateur. Voici une formule de mélange de couleurs bien connue pour ce cas:

\ begin {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}


L'ordre de la disposition des fragments est important pour lui: la couleur du fragment proche et son opacité sont notées C près et α , et la couleur résultante de tous les fragments qui se trouvent derrière lui est notée C loin . L'opacité est une propriété qui prend des valeurs de 0 à 1, où 0 signifie que l'objet est si transparent qu'il n'est pas visible et 1 - qu'il est si opaque que rien n'est visible derrière lui .

Pour utiliser cette formule, vous devez d'abord trier les fragments par profondeur. Imaginez combien de maux de tête cela implique! En général, le tri doit être effectué dans chaque trame. Si vous triez des objets, certains objets de forme complexe devront être coupés en morceaux et triés en fonction de la profondeur des pièces coupées (en particulier, pour les surfaces qui se croisent, cela devra certainement être fait). Si vous triez les fragments, le tri se fera dans les shaders. Cette approche est appelée «Transparence indépendante de l'ordre» (OIT), et elle utilise une liste liée stockée dans la mémoire de la carte vidéo. Il est presque irréaliste de prévoir la quantité de mémoire à allouer à cette liste. Et s'il n'y a pas assez de mémoire, des artefacts apparaîtront à l'écran.

Heureusement pour ceux qui peuvent contrôler combien d'objets translucides sont placés sur la scène et où ils sont les uns par rapport aux autres. Mais si vous faites de la CAO, vous aurez autant d'objets transparents que l'utilisateur le souhaite et ils seront localisés de manière aléatoire.

Vous comprenez maintenant le désir de certaines personnes de simplifier leur vie et de trouver une formule pour mélanger les couleurs qui ne nécessite pas de tri. Une telle formule se trouve dans l'article auquel j'ai fait référence au début. Il y a même plusieurs formules là-bas, mais la meilleure selon les auteurs (et à mon avis aussi) est celle-ci:

\ begin {matrix} C = {{\ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}} (1- \ prod_ {i = 1} ^ {n} (1- \ alpha_i)) + C_0 \ prod_ {i = 1} ^ {n} (1- \ alpha_i) && (2) \ end {matrix}




Dans la capture d'écran, il y a des groupes de triangles translucides situés en quatre couches de profondeur. À gauche, ils sont rendus en utilisant la technique WBOIT. À droite, une image obtenue à l'aide de la formule (1), mélange classique de couleurs, en tenant compte de l'ordre de la disposition des fragments. Ensuite, je l'appellerai CODB (mélange classique dépendant de l'ordre).

Avant de commencer le rendu des objets transparents, nous devons rendre tous les objets opaques. Après cela, les objets transparents sont rendus avec un test de profondeur, mais sans écrire dans le tampon de profondeur (cela se fait comme ceci: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ). C'est-à-dire, c'est ce qui se passe à un point avec certaines coordonnées d'écran (x, y): les fragments transparents qui sont plus proches que l'opaque passent le test de profondeur, quelle que soit leur position en profondeur par rapport aux fragments transparents déjà dessinés, et les fragments transparents qui apparaissent plus loin opaques, ne réussissent pas le test de profondeur et, par conséquent, sont jetés.

C 0 dans la formule (2) est la couleur d'un fragment opaque, au-dessus duquel sont dessinés des fragments transparents, dont nous avons n morceaux, indiqués par les indices 1 à n. C i est la couleur du i-ème fragment transparent, α i est son opacité.

Si vous regardez attentivement, la formule (2) est un peu comme la formule (1). Si vous imaginez que Est C près , C 0 est C loin et - c'est α , alors ce sera la 1ère formule, une à une. Et vraiment - c'est la moyenne pondérée des couleurs des fragments transparents (le centre de masse est déterminé en mécanique par la même formule), ce sera la couleur du fragment C le plus proche près . C 0 est la couleur du fragment opaque situé derrière tous les fragments, pour lequel nous avons calculé cette moyenne pondérée, et elle passera pour C loin . Autrement dit, nous avons remplacé tous les fragments transparents par un fragment «moyenné» et appliqué la formule standard pour mélanger les couleurs - formule (1). Quelle est cette formule astucieuse pour α que les auteurs de l'article original nous proposent?

 alpha=1 prodi=1n(1 alphai)


Il s'agit d'une fonction scalaire dans un espace à n dimensions. Rappelons donc l'analyse différentielle des fonctions de plusieurs variables. Étant donné que tous les α i appartiennent à la plage de 0 à 1, la dérivée partielle par rapport à l'une des variables sera toujours une constante non négative. Cela signifie que l'opacité du fragment «moyen» augmente avec l'augmentation de l'opacité de n'importe lequel des fragments transparents, et c'est exactement ce dont nous avons besoin. De plus, il augmente linéairement.

Si l'opacité d'un fragment est de 0, alors elle n'est pas visible du tout, elle n'affecte pas la couleur résultante.

Si l'opacité d'au moins un fragment est 1, alors α est 1. Autrement dit, le fragment opaque devient invisible, ce qui est généralement bon. Seuls les fragments transparents situés derrière le fragment avec opacité = 1 brillent encore à travers lui et affectent la couleur résultante:



Ici, un triangle orange se trouve au-dessus, vert en dessous, et gris et cyan sous vert, et tout cela sur un fond noir. Opacité bleue = 1, tous les autres - 0,5. L'image de droite est ce qu'elle devrait être. Comme vous pouvez le voir, WBOIT a l'air dégoûtant. Le seul endroit où la couleur orange normale apparaît est le bord du triangle vert, entouré d'une ligne blanche opaque. Comme je viens de le dire, un fragment opaque est invisible si l'opacité du fragment transparent est 1.

C'est encore mieux vu ici:



Le triangle orange a une opacité de 1, le vert avec la transparence désactivée est simplement dessiné avec les objets opaques. Il semble que le triangle VERT brille à travers ORANGE à travers le triangle orange.

Pour que l'image soit décente, le plus simple est de ne pas attribuer une opacité élevée aux objets. Dans mon projet de travail, je n'autorise pas la définition d'opacité supérieure à 0,5. Il s'agit de CAO 3D, dans lequel les objets sont dessinés schématiquement, et un réalisme spécial n'est pas nécessaire, une telle restriction est donc autorisée ici.

Avec des valeurs d'opacité faibles, les images de gauche et de droite se ressemblent presque:



Et avec haut, ils diffèrent sensiblement:



Voici à quoi ressemble un polyèdre transparent:




Le polyèdre a des faces horizontales orange latérales et vertes. Malheureusement, vous ne comprendrez pas cela à première vue, c'est-à-dire l'image ne semble pas convaincante. Là où il y a un mur orange devant, vous avez besoin de plus que l'orange, et où le vert est plus que vert. Il sera beaucoup mieux de dessiner des visages dans une seule couleur:



WBOIT basé sur la profondeur


Afin de compenser en quelque sorte le manque de tri par profondeur, les auteurs de l'article ont proposé plusieurs options pour ajouter de la profondeur à la formule (2). Cela rend la mise en œuvre plus difficile et le résultat moins prévisible et dépend des caractéristiques d'une scène tridimensionnelle particulière. Je ne me suis pas plongé dans ce sujet, alors peu importe - je propose de lire l'article.

On fait valoir que WBOIT est parfois capable de quelque chose que la transparence du tri classique ne peut pas. Par exemple, vous dessinez la fumée comme un système de particules utilisant seulement deux particules - avec de la fumée foncée et claire. Lorsqu'une particule passe à travers une autre, le mélange de couleurs classique avec le tri donne un résultat moche - la couleur de la fumée de la lumière devient fortement sombre. L'article dit que le WBOIT sensible à la profondeur permet une transition en douceur et semble plus crédible. On peut en dire autant de la modélisation de la fourrure et des cheveux sous forme de tubes fins.

Code


Maintenant, comment implémenter la formule (2) sur OpenGL. L'exemple de code se trouve sur Github ( lien ), et la plupart des images de l'article sont de là. Vous pouvez collecter et jouer avec mes triangles. Le framework Qt est utilisé.

Pour ceux qui commencent tout juste à étudier le rendu des objets transparents, je recommande ces deux articles:

Apprenez OpenGL. Leçon 4.3 - Mélanger les couleurs
Algorithme de transparence indépendant de l'ordre utilisant des listes liées sur Direct3D 11 et OpenGL 4

Le second, cependant, n'est pas si important pour comprendre ce matériel, mais le premier est une lecture incontournable.

Pour calculer la formule (2), nous avons besoin de 2 tampons d'image supplémentaires, 3 textures multi-échantillons et un tampon de rendu, dans lequel nous allons écrire la profondeur. Dans la première texture - colorTextureNT (NT signifie non transparent) - nous rendrons des objets opaques. Il a le type GL_RGB10_A2. La deuxième texture (colorTexture) sera de type GL_RGBA16F; dans les 3 premiers composants de cette texture, nous écrirons ce morceau de formule (2): dans le quatrième - . Une autre texture de type GL_R16 (alphaTexture) contiendra .

Vous devez d'abord créer ces objets pour obtenir leurs identifiants depuis OpenGL:

  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

Comme je l'ai dit, le framework Qt est utilisé ici, et tous les appels OpenGL passent par un objet de type QOpenGLFunctions_4_5_Core, qui est toujours appelé f pour moi.

Vous devez maintenant allouer de la mémoire:

  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

Et configurez les tampons d'image:

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

Lors de la deuxième étape de rendu, la sortie du fragment shader ira à deux textures à la fois, et cela doit être explicitement spécifié à l'aide de glDrawBuffers.

La plupart de ce code est exécuté une fois, au démarrage du programme. Le code qui alloue de la mémoire aux textures et aux tampons de rendu est appelé chaque fois que la fenêtre est redimensionnée. Vient ensuite le code de rendu, qui est appelé chaque fois que la fenêtre est redessinée.

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ...   ... 

Nous venons de dessiner tous les objets opaques sur la texture colorTextureNT et d'écrire les profondeurs dans le tampon de rendu. Avant d'utiliser le même tampon de rendu à l'étape suivante du dessin, vous devez vous assurer que toutes les profondeurs des objets opaques y sont déjà écrites. Pour cela, GL_FRAMEBUFFER_BARRIER_BIT est utilisé. Après avoir rendu les objets transparents, nous appelons la fonction ApplyTextures (), qui lancera la dernière étape du rendu, dans laquelle le fragment shader lira les données des textures colorTextureNT, colorTexture et alphaTexture pour appliquer la formule (2). Les textures auraient dû être entièrement écrites d'ici là, donc avant d'appeler ApplyTextures (), nous utilisons GL_TEXTURE_FETCH_BARRIER_BIT.

  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ...   ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO est le framebuffer à travers lequel nous affichons l'image. Dans la plupart des cas, c'est 0, mais dans Qt c'est QOpenGLWidget :: defaultFramebufferObject ().

Chaque fois que le fragment shader est appelé, nous aurons des informations sur la couleur et l'opacité du fragment actuel. Mais à la sortie dans la texture colorTexture nous voulons obtenir la somme (et dans la texture alphaTexture le produit) de certaines fonctions de ces quantités. Le mélange est utilisé pour cela. De plus, puisque pour la première texture nous calculons la somme, et pour la seconde - le produit, les paramètres de fusion (glBlendFunc et glBlendEquation) pour chaque pièce jointe doivent être définis séparément.

Voici le contenu de la fonction PrepareToTransparentRendering ():

  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 

Et le contenu de la fonction CleanupAfterTransparentRendering ():

  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

Dans mon fragment shader, l'opacité est indiquée par la lettre w. Le produit de la couleur par w et w lui-même est émis vers un paramètre de sortie et 1 - w vers un autre. Pour chaque paramètre de sortie, un qualificatif de disposition est défini sous la forme «location = X», où X est l'index de l'élément dans le tableau de pièces jointes, que nous avons transmis à glDrawBuffers dans la 3e liste (en particulier, le paramètre de sortie avec location = 0 est envoyé à la texture liée à GL_COLOR_ATTACHMENT0 et le paramètre avec location = 1 - dans la texture attachée à GL_COLOR_ATTACHMENT1). Les mêmes nombres sont utilisés dans les fonctions glBlendFunci et glBlendEquationi pour indiquer le numéro de pièce jointe pour lequel nous avons défini les paramètres de fusion.

Fragment Shader:

 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

Dans la fonction ApplyTextures (), nous dessinons simplement un rectangle sur toute la fenêtre. Le fragment shader demande des données de toutes les textures que nous avons créées, en utilisant les coordonnées d'écran actuelles comme coordonnées de texture et le numéro d'échantillon actuel (gl_SampleID) comme numéro d'échantillon dans la texture multi-échantillon. L'utilisation de la variable gl_SampleID dans le shader active automatiquement le mode lorsque le fragment shader est appelé une fois pour chaque échantillon (dans des conditions normales, il est appelé une fois pour le pixel entier, et le résultat est écrit sur tous les échantillons qui étaient à l'intérieur de la primitive).

Il n'y a rien de remarquable dans le vertex shader:

 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 

Fragment Shader:

 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

Et enfin, le contenu de la fonction ApplyTextures ():

  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 

Eh bien, ce serait bien de libérer les ressources OpenGL après la fin. J'ai ce code appelé dans le destructeur de mon widget OpenGL:

  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

Source: https://habr.com/ru/post/fr457284/


All Articles