Entendiendo el Z-Buffer

Programando un motor 3D desde cero - Brakeza3D

Entendiendo el Z-Buffer

Bienvenidos de nuevo! En el último artículo implementamos una técnica para disponer de Billboards: elementos 2D incrustados en nuestra escena 3D. Hoy voy a hablaros de algo que estoy seguro que muchos de vosotros ya conocéis: El Z-Buffer o Buffer de profundidad. Vamos a programar nuestro propio sistema de Z-Buffer!

El principal objetivo del Z-Buffer es ayudarnos a decidir qué se ve y qué no en una escena 3D.

El problema es el siguiente: si observáis la siguiente imagen, podéis ver como múltiples triángulos interseccionan entre sí, en unos de los casos más exigentes que podemos encontrar. Es un problema del que no habíamos hablado hasta el momento a la hora de rasterizar un triángulo.

Sin Z-Buffer, si intentamos que nuestro rasterizador dibuje los triángulos de la imagen superior de uno en uno, renderizaríamos incorrectamente la escena.

Analicemos la geometría de la imagen superior: tenemos varios triángulos y un plano amarillo que, supongamos estará formado por dos triángulos que son dibujados en último lugar. El resultado será que se superpondrán al resto, quedando prácticamente todos tapados, por tanto, incorrectamente. El Z-Buffer se encarga de resolver este tipo de problemas.

Conviene no confundir con el Back-Face culling que ocultaba los caras al completo de una maya si estaban «hacia atrás». El Z-Buffer se encarga de resolver el problema cuando sabemos que todos los triángulos que vamos a renderizar son visibles en pantalla, pero parcialmente, debido a sus intersecciones.

Algunos pensaréis que si dibujamos los triángulos en un riguroso orden desde atrás (far plane) hacia adelante, (la cámara), no tendríamos problemas de profundidad. Esto sería cierto siempre y cuando ningún triángulo interseccionase entre si. Aprovecho para ponerle nombre a este sistema de ordenación/pintado: Algoritmo del pintor.

La realidad, es que no siempre podemos garantizar que el orden de entrada de los triángulos al render es correlativo respecto a su profundidad. Ordenar la geometría supondrá una gran optimización en términos de Z-Buffering, aunque un consumo de CPU extra si deseamos implementarlo. Como siempre, conviene hacer pruebas de casos concretos y balancear este tipo de cosas acorde a nuestras exigencias en escena.

Por citar mi experiencia: en Brakeza3D la geometría se envía al render ordenada parcialmente. Concretamente la del mapa BSP va ordenada (el grueso de la escena), mientras que los objetos (mayas, billboards…) se procesan simplemente por orden de creación en código. Una de las ventajas de los BSP del Quake (y su algoritmo PVS) es que el orden de los triángulos del mapa ya viene precacheado.

Triángulo vs Píxel

Podéis encontrar en Internet técnicas de Z-Buffer aplicado a nivel de triángulo, es decir,  en lugar de ser una operación «per-pixel», pasa a ser una operación «per-triangle», reduciendo drásticamente sus ciclos de CPU, no obstante,  son técnicas de optimización q funcionan mejor o peor según el tipo de escenas que estemos renderizando y que realmente incorporan cierta tolerancia a fallos. Si queréis un Z-Buffer perfecto, conviene realizarlo a nivel de pixel. Además, habrá veces que necesitemos renderizar triángulos que interseccionan entre sí, como los que se ven en la anterior  imagen, donde la manipulación del Z-Buffer por pixel se hace inevitable para alcanzar esa precisión.

Es por tanto el rasterizador el que irá consultando el Z-Buffer con el fin de saber si un pixel concreto en un triángulo, se encuentra detrás de algun otro pixel de otro triángulo que ya hubiésemos procesado. De no haber ninguno, actualizaremos el buffer para ese pixel y continuaremos renderizando el triángulo.

Feature vs Optimice

Como ya he mencionado, aunque el objetivo principal del Z-Buffer es la determinación de la visibilidad o no de un pixel, en determinadas situaciones (como el orden de entrada, ya mencionado) puede tener un efecto optimizador, ya que nos permitirá evitar el procesamiento completo de píxeles que estén detrás de otros píxeles. Nos ahorramos su UV, lightmapping y cualquier mapeo de atributos que ya no será necesario.

Z-Buffer GPU vs CPU

En la práctica, podemos ver al Z-Buffer como un array del tamaño de la resolución de la ventana, en el que almacenaremos valores de profundidad por cada pixel que vayamos a dibujar en pantalla. Mediante una sencilla implementación que llamaremos «test de profundidad» determinaremos si un pixel debe procesarse o no.

Es por tanto un buffer de lectura/escritura frecuente y que debemos limpiar en cada frame.

Por supuesto, en la actualidad, los engines modernos implementan el Z-Buffer por hardware. Paradójicamente, la implementación «sencilla» del Z-Buffer por software puede llegar a ser más rápida q por GPU, debido al gran número de I/O entre RAM/GPU y que las operaciones son dependientes en base al propio buffer (se producen mucho wait). En resumen, el Z-Buffer por GPU aunque posible, no es fácilmente paralelizable. Hablaremos de esto en artículos mas específicos con OpenCL.

Código

En Brakeza3D, el Z-Buffer tiene un aspecto similar al siguiente:

float *depthBuffer = new float[sizeBuffers];

Si recordáis el artículo sobre los vértices 3D y sus transformaciones y nos situamos en el rasterizador: en ese momento ya conocéis el punto(x, y) en 2D (Screen Coordinates) para el píxel que estáis procesando. Con este punto, podemos obtener el offset adecuado para consultar el Z-Buffer:

int bufferIndex = ( y * engineSetup->screenWidth ) + x;

En forma de función:

float EngineBuffers::getDepthBuffer(int x, int y)
{
return depthBuffer[y * screenWidth + x];
}

De igual forma que vimos en el artículo sobre mapeo de texturas UV, haremos uso de las coordenadas baricéntricas (alpha, theta y gamma) para calcular la profundidad del vértice.

float depth = alpha * t->An.z + theta * t->Bn.z + gamma * t->Cn.z;

Sólo nos falta implementar el ya mencionado «test de profundidad«, que no es más que un simple condicional:

if ( depth < buffers->getDepthBuffer( bufferIndex )) {
// ...rasterización del píxel
}

El Z-Buffer, descarta un pixel, si en el buffer de profundidad para ese pixel (x,y), hay un valor menor que el actual, es decir, otro píxel que estaría más cerca de la cámara.

Es un proceso con ciertas similitudes a la creación de un lightmapping, pero desde el punto de vista de la cámara. Si interpretásemos el buffer de profundidad como una imagen, veríamos un degradado. Por ejemplo:

Z Buffer image

 

Resumen

Hemos visto una sencilla implementación de Z-Buffer por software que nos resolverá a nivel de pixel los problemas de superposición de geometría en escena. En el futuro profundizaremos en la implementación del Z-Buffer por hardware.  Hasta la próxima!

 

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *