El irresoluble problema de la iluminación

La ecuación de renderizado para representar la realidad con la máxima fiabilidad posible es la siguiente:

5c8e5100fc6df22c8938ce77ed686a42

El problema de dicha ecuación es que por lo general no tiene solución predecible a la hora de tratar la iluminación por lo que lo algoritmos de renderizado lo que realizan son aproximaciones a dicha ecuación y en ellos podemos diferenciar dos tipos:

  • Los que tratan la iluminación de manera global.
  • Los que tratan la iluminación de manera local.

Cuando una fuente en la vida real afecta a un entorno lo que hace es afectar a todo el entorno a la vez y la interacción de luz con dicho entorno afecta al resto de objetos del entorno. La iluminación global es cuando la luz es tratada teniendo en cuenta toda la interacción de la luz con los objetos al mismo tiempo. La escena es mas cercana a la realidad por el hecho que se encargan de solventar los múltiples rebotes de las fuentes de luz en la escena de un solo paso de renderizado antes de texturizar la escena.

Captura de pantalla 2015-12-18 a las 13.02.09

La cantidad de cálculos que se necesita para iluminar una escena es tan grande que utilizar técnicas que tengan en cuenta la iluminación global a la hora de renderizar no es viable de cara a los videojuegos por la cantidad de cálculos a realizar que esto supone. La base para este tipo de técnicas es el rayo. Cara rayo es el trazo lineal entre la cámara y el objeto más cercano en un espacio tridimensional en linea recta. El rayo posee la posición, la magnitud y la dirección. Cuando el rayo se encuentra con un objeto en esa distancia (un pixel) entonces es cuando devuelve la información hacía la cámara, esto es llamado Ray Casting y se vio en los juegos 3D previos a la era polígonal como fue Doom.

doom2shoot2

A este sistema se le llamo Ray Casting, que en realidad es generación de una escena pero sin mas rebote que el retorno de la iluminación hacía la cámara.

Captura de pantalla 2015-12-20 a las 13.11.09

El Ray Casting genera un solo rayo por cada pixel de la imagen, su algoritmo de forma simplificada es el siguiente:

  1. Por cada pixel (x,y) lanza un rayo desde la cámara a través de cada uno de ellos y calcula la intersección más cercana.
  2. Por cada una de estas interacciones calcula la normal de la superficie.
  3. Por cada fuente de luz, haz modificaciones sobre el pixel afectado.

Pero la iluminación no rebota, por lo que el comportamiento de las superficies no es simulado y esta no es más que una simple variación de color. ¿Pero que ocurre cuando decidimos simular el comportamiento de la luz y hacer que esta rebote en lo objetos como en la vida real? Entonces que pasamos del Ray Casting a lo que es el Ray Tracing que es un modelo de renderizado que tiene en cuenta el hecho que la iluminación es recursiva y puede representar los objetos según el comportamiento de la luz en ellos.

Captura de pantalla 2015-12-20 a las 13.19.18

Una iluminación de un solo rebote, el primero es siempre el retorno del rayo hacía la cámara, necesita una capacidad de cálculo de (Resolución horizontal*Resolución Vertical*Operaciones por pixel) pero si ya añadimos como mínimo otro rebote entonces pasa a ser de (Resolución horizontal*Resolución Vertical*Operaciones por pixel)^2 y tenemos que tener en cuenta que el hecho de aparecer ciertos elementos como reflejos y refracciones hace que la cantidad de cálculos por pixel aumente, pero la iluminación de dos rebotes no es suficiente para representar la realidad por lo que necesitamos iluminación de tres rebotes como mínimo y entonces la cosa se va a (Resolución horizontal*Resolución Vertical*Operaciones por pixel)^3. Es decir, si la generación de una escena con 1 millón de pixeles requiriese unas 6 operaciones por pixel mínimo entonces con un rebote estaríamos hablando 6 millones de operaciones pero con dos estaríamos hablando de 36 billones y con tres rebotes unos 216 trillones… La capacidad de cálculo necesaria aumenta una barbaridad solo con añadir el segundo rebote por lo que hay que buscar un método más efectivo de renderizado de cara al renderizado a tiempo real.

¿Cual es el metodo utilizado en los videojuegos? Pues calcularlo de forma local, es decir… En vez de tener en cuenta como afectan los rayos a toda la escena tenemos en cuenta como afectan a cada parte de la escena y una escena en 3D esta formada por triangulos por lo que tenemos en cuenta como afecta la iluminación a cada triángulo de manera local y solo a ese triángulo.

gl-pipeline

En el caso de la iluminación local como máximo tenemos dos rebotes de luz, pero para poder conseguir una escena con do rebotes de luz tiene unas necesidades de computación infinitisimamente menores que el Ray Tracing y de ahí su uso en videojuegos. La iluminación local es un modelo de iluminación simplificado que se basa en tres parámetros:

Captura de pantalla 2015-12-18 a las 14.18.06

  1. La posición de la fuente de luz.
  2. La posición de la cámara.
  3. Las propiedades de la superficie  a la que va enfocada la luz (Shader).

¿Que significa esto? Ah, es sencillo… Resulta que desde OpenGL y sus derivados (Direct3D, Vulkan, las APIs propietarias de sistemas cerrados) no se puede conseguir realizar la iluminación global por el simple hecho de que tienen en cuenta la iluminación de la escena no por cada pixel sino por cada triángulo y como afecta esta a cada uno de ellos. Este modelo de iluminación es más limitado porque es una aproximación, de ahí a que en videojuego pese a que hemos conseguido una calidad en el modelado y las texturas realmente notables seguimos teniendo el problema de la iluminación y este no se resolverá a corto plazo a no ser que haya un cambio de 180º que será imposible porque implicara cambiar toda una tradición de hardware, software y herramientas de desarrollo.

Por lo que el dilema de los últimos años ha sido el de:

¿Como conseguir el tercer rebote desde el modelo de iluminación local del Rasterizado?

Principalmente existen dos soluciones encima de la mesa. La primera de ellas es la de los Octree pero para entender su utilidad tenemos que tener en cuenta que es una versión simplificada de una estructura de datos utilizada en el Ray Tracing donde se utiliza el KD-Tree, la idea de un KD-Tree es almacenar en un árbol el recorrido de una haz de luz y sus hijos que son las nuevas fuentes de luz generadas por cara rebote ya que la luz el rebotar con un objeto se comporta de tres maneras distintas.

Image3

Cada material genera luz ambiental, dificula y especular de forma combinada en mayor o menor grado. ¿Y como funciona el Octre? Para empezar no almacena la trayectoria de la luz sino que es una división espacial de la escena tridimensional.

Octree

Lo que se hace no es almacenar la trayectoria de la iluminación por cada pixel sino por cada uno de los cubos que forman el Octree, cuanto más niveles tiene el Octree más precisión tiene la iluminación de la escena por lo que de nuevo estamos ante una aproximación y como bien sabréis una aproximación no es lo mismo que el dato real.

VolumeRayCastingWithRay2

Es la idea que parece tener más futuro de cara a los videojuegos para representar el tercer rebote desde el momento en que DirectX 12 tiene soporte para los llamados Volumed Tile Resources y lo más seguro es que el resto de APIs contemporáneas también lo tienen o lo adoptaran en un futuro.

volume-tiled-resources-645x351

Para calcular bien un Octree de forma no-paralela necesitaríamos una cache enorme que almacenar y las GPUs no son muy buenas recorriendo estructuras de datos complejas por lo que lo mejor es dividir el trabajo por nodos y dejar que los Shaders de la GPU trabajen de forma paralela en las diferentes parte del Octree. Es decir… dividir el trabajo entre las decenas por no decir centenares de núcleos de la GPU. ¿Pero quien o que se encarga de recorrer la estructura de datos del Octree? Pues en este caso deberíamos dedicar la potencia de la CPU para hacerlo.

La siguiente presentación de 2007 explica como funciona el proceso del Octree para la iluminación:

El pase de diapositivas requiere JavaScript.

La presentación es de 2007 cuando las GPUs aún no soportaban múltiples contextos, hoy en día si que lo hacen y es posible cargar parte de la generación del Octree en la GPU como un proceso fuera del pipeline gráfico y desde el Compute Shader de la GPU.

Captura de pantalla 2015-12-17 a las 12.11.14

El problema de las estructuras de datos en una GPU es que estas han de ser simétricas por el hecho que lo que hace una GPU es aplicar el mismo programa a una cantidad de elementos concreto. Es por ello que un KD-Tree no se puede aplicar a una GPU convencional pero si un Octree, ya que el recorrido del Octree es homogéneo y por tanto uniforme mientras que el del KD-Tree es heterogéneo.

KDvsOCT

No obstante la generación del Octree y su actualización requiere un tiempo importante de renderizado, es por ello que esta planteando de cara al futuro el añadido de una estructura en el pipeline que permite recorrerlos de forma más efectiva sin que el resto de componentes se vean afectados.

OctreeXbox8

Pero esto ya es harina de otro costal por lo que voy a pasar al otro método, al llamado Ray Tracing Híbido. ¿Y en que consiste? Tiene trampa porque se esta vendiendo como la aplicación del Ray Tracing en una GPU cuando realmente es la implantación del Ray Tracing en el rasterizado.

slide_10

La que ha aplicado esto es Imagination Technologies, la idea no es generar la escena a través del Ray Tracing sino generarla a través del rasterizado… ¿Entonces para que utilizar el Ray Tracing?

dataflow

La idea de Imagination la creo una empresa llamada Caustic que creo que CausticGL, una expansión del OpenGL ES 2.0. Dicha empresa fue comprada por Imagination hace unos años por lo que al contrario de la idea de los Octree estamos hablando de una solución propietaria de una compañía en concreto en conflicto con una solución más estandarizada como es la del Octree. El elemento principal son los llamados Ray Shaders. Cada uno de estos Ray Shaders esta asociado con un objeto de la escena y cuando estos interseccionan con el objeto determinan la iluminación y otros efectos del material para acumular dicha información en el búfer de imagen.

¿Pero acaso esto no es iluminación global al tratarse de Ray Tracing? Es que ahí esta la trampa, en primer lugar no podemos olvidar que los PowerVR son Tile Renderers por lo que calculan cada parte de la escena de manera local.

tiling

En segundo lugar tenemos que tener en cuenta como funciona este modelo:

08_Ray-tracing-in-games_hybrid

En esta caso es una técnica de renderizado por diferido donde primero generamos un G-Buffer y luego este es utilizado como referencia por el trazador de rayos, el cual es una pieza de hardware propietaria que se encuentra en dicha GPU.

1_PowerVR GR6500 GPU - PowerVR Wizard GPUs

Primero, el trazador de rayos necesita toda la geometría con la que los rayos vana internacionar, de ahí a que primero se redecir la escena generando un Geometry Buffer. Lo que hace a continuación el trazador de rayos es tomar esta geometría y crear una base de datos 3D de la escena. Dado que trabajamos con un Tile Renderer la geometría de la escena será que lo haya en dicho Tile y por tanto no será tan compleja como la base de datos generada con la escena al completo. Por cada pixel del G-Buffer tienes las propiedades de la superficie que es visible a la cámara.

maxresdefault

Fijaos que esto no es lo mismo que generar la escena con el trazado de rayos, aquí la escena ya esta generada previamente en el G-Buffer. ¿Y cuales son las consecuencias de ello? Pues que en vez de emitir por rayo desde una cámara lo que emites son los rayos desde la superficie que define el G-Buffer por lo que no se empieza desde cero y el resultado es el hecho de poder enviar un tercer rebote de luz… Es decir… Los dos primeros rebotes se calculan desde el rasterizado y el tercero desde el Ray Tracing pero teniendo ya acumulada la información de los otros dos rebotes anteriores.

Estas dos soluciones por su naturaleza no son globales sino locales, el motivo por el cual son locales es porque las GPUs no trabajan a nivel de cache con una gran cache general sino en pequeñas caches asignadas a cada grupo de stream processors. Hay que tener en cuenta que para almacenar el recorrido de la luz en la escena el método más efectivo es un KD-Tree para toda la escena pero estos tienen un tamaño determinado bastante y por tanto en este caso se necesitaría que cada cache de los stream processors tuviese un tamaño lo suficientemente grande y fuesen coherentes entre si, es decir… Algo a nivel físico que sería inviable.

Eso es todo.

Anuncios