Precisión de profundidad visualizada

原文:Precisión de profundidad visualizada | Desarrollador NVIDIA

La precisión de profundidad es un dolor en el culo con el que todo programador de gráficos tiene que luchar tarde o temprano. Se han escrito muchos artículos y documentos sobre el tema, y ​​se encuentran una variedad de configuraciones y formatos de búfer de profundidad diferentes en diferentes juegos, motores y dispositivos.

Debido a la forma en que interactúa con la proyección en perspectiva, el mapeo de profundidad del hardware de GPU es un poco recóndito y estudiar las ecuaciones puede no hacer que las cosas sean inmediatamente obvias. Para obtener una intuición de cómo funciona, es útil hacer algunos dibujos.

Este artículo tiene tres partes principales. En la primera parte, trato de proporcionar alguna motivación para el mapeo de profundidad no lineal. En segundo lugar, presento algunos diagramas para ayudar a comprender cómo funciona el mapeo de profundidad no lineal en diferentes situaciones, de manera intuitiva y visual. La tercera parte es una discusión y reproducción de los principales resultados de Tightening the Precision of Perspective Rendering de Paul Upchurch y Mathieu Desbrun (2012), sobre los efectos del error de redondeo de coma flotante en la precisión de profundidad.

¿Por qué 1/z?

Los búferes de profundidad del hardware de la GPU no suelen almacenar una representación lineal de la distancia que se encuentra un objeto frente a la cámara, al contrario de lo que uno podría esperar ingenuamente cuando se encuentra con esto por primera vez. En cambio, el búfer de profundidad almacena un valor proporcional al recíproco de la profundidad del espacio mundial. Quiero motivar brevemente esta convención.

En este artículo, usaré d para representar el valor almacenado en el búfer de profundidad (en [0, 1]) y z para representar la profundidad del espacio mundial, es decir, la distancia a lo largo del eje de la vista, en unidades mundiales como metros. En general, la relación entre ellos es de la forma

donde a,b son constantes relacionadas con la configuración del plano cercano y lejano. En otras palabras, d es siempre una reasignación lineal de 1/z .

A primera vista, puedes imaginarte tomando d como cualquier función de z que quieras. Entonces, ¿por qué esta elección en particular? Hay dos razones principales.

Primero, 1/z encaja naturalmente en el marco de las proyecciones en perspectiva. Esta es la clase de transformación más general que garantiza la conservación de las líneas rectas, lo que la hace conveniente para la rasterización de hardware, ya que los bordes rectos de los triángulos permanecen rectos en el espacio de la pantalla. Podemos generar reasignaciones lineales de 1/z aprovechando la división de perspectiva que ya realiza el hardware:

El verdadero poder de este enfoque, por supuesto, es que la matriz de proyección se puede multiplicar con otras matrices, lo que le permite combinar muchas etapas de transformación en una sola.

La segunda razón es que 1/z es lineal en el espacio de la pantalla, como señaló Emil Persson . Por lo tanto, es fácil interpolar d a través de un triángulo mientras se rasteriza, y cosas como los búferes Z jerárquicos, la selección temprana de Z y la compresión del búfer de profundidad son mucho más fáciles de hacer.

Graficar mapas de profundidad

Las ecuaciones son difíciles; ¡Veamos algunas fotos!

La forma de leer estos gráficos es de izquierda a derecha y luego hacia abajo. Comience con d , trazado en el eje izquierdo. Debido a que d puede ser una reasignación lineal arbitraria de 1/z , podemos colocar 0 y 1 donde queramos en este eje. Las marcas de verificación indican distintos valores de búfer de profundidad. Para fines ilustrativos, estoy simulando un búfer de profundidad de entero normalizado de 4 bits, por lo que hay 16 marcas de verificación espaciadas uniformemente.

Traza las marcas de verificación horizontalmente hasta donde tocan la curva 1/z , luego hacia abajo hasta el eje inferior. Ahí es donde caen los distintos valores en el rango de profundidad del espacio mundial.

El gráfico anterior muestra el mapeo de profundidad "estándar" utilizado en D3D y API similares. Inmediatamente puede ver cómo la curva 1/z lleva a agrupar valores cercanos al plano cercano, y los valores cercanos al plano lejano están bastante dispersos.

También es fácil ver por qué el plano cercano tiene un efecto tan profundo en la precisión de la profundidad. Tirar del plano cercano hará que el rango d se dispare hacia la asíntota de la curva 1/z , lo que conducirá a una distribución de valores aún más asimétrica:

Del mismo modo, es fácil ver en este contexto por qué empujar el plano lejano hasta el infinito no tiene tanto efecto. Simplemente significa extender el rango d ligeramente hacia abajo a 1/z=0 :

¿Qué pasa con la profundidad de coma flotante? El siguiente gráfico agrega marcas correspondientes a un formato flotante simulado con 3 bits de exponente y 3 bits de mantisa:

Ahora hay 40 valores distintos en [0, 1], bastante más que los 16 valores anteriores, pero la mayoría de ellos están agrupados inútilmente en el plano cercano donde realmente no necesitábamos más precisión.

Un truco ahora ampliamente conocido es invertir el rango de profundidad, mapeando el plano cercano a d=1 y el plano lejano a d=0 :

¡Mucho mejor! Ahora, la distribución casi logarítmica del punto flotante cancela un poco la no linealidad 1/z , lo que nos brinda una precisión similar en el plano cercano a un búfer de profundidad entero, y una precisión muy mejorada en cualquier otro lugar. La precisión empeora muy lentamente a medida que se aleja.

El truco de la Z invertida probablemente se haya reinventado de forma independiente varias veces, pero se remonta al menos a un artículo de SIGGRAPH '99 de Eugene Lapidous y Guofang Jiao (desafortunadamente, no hay un enlace de acceso abierto disponible). Más recientemente, se volvió a popularizar en publicaciones de blog de Matt Pettineo y Brano Kemen , y en la charla SIGGRAPH 2012 de Emil Persson: Creación de vastos mundos de juego .

Todos los diagramas anteriores asumieron [0, 1] como el rango de profundidad posterior a la proyección, que es la convención D3D. ¿Qué pasa con OpenGL?

OpenGL por defecto asume un rango de profundidad de post-proyección [-1, 1]. Esto no hace una diferencia para los formatos de enteros, pero con el punto flotante, toda la precisión se atasca inútilmente en el medio. (El valor se asigna a [0, 1] para almacenarlo en el búfer de profundidad más tarde, pero eso no ayuda, ya que la asignación inicial a [-1, 1] ya ha destruido toda la precisión en la mitad más alejada del rango .) Y por simetría, el truco de la Z invertida no hará nada aquí.

Afortunadamente, en OpenGL de escritorio puede solucionar este problema con la extensión ARB_clip_control ampliamente compatible (ahora también central en OpenGL 4.5 como glClipControl ). Desafortunadamente, en GL ES no tienes suerte.

Los efectos del error de redondeo

El mapeo 1/z y la elección del búfer de profundidad flotante versus entero son una gran parte de la historia de la precisión, pero no toda. Incluso si tiene suficiente precisión de profundidad para representar la escena que está tratando de representar, es fácil terminar con su precisión controlada por un error en la aritmética del proceso de transformación de vértices.

Como se mencionó anteriormente, Upchurch y Desbrun estudiaron esto y propusieron dos recomendaciones principales para minimizar el error de redondeo:

  1. Usa un plano lejano infinito.
  2. Mantenga la matriz de proyección separada de otras matrices y aplíquela en una operación separada en el sombreador de vértices, en lugar de componerla en la matriz de vista.

Upchurch y Desbrun propusieron estas recomendaciones a través de una técnica analítica, basada en tratar los errores de redondeo como pequeñas perturbaciones aleatorias introducidas en cada operación aritmética, y seguirlas hasta el primer orden a través del proceso de transformación. Decidí comprobar los resultados mediante simulación directa.

Mi código fuente está aquí : Python 3.4 con numpy. Funciona generando una secuencia de puntos aleatorios, ordenados por profundidad, espaciados lineal o logarítmicamente entre los planos cercano y lejano. Luego, pasa los puntos a través de las matrices de vista y proyección y la división de perspectiva, utilizando una precisión flotante de 32 bits en todo momento, y opcionalmente cuantiza el resultado final a un entero de 24 bits. Finalmente, recorre la secuencia y cuenta cuántas veces dos puntos adyacentes (que originalmente tenían distintas profundidades) se han vuelto indistinguibles porque se asignaron al mismo valor de profundidad o han cambiado de orden. En otras palabras, mide la tasa a la que ocurren los errores de comparación de profundidad, lo que corresponde a problemas como Z-fighting, en diferentes escenarios.

Aquí están los resultados obtenidos para cerca = 0.1, lejos = 10K, con profundidades espaciadas linealmente de 10K. (Probé el espaciado logarítmico de profundidad y otras proporciones cercanas/lejanas también, y aunque los números detallados variaban, las tendencias generales en los resultados eran las mismas).

En la tabla, "indist" significa indistinguible (dos profundidades cercanas asignadas al mismo valor de búfer de profundidad final), y "swap" significa que dos profundidades cercanas intercambiaron el orden.


Matriz de proyección de vista precompuesta
Matrices de vista y
proyección separadas
flotar32 int24 flotar32 int24
Valores Z inalterados
(prueba de control)
0% indist
0% permuta
0% indist
0% permuta
0% indist
0% permuta
0% indist
0% permuta
Proyección estándar 45% inversión
18% permuta
45% inversión
18% permuta
77% indist
0% permuta
77% indist
0% permuta
Plano lejano infinito 45% inversión
18% permuta
45% inversión
18% permuta
76% indist
0% permuta
76% indist
0% permuta
Z invertida 0% indist
0% permuta
76% indist
0% permuta
0% indist
0% permuta
76% indist
0% permuta
Infinito + Z invertida 0% indist
0% permuta
76% indist
0% permuta
0% indist
0% permuta
76% indist
0% permuta
estándar de estilo GL 56% inversión
12% permuta
56% inversión
12% permuta
77% indist
0% permuta
77% indist
0% permuta
infinito estilo GL 59% inversión
10% permuta
59% inversión
10% permuta
77% indist
0% permuta
77% indist
0% permuta

¡Disculpas por no graficar esto, pero hay demasiadas dimensiones para que sea fácil de graficar! En cualquier caso, mirando los números, algunos resultados generales son claros.

  • No hay diferencia entre los búferes de profundidad flotantes y enteros en la mayoría de las configuraciones. El error aritmético inunda el error de cuantificación. En parte, esto se debe a que float32 e int24 tienen un ulp de casi el mismo tamaño en [0.5, 1] ​​(porque float32 tiene una mantisa de 23 bits), por lo que en realidad casi no hay error de cuantificación adicional en la gran mayoría del rango de profundidad.
  • En muchos casos, separar las matrices de vista y proyección (siguiendo la recomendación de Upchurch y Desbrun) supone alguna mejora. Si bien no reduce la tasa de error general, parece convertir los intercambios en indistinguibles, lo cual es un paso en la dirección correcta.
  • Un plano lejano infinito hace solo una diferencia minúscula en las tasas de error. Upchurch y Desbrun predijeron una reducción del 25 % en el error numérico absoluto , pero no parece traducirse en una tasa reducida de errores de comparación .

Sin embargo, los puntos anteriores son prácticamente irrelevantes, porque el resultado real que importa aquí es: el mapeo de Z invertida es básicamente mágico . Échale un vistazo:

  • La Z invertida con un búfer de profundidad flotante proporciona una tasa de error cero en esta prueba. Ahora, por supuesto, puede hacer que genere algunos errores si sigue ajustando el espaciado de los valores de profundidad de entrada. Aún así, la Z invertida con flotador es ridículamente más precisa que cualquiera de las otras opciones.
  • Reversed-Z con un búfer de profundidad de enteros es tan bueno como cualquiera de las otras opciones de enteros.
  • La Z invertida borra las distinciones entre matrices de vista/proyección precompuestas versus separadas y planos lejanos finitos versus infinitos. En otras palabras, con Z invertida puede componer su matriz de proyección con otras matrices, y puede usar cualquier plano lejano que desee, sin afectar la precisión en absoluto.

Creo que la conclusión aquí es clara. ¡En cualquier situación de proyección en perspectiva, simplemente use un búfer de profundidad de coma flotante con Z invertida! Y si no puede usar un búfer de profundidad de coma flotante, aún debe usar la Z invertida. No es una panacea para todos los problemas de precisión, especialmente si está construyendo un entorno de mundo abierto que contiene rangos de profundidad extremos. Pero es un gran comienzo.

Nathan es programador de gráficos y actualmente trabaja en NVIDIA en el equipo de software DevTech. Puedes leer más en su blog aquí .

Supongo que te gusta

Origin blog.csdn.net/tangyin025/article/details/126567371
Recomendado
Clasificación