
Reverse Z-Buffer
El Z-Buffer, o Depth-Buffer, se utiliza en gráficos por computadora para determinar la visibilidad de los objetos en cada píxel.
Cada vez que dibujamos un objeto, también almacenamos la distancia a la cámara en el Z-Buffer.
La próxima vez que dibujamos un objeto, podemos comprobar en el Z-Buffer cuál era el valor previamente almacenado. Si el valor en el Z-Buffer es más cercano que el objeto que estamos dibujando, sabemos que el objeto está ocluido, por lo que no dibujamos el píxel. Esta técnica se denomina a veces depth testing.
El enfoque de depth testing es omnipresente en todos los motores gráficos, y Evergine no es una excepción. Sin embargo, como veremos, el enfoque ingenuo no ofrece los mejores resultados en términos de precisión. En esta versión hemos introducido Reverse Z-Buffering, que mejora considerablemente la precisión, especialmente para objetos distantes.
Z-Buffer tradicional
El enfoque tradicional del Z-buffering almacena los valores de profundidad normalizados en el rango [0, 1]. Donde 0 es el valor más cercano y 1 es el más lejano.
En la siguiente imagen tenemos una cámara 3D que observa un cubo.
La línea verde representa el plano “near”; los píxeles más cercanos que el plano near son descartados (clipped).
La línea roja representa el plano “far”; los píxeles más lejanos que el plano far son descartados.

La línea magenta representa la distancia al píxel que estamos dibujando (d).
El valor normalizado del depth buffer se calculará mediante la fórmula:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Nótese que si d es igual a near, el valor resultante es cero.
Del mismo modo, si d es igual a far, el valor resultante es uno.
En la siguiente imagen se puede ver cómo podría verse un depth buffer típico.

Problemas de precisión
Para ilustrar los problemas de precisión que pueden ocurrir con el Depth-Buffering tradicional, hemos construido la siguiente demo.

Las esferas en esta escena son en realidad dos esferas: una dentro de la otra.
La disposición de estas esferas anidadas se muestra en la siguiente imagen. Las dos capas están colocadas muy cerca entre sí.

Obviamente, la esfera roja interior no debería ser visible. Sin embargo, como se puede ver en nuestra demo, a medida que la distancia a la cámara aumenta, comenzamos a ver los artefactos rojos.
El fenómeno se conoce como aliasing. Debido a la precisión de la representación numérica, una vez que los valores de profundidad han sido normalizados, se mapean al mismo valor numérico, por lo que no podemos distinguir cuál está más cerca.
Una opción es aumentar el número de bits usados para representar el valor en el Depth-Buffer, pero la mayoría del hardware no puede superar los 32 bits, y 32 bits no son suficientes.
Reverse Depth
Este problema de precisión puede resolverse usando una técnica llamada Reverse Depth-Buffering.
La idea detrás de esta técnica es muy simple. En lugar de la fórmula habitual para calcular la profundidad normalizada:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Intercambiamos los planos near y far:
depthBuffer=(near)/(near-far)-(near⋅far)/(near-far)⋅1/d
Con esta fórmula, el plano near se mapea a 1 y el plano far se mapea a 0. Por lo tanto, el depth buffer parece invertido.

Con este pequeño cambio, junto con el formato de profundidad en coma flotante, nuestra demo se vuelve completamente verde.

¿Por qué Reverse-Z mejora la precisión?
Reverse-Z es una técnica muy simple: simplemente intercambia los planos near y far. Sin embargo, los resultados son muy buenos.
Además, la técnica parece ser extremadamente robusta; seleccionar los planos near y far ya no requiere un ajuste fino.
De hecho, se puede establecer el plano far en infinito y todo funcionará perfectamente. Poder manejar una distancia de visión infinita es muy importante para aplicaciones que necesitan gestionar mundos enormes. Por ejemplo, renderizar la Tierra entera, como hacemos en nuestro nuevo Addon de Cesium. El plano near también puede ser mucho más cercano, aunque no puede ser cero.
Si te preguntas por qué funciona Reverse-Z, en esta sección profundizaremos en las matemáticas y los detalles técnicos.
Representaciones UNorm y Float
Los valores de profundidad proyectados se almacenan en el rango [0, 1].
Hay infinitos números reales entre 0 y 1. Sin embargo, los ordenadores no pueden representarlos todos exactamente, de la misma manera que no puedes escribir todos los dígitos de pi en un papel. Por eso los ordenadores utilizan diferentes representaciones para aproximar los números reales, del mismo modo que puedes aproximar pi escribiendo los primeros 7 dígitos, por ejemplo.
Hay dos tipos de formatos utilizados para representar los valores de profundidad en el depth buffer: UNorm y float.
UNorm significa unsigned normalized (normalizado sin signo). Se utiliza un entero sin signo para representar números reales en el rango [0, 1].
Los valores representados se distribuyen uniformemente en el rango [0, 1].
La siguiente imagen muestra los valores representados en UNorm. Como se puede ver, el espaciado entre los valores representados es constante.

Float hace referencia a la representación en coma flotante.
La representación float cubre el rango (-∞, +∞).
Los floats de precisión simple usan 32 bits, que se dividen en 3 partes: signo, exponente y fracción.

(Fuente: https://en.wikipedia.org/wiki/Single-precision_floating-point_format#/media/File:Float_example.svg )
No podemos cubrir todos los detalles de la notación de coma flotante en este artículo, así que dejaré estos dos enlaces para el lector curioso: https://floating-point-gui.de, https://en.wikipedia.org/wiki/Single-precision_floating-point_format.
Lo importante de los floats, en el contexto de nuestro problema, es la manera en que los valores representados se distribuyen en el rango [0, 1], que es el rango al que se mapean los valores de profundidad proyectados.

Como se puede ver en la figura anterior, las muestras no están distribuidas uniformemente. Hay más precisión dedicada a los valores cercanos a cero. Esta es una propiedad interesante para aplicaciones de computación científica, pero también sacaremos provecho de esta peculiaridad.
Volvamos ahora a nuestra fórmula de proyección de profundidad:
depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d
Si representamos gráficamente esta fórmula, obtendremos esta curva:

El eje X representa la profundidad lineal. El eje Y muestra la profundidad proyectada en el rango [0, 1].
Nótese lo rápido que crece esta función.
En el eje Y se puede ver que las muestras están distribuidas uniformemente, ya que estamos usando representación UNorm.
El eje X tiene una serie de pequeñas marcas. Estas corresponden a las marcas distribuidas uniformemente en Y, antes de la proyección. Nótese cómo la mayoría de las marcas están agrupadas cerca del plano near, mientras que la densidad de marcas es muy baja al acercarse al plano far.
Y eso fue con una elección muy favorable de los planos near y far. Veamos qué ocurre con near = 0.1 y far = 100.

El problema aquí es que los valores de profundidad de 20 a 100 se mapean todos a 1. Por tanto, una vez proyectados, no podemos distinguir si 45 es menor que 46.
Usar notación de coma flotante no ayuda. De hecho, lo empeora aún más, ya que los valores de profundidad lineal representados estarán aún más concentrados hacia el plano near.

Hasta ahora hemos visto que hay dos factores que contribuyen a la distribución desigual de las muestras, favoreciendo los valores cercanos al plano near: 1) La curva de la fórmula de proyección 2) La notación de coma flotante
La idea de Reverse-Z es invertir la fórmula de proyección. Esto creará la distribución opuesta de muestras, favoreciendo las distancias cercanas al plano far. La notación de coma flotante, al tener el efecto contrario, lo compensará, y acabaremos con una distribución más equilibrada de las muestras. Observa la distribución de las marcas en el eje X de la siguiente imagen.

Cómo afecta Reverse Z como usuario de Evergine
Tras actualizar a la nueva versión, reverse depth se activará automáticamente.
Para la mayoría de los proyectos esto no tendrá ninguna implicación, pero a continuación se describen dos casos de uso que podrían verse afectados.
Shaders que usan el depth buffer
Si has escrito shaders personalizados que leen el depth buffer en bruto, deberás modificar tu shader en consecuencia.
Hemos añadido algunos parámetros de shader que te ayudarán a convertir reverse-depth a forward depth.

ReverseDepth vale 0 si estamos renderizando con forward depth buffering tradicional, y 1 si estamos usando reverse depth. Sin embargo, a partir de ahora siempre valdrá 1.
ReverseDepthFactor está pensado para convertir a forward depth con la siguiente función:
// Esta función convierte un valor reverse-depth a forward-depth si es necesario, sin ramificaciones
inline float forwardDepth(float depth)
{
return depth * ReverseDepthFactor + ReverseDepth;
}Render Layers personalizados
Si tienes Render Layers personalizados en tu proyecto, tendrás que invertir la condición del depth test.

En la documentación de Evergine para la nueva versión, encontrarás una guía de migración con algunos scripts diseñados para automatizar este proceso.


