Précision de la profondeur clairement

La précision de la profondeur est une douleur dans le cul que tout programmeur graphique devra tôt ou tard affronter. De nombreux articles et ouvrages ont été écrits sur ce sujet. Et dans différents jeux et moteurs, et sur différentes plates-formes, vous pouvez voir de nombreux formats et paramètres différents pour le tampon de profondeur .

La conversion de la profondeur sur le GPU ne semble pas évidente en raison de son interaction avec la projection en perspective, et l'étude des équations ne clarifie pas la situation. Pour comprendre comment cela fonctionne, il est utile de dessiner quelques images.

image

Cet article est divisé en 3 parties:

  1. Je vais essayer d'expliquer la motivation de la conversion de profondeur non linéaire .
  2. Je présenterai plusieurs graphiques qui vous aideront à comprendre comment fonctionne la conversion de profondeur non linéaire dans différentes situations, intuitivement et visuellement.
  3. Une discussion des principales conclusions du resserrement de la précision du rendu en perspective [Paul Upchurch, Mathieu Desbrun (2012)] concernant l'effet des erreurs d'arrondi à virgule flottante sur la précision de la profondeur.


Pourquoi 1 / z?


Un tampon de profondeur GPU matériel ne stocke généralement pas une représentation linéaire de la distance entre l'objet et la caméra, contrairement à ce que l'on attend naïvement de lui lors de la première réunion. Au lieu de cela, le tampon de profondeur stocke des valeurs inversement proportionnelles à la profondeur de l'espace de vue. Je veux décrire brièvement la motivation d'une telle décision.

Dans cet article, j'utiliserai d pour représenter les valeurs stockées dans le tampon de profondeur (dans la plage [0, 1] pour DirectX), et z pour représenter l'espace de vue en profondeur, c'est-à-dire La distance réelle de la caméra, en unités mondiales, par exemple en mètres. En général, la relation entre eux a la forme suivante:

image

a, b sont les constantes associées aux paramètres proche et éloigné des plans. En d'autres termes, d est toujours une transformation linéaire à partir de 1 / z .

À première vue, il peut sembler que n'importe quelle fonction de z peut être considérée comme d . Alors pourquoi regarde-t-elle de cette façon? Il y a deux raisons principales à cela.

Tout d'abord, 1 / z s'intègre naturellement dans la projection en perspective. Et c'est la classe de transformations la plus élémentaire, qui est garantie de préserver les lignes droites. Par conséquent, la projection en perspective convient à la pixellisation matérielle, car les bords droits des triangles restent droits sur l'écran. Nous pouvons obtenir une transformation linéaire à partir de 1 / z , en profitant de la division en perspective que le GPU effectue déjà:

image

Bien sûr, la véritable force de cette approche est que la matrice de projection peut être multipliée par d'autres matrices, ce qui vous permet de combiner de nombreuses transformations en une seule.

La deuxième raison est que 1 / z est linéaire dans l'espace d'écran, comme l'a noté Emil Persson . Cela facilite l'interpolation de d dans le triangle lors de la pixellisation, et des choses comme les tampons Z hiérarchiques , les premières éliminations Z et le tampon de profondeur de compression .

En bref de l'article
Alors que la valeur de w (profondeur de l'espace de vue) est linéaire dans l'espace de vue, elle est non linéaire dans l'espace de l'écran. z (profondeur) , non linéaire dans l'espace de visualisation, par contre linéaire dans l'espace d'écran. Cela peut être facilement vérifié avec un simple shader DX10:

float dx = ddx(In.position.z); float dy = ddy(In.position.z); return 1000.0 * float4(abs(dx), abs(dy), 0, 0); 

Ici, In.position est SV_Position. Le résultat ressemble à ceci:

image

Notez que toutes les surfaces sont monochromes. La différence de z d'un pixel à l'autre est la même pour n'importe quelle primitive. Ceci est très important pour le GPU. Une des raisons est que l'interpolation z est moins chère que l'interpolation w . Pour z, il n'est pas nécessaire d'effectuer une correction de perspective. Avec des unités matérielles moins chères, vous pouvez traiter plus de pixels par cycle avec le même budget pour les transistors. Naturellement, cela est très important pour le passage pré-z et la carte d'ombre . Avec le matériel moderne, la linéarité dans l'espace d'écran est également une fonctionnalité très utile pour les optimisations z. Étant donné que le gradient est linéaire pour toute la primitive, il est également relativement facile de calculer la plage de profondeur exacte au sein de la tuile pour l' abattage Hi-z . Cela signifie également que la compression z est possible. Avec un Δz constant en x et y, vous n'avez pas besoin de stocker beaucoup d'informations pour pouvoir restaurer complètement toutes les valeurs z dans une tuile, à condition que la primitive ait couvert la tuile entière.

Graphiques de profondeur


Les équations sont compliquées, regardons quelques photos!

image

La façon de lire ces graphiques est de gauche à droite, puis vers le bas. Commencez par d sur l'axe gauche. Puisque d peut être une transformation linéaire arbitraire à partir de 1 / z , nous pouvons organiser 0 et 1 à n'importe quel endroit convenable sur l'axe. Les marques indiquent différentes valeurs de tampon de profondeur . Pour des raisons de clarté, je modélise un tampon de profondeur normalisé à 4 bits, donc il y a 16 marques régulièrement espacées.

Le graphique ci-dessus montre la conversion de profondeur "standard" de la vanille en D3D et API similaires. Vous pouvez immédiatement remarquer comment, en raison de la courbe 1 / z , les valeurs proches du plan proche sont regroupées et les valeurs proches du plan lointain sont dispersées.

Il est également facile de comprendre pourquoi la proximité d'un plan affecte tellement la précision de la profondeur. La distance près du plan entraînera une augmentation rapide des valeurs d par rapport aux valeurs z , ce qui conduira à une distribution encore plus inégale des valeurs:

image

De même, dans ce contexte, il est facile de voir pourquoi le déplacement du plan éloigné à l'infini n'a pas un si grand effet. Cela signifie simplement étendre la plage de d à 1 / z = 0 :

image

Mais qu'en est-il de la profondeur en virgule flottante? Le graphique suivant a été ajouté des repères correspondant au format float avec 3 bits de l'exposant et 3 bits de la mantisse:

image

Maintenant, dans l'intervalle [0,1], il y a 40 valeurs différentes - un peu plus de 16 valeurs plus tôt, mais la plupart d'entre elles sont inutilement regroupées près du plan proche (plus près de 0, le flotteur a une précision plus élevée), où nous n'avons vraiment pas besoin de beaucoup de précision.

Maintenant, une astuce bien connue consiste à inverser la profondeur, en affichant le plan proche sur d = 1 et le plan lointain sur d = 0 :

image

Bien mieux! Maintenant, la distribution quasi-logarithmique du flotteur compense en quelque sorte la non-linéarité de 1 / z , tandis que plus proche du plan proche, elle donne une précision similaire à la mémoire tampon de profondeur entière, et donne une précision beaucoup plus grande ailleurs. La précision de la profondeur se détériore très lentement si vous vous éloignez de l'appareil photo.

L'astuce Z inversé a peut-être été réinventée indépendamment plusieurs fois, mais au moins la première mention était dans l'article de SIGGRAPH '99 [Eugene Lapidous et Guofang Jiao (malheureusement non accessible au public)]. Et récemment, il a été mentionné à nouveau sur le blog par Matt Petineo et Brano Kemen , et dans un discours d'Emil Persson Création de vastes mondes de jeux SIGGRAPH 2012.

Tous les graphiques précédents supposaient une plage de profondeur [0,1] après la projection, ce qui est une convention dans D3D. Et OpenGL ?

image

OpenGL suppose par défaut une plage de profondeur [-1, 1] après la projection. Pour les formats entiers, rien ne change, mais pour la virgule flottante, toute précision est concentrée inutile au milieu. (La valeur de profondeur est mappée sur la plage [0,1] pour un stockage ultérieur dans le tampon de profondeur, mais cela n'aide pas, car le mappage initial sur [-1,1] a déjà détruit toute la précision dans la moitié éloignée de la plage.) Et à cause de la symétrie, l'astuce inversé-Z ne fonctionnera pas ici.

Heureusement, dans OpenGL de bureau, cela peut être corrigé en utilisant l'extension largement prise en charge ARB_clip_control (également à partir d'OpenGL 4.5, glClipControl est inclus dans la norme). Malheureusement, GL ES est en vol.

L'effet des erreurs d'arrondi


La conversion de 1 / z et le choix du tampon de profondeur float vs int est une grande partie de l'histoire de la précision, mais pas tout. Même si vous avez une précision de profondeur suffisante pour représenter la scène que vous essayez de rendre, il est facile de dégrader la précision avec des erreurs arithmétiques pendant le processus de conversion de vertex.

Au début de l'article, il a été mentionné que Upchurch et Desbrun ont étudié ce problème. Ils ont proposé deux recommandations principales pour minimiser les erreurs d'arrondi:

  1. Utilisez un plan éloigné infini.
  2. Gardez la matrice de projection distincte des autres matrices et appliquez-la en tant qu'opération distincte dans le vertex shader, plutôt que de la combiner avec la matrice de vue.

Upchurch et Desbrun ont formulé ces recommandations en utilisant une méthode analytique basée sur le traitement des erreurs d'arrondi comme de petites erreurs aléatoires présentées dans chaque opération arithmétique et leur suivi au premier ordre dans le processus de conversion. J'ai décidé de tester les résultats en pratique.

Les sources ici sont Python 3.4 et numpy. Le programme fonctionne comme suit: une séquence de points aléatoires est générée, ordonnée par la profondeur, située linéairement ou logarithmiquement entre les plans proche et lointain. Ensuite, les points sont multipliés par les matrices de vue et de projection et la division en perspective est effectuée, en utilisant des flottants 32 bits, et éventuellement le résultat final est converti en un entier 24 bits. À la fin, il passe par la séquence et compte combien de fois 2 points voisins (qui avaient initialement des profondeurs différentes) sont devenus identiques, car ils avaient la même profondeur ou l'ordre a changé du tout. En d'autres termes, le programme mesure la fréquence à laquelle les erreurs de comparaison de profondeur se produisent - ce qui correspond à des problèmes tels que les combats Z - dans divers scénarios.

Voici les résultats pour near = 0,1, far = 10K, avec une profondeur linéaire de 10K. (J'ai essayé l'intervalle de profondeur logarithmique et d'autres ratios proches / lointains, et bien que les nombres spécifiques varient, les tendances générales des résultats étaient les mêmes.)

Dans le tableau, «eq» - deux points avec la profondeur la plus proche obtiennent la même valeur dans le tampon de profondeur, et «swap» - deux points avec la profondeur la plus proche sont échangés.
Matrice composite de vision et de projectionMatrices de vue et de projection séparées
float32int24float32int24
Valeurs Z inchangées (test de contrôle)0% éq
Échange de 0%
0% éq
Échange de 0%
0% éq
Échange de 0%
0% éq
Échange de 0%
Projection standard45% éq
Échange de 18%
45% éq
Échange de 18%
77% éq
Échange de 0%
77% éq
Échange de 0%
À l'infini45% éq
Échange de 18%
45% éq
Échange de 18%
76% éq
Échange de 0%
76% éq
Échange de 0%
Z inversé0% éq
Échange de 0%
76% éq
Échange de 0%
0% éq
Échange de 0%
76% éq
Échange de 0%
Infini + inversé-Z0% éq
Échange de 0%
76% éq
Échange de 0%
0% éq
Échange de 0%
76% éq
Échange de 0%
Standard + style GL56% éq
Échange de 12%
56% éq
Échange de 12%
77% éq
Échange de 0%
77% éq
Échange de 0%
Infini + style GL59% éq
Échange de 10%
59% éq
Échange de 10%
77% éq
Échange de 0%
77% éq
Échange de 0%

Je m'excuse du fait que sans graphique, il y a trop de dimension ici et que je ne peux pas le construire! Dans tous les cas, en regardant les chiffres, les conclusions suivantes sont évidentes:

  • Dans la plupart des cas, il n'y a pas de différence entre int et float depth buffer . Erreurs arithmétiques pour le calcul des erreurs de priorité de profondeur lors de la conversion en int. En partie parce que float32 et int24 ont ULP presque égal (l'unité de moindre précision est la distance au nombre voisin le plus proche) de [0,5.1] (puisque float32 a une mantisse de 23 bits), donc une erreur de conversion n'est pas ajoutée sur presque toute la plage de profondeur en int.
  • Dans la plupart des cas, la séparation des matrices de vue et de projection (suivant les recommandations d'Upchurch et Desbrun) améliore le résultat. Malgré le fait que le taux d'erreur global ne diminue pas, les «swaps» deviennent des valeurs égales, et c'est un pas dans la bonne direction.
  • Le plan éloigné infini modifie légèrement la fréquence des erreurs. Upchurch et Desbrun ont prédit une réduction de 25% de la fréquence des erreurs numériques (erreurs de précision), mais cela ne semble pas conduire à une diminution de la fréquence des erreurs de comparaison.

Cependant, les résultats ci-dessus ne sont pas réels par rapport à la magie inversée-Z . Vérifier:

  • Le Z inversé avec tampon de profondeur de flottement donne un taux d'erreur nul dans le test. Maintenant, bien sûr, vous pouvez obtenir des erreurs si vous continuez à augmenter l'intervalle des valeurs de profondeur d'entrée. Cependant, le Z inversé avec flotteur est ridiculement plus précis que toute autre option.
  • Le Z inversé avec un tampon de profondeur entier est aussi bon que les autres options entières.
  • Le Z inversé brouille la distinction entre les matrices de vue / projection composites et séparées et les plans lointains finis et infinis. En d'autres termes, avec Z inversé, vous pouvez multiplier la projection avec d'autres matrices et utiliser le plan éloigné de votre choix, sans compromettre la précision.

Conclusion


Je pense que la conclusion est claire. Dans toutes les situations, lorsqu'il s'agit de projection en perspective, utilisez simplement un tampon de profondeur flottante et un Z inversé ! Et si vous ne parvenez pas à utiliser le tampon de profondeur de flottement, vous devez toujours utiliser le Z inversé. Ce n'est pas une panacée pour tous les maux, surtout si vous créez un environnement de monde ouvert avec des plages de profondeur extrêmes. Mais c'est un bon début.

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


All Articles