Occlusion Culling (IV de IV)

************************************************************************************************

Back-face culling

Frustum Culling

Occlusion Culling

Optimizaciones para el Occlusion Culling

************************************************************************************************

En el post anterior explicamos los principios básicos del occlusión culling, y este último post lo dedicaremos a la implementación de dicha técnica de forma eficiente. 

Recordemos rápidamente en que consistía el occlusión culling. El objectivo es no enviar a la tarjeta gráfica aquellos objetos que van a ser eclipsados y para ello renderizabamos los ObjectBounds de los objetos de nuestra escena ordenados de más cercanos a más lejanos. Tras cada dibujado le preguntamos a la tarjeta gráfica mediante occlusion query cuantos pixeles se han renderizado del ObjectBounds si este es igual 0 no enviaremos el “objeto complejo” a la GPU.

Un ejemplo con imágenes sería:

image

Renderizamos los ObjectBounds de delante hacia atrás.

image

En dicha iteración pintamos un ObjectBound que quedará por detrás de los ya dibujados

image

El resultado de la occlusion query para dicho ObjectBound serán 0 pixeles, de forma que el objeto “complejo” que tenía por Boundingbox este último cubo será descartado del render.

 

El primer problema que nos encontramos es que la CPU debe esperar a que la GPU le devuelva un resultado, y como ya sabemos la transferencia entre CPU y GPU es muy costosa. En el algoritmo original esta espera la realizamos para cada ObjectBound:

 

image

 

Como se puede apreciar en la imagen tanto la CPU como la GPU está ociosas durante ciertos periodos de tiempo, y mientras una trabaja la otra espera.

Sumado a esto tenemos el problema de la pérdida de paralelismo, es decir la CPU y GPU suelen trabajar en paralelo para conseguir un mayor rendimiento (cuando la CPU termina de preparar un frame se lo envía a la GPU para que empiece a renderizarlo y mientras tanto la CPU empieza a preparar el frame siguiente).

Afortunadamente las gráficas de hoy dan solución a este problema, permitiéndonos almacenar el resultado de varias occlusion query en la GPU. Esto significa que las occlusion queries pueden ser batched, podemos lanzar varias encadenadas y luego ir recogiendo los resultados de forma que los tiempos de espera quedan enmascarados. Es importante resaltar que algunas GPU tienen limitado el número de resultados que pueden almacenar, por lo que en dichos casos tendremos que lanzar las queries en grupos del número máximo soportado.

 

image

 

En este gráfico se puede observar visualmente la mejora de rendimiento tras usar el batching de queries.

 

En el gráfico anterior vimos una mejora notable en el paralelismo de la CPU y GPU, usando dicha técnica eliminamos (enmascaramos) la mayoría de la esperas producidas a nivel de GPU, pero aún tenemos un “parón” en GPU ya que tenemos que esperar el resultado de la última query lanzada.

Para evitar esta latencia podemos aplicar la siguiente idea, recoger el resultado de las últimas queries en el frame siguiente.

 

image

 

De esta forma eliminamos esa última espera que nos quedaba a nivel de GPU, (en la CPU es normal e importante que nos queden espacios ociosos ya que este se encarga también de actualizar la escena, lo que puede implicar IA, Física…)

 

Hay más temas a tener en cuenta, como por ejemplo:

– Los ObjectBounds deben ajustarse lo mejor posible a los objetos para no tener problemas de oclusiones no deseadas.

fig29-01

En este caso estaríamos aproximando el árbol mediante un boundingBox, y tras aplicar occlusion culling el resultado sería que el coche no se dibujaría. Una solución a este problema pasaría por mejorar nuestra aproximación usando por ejemplo varios boundingbox (uno para el tronco y otro para la copa). El ajuste optimo para nuestro tipo de escena será el objetivo a conseguir para que nunca se produzcan errores de este tipo.

 

– Algo a tener también en cuenta sería el aplicar algoritmo de visibilidad espacial para poder descartar gran cantidad de objetos más rápidamente y reducir al mínimo el número de queries a lanzar.

image

Como se puede ver en este gráfico el 90% del tiempo lo empleamos en comprobar objetos para los cuales el resultado del test será negativo. Para evitar esto podríamos usar una jerarquía o una agrupaciones de objetos para que tras descartar el boundingbox de una agrupación de objetos, todos los objetos que contiene pueden ser descartados directamente.

 

Por último comentar que esta técnica hace posible que los juegos AAA (triple A) actuales tengan esa gran cantidad de detalles, y todos los engines de las grandes empresas lo usan. Incluso existe una empresa con gran éxito llamada Umbra Software la cual tiene como producto estrella un middleware dedicado a realizar esta técnica de forma eficiente en varias plataformas. Aquí os dejo unos vídeos en los que podéis ver algunos ejemplos de como funciona el occlusion booster que tienen implementado.

 

GPU accelerated occlusion culling

 

Umbra Software GDC 2009