GLSL: Centre ou Centroid? Ou quand les shaders attaquent

En modifiant le shader pour le prochain jeu, je suis tombé sur un artefact désagréable qui ne se manifeste que lorsque le matériel MSAA est activé. Dans la capture d'écran du paysage, vous pouvez voir quelques pixels trop lumineux. Les valeurs chromatiques de plusieurs d'entre elles étaient si grandes qu'après la floraison, elles se sont transformées en «fantômes» multicolores.

image

J'attire votre attention sur la traduction d'un article qui explique en détail la raison de ce phénomène et la manière de le gérer.

image

Figure 1 - Images correctes (à gauche) et incorrectes (à droite). Faites attention à la barre jaune sur le bord gauche de l'image «incorrecte». Bien que la variable myMixer varie de 0 à 1, elle dépasse en quelque sorte cette plage dans l'image «incorrecte».

Considérons un shader de fragment simple avec une transformation non linéaire simple:

smooth in float myMixer; //      . //  sqrt    . void main( void ) { const vec3 blue = vec3( 0.0, 0.0, 1.0 ); const vec3 yellow = vec3( 1.0, 1.0, 0.0 ); float a = sqrt( myMixer ); //    myMixer < 0.0 vec3 color = mix( blue, yellow, a ); //   gl_FragColor = vec4( color, 1.0 ); } 

D'où vient la bande jaune à gauche dans l'image incorrecte? Pour mieux comprendre ce qui a mal tourné, examinons d'abord un cas dans lequel tout fonctionne correctement (presque) toujours.

image

Il s'agit d'une pixellisation classique avec un échantillon. Les carrés gris sont des pixels et les points jaunes sont les centres des pixels situés aux coordonnées de la fenêtre demi-entier (par défaut, les coordonnées du pixel inférieur gauche dans gl_FragCoord sont (0,5, 0,5) - trans. ).

image

Dans l'image ci-dessus, la ligne sécante sépare le demi-espace de la primitive. Au-dessus et à gauche de cette ligne, la variable myMixer est positive, et en dessous et à droite est négative.

La pixellisation classique à un échantillon classe les pixels en fonction de l'appartenance primitive et crée des fragments uniquement pour les pixels dont le centre se trouve à l'intérieur de la primitive. Dans cet exemple, six fragments seront produits, montrés en haut à gauche. Les pixels marqués en couleur atténuée ne tombent pas dans la primitive. Aucun fragment ne sera généré pour eux.

image

Le vert indique les points auxquels le fragment shader sera calculé. La valeur de myMixer sera calculée pour le centre de chaque pixel. Notez que les points verts sont au-dessus et à gauche de la ligne, donc les valeurs de myMixer seront positives. Toutes les données d'entrée associées aux sommets (variables variables ou variables d'entrée / sortie) seront également interpolées à ces points.

Notre shader simple n'utilise pas de dérivés (explicites ou implicites, par exemple, lors de l'échantillonnage à partir d'une texture avec des niveaux de mip), mais les dérivés dFdx (horizontal) et dFdy (vertical) sont marqués de flèches. À l'intérieur de la primitive, ils sont assez bien définis et réguliers.

Pour résumer: dans une seule sélection, les fragments ne sont générés que si le centre du pixel tombe "à l'intérieur" de la primitive, les données du fragment sont calculées pour le centre du pixel, l'interpolation des données de sommet et le calcul du shader sont effectués uniquement à l'intérieur de la primitive. Tout est bon et "correct". (Presque toujours. Pour l'instant, omettons les inexactitudes de certains dérivés sur les pixels le long de la bordure de la primitive).

Donc, tout est (presque) excellent pour la pixellisation avec une seule sélection. Mais qu'est-ce qui peut mal tourner lorsque le multi-échantillonnage est activé?

image

Il s'agit d'une pixellisation classique à échantillonnage multiple. Les carrés gris indiquent les pixels. Les points jaunes sont des centres de pixels en coordonnées demi-entières. Aux points bleus, l'échantillonnage a lieu. Cet exemple montre un schéma simple de deux échantillons tournés. Tous les arguments peuvent être généralisés pour un nombre arbitraire d'échantillons.

image

La ligne sépare toujours le demi-espace de la primitive. Au-dessus et à gauche de celui-ci, la valeur de myMixer est positive. Plus bas et à droite - négatif.

Lors de la pixellisation avec multi-échantillonnage, le classificateur de pixels générera un fragment si au moins un échantillon de pixels tombe à l'intérieur de la primitive.

Dans cet exemple, 10 fragments seront générés, affichés dans le demi-plan supérieur gauche. Notez les quatre fragments ajoutés le long du visage, dans lesquels un échantillon tombe à l'intérieur de la primitive, bien que le centre soit à l'extérieur. Les pixels en dehors de la primitive sont toujours marqués comme étant sombres.

image

Que se passera-t-il lors du calcul au centre du pixel?

Le shader sera calculé en points verts et rouges pour chacun des fragments. Les données associées à myMixer sont calculées au centre de chaque pixel. En points verts, ces valeurs seront positives, car elles sont au-dessus et à gauche de la bordure. Les points rouges sont en dehors de la primitive, car les valeurs de myMixer sont négatives. Aux points rouges, les données associées sont extrapolées au lieu d'une interpolation.

Dans notre shader, les valeurs sqrt (myMixer) ne sont pas définies avec un myMixer négatif. Même lorsque les valeurs myMixer enregistrées par le vertex shader se situent dans l'intervalle de zéro à un, dans le fragment shader myMixer peut dépasser cet intervalle en raison de l'extrapolation. Ainsi, avec un myMixer négatif, le résultat du fragment shader n'est pas défini.

image

Nous envisageons toujours de calculer le shader au centre des pixels, les flèches de la figure montrent dFdx et dFdy. Sur les fragments internes du polygone, ils sont assez bien définis car tous les calculs sont effectués au centre de pixels situés à intervalles égaux.

image

Que se passera-t-il lors du calcul à des points autres que les centres des pixels?

Les points verts sont les points auxquels le shader sera calculé. La valeur associée de myMixer est calculée dans le centre de gravité de chaque pixel.

Le centre de gravité d'un pixel est le centre de gravité de l'intersection du carré du pixel et de l'intérieur de la primitive. Pour un pixel entièrement couvert, le centroïde est le centre. Pour un pixel partiellement couvert, le centroïde est généralement différent du centre.

Le standard OpenGL permet à une implémentation de sélectionner un point arbitraire à l'intersection d'une primitive et d'un pixel au lieu d'un centroïde idéal. Par exemple, il pourrait s'agir d'un point d'échantillonnage.

Dans cet exemple, si le centre se trouve à l'intérieur de la primitive, les données de sommet sont calculées pour le centre. Sinon, ils sont calculés à l'un des points d'échantillonnage situés à l'intérieur de la primitive. Cela se produit pour quatre pixels le long de la bordure. Tous les points verts se trouvent au-dessus et à gauche de la bordure, de sorte que les valeurs qu'ils contiennent sont toujours interpolées et jamais extrapolées.

Pourquoi ne pas toujours calculer le shader centroïde? En général, c'est plus cher que l'informatique au centre. Cependant, ce n'est pas le facteur principal.

Tout tourne autour du calcul des dérivés. Faites attention aux flèches entre les points verts. La distance entre eux n'est pas la même pour différentes paires de points. De plus, y n'est pas constant pour dFdx et x n'est pas constant pour dFdy. Les dérivés sont moins précis lorsqu'ils sont calculés en centroïdes .

C'est un compromis, et donc OpenGL, à partir de GLSL 1.20, offre au développeur du shader un choix entre le centre et le centroïde en utilisant le qualificatif centroïde:

 centroid in float myMixer; //  centroid  smooth //      . //  sqrt    . void main( void ) { const vec3 blue = vec3( 0.0, 0.0, 1.0 ); const vec3 yellow = vec3( 1.0, 1.0, 0.0 ); float a = sqrt( myMixer ); //    myMixer < 0.0 vec3 color = mix( blue, yellow, a ); //   gl_FragColor = vec4( color, 1.0 ); } 

Quand faut-il utiliser le centroïde?

  1. Lorsqu'une valeur extrapolée peut conduire à des résultats vagues. Portez une attention particulière aux fonctions intégrées, dont la description indique "le résultat n'est pas défini si ..."
  2. Lorsqu'une valeur extrapolée est utilisée avec une fonction très non linéaire ou discontinue. Ceux-ci incluent la fonction de pas ou le calcul de la fusée, en particulier lorsque l'exposant est suffisamment grand.

Quand ne devriez-vous pas utiliser un centroïde?

  1. Si vous avez besoin de dérivés exacts. Les dérivés peuvent être explicites (appel dFdx) ou implicites, par exemple, des échantillons de textures avec des niveaux de mip ou avec un filtrage anisotrope. Dans la spécification GLSL, les dérivés dans les centroïdes sont considérés comme si inutilisables qu'ils sont déclarés non définis. Dans de tels cas, essayez d'écrire:

     centroid in float myMixer; //  ! smooth in float myCenterMixer; //     . 

  2. Si une grille est rendue dans laquelle la plupart des frontières des primitives sont internes et toujours bien définies. L'exemple le plus simple est une bande de 100 triangles (TRIANGLE_STRIP), dans laquelle seuls le premier et le dernier triangles sont soumis à extrapolation. Le qualificatif centroïde entraînera une interpolation sur ces deux triangles au prix d'une perte de précision et de continuité sur les 98 triangles restants.
  3. Si vous savez que des artefacts peuvent apparaître à partir d'une fonction indéfinie, non linéaire ou discontinue, mais en pratique, ces artefacts se révèlent presque invisibles. Si le shader n'attaque pas - ne le corrigez pas!

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


All Articles