
Reverse Z-Buffer
Le Z-Buffer, ou Depth-Buffer, est utilisé en infographie pour déterminer la visibilité des objets dans chaque pixel.
Chaque fois que nous dessinons un objet, nous stockons également sa distance à la caméra dans le Z-Buffer.
Lorsque nous dessinons l’objet suivant, nous pouvons vérifier dans le Z-Buffer la valeur précédemment stockée. Si la valeur du Z-Buffer est plus proche que l’objet en cours de dessin, nous savons que l’objet est occulté et nous ne dessinons donc pas le pixel. Cette technique est parfois appelée depth testing (test de profondeur).
Le test de profondeur est omniprésent dans tous les moteurs graphiques, et Evergine ne fait pas exception. Cependant, comme nous allons le voir, l’approche naïve ne donne pas les meilleurs résultats en termes de précision. Dans cette release, nous avons introduit le Reverse Z-Buffering, qui améliore considérablement la précision, en particulier pour les objets éloignés.
Z-Buffer traditionnel
L’approche traditionnelle du Z-buffering stocke des valeurs de profondeur normalisées dans l’intervalle [0, 1], où 0 correspond à la valeur la plus proche et 1 à la plus éloignée.
Dans l’image suivante, une caméra 3D regarde un cube.
La ligne verte représente le plan « near » : les pixels plus proches que ce plan sont écartés (clipping).
La ligne rouge représente le plan « far » : les pixels situés au-delà de ce plan sont écartés.

La ligne magenta représente la distance jusqu’au pixel en cours de dessin (d).
La valeur normalisée du depth buffer est calculée à l’aide de la formule suivante :
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Remarquez que si d est égal à near, la valeur obtenue est zéro.
De même, si d est égal à far, la valeur obtenue est un.
Dans l’image suivante, vous pouvez voir à quoi peut ressembler un depth buffer typique.

Problèmes de précision
Pour illustrer les problèmes de précision qui peuvent survenir avec le depth buffering traditionnel, nous avons créé la démo suivante.

Les sphères de cette scène sont en réalité deux sphères : l’une à l’intérieur de l’autre.
La disposition de ces sphères superposées est représentée dans l’image suivante. Les deux couches sont placées très près l’une de l’autre.

Bien entendu, la sphère rouge intérieure ne devrait pas être visible. Pourtant, comme vous pouvez le voir dans notre démo, à mesure que la distance à la caméra augmente, des artefacts rouges commencent à apparaître.
Ce phénomène est connu sous le nom d’aliasing. En raison de la précision de la représentation numérique, une fois normalisées, les valeurs de profondeur correspondent à la même valeur numérique : impossible alors de savoir laquelle est la plus proche.
Une option consiste à augmenter le nombre de bits utilisés pour représenter la valeur dans le depth buffer, mais la plupart des matériels ne peuvent pas dépasser 32 bits, et 32 bits ne suffisent pas.
Profondeur inversée
Ce problème de précision peut être résolu grâce à une technique appelée Reverse Depth-Buffering.
L’idée de cette technique est très simple. Au lieu de la formule habituelle utilisée pour calculer la profondeur normalisée :
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Nous inversons les plans near et far :
depthBuffer=(near)/(near-far)-(near⋅far)/(near-far)⋅1/d
Avec cette formule, le plan near correspond à 1 et le plan far à 0. Le depth buffer apparaît donc inversé.

Avec ce petit changement, associé à un format de profondeur en flottant, notre démo devient entièrement verte.

Pourquoi le Reverse-Z améliore-t-il la précision ?
Le Reverse-Z est une technique très simple : il suffit d’inverser les plans near et far. Et pourtant, les résultats sont excellents.
De plus, la technique semble extrêmement robuste : le choix des plans near et far ne nécessite plus d’ajustement fin.
En fait, vous pouvez même placer le plan far à l’infini et tout fonctionnera parfaitement. Pouvoir gérer une distance de vue infinie est un atout majeur pour les applications qui doivent gérer des mondes immenses, par exemple pour rendre la Terre entière, comme nous le faisons dans notre nouvel add-on Cesium. Le plan near peut également être beaucoup plus proche, sans toutefois pouvoir être nul.
Si vous vous demandez pourquoi le Reverse-Z fonctionne, nous entrons dans cette section dans les détails mathématiques et techniques.
Représentations UNorm et Float
Les valeurs de profondeur projetées sont stockées dans l’intervalle [0, 1].
Il existe une infinité de nombres réels entre 0 et 1. Cependant, les ordinateurs ne peuvent pas tous les représenter exactement, de la même manière que vous ne pouvez pas écrire toutes les décimales de pi sur une feuille. Les ordinateurs utilisent donc différentes représentations pour approcher les nombres réels, tout comme vous pouvez approcher pi en écrivant par exemple ses 7 premiers chiffres.
Deux types de formats sont utilisés pour représenter les valeurs de profondeur dans le depth buffer : UNorm et float.
UNorm signifie « unsigned normalized » (non signé normalisé). Un entier non signé est utilisé pour représenter des nombres réels dans l’intervalle [0, 1].
Les valeurs représentées sont réparties uniformément dans l’intervalle [0, 1].
L’image suivante montre les valeurs représentées en UNorm. Comme vous pouvez le voir, l’espacement entre les valeurs représentées est constant.

Float désigne la représentation en virgule flottante.
La représentation float couvre l’intervalle (-∞, +∞).
Les flottants simple précision utilisent 32 bits, répartis en 3 parties : signe, exposant et mantisse.

(Source : https://en.wikipedia.org/wiki/Single-precision_floating-point_format#/media/File:Float_example.svg)
Nous ne pouvons pas couvrir tous les détails de la notation en virgule flottante dans cet article ; je laisse donc ces deux liens au lecteur curieux : https://floating-point-gui.de, https://en.wikipedia.org/wiki/Single-precision_floating-point_format.
Ce qu’il faut retenir sur les flottants, dans le contexte de notre problème, c’est la manière dont les valeurs représentées sont réparties dans l’intervalle [0, 1], celui dans lequel sont projetées les valeurs de profondeur.

Comme le montre la figure précédente, les échantillons ne sont pas répartis uniformément. La précision est plus grande pour les valeurs proches de zéro. C’est une propriété intéressante pour les applications de calcul scientifique, mais nous allons nous aussi tirer parti de cette particularité.
Revenons maintenant à notre formule de projection de profondeur :
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Si nous traçons cette formule, nous obtenons cette courbe :

L’axe X représente la profondeur linéaire. L’axe Y donne la profondeur projetée dans l’intervalle [0, 1].
Remarquez à quel point cette fonction croît rapidement.
Sur l’axe Y, vous pouvez voir que les échantillons sont répartis uniformément, puisque nous utilisons la représentation UNorm.
L’axe X comporte une série de petites graduations. Elles correspondent aux graduations uniformément réparties sur Y, avant projection. Remarquez que la plupart des graduations sont regroupées près du plan near, tandis que leur densité devient très faible à l’approche du plan far.
Et ce, avec un choix très favorable des plans near et far. Voyez ce qui se passe avec near = 0.1 et far = 100.

Le problème ici est que les valeurs de profondeur de 20 à 100 correspondent toutes à 1. Une fois projetées, il est donc impossible de savoir si 45 est inférieur à 46.
La notation en virgule flottante n’aide pas ici. En fait, elle aggrave même la situation, puisque les valeurs de profondeur linéaire représentées seront encore plus concentrées vers near.

Nous avons donc vu que deux éléments contribuent à la répartition inégale des échantillons, en favorisant les valeurs proches du plan near : 1) la courbe de la formule de projection ; 2) la notation en virgule flottante
L’idée du Reverse-Z est d’inverser la formule de projection. On obtient ainsi la répartition inverse des échantillons, qui favorise les distances proches du plan far. La notation en virgule flottante, qui a l’effet opposé, vient compenser cela, et l’on obtient une répartition des échantillons plus équilibrée. Observez la répartition des graduations sur l’axe X de l’image suivante.

Ce que le Reverse Z change pour vous en tant qu’utilisateur d’Evergine
Après la mise à jour vers la nouvelle version, la profondeur inversée sera activée automatiquement.
Pour la plupart des projets, cela n’aura aucune conséquence, mais voici deux cas d’usage susceptibles de ne plus fonctionner.
Shaders qui utilisent le depth buffer
Si vous avez écrit des shaders personnalisés qui lisent directement le depth buffer, vous devrez les adapter en conséquence.
Nous avons ajouté des paramètres de shader qui vous aideront à convertir la profondeur inversée en profondeur classique.

ReverseDepth vaut 0 si le rendu utilise le depth buffering classique, et 1 si nous utilisons la profondeur inversée. Il vaudra cependant toujours 1 désormais.
ReverseDepthFactor est destiné à la conversion en profondeur classique à l’aide de la fonction suivante :
// This is a function that converts a reverse-depth value to forward-depth, if needed, without branching
inline float forwardDepth(float depth)
{
return depth * ReverseDepthFactor + ReverseDepth;
}Render layers personnalisés
Si votre projet comporte des Render Layers personnalisés, vous devrez inverser la condition du test de profondeur.

Dans la documentation d’Evergine de la nouvelle release, vous trouverez un guide de migration avec des scripts conçus pour automatiser ce processus.
