Deferred Shading

Introducción

Para renderizar objetos en pantalla siempre es necesaria una metodología. Antes de que se pensara en técnicas como el Deferred Shading había principalmente dos.

La primera metodología a explicar hace referencia al Multiple Pass, que consiste en renderizar N veces un objeto en pantalla siendo N el número de luces que afectan a dicho objeto y mediante una función de blending poder obtener el resultado deseado. Esta metodología implica una penalización de rendimiento al mandar muchas veces los objetos a ser renderizados.

La segunda metodología viable es la Single Pass, que mediante un shader mucho más complejo puede tratar N luces en una sola pasada. Ya no es necesario mandar a renderizar N veces los objetos pero el numero combinatorio de los parámetros del shader da como resultado una explosión de shaders posibles. Esta explosión es debido a que no se tienden a utilizar condiciones dinámicas en el código, y por eso, es necesario compilar todas las posibilidades de los parámetros del shader en shaders específicos.

Deferred Shading

La metodología de Deferred Shading aparece para optimizar la cantidad de re-pintados que puede llegar a sufrir un píxel, donde se pierde mucho esfuerzo por parte de la tarjeta grafica.

Imaginemos en siguiente ejemplo:

Tenemos N elementos que queremos pintar en pantalla y estamos en el bucle de pintado. Mandamos a renderizar la primera malla, esta se descompone en triángulos y a su vez es descompuesta en píxeles donde cada uno de ellos es iluminado mediante las ecuaciones de iluminación pertinente.

Uno de esos píxeles es el píxel con coordenadas (205,34), y evidentemente acaba de recibir color de esta última malla.
En la siguiente vuelta del bucle de pintado, otra malla es descompuesta en triangulos, posteriormente en píxeles y por casualidad, esa malla estaba por delante de la malla anterior y efectivamente, nuestro píxel (205,34) vuelve a recibir color, pero esta vez de la última malla.

En la siguiente vuelta, se trata de una malla que esta por detrás de estas dos últimas y gracias al z-buffer y a la comparación de profundidad, el píxel (205,34) que también estaba ocupando esta malla es descartado por estar ésta por detrás.

Entramos en la vuelta N y existe otra malla que esta por delante de todo lo anterior y una vez más, cubre el píxel (205,34) y este vuelve a recibir color de esta última malla.

Como podemos observar, cada vez que un píxel recibe color, todo el pixer shader es ejecutado para calcular ese color. Si para generar un frame completo, hay píxeles que son cubiertos más de una vez, podemos observar como nuestro píxel shader se puede ejecutar tranquilamente 6 o 7 veces solo para cubrir 1 píxel.

Debido a este problema de re-pintado de los píxeles aparece el Deferred Shading que separa por un lado la obtención de información para calcular la luz y por otro lado el cálculo propiamente de la luz.

Con esta separación de los pasos, el Deferred Shading es capaz de hacer una pasada inicial muy rápida con toda la geometría y obtener unos buffers repletos con toda la información necesaria para posteriormente calcular la luz.

En una pasada posterior e iterando por cada una de las luces, éstas añaden su información a los píxeles de la pasada anterior. Esto nos da como resultado píxeles que reciben luz de varias luces pero nunca píxeles que se ven ocluidos por otros.
A continuación, paso a mostrar las etapas del Deferred Shading

Diagrama de las etapas
deferred_diagram.jpg

Etapa 1: Cálculo del G-Buffer

El cálculo del G-Buffer es simplemente recopilar toda la información que necesita un píxel para que posteriormente podamos calcular la luz incidente en el.

Normalmente se renderizan todas las mallas que se ven en escena con un shader que puede escribir información en 4 Render Targets a la vez para optimizar en pasadas.

Cada uno de estos Render Target almacenara información necesaria para el proceso posterior del cálculo de iluminación. Para un píxel dado, vamos a necesitar información como:

  1. Albedo: Lo que yo siempre conocí como la componente difusa, básicamente es el color que nos da la textura que tiene asociada ese objeto. (La componente difusa se refiere al color que proporciona la luz a ese píxel según su posición, normal, etc.)
  2. Normal: La normal en dicho píxel
  3. Especular: Los valores de la componente especular para poder saber cuánto de brillo tiene ese píxel.
  4. Posición: La posición 3D de ese píxel en el mundo para poder realizar correctamente atenuaciones de luz.
  5. Iluminación Off-Line: Básicamente la información que nos dan los LightMaps para hacer más real el objeto o la escena.
  6. Iluminación Emisiva: Luz de neón seria el mejor ejemplo. Es básicamente toda esa luz que no se ve afectada por las demás luces de la escena (Es irreal pero de momento se hace así).

Una vez hecho el inventario de datos que necesitamos y sabiendo que como máximo tenemos 4 Render Targets a 4 componentes por cada píxel en cada uno de ellos, necesitamos rellenar dichas componentes para posteriormente usarlas. Es necesario recordar que en todo el proceso del cálculo del G-Buffer NO se calcula ningún tipo de iluminación dinámica.

A continuación podremos observar una plantilla vacía de lo que sería el píxel (N,M) de cada uno de los 4 Render Target y que los 4 juntos nos ayudarían a guardar la información necesaria para el futuro.

<Vacio> <Vacio> <Vacio> <Vacio>
<Vacio> <Vacio> <Vacio> <Vacio>
<Vacio> <Vacio> <Vacio> <Vacio>
<Vacio> <Vacio> <Vacio> <Vacio>

Es necesario ir rellenando cada uno de los canales de color con información que hemos explicado anteriormente.
Una posible distribución sería la siguiente:

Albedo.Red Albedo.Green Albedo.Blue <Vacio>
Normal.x Normal.y Normal.z <Vacio>
<Vacio> <Vacio> <Vacio> <Vacio>
Specular.Red Specular.Green Specular.Blue <Vacio>

¿Si cada uno de los canales es de 8 Bits, y una posición 3D son 3 floats de 4 Bytes cada uno, como es posible hacer entrar 3 * 4 * 8 Bits = 96 Bits en esta solución?

La respuesta es que la posición NO se almacena como tal, y por eso, necesitamos otro método para poder ‘recuperar’ la posición en la siguiente fase.

La solución pasa por almacenar la distancia entre el ojo de la cámara y el píxel que estamos tratando en ese momento. Con la distancia o profundidad, seremos capaces posteriormente de recuperar la posición 3D del píxel.

A continuación muestro 3 plantillas de los Render Targets de 2 engines conocidos y 1 de desconocido (el desconocido es el iL-engine, el que estoy haciendo yo ;) )

Plantilla Killer Zone 2

LightAcum.Red LightAcum.Green LightAcum.Blue Intensity
Normal.x Normal.y
Motion.x Motion.y Specular.Power Specular.Intensity
Albedo.Red Albedo.Green Albedo.Blue Sun Occlusion

Si lo comentamos un poco:

  • El Primer Render Target tiene 8 bits * 4 canales = 32 bits y almacena la información de acumulación de luz mas una componente de intensidad con un 99.9% de probabilidad de que sea para efectos de HDR. Estas tres componentes deberían ser inicializadas a color negro por no haber luz calculada aun, pero en vez de eso, la inicializan con la información proveniente de varios puntos pre calculados con esféricos harmónicos. Esto dota al Render Target de una pseudo iluminación global inicial.
  • El Segundo Render Target tiene 16 bits * 2 canales = 32 bits y almacena las componentes X,Y de la normal del píxel. La Z se consigue haciendo Z = sqrt( 1.0f – (Normal.x * Normal.x) – (Normal.y * Normal.y) )
  • El Tercer Render Target almacena un vector 2D para calcular esos blurs en la pantalla según el movimiento de las cosas y 2 componentes para la especular. Mirar el paper del KZ2 para ver mejor eso de la especular.
  • El Cuarto Render Target almacena el color del obejto sin iluminar, es decir, el color de la textura, y como 4 componente, añaden el Sun Occlusion, que deberá ser parecido al Ambient Occlusion.

Estos, al programar para Play Station 3, esta les da acceso al Depth Buffer, y pueden obtener la distancia o profundidad de ese buffer sin afectar a los 4 que hemos comentado.

Plantilla StarCraft 2

Emisive.Red Emisive.Green Emisive.Blue <Vacio>
Normal.x Normal.y Normal.z Depth
Albedo.Red Albedo.Green Albedo.Blue Ambient Occlusion
Specular.Red Specular.Green Specular.Blue <Vacio>

Si lo comentamos un poco:

  • El Primer Render Target tiene 16 bits * 4 canales = 64 bits y almacena la información de lightmap mas una componente emisiva para hacer glows y demás historias.
  • El Segundo Render Target tiene 16 bits * 4 canales = 64 bits y almacena las componentes X,Y,Z de la normal del píxel. La cuarta componente es la distancia entre el ojo de la cámara y el píxel.
  • El Tercer Render Target almacena el color del objeto sin iluminar, es decir, el color de la textura, y como 4 componente, almacenan el Ambient Occlusion.
  • El Cuarto Render Target almacena la componente especular dejando vacio el cuarto canal.

Blizzard en este caso opta por Render Targets el doble de gruesos para almacenar información. Mirar su documento para mucha más información.

Plantilla iL-engine

Albedo.Red Albedo.Green Albedo.Blue Albedo.Mask
Normal.x Normal.y Specular.Power Specular.Intensity
Oren-Nayar Values Depth
LightMap.Red LightMap.Green LightMap.Blue LightMap.Mask

Si lo comentamos un poco:

  • El Primer Render Target tiene 16 bits * 4 canales = 64 bits y almacena la información de Albedo, es decir, el color de la textura del objeto sin más. Como cuarta componente podemos ver una máscara, la cual nos indica si ese píxel puede recibir iluminación dinámica o no.

Ej: El cielo no necesita ningún tipo de iluminación por parte de las luces de la escena.

  • El Segundo Render Target tiene 16 bits * 4 canales = 64 bits y almacena las componentes X,Y de la normal del píxel. La tercera y cuarta componente son las componentes de la especular.
  • El Tercer Render Target tiene 32 bits * 2 canales = 64 bits y almacena en la primera componente los dos valores necesarios para la ecuación Oren-Nayar en un solo valor numérico como C = (A * 10.0f) + B; para recuperar A y B es necesario hacer operaciones de modf, aunque ya veremos si al final compensa esto del Oren-Nayar.
  • El Cuarto Render Target almacena la información de lightmap y también una máscara para limitar la iluminación dinámica en píxeles con iluminación emisiva. Falta pensar si se puede hacer eso mismo con la máscara del primer render target

Se puede observar que esta plantilla coge ideas de todas las anteriores, aunque falta por investigar mucho más y ver si las decisiones son acertadas o no.

Etapa 2: Cálculo de la Iluminación

En proceso…

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License