Deferred Shading In Callisto
Upcoming SlideShare
Loading in...5
×
 

Deferred Shading In Callisto

on

  • 960 views

 

Statistics

Views

Total Views
960
Views on SlideShare
934
Embed Views
26

Actions

Likes
0
Downloads
0
Comments
0

3 Embeds 26

http://www.linkedin.com 16
http://www.lmodules.com 6
https://www.linkedin.com 4

Accessibility

Categories

Upload Details

Uploaded via as Microsoft Word

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Deferred Shading In Callisto Deferred Shading In Callisto Document Transcript

  • Deferred Shading in Callisto Callisto ENGINE
    Deferred Shading in Callisto

    Tabla de contenido TOC o " 1-3" h z u Introducción PAGEREF _Toc210150043 h 3Single Pass – Multiple Lights PAGEREF _Toc210150044 h 3Multiple Passes – Multiple Lights PAGEREF _Toc210150045 h 4Deferred Shading PAGEREF _Toc210150046 h 4Historia PAGEREF _Toc210150047 h 5G-Buffer PAGEREF _Toc210150048 h 7Multiple Render Targets PAGEREF _Toc210150049 h 7Position PAGEREF _Toc210150050 h 10Normal PAGEREF _Toc210150051 h 10Albedo PAGEREF _Toc210150052 h 12GBuffer Pass PAGEREF _Toc210150053 h 13Vertex Shader PAGEREF _Toc210150054 h 13Pixel Shader PAGEREF _Toc210150055 h 13Shading Pass PAGEREF _Toc210150056 h 15Vertex Shader PAGEREF _Toc210150057 h 16Pixel Shader PAGEREF _Toc210150058 h 17Composición final PAGEREF _Toc210150059 h 18Optimizaciones PAGEREF _Toc210150060 h 19Diseño del G-Buffer PAGEREF _Toc210150061 h 19Empaquetado de atributos PAGEREF _Toc210150062 h 20Eye-Space Normals PAGEREF _Toc210150063 h 20Position from Depth PAGEREF _Toc210150064 h 22Volúmenes de luz PAGEREF _Toc210150065 h 25Fase de Actualización PAGEREF _Toc210150066 h 26Fase de Renderizado PAGEREF _Toc210150067 h 28Shader LOD PAGEREF _Toc210150068 h 36Otras optimizaciones PAGEREF _Toc210150069 h 37Inconvenientes PAGEREF _Toc210150070 h 38Transparencia PAGEREF _Toc210150071 h 38Anti-aliasing PAGEREF _Toc210150072 h 38Múltiples Materiales PAGEREF _Toc210150073 h 38DS en DirectX10 PAGEREF _Toc210150074 h 39Integración PAGEREF _Toc210150075 h 40Sombras PAGEREF _Toc210150076 h 40Ambient Occlusion (SSAO) PAGEREF _Toc210150077 h 40Efectos de Post-Procesado PAGEREF _Toc210150078 h 40Glow PAGEREF _Toc210150079 h 40HDR PAGEREF _Toc210150080 h 40Motion Blur PAGEREF _Toc210150081 h 40Depth of Field PAGEREF _Toc210150082 h 40Relief / Parallax Mapping PAGEREF _Toc210150083 h 40Skinned Meshes/Skin shader PAGEREF _Toc210150084 h 40Blend Shapes PAGEREF _Toc210150085 h 40Conclusión PAGEREF _Toc210150086 h 41Bibliografía PAGEREF _Toc210150087 h 42
    Introducción
    El cálculo de iluminación de una escena es un aspecto que ha evolucionado de una manera espectacular gracias al incesante progreso del hardware gráfico.
    Hace algunos años la iluminación de un escenario consistía en el cálculo por vértice de 1-2 luces que solían proyectar sombras planas y se complementaban mediante el uso de lightmaps. Por el contrario los juegos actuales hacen uso de todo el potencial gráfico donde cada objeto en escena es iluminado simultáneamente por varias luces, dando lugar a una maraña de complejos efectos de sombreado a nivel de pixel todo ello en tiempo real. ( REF _Ref205225064 h Ilustración 1 - Neverwinter Nights (2002) VS Mass Effect (2008) )
    Ilustración SEQ Ilustración * ARABIC 1 - Neverwinter Nights (2002) VS Mass Effect (2008)

    Se puede afirmar que son dos las opciones especialmente utilizadas a la hora de realizar todos los cálculos de iluminación en tiempo real para una escena: ejecutar una única pasada y calcular todas las luces en la misma (single pass-multiple lights) ó realizar el cálculo en varias pasadas (multiple pass-multiple lights).
    Estas técnicas se engloban con el sobrenombre de forward shading y cada una de ellas conlleva ciertos inconvenientes que se describen a continuación.
    Single Pass – Multiple Lights
    La primera opción de iluminación en tiempo real consiste en utilizar una única pasada con el fin de calcular la aportación final de todas las luces sobre un mismo objeto.
    El siguiente seudocódigo ilustra el enunciado anterior:
    for each object do for each light do in a single shader framebuffer = brdf(object,light);
    Se trata de la aproximación más simple de las dos, por lo que posee algunas limitaciones e inconvenientes:
    Ya que la iluminación es computada por objeto, puede llegar a ocurrir que se realice un proceso de renderizado y sombreado de la superficie correspondiente a un objeto y que posteriormente sea cubierta por la superficie correspondiente a otro, por lo que se habrá realizado una computación innecesaria.
    Otro problema se traduce en la cantidad de combinaciones resultantes al intentar recrear en una única plantilla los distintos tipos de luces/materiales resultantes. Como todo el proceso de sombreado se realiza en un solo shader, que posee una cantidad limitada de instrucciones, esta técnica solamente es viable para un número reducido de luces.
    Por último, su integración con sombras es bastante complicada y la utilización de shadow maps puede llegar a consumir bastante VRAM.
    Multiple Passes – Multiple Lights
    En ésta segunda opción todos los cálculos son realizados por fuente de iluminación, de tal forma que en cada pasada, para cada luz existente en escena, todos los objetos influenciados por la misma son sombreados.
    Como en el ejemplo anterior se proporciona el siguiente seudocódigo:
    for each light dofor each object affected by light doframebuffer += brdf(object,light);
    Asimismo se detectan algunos problemas o limitaciones:
    Como en el caso anterior, se realizan cálculos innecesarios por redibujado de pixeles.
    En cada pasada que se envía el mismo objeto (ya que puede ser iluminado por varias luces a la vez), éste es procesado nuevamente por el vertex shader por lo que se generan una y otra vez las mismas transformaciones. De la misma forma, en cada una de las pasadas se vuelven a aplicar los filtros anisotrópicos.
    Por último, esta técnica produce un elevado batching (número de draw calls o llamadas a dibujado) que en el peor de los casos es del orden O(num_lights * num_objects).
    Deferred Shading
    Para representar un efecto complejo, se requerirán varias pasadas para computar el color final de cada pixel. Si se utilizan las dos técnicas descritas anteriormente, se repetirá el envío de la geometría una vez por cada pasada necesaria.
    A diferencia de las técnicas anteriores en las que se envía la geometría e inmediatamente se aplican los shaders correspondientes, la técnica denominada deferred shading consiste en enviar el grueso de geometría de la escena únicamente una vez, almacenando en dicha pasada todos los atributos de iluminación (posición, normales, color, etc.) en memoria de video local (G-Buffer) de tal forma que se puedan usar en las siguientes pasadas.
    En estas últimas pasadas mencionadas se creará un rectángulo alineado en pantalla en el que, mediante un postproceso en 2D donde se utilizan los atributos del G-Buffer como entrada, se calculará el color resultante para cada pixel.
    De esta forma, la gran ventaja de esta técnica respecto a las comentadas con anterioridad radica en el hecho de que se reduce sobremanera la complejidad computacional, siendo en el peor de los casos del orden de O(num_lights + num_objects).
    El hecho de reducir toda esa tasa de transferencia de vértices que antes se procesaba en cada pasada y ahora solamente es enviada en la correspondiente a la creación del G-Buffer, podrá servir para incrementar la cantidad de polígonos en la escena aumentando el realismo de la misma sin comprometer al rendimiento.
    Tampoco existirá un sobre redibujado de pixeles (solo en la pasada en la que se rellena el G-Buffer) ya que el color de cada pixel es calculado solamente una vez.
    El siguiente seudocódigo ilustra de forma clara la naturaleza de la técnica:
    for each object dorender lighting properties of object to G-Buffer;for each light doframebuffer += brdf (G-buffer,light);
    Historia
    La técnica fue inicialmente introducida en 1988 por Michael Deering et al en el SIGGRAPH. En dicho trabajo, aparte de no mencionar en ningún caso el término “deferred”, los autores proponían un sistema VLSI (Very Large Scale Integration ) donde un pipeline procesaba la geometría mientras otro pipeline se encargaba de aplicar Phong shading, con varias fuentes emisoras de luz, sobre dicha geometría.
    21145-1889Posteriormente al estudio inicial de Deering et al, el siguiente trabajo con relevancia dentro del ámbito de la técnica deferred shading corresponde al realizado por Saito and Takahashi en 1990. Los autores proponen una técnica de renderizado que generaba imágenes 3D y las mejoraba utilizando líneas, patrones, discontinuidades, bordes, obtenidos a partir de las diferentes propiedades de la geometría (normales, profundidad, etc.) almacenadas en buffers de geometría (G-Buffers).
    Otro estudio relacionado con la técnica fue el realizado por Ellsworth en 1991, que investigó arquitecturas paralelas y algoritmos para síntesis en tiempo real de imágenes en alta calidad usando deferred shading.
    Ilustración SEQ Ilustración * ARABIC 2 – Rendering of 3d-shapes (Saito and Takahashi)
    Poco más tarde, en 1992, el grupo de investigación de gráficos por computador UNC propuso la arquitectura PixelFlow para la generación de imágenes a alta velocidad. En dicho artículo Molnar et al usaban deferred shading para reducir los cálculos realizados en los complejos modelos de sombreado utilizados en su arquitectura.
    Despues de los artículos mencionados han existido otras investigaciones y desarrollos que han utilizado la técnica deferred shading ( Nicolas Thibieroz, Shawn Hargreaves, Randima Fernando, Ariel Brunetto, Frank Pluig entre otros).
    22225635Recientemente, Oles Shishkovtsov ha escrito un capítulo en el libro “GPU Gems 2” en el que describe los detalles de la técnica deferred shading usada en el juego S.T.A.L.K.E.R.
    Aún más reciente es el artículo escrito por Rusty Koonce en el libro “GPU Gems 3”, continuación obligada al escrito por Shishkovtsov. Mientras que éste último cubre los aspectos fundamentales de la técnica, el artículo de Koonce enfatiza los problemas, técnicas y soluciones encontradas mientras se trabajaba en la realización del motor de renderizado del juego “Tabula Rasa”, basado en la técnica deferred shading.
    Ilustración SEQ Ilustración * ARABIC 3 – S.T.A.L.K.E.R. Shadow of Chernobyl
    215392068580
    Para finalizar existen recientes técnicas (Light Indexed Deferred Lighting [Damien Trebilco 2007] y Light Pre-Pass Rendering, [Wolfgang Engel 2008] ) basadas en deferred shading que intentan solucionar varios problemas derivados de su naturaleza, como es el caso de transparencias, AA y poder gestionar varios BRDF’s en la escena. En ellas se utiliza el G-Buffer para almacenar otro tipo de información y utilizarla en una pasada posterior usando forward rendering.
    Ilustración 4 - Tabula Rasa
    G-Buffer
    En contraposición al comportamiento de cualquier sistema basado en forward rendering, en uno basado en deferred shading, tanto la iluminación como los demás efectos no son calculados en la misma pasada en la que se envía y procesa la geometría de la escena.
    Existirá en este caso una primera pasada en la que la geometría es procesada y todos los atributos asociados a la misma como posición, normales, albedo, etc., son almacenados en varias texturas formando un buffer auxiliar denominado G-Buffer. Dichas texturas serán utilizadas en sucesivas pasadas para realizar todos los cálculos correspondientes al color de cada pixel sin necesidad de volver a enviar de nuevo la totalidad de geometría de la escena.
    Para almacenar atributos como la posición, donde es necesario un elevado rango de representación, se hará casi obligatorio el uso de texturas de punto flotante. Aunque existen diferentes métodos de empaquetado para almacenar en texturas de menor precisión, la mayoría de los sistemas actuales soportan este tipo de texturas de punto flotante.
    Multiple Render Targets
    Anteriormente (DirectX 8) solo se podía escribir en una sola textura (render target) utilizando un máximo de 32 bits consistentes en 4 componentes de color (8 bits por componente).
    RT
    A B G R RT
    Por lo tanto si se opta por utilizar un único render target, se deberán realizar múltiples pasadas con el fin de almacenar todos los atributos seleccionados: position, normals, albedo, etc., por lo que habría que enviar en cada pasada toda la geometría resultando una técnica casi ineficiente, cercana al comportamiento y rendimiento de un forward renderer.
    DirectX 9 aportó una nueva característica denominada Multiple Render Targets (MRT) en la que se permitía utilizar hasta cuatro diferentes render targets en los que escribir en una única pasada, y aumentando la precisión anterior de 32 bits a 512 bits (varía respecto al dispositivo utilizado).
    De esta forma se podrían empaquetar los atributos necesarios del G-Buffer en MRTs, organizándolos de forma inteligente, de tal manera que se consiguiesen escribir en una sola pasada y en el menor número de texturas posibles.
    En general los MRTs poseen las siguientes limitaciones o características:
    Deben poseer el mismo tamaño. Esto no es del todo cierto, ya que se podrá utilizar una profundidad de bits diferente si el dispositivo soporta D3DPMISCCAPS_MRTINDEPENDENTBITDEPTHS. Prácticamente todas las tarjetas del mercado soportan dicha característica.
    Se pueden mezclar RTs con diferente número de canales, es decir, se podría realizar la siguiente configuración de MRTs:
    RT0 : G16R16F
    RT1 : A8R8G8B8
    RT2 : R32F
    Dithering, alpha testing, fogging, blending, o masking solamente podrán ser utilizados si el dispositivo tiene marcado el bit D3DPMISCCAPS_MRTPOSTPIXELSHADERBLENDING.
    No soportan MSAA (MultiSample Anti-Aliasing) por hardware. En DirectX 10, como se comentará más adelante, se solventa éste problema. Mientras tanto en DirectX 9 se deberán utilizar otros métodos más intrusivos para obtener una aproximación al AA.
    El uso de todos estos MRTs da como resultado la utilización de gran cantidad de memoria VRAM así como de ancho de banda del dispositivo. Cuando se diseña un sistema basado en deferred shading habrá que tener especial cuidado en seleccionar qué valores son almacenados en el G-Buffer y sobre todo de qué forma serán almacenados para reducir tanto VRAM como ancho de banda. Existen varias técnicas para optimizar el empaquetado y diseño del G-Buffer, todas ellas se comentarán en posteriores apartados (Ver Optimizaciones).
    Un posible diseño del G-Buffer, bastante simple y sin optimizar, correspondería al que se muestra a continuación:
    NORMAL.XLIBRECOLOR.BCOLOR.GCOLOR.RPOSITION.ZPOSITION.XNORMAL.ZPOSITION.YLIBRENORMAL.YG16R16FRT3RT2RT1RT0
    A2RGB10 RGBA8G16R16F
    LIBRE
    En total serían 128 bits por pixel. A una resolución de 1024x768 correspondería a un total de 12 MB de memoria VRAM. Éste no sería el principal inconveniente dada la gran cantidad de memoria que proporciona el hardware actual.
    La mayor desventaja y la que proporciona un mayor riesgo de producir bottlenecks en el rendimiento de la técnica corresponde al ancho de banda requerido en cada fotograma aunque, como ocurre en el caso anterior, este inconveniente queda mitigado gracias a la potencia que proporcionan los dispositivos 3D actuales.
    La siguiente tabla representa un cálculo aproximado del ancho de banda necesario, es decir, la cantidad de memoria que es necesario transferir por el bus del dispositivo en cada fotograma, para un escenario particular de 1024x768 y diferentes opciones de bpp.
    32 bpp64 bpp128 bppnMRTs = 214 GB/seg18 GB/seg26 GB/segnMRTs = 316 GB/seg22 GB/seg34 GB/segnMRTs = 418 GB/seg26 GB/seg42 GB/seg
    Si estos valores son comparados con un listado de características correspondientes a tarjetas gráficas de la serie 8 de NVIDIA, podemos comprobar cómo, para dispositivos de gama media/baja dentro de la serie, se hace patente las limitaciones impuestas por el ancho de banda a utilizar.
    A continuación se describirá el proceso de creación de cada una de las superficies de almacenamiento que forman el G-Buffer final, siempre siguiendo el ejemplo de configuración anterior donde los atributos que se mapean corresponden a position, normals y diffuse albedo.
    Position
    Corresponde a la posición del pixel y puede estar representada en diferentes sistemas de coordenadas, aunque las más lógicas corresponden a view/world space.
    En el ejemplo se han utilizado coordenadas en view space, por lo que ha sido necesario multiplicar cada vértice en el vertex shader por la matriz de transformación correspondiente. Ya en el pixel shader se almacenará cada coordenada (x,y,z) en la RT correspondiente.
    Las siguientes imágenes muestran las texturas correspondientes a RT0 y RT1, en las que se han mapeado (position.x, position.y) y (position.z) respectivamente:
    Ilustración SEQ Ilustración * ARABIC 5 - RT0 (pos.x, pos.y) y RT1 (pos.z)
    Aunque la textura en la que se empaqueta la coordenada z del pixel presente un color constante, al tratarse de una textura de punto flotante estará almacenando un mayor rango de valores que el aparentemente representado.
    Normal
    Se trata del vector normal correspondiente a cada pixel almacenado en view space.
    -4381574930Como es costumbre, se utilizarán mapas de vectores normales definidos en tangent space (ó texture space), que no viene a ser más que otro sistema de coordenadas en el que se indica la orientación de la superficie de la textura en cada vértice.
    A partir de dicho sistema de coordenadas se podrán realizar técnicas como bump mapping ó incluso parallax mapping/relief mapping que serán comentadas en apartados posteriores.
    Ilustración 6 - Mapa de normales en Tangent SpaceEn la ilustración 6 se puede observar un ejemplo de mapa de normales en tangent space en el que se representan los detalles que se van a añadir al conjunto de polígonos con el fin de dar el aspecto final de una pared de ladrillos.
    Volviendo al caso de ejemplo que se expone en el presente apartado, ya que el sistema utilizado corresponde a coordenadas en view space, habrá que obtener los vectores normales en dicho sistema.
    Para ello habrá que formar la matriz tangencial utilizando los vectores tangent, binormal y normal que a su vez estarán en view space.
    MVS= TxTyTzBxByBzNxNyNz
    Por último habrá que multiplicar el vector normal obtenido de samplear el mapa de normales por la matriz tangencial anterior, resultando el vector normal en el sistema de coordenadas deseado:
    NVS=NTS × MVS
    Posteriormente, el vector resultante de la ecuación anterior será normalizado para no perder precisión en los cálculos posteriores.
    De la misma manera se suelen utilizar texturas de formato coma flotante (16F) para almacenar con suficiente exactitud el valor obtenido, aunque es más económico y la mayoría de las ocasiones resulta suficiente disponer de una textura con formato entero de 32 bits (A2RGB10). De esta forma los canales RGB, los tres de 10 bits cada uno, se utilizarán para almacenar las coordenadas (x,y,z) del vector normal, quedando el canal A libre.
    La siguiente imagen muestra la RT2 correspondiente al G-Buffer, en la que se ha mapeado N (normal.x, normal.y, normal.z) en los canales RGB, dejando libre el canal Alfa:
    Ilustración 7 - RT2 (view space normals)
    Albedo
    El color de cada pixel correspondiente a la componente difusa (color propio del objeto) es tomado de la textura denominada comúnmente albedo map asociada al modelo.
    Como cada componente de color RGBA se puede representar en un rango 0-255, se utilizará una textura de 32 bits, con 8 bits por cada canal.
    La siguiente imagen muestra la RT3 correspondiente al G-Buffer, en la que se ha mapeado D (diffuse.x, diffuse.y, diffuse.z) en los canales RGB, dejando libre el canal Alfa:
    Ilustración 8 - RT3 (albedo map)
    De la misma forma se podrían almacenar otro tipo de atributos en los canales no utilizados, como mapas que definan la componente especular (specular power / specular intensity).
    Una vez definida la composición y estructura del G-Buffer de forma teórica, se procederá a mostrar en el siguiente apartado de una forma absolutamente práctica el proceso de creación de cada una de las texturas que forman el FAT Buffer.
    GBuffer Pass
    La primera pasada se encargará de almacenar los atributos seleccionados (position, normal, albedo) en cada una de las textura que forman el G-Buffer.
    Vertex Shader
    El vertex shader es realmente simple. Aparte de transformar la posición para expresarla en clip space (AKA screen space), se propagan otra serie de vectores:
    Se transforma el vector posición en view space que corresponderá al valor almacenado en la RT0/RT1.
    Normal, binormal, tangent se transforman de object space a view space mediante la matriz de transformación correspondiente. Estos vectores, como se comentó anteriormente, formarán la matriz MVS utilizada en la creación del vector normal (en view space) almacenado en RT2.
    Se propaga sin ningún tipo de transformación las coordenadas del albedo map, que se escribirán en la RT3.
    VS_OUTPUT_GBUFFERPASS VS_GBUFFER( VS_INPUT_GBUFFERPASS IN ){VS_OUTPUT_GBUFFERPASS Out;Out.Pos = mul(IN.pos, g_mWorldViewProjection);float4 pp = mul( IN.pos, g_mWorldView );Out.WorldPos = pp;Out.interPos = mul(IN.pos, g_mWorldView).xyz;Out.normal = mul(IN.normal, (float3x3)g_mWorldView);Out.binormal = mul(IN.binormal, (float3x3)g_mWorldView);Out.tangent = mul(IN.tangent, (float3x3)g_mWorldView);Out.texCoord = IN.texcoord; return Out;}
    Pixel Shader
    Ya en el pixel shader, se generarán los colores que se van a propagar a los MRTs configurados.
    Para obtener el color de la componente difusa albedo (Out.Color) simplemente habrá que samplear la textura correspondiente a partir de las coordenadas propagadas desde el vertex shader.
    El vector position se propagará sin ningún tipo de cálculo posterior hacia los RTs correspondientes: Out.PosXY y Out.PosZ.
    En cuanto a las normales, habrá que samplear el vector de la textura correspondiente y transformarlo a view space utilizando la matriz MVS (objToTangentSpace). El resultado se propagará hacia el RT Out.Normal.

    PS_OUTPUT_GBUFFERPASS PS_GBUFFER( VS_OUTPUT_GBUFFERPASS IN ){PS_OUTPUT_GBUFFERPASS Out;float3 base = tex2D(Textura2D0, IN.texCoord);float3 bump = tex2D(Textura2D1, IN.texCoord) * 2 - 1;bump = normalize(bump); float3x3 objToTangentSpace = float3x3( IN.tangent, IN.binormal, IN.normal );Out.PosXY= float4(IN.interPos.x, IN.interPos.y, 0, 1);Out.PosZ= float4(IN.interPos.z, 0, 0, 1);float3 normal = normalize( mul( bump, objToTangentSpace ) );//view space bump texture space ( [-1;+1] [0;1] )normal = normal * 0.5 + 0.5;Out.Normal = float4(normal.x, normal.y, normal.z, 1);Out.Color = float4(base, 1); return Out;}
    Shading Pass
    Como se había comentado en anteriores apartados, el G-Buffer generado se utilizará en ésta pasada para realizar el cálculo de iluminación sin necesidad de enviar de nuevo todo el grueso de geometría de la escena.
    Simplemente será necesario un rectángulo alineado en pantalla de tal forma que cada uno de los pixeles de su superficie presente su homólogo en todas las texturas del G-Buffer.
    Para ello hay que tener en cuenta que los sistemas de coordenadas correspondientes a pixels y texels en Direct3D 9 difieren en la situación de su origen de coordenadas.
    El primero considera el centro del pixel como el origen del sistema de coordenadas:
    Ilustración 9 - Pixel Coordinate System
    Mientras que el segundo considera como origen de coordenadas la esquina superior izquierda del pixel:
    Ilustración 10 - Texel Coordinate System
    Por lo tanto para alinear correctamente texels con pixels habrá que estipular un desplazamiento proporcional al tamaño del texel, como se representa en la siguiente ilustración:
    RECTÁNGULO ALINEADO EN PANTALLAWH∆U = 12 × W∆V = 12 × Ht=(∆U ,∆V)t=(1+ ∆U ,∆V)t=(∆U ,1+ ∆V)t=(1+ ∆U ,1+ ∆V)
    Vertex Shader
    El vertex shader es tan simple como propagar la posición de los vértices que forman el rectángulo alineado en pantalla y realizar la corrección de las coordenadas de textura indicada en el apartado anterior.
    VS_OUTPUT_SCREENQUAD VS_RenderScreenQuad( VS_INPUT_SCREENQUAD IN ){VS_OUTPUT_SCREENQUAD OUT;OUT.Position = IN.Position;OUT.TC0 = IN.TC0 + vTexelSize;OUT.EyeScreenRay = IN.FrustumFar; return OUT;}
    La variable vTexelSize se ha definido como un float2 de la siguiente forma:
    float2 vTexelSize = float2(1.0f / (2.0f * screenWidth), 1.0f / (2.0f * screenHeight));
    Pixel Shader
    Por último en el pixel shader se realizarán los cálculos necesarios para computar la iluminación de cada pixel en pantalla.
    La ejecución del pixel shader es individual para cada fuente de luz por lo que se realizará tantas veces como fuentes de luz existan. El resultando de cada iteración se irá acumulando en el framebuffer mediante transparencia aditiva (additive blending).
    float4 PS_RenderLight(VS_OUTPUT_LIGHT IN) : COLOR0 {half3 pos = float3(0,0,0);pos.xy = tex2D(GBufferTexture1, IN.PosProj).xy;pos.z = tex2D(GBufferTexture2, IN.PosProj).x; half3 eyeVec = camPos - pos;half3 lightVec = (1.0 / PointLightRange) * (PointLightPosition - pos);half3 normal;// normales en texture space [0,1] -> convertir a [-1,+1]normal.xyz = tex2D(GBufferTexture4, IN.PosProj).xyz * 2 - 1;half4 base = tex2D(GBufferTexture3, IN.PosProj);half light_l = length(lightVec);half atten = tex1D(AttenuationMap,light_l).r;half diffuse = 0.0f;half specular = 0.0f;//ambienthalf3 lighting = 0.1 * base;diffuse = saturate(dot(normalize(lightVec), normal));specular = pow(saturate(dot(reflect(normalize(-eyeVec), normal), normalize(lightVec))), 16);lighting = PointLightColor * (diffuse * base + 0.7 * specular) * atten;return half4(lighting, 1.0f);}
    En el ejemplo se utiliza Phong Shading como modelo de iluminación teniendo en cuenta una distancia de atenuación para cada fuente de luz.
    Esta distancia de atenuación es obtenida de una textura en función de la posición de la fuente de luz y del rango máximo de iluminación de la misma.
    La textura de atenuación es similar a la siguiente:
    En resumen, en el pixel shader se realizan las siguientes acciones:
    Se recuperan los atributos almacenados anteriormente en el G-Buffer.
    Se calcula la atenuación de la luz basada en la posición actual y en el rango máximo de iluminación.
    Se realiza el cálculo de iluminación correspondiente al pixel que, a grandes rasgos, se puede definir como:
    Iads= att(Idiff+ Ispec)
    Itotal = Iambient+ Iads
    En el pixel shader solamente se realiza el cálculo correspondiente a Iads. La componente Iambient se deberá calcular en una pasada diferente y añadir posteriormente al resultado final.
    Composición final
    El resultado final generado como salida del pixel shader anterior, para una escena iluminada por 11 fuentes de luz diferentes, es similar al que presenta la siguiente figura:
    Ilustración 11 - Resultado final
    Optimizaciones
    El caso de ejemplo presentado en el apartado anterior se puede considerar el acercamiento más simple a la técnica deferred shading, tanto por el número de atributos almacenados en el G-Buffer como por el hecho de que no está optimizado de ninguna manera.
    A continuación se describen una serie de optimizaciones aplicables a lo largo del ciclo de vida de la técnica, desde la creación del G-Buffer hasta el cálculo de la iluminación, con el fin de mejorar el rendimiento final.
    Diseño del G-Buffer
    Si se decidiesen almacenar otros atributos en el G-Buffer (specular power, specular intensity, occlusion terms, motion vectors, etc.), el número de canales libres en los RTs no serían suficientes.
    Tampoco existiría la posibilidad de añadir nuevos RTs, ya que solo se pueden utilizar simultáneamente los cuatro ya presentes (esto para DX9, en DX10 es aumentado hasta 8).
    Por lo tanto, variar el tamaño de cada uno de los RTs sería una de las opciones a contemplar. El inconveniente vendría en modo de penalización en cuanto a memoria VRAM y ancho de banda que se verían aumentados de forma prohibitiva para algunos dispositivos.
    Supongamos que se desean almacenar los siguientes atributos en el G-Buffer:
    Position, 3 x FP16
    Normals, 3 x FP16
    Diffuse Albedo, 3 x I8
    Specular Power, 1 x I8
    Specular Intensity, 1 x I8
    Motion Vectors, 2 x I8
    Material ID, 1 x I8
    En total sumarian 160 bits, 32 bits mayor que el límite que proporciona el diseño del G-Buffer anterior en el que cada RT era de 32 bits.
    Si se aumenta el tamaño de cada RT a 64 bits se obtiene la cifra de 192 bits para 3 RTs. Se habría conseguido disminuir el número de texturas en memoria (menor gasto de VRAM) pero el ancho de banda, que es el que produce mayor riesgo de bottlenecks, se habría aumentado.
    Las optimizaciones que se comentan a continuación consiguen reducir el número de bits utilizados para almacenar los atributos, reduciendo el ancho de banda utilizado en cada fotograma.
    Hay que considerar que toda esta reducción de espacio conlleva un coste computacional adicional, por lo que será conveniente analizar detenidamente que solución merece ser aceptada como la más apropiada en cada caso.
    Empaquetado de atributos
    A la hora de almacenar ciertos atributos se puede reducir el tamaño utilizado, siempre teniendo en cuenta que a menor tamaño de almacenamiento, menor precisión a la hora de realizar los cálculos de iluminación pertinentes y por lo tanto menor calidad final.
    Reducir el número de bits para almacenar un atributo se solía utilizar para conseguir un G-Buffer de RTs enteros en dispositivos que no soportaban texturas en coma flotante. Actualmente, la mayoría de los dispositivos presentes en el mercado (si no todos) soportan este tipo de texturas, por lo que la acción de empaquetar a menor precisión simplemente tiene como objetivo conseguir mejorar el rendimiento global de la técnica.
    Las siguientes funciones HLSL realizan un proceso de empaquetado/desempaquetado de FP16 I8:
    half2 PackFloat16( half depth ){ depth /= 4; half Integer = floor(depth); half fraction= frac(depth); return half2( Integer/256, fraction); }half UnpackFloat16( half2 depth ){ const half2 unpack = {1024.0f, 4.0f}; return dot(unpack, depth);}
    Eye-Space Normals
    Si se usan vectores unitarios como normales, se puede llegar a calcular un componente a partir de los otros dos utilizando la siguiente ecuación:
    z= ±1- x2- y2
    Como se puede observar el componente calculado puede ser positivo o negativo. Sin embargo, si todo el cálculo de la iluminación es realizado en view space, en casi todas las ocasiones los polígonos presentarán un valor positivo o negativo en su componente Z, dependiendo del modo de representación utilizado (OpenGL positivo, Direct3D negativo).
    Solamente en contadas ocasiones, en las que el FOV es exageradamente amplio, el valor de la componente Z puede cambiar de signo.
    La siguiente figura ilustra lo expuesto:
    EYE+ -NV
    Para evitar en estos casos la incorrecta iluminación producida por el cambio de signo, se puede reservar un bit de uno de los dos canales que albergan las componentes x e y para almacenar el signo correspondiente. El RT quedaría así:
    CANAL R (I8/FP16) 8-16 bits para almacenar normal.x
    CANAL G (I8/FP16) 7-15 bits para almacenar normal.y, 1 bit para almacenar el signo
    La optimización expuesta reduce en un canal el tamaño final para almacenar el vector normal, a costa de un coste computacional adicional.
    Se podría evitar este coste computacional utilizando una textura en la que cada texel albergase el valor de la componente z. De esta forma al samplear la textura con los valores u y v correspondientes a las componentes x e y de la normal, se obtendría la componente z.
    Esta otra solución plantea el inconveniente de que se aumenta el coste del ancho de banda utilizado por la aplicación, que es lo que se planteaba en un principio reducir para mejorar el rendimiento final de la técnica.
    Por último, a la hora de almacenar los valores se pueden utilizar los métodos de empaquetado expuestos en el apartado anterior, indicando de nuevo que, en el caso de las normales necesarias para el cálculo de iluminación y otras técnicas como bump mapping, la pérdida de precisión se transforma en una pérdida de calidad visual notable.
    La siguiente tabla, sacada de Shishkovtsov[GPU GEMS 2], presenta todas las posibles opciones de configuración del RT que alberga las normales:

    A8R8G8B8A2R10G10B10R16FG16FA16R16G16B16FHardware SupportAllRadeon 9500 or betterGeforce FX or betterRadeon 9500 or betterGeforce FX or betterRadeon 9500 or betterDeferring CostOne madOne madNoneNoneDecoding CostOne madOne nrmOne madOne nrmOne movOne dp2aOne rsqOne rcpSampling and Storage32 bits32 bits32 bits64 bitsQualityPoorGood for rough surfacesExcellentExcellentFree Components11 (2 bits, very low precision)01
    Position from Depth
    Si recordamos el diseño del G-Buffer del caso de ejemplo, para almacenar el vector position se han utilizado 3 canales de 16 bits cada uno (48 bits en total).
    El tamaño por componente establecido puede ser suficiente cuando se utiliza un sistema de coordenadas en view space, pero en world space las distancias pueden hacerse tan grandes que sea necesario utilizar precisiones de 32 bits, por lo que trastocarían la estructura del G-Buffer de forma considerable, así como se aumentarían los dichosos ancho de banda y VRAM que tan de cabeza traen en la fase de diseño.
    La solución consiste en almacenar en el G-Buffer solamente la componente z del vector position en view space, normalizada entre el rango [0.0, distancia al plano más alejado del view frustum]. De esta forma, en vez de utilizar 48 bits para almacenar las tres componentes del vector position, se utilizarán 32 bits para almacenar la componente LESD (linear eye-space depth).
    En fase de creación del G-Buffer, más concretamente en el Pixel Shader, la componente LESD se calcula y propaga posteriormente de la siguiente manera:
    …float fDepth = IN.ViewPos.z / frustumCoord.z;Out.Pos = float4(fDepth, 0, 0, 1);…
    La RT resultante tendrá el siguiente aspecto:
    Ilustración 12 - Linear Eye-Space Depth
    Para obtener las componentes x e y a partir del LESD se procederá como se comenta a continuación.
    Cuando se envía el rectángulo alineado en pantalla hacia el vertex shader, aparte de la información relativa a posición y coordenadas de textura, se pasará un vector adicional correspondiente a una de las esquinas del plano más alejado del view frustum.
    La esquina seleccionada del view frustum se corresponderá con la esquina del rectángulo alineado como indica la siguiente imagen:
    RECTÁNGULO ALINEADO EN PANTALLAFAR FRUSTUM PLANEVRTR VFPTR
    Al propagar el vector VFPTR desde el vertex shader hacia el pixel shader, se verá interpolado de tal forma que se corresponderá con el pixel a calcular:
    RECTÁNGULO ALINEADO EN PANTALLAFAR FRUSTUM PLANEPR PFP
    Por último para recuperar el vector position original, bastará con multiplicar el vector PFP por la profundidad obtenida del G-Buffer como se muestra a continuación:
    VP=Pfp × Pdepth
    El siguiente bloque de código corresponde a los cambios realizados tanto en el vertex shader como en el pixel shader correspondientes a la segunda pasada (cálculo de iluminación):
    //VERTEX SHADER…//FrustumFar esquina correspondiente al plano más alejado del view frustumOUT.EyeScreenRay = IN.FrustumFar;…//PIXEL SHADER…//Se obtiene la profundidad correspondiente al pixelfloatpDepth = tex2D(GBufferTexture1, IN.TC0).r; //Vp = Pfp x Pdepth pos = (IN.EyeScreenRay * pDepth);…
    Gracias a ésta optimización se ha reducido el número de bits utilizados en la construcción del G-Buffer final del ejemplo, así como el número de RTs de almacenamiento.
    La estructura resultante es similar a la siguiente:
    RT0 Depth : R32F (32 bits, nada libre)
    RT1 Normals (x,y) + sign bit : G16R16F (32 bits, nada libre)
    RT2 Albedo : A8R8G8B8 (32 bits, 1 canal de 8 bits libre)
    En total 96 bits organizados en 3 RTs, dando lugar a un diseño de G-Buffer de lo más optimizado posible.
    Si al final se adopta la estructura propuesta en el apartado Diseño del G-Buffer se necesitaría solamente un RT más cuya composición sería la siguiente:
    RT3 Specular Intensity (8 bits), Motion Vectors (16 bits), Material Id (8 bits) : A8R8G8B8 (32 bits, nada libre)
    Y el canal libre de RT2 albergaría el atributo resultante, Specular Power, de 8 bits.
    Se obtendría pues un diseño muy completo de G-Buffer compuesto por un gran número de atributos, en solo 4 RTs y con un total de 128 bits por pixel, margen adecuado para evitar un gasto desorbitado de VRAM o el consumo peligroso de ancho de banda de sistema.
    Volúmenes de luz
    Se ha visto como optimizando el diseño del G-Buffer se consigue reducir el ancho de banda total en cada fotograma.
    9588529210Una vez generado el G-Buffer, éste es utilizado en cada pasada correspondiente al cálculo de iluminación para computar el color de cada uno de los pixeles de la pantalla.
    Ya que cada fuente de luz posee una atenuación determinada, existirán pixeles en pantalla que no estarán influenciados por ninguna luz, por lo tanto el cálculo realizado para los mismos se transforma en una pérdida de rendimiento bastante acusada.
    De esta forma, en vez de aplicar el pixel shader para todos los pixeles en pantalla, se aplicará a todos aquellos que estén influenciados al menos por un volumen de luz.
    Existen varios tipos de fuente de luz, cada uno de ellos se puede asociar con una primitiva que representa su volumen final:
    Point Lights, que transmiten luz con la misma intensidad en todas direcciones. Su volumen de luz corresponde a una esfera.
    Spot Lights, que transmiten luz en una dirección fija determinada mediante un cono, que a su vez representa su volumen.
    Directional lights, cuya luz se transmite por la escena en una misma dirección. Como ejemplo se puede considerar la luz del sol, que no sufre de atenuación y cuyo volumen es un rectángulo que cubre la pantalla entera.
    Una vez descritos los diferentes volúmenes de luz, se pueden definir dos fases diferentes a la hora de establecer el proceso de optimización en el periodo de iluminación: fase de actualización y fase de renderizado.
    Fase de Actualización
    En la fase de actualización se intenta reducir el número de fuentes de luz que afectan a la escena, aplicando uno a uno los siguientes enunciados:
    Se descartarán las luces cuya influencia no sea apreciable una vez aplicados los algoritmos de visibilidad y oclusión apropiados. Los volúmenes de luz que queden completamente ocultos por otros elementos de la escena o que estén fuera del view frustum no entrarán a formar parte del cálculo de iluminación.
    Se proyectarán los volúmenes de luz en pantalla (screen space).
    Todas aquellas fuentes de luz que afecten a una misma región de pixeles se podrán combinar de tal forma que la fuente resultante proporcione una intensidad que simule la existencia de las fuentes originales.
    De la misma forma, las fuentes de luz cuyo volumen proyectado sea tan pequeño que solamente influencia a pocos pixeles (bien sea porque la fuente de luz sea pequeña o porque se encuentre alejada), podrá ser descartada.
    Por último, se puede definir un número máximo predefinido de fuentes de luz que pueden afectar al mismo tiempo a cada pixel, escogiendo las más grandes, intensas y cercanas. De esta forma si el frame rate obtenido en el fotograma es bastante elevado, se puede elevar el número de fuentes de luz que pueden afectar a cada pixel. Por el contrario, si el frame rate es bajo, se puede reducir este número en beneficio del rendimiento.
    La siguiente imagen muestra un claro ejemplo de los volúmenes de luz calculados en la fase de actualización, que serán utilizados individualmente en la siguiente fase para aplicar las diferentes optimizaciones existentes.
    Fase de Renderizado
    Se pueden definir dos grupos en los que clasificar los efectos especiales y de iluminación: fuentes globales y fuentes locales.
    Las fuentes globales afectarán a todos los pixeles, por lo que implica que los efectos se ejecutarán procesando la escena con el típico rectángulo en pantalla. Ejemplos de fuentes globales son las luces que iluminan todo el “mundo virtual” como la del sol, luces que se encuentran demasiado tan cerca de la cámara que la contienen dentro de su volumen ó efectos que se aplican a pantalla completa, como Depth of Field, fog, etc.
    Ya que en la técnica deferred shading el coste de procesamiento es directamente proporcional al número de píxeles afectados, las fuentes globales son las más costosas de todas. Afortunadamente no se suelen usar en cantidades que reduzcan de forma alarmante el rendimiento del sistema.
    Las fuentes locales, al contrario de las anteriores, solamente afectan a regiones específicas de la escena. El beneficio en cuanto a las fuentes globales es evidente: solamente se procesarán los píxeles de pantalla que sean afectados al menos por una región.
    Existen diferentes métodos para determinar los pixeles que deberán ser procesados: Scissor Test, Stencil Cull, Z-Cull.
    Scissor Test
    La técnica consiste en utilizar el volumen que engloba la fuente de luz, calculado en la fase de actualización, para generar a partir del mismo un rectángulo que, una vez proyectado en pantalla, contendrá los pixeles sobre los que se realizarán los cálculos de iluminación pertinentes.
    Obviamente esta técnica es solamente adecuada para fuentes de luz del tipo point light o spot light ya que las directional light no tienen una posición en el espacio, por lo que se considera que afectan a toda la pantalla.
    El siguiente bloque de código [NVIDIA 2004], solamente aplicable a point lights, se encarga de calcular el rectángulo a partir de la posición de la fuente de luz y de su distancia de atenuación.
    RECT DetermineClipRect(const D3DXVECTOR3& position, const float range){//compute 3D bounding box of light in world spaceD3DXVECTOR3 bbox3D[8];bbox3D[0].x = position.x - range; bbox3D[0].y = position.y + range; bbox3D[0].z = position.z - range;bbox3D[1].x = position.x + range; bbox3D[1].y = position.y + range; bbox3D[1].z = position.z - range;bbox3D[2].x = position.x - range; bbox3D[2].y = position.y - range; bbox3D[2].z = position.z - range;bbox3D[3].x = position.x + range; bbox3D[3].y = position.y - range; bbox3D[3].z = position.z - range;bbox3D[4].x = position.x - range; bbox3D[4].y = position.y + range; bbox3D[4].z = position.z + range;bbox3D[5].x = position.x + range; bbox3D[5].y = position.y + range; bbox3D[5].z = position.z + range;bbox3D[6].x = position.x - range; bbox3D[6].y = position.y - range; bbox3D[6].z = position.z + range;bbox3D[7].x = position.x + range; bbox3D[7].y = position.y - range; bbox3D[7].z = position.z + range;
    //project coordinatesD3DXMATRIX viewProjMat = m_View * m_Projection;D3DXVECTOR2 projBox[8];for (int i = 0; i < 8; ++i){D3DXVECTOR4 projPoint;D3DXVec3Transform(&projPoint, &bbox3D[i], &viewProjMat);projBox[i].x = projPoint.x / projPoint.w; projBox[i].y = projPoint.y / projPoint.w;//clip to extentsif (projBox[i].x < -1.0f)projBox[i].x = -1.0f;else if (projBox[i].x > 1.0f)projBox[i].x = 1.0f;if (projBox[i].y < -1.0f)projBox[i].y = -1.0f;else if (projBox[i].y > 1.0f)projBox[i].y = 1.0f;//go to pixel coordinatesprojBox[i].x = ((projBox[i].x + 1.0f) / 2.0f) * iScreenWidth;projBox[i].y = ((-projBox[i].y + 1.0f) / 2.0f) * iScreenHeight;}//compute 2D bounding box of projected coordinatesunsigned int minX = 0xFFFFFFFF;unsigned int maxX = 0x00000000;unsigned int minY = 0xFFFFFFFF;unsigned int maxY = 0x00000000;for (int i = 0; i < 8; ++i){unsigned int x = static_cast(projBox[i].x);unsigned int y = static_cast(projBox[i].y);if (x < minX)minX = x;if (x > maxX)maxX = x;if (y < minY)minY = y;if (y > maxY)maxY = y;}RECT bbox2D;bbox2D.top = minY;bbox2D.bottom = maxY;bbox2D.left = minX;bbox2D.right = maxX;return bbox2D;}
    En Direct3D se activará el scissor test de la siguiente manera:
    ...pd3dDevice->SetRenderState(D3DRS_SCISSORTESTENABLE, TRUE);...RECT rect = DetermineClipRect(light.Position, light.Range);pd3dDevice->SetScissorRect(&rect);
    La siguiente imagen ilustra el rectángulo resultante proyectado en pantalla:
    En el caso de spot lights habrá que proceder de forma distinta ya que el volumen de un cono difícilmente se pude aproximar mediante una esfera. En este caso se utilizarán bounding boxes alineados hacia la dirección de la luz. De la misma forma que en el caso anterior, el bounding box será proyectado en pantalla con el fin de obtener el rectángulo deseado.
    El algoritmo para calcular el rectángulo final será similar al siguiente:
    Crear un bounding box que encierre el cono de la forma más ajustada posible.
    Proyectar las esquinas del bounding box al plano de pantalla.
    Determinar a partir de la proyección las esquinas absolutas, es decir, la esquina superior izquierda, la esquina superior derecha, etc.
    Si una de las esquinas se encuentra detrás del plano significará que la cámara se encuentra dentro del volumen, por lo que la luz se aplicará a toda la pantalla.
    Utilizar las esquinas para formar el rectángulo y posteriormente pasárselo a Direct3D como en el caso anterior.
    Z-Cull
    La aproximación más sencilla y en la que se obtienen buenos resultados a favor de un aceptable rendimiento consiste en proyectar el volumen de luz y utilizar el Z-Buffer para determinar el conjunto de pixeles final en los que se va a realizar el cómputo de iluminación.
    El algoritmo es el siguiente:
    Si la cámara se encuentra dentro del volumen de luz, habrá que proyectar las caras traseras (backfaces) del volumen de luz. En ese caso se utilizará ZFunc= Greater para indicar en qué pixeles se realizará el cálculo de iluminación.
    VIEW FRUSTUMGREATER
    Si la cámara corta el plano más alejado del view frustum, habrá que proyectar las caras delanteras (frontfaces) del volumen de luz. En ese caso se utilizará ZFunc= Less para indicar en qué pixeles se realizará el cálculo de iluminación.
    VIEW FRUSTUMLESS
    En los demás casos se puede utilizar el procedimiento del primer punto, proyectando las caras traseras (backfaces) del volumen de luz y utilizando ZFunc= GreaterEqual para indicar en qué pixeles se realizará el cálculo de iluminación.
    VIEW FRUSTUMGREATER
    La siguiente imagen muestra la totalidad de pixeles que se procesarían en el pixel shader al no efectuar Z-Culling. Nótese que, aunque se eliminan del cálculo los pixeles que no se encuentran dentro de la proyección del volumen de luz, todavía se están calculando pixeles correspondientes a fragmentos que no se encuentran físicamente dentro del volumen.
    Cuando se activa el Z-Culling, todos los pixeles que fallen el depth test establecido anteriormente no entrarán en el cómputo del pixel shader, por lo que el rendimiento se verá favorecido. En la siguiente imagen se puede comprobar la amplia región de pixeles dentro de la proyección del volumen de luz que se ha excluido del cálculo de iluminación.
    Pero el Z-Cull realizado no excluye todos los pixeles dentro del volumen de luz. Su principal inconveniente es que todos los pixeles cuyo índice Z es menor al de los backfaces de la esfera son procesados por el pixel shader.
    En la imagen anterior coincide que no se encuentra ningún elemento delante de la esfera por lo que el Z-Cull trabajará de la forma deseada todos esos casos, pero en la siguiente imagen se muestra como, aunque la columna se encuentra fuera del volumen de luz, los pixeles que se encuentran dentro de la proyección del volumen serán igualmente calculados.
    Ilustración 13 - Inconveniente Z-Cull
    Stencil Cull
    El siguiente método para descartar cálculos de iluminación innecesarios en pixeles que no están siendo afectados por ninguna fuente de luz se denomina Stencil Culling. A diferencia del anterior, este sí que excluye todos los pixeles fuera del volumen de luz.
    Se trata de un algoritmo que utiliza dos pasadas por cada fuente de luz:
    La primera pasada no afecta mucho al rendimiento global ya que se desactiva la escritura de color en el buffer. Se tendrán que proporcionar los siguientes estados de renderizado:
    Depth-Func = LESS
    Stencil Func = ALWAYS
    Stencil Pass = REPLACE (con el valor X)
    Todos los demás operandos (Stencil Fail, Stencil Pass) a KEEP
    CullMode = (Front Faces)
    En la imagen se pueden ver las caras (back-faces en esta pasada) del volumen de luz que pasan el depth test.
    El paralelepípedo P posee un z-index menor por lo que en la zona que corta a la proyección del volumen (marcada con las líneas rojas) no se pasa el depth test.
    Todos los pixeles englobados entre las líneas azul-verdes serían calculados, justo como ocurre en el método anterior Z-Cull correspondiente a la Ilustración 13.
    VIEW FRUSTUMP
    La segunda pasada se realiza con el shader de iluminación. Se tendrán que proporcionar los siguientes estados de renderizado:
    Depth-Func = GREATEREQUAL
    Stencil Func = EQUAL
    Stencil Ref = X
    Todos los demás operandos (Stencil Fail, Stencil Pass) a KEEP
    CullMode = (Back Faces)
    De esta forma, solo a las caras que en la primera iteración pasaron el stencil test (marcadas con un 1 en el mismo) se filtrarán a partir del segundo test.
    La siguiente imagen muestra, una vez pasado el segundo depth test y filtrar por el stencil buffer, la región final en la que se realizará el cálculo de iluminación (marcada por líneas verdes y rojas):
    VIEW FRUSTUMP
    Como se puede observar en la siguiente captura de pantalla, la columna no se procesará por el pixel shader ya que no se encuentra dentro del volumen de luz.
    Como inconvenientes, se pueden destacar dos de baja importancia:
    El cambio continuo de RenderStates por fuente de luz, por lo que no sería posible procesar todas en una sola pasada.
    La existencia de dos pasadas por fuente de luz, aunque en la primera se desactive la escritura de color, baja el rendimiento. Sin embargo, en la mayoría de los casos es con la que mejor rendimiento se obtiene.