Ponto V!

Home XNA Shaders Deferred Shading Parte 1
Thiago Dias Pastor
Deferred Shading Parte 1Imprimir
Escrito por Thiago Dias Pastor

Olá Pessoal !!! Este post marca o inicio de uma serie sobre a técnica de renderizacao Deferred Shading. Em relação aos artigos anteriores, esta serie tende a ser um pouco mais complexa, é pré requisito o pessoal dar uma lida nos artigos do Vinicius Godoy sobre Matematica e na série do Ploobs sobre Computacao Grafica. (contem vários artigos que foram publicados aqui no Ponto V) No inicio tudo parecerá bem nebuloso, porém com o tempo os conceitos iram ficar mais claros.

Planejo inicialmente apresentar a teoria por de trás desta técnica, discutir os prós/contras de utilizá-la e finalmente apresentar uma implementação da mesma em XNA 4.0 (uma versão simplificada do render da PloobsEngine).

Alguns conceitos que apresentarei não estarão formalmente corretos, sempre que possível priorizarei o entendimento e a intuição.

Deferred Shading é uma técnica de renderização que recentemente vem ganhando muito espaço entre os desenvolvedores de jogos (Starcraft 2 e Crysis 2 são ótimos exemplo, veja uma lista um pouco mais completa aqui). Apesar de ter sido proposta inicialmente por Deering et al. [1988] há mais de vinte anos (período em que os aceleradores gráficos estavam dando os seus primeiros passos), somente com o hardware gráfico atual é possível utilizá-la em tempo real rodando em computadores pessoais.

Em linhas gerais, a técnica deferred shading consiste em efetuar num primeiro passo a extração de alguns dados dos modelos de uma cena (como normais, cor difusa...) e salvá-los em buffers, como ilustrado na figura abaixo. A seguir efetua-se o processo de iluminação sobre estes buffers (como uma fase de PostProcessing) e finalmente combina-se as informações recém calculadas com as do primeiro passo para obter a imagem final. (relaxa =P, tudo será explicado a seguir)

A figura abaixo mostra esquematicamente as fases quer compõem uma arquitetura deferred render clássica baseada no modelo de iluminação de Phong.

A esquerda estao os buffers extraidos das geometrias, no meio a fase de iluminacao e a direita esta a imagem final

Figure 1: A esquerda estao os buffers extraidos das geometrias, no meio a fase de iluminacao e a direita esta a imagem final

Nomenclatura: Deferred Rendering ou Deferred Shading ou Deferred Lighting?

O termo deferred rendering é usado para descrever um grande número de técnicas. Todas elas têm em comum o fato de postergar o processamento de alguma coisa, porém diferem no que é postergado. Este artigo se referirá somente ao adiamento do cálculo de iluminação. Os termos Deferred lighting e Deferred shading serão usados indiscriminadamente e se referirão a este tipo de postergamento.

Teoria

Esta seção descreverá o funcionamento do deferred shading. Inicialmente serão abordados alguns conceitos iniciais e em seguida cada etapa da técnica será explicada com detalhes.

Texturas como arrays

Texturas podem ser usadas como arrays de uma, duas ou até três dimensões. Este procedimento será utilizado diversas vezes em deferred shading.

Texturas não são arrays convencionais, as principais diferenças são:

  • Uma textura não passa de uma região de memória (buffer) da placa de vídeos que é interpretada de acordo com seu tipo. Os principais tipos usados são:
    • RGBA32 (8 bits para cada canal) cujos valores de cada canal podem variar entre 0 e 255(ou seja, uma precisão de )
    • o SINGLE (um canal apenas de 32 bits) quer armazena um único valor com uma precisão grande
    • o HALFVECTOR4 (16 bits para cada canal) que armazena os valores de cada cana em ponto flutuante.
  • Os valores de uma textura são acessados através de coordenas de textura UV (que variam entre 0 e 1). Ao acessar um texel de uma textura, o hardware pode realizar uma operação de filtragem (como Linear, Anisotropic, Point ...) e isto pode em alguns casos recuperar um valor não desejado. Tecnicamente, filtragem é uma maneira de diminuir o inevitável aliasing que surge ao realizar amostragem de uma representação discreta.
  • Em alguns momentos deseja-se colocar valores negativos (intervalo -1 até 1) em texturas do tipo RGBA32, para isso deve-se executar uma operação de packing para converter os valores do espaço [-1,1] para [0,1]. Para ler os valores desta textura devemos realizar o unpacking.
  • Texturas são ótimas em lidar com situações em que são feitos acessos fora de seus limites (UV > 1), pode-se configurá-la de alguns modos quando isto ocorrer:
  • Modo Clamp, a textura irá repetir o último texel mais próximo a borda. (Ex: UV 1.5,1 é idêntico ao UV (1,1) )
  • Mode Wrap, que ira realizar uma operação de módulo sobre o UV da textura de graça (Ex: acessar uma textura passando UV como 1.2 e 1.4 ira acessar na verdade o texel 0.2 0.4).

Render Target

De uma maneira simples, render target é o local aonde renderizamos nossos modelos. Este local pode ser a região da memória de vídeo cujo conteúdo será enviado para a placa de vídeo (BackBuffer), pode ser uma textura que depois será associada a um modelo , pode ser um CubeMap ....

As APIs gráficas permitem que o programador escolha em qual render target renderizar os modelos em um determinado passo.

Multiple Render Target

As placas de video mais recentes permitem a renderizacao de um modelo em diversos render targets simultamente. Podemos criar um pixel shader que salva informações em mais de um render target em uma mesma execução. Esta técnica geralmente é abreviada como MRT.

Quad Rendering

Em linhas gerais, consiste em uma maneira de usar a GPU como um processador de imagem. Temos como entrada uma textura, um pixel shader que rodará uma vez para cada texel dela, e como saída a textura de entrada processada.

Para mais detalhes, veja este tutorial (é interessante entender como que este procedimento de fato funciona, ele é muiiito usado em computação gráfica).

Geometric Buffer (GBuffer)

G-Buffer é um conceito bastante utilizado em Deferred Shading, que consiste em bufferes (diversas texturas) que guardam informações (como posições, normais e outras coisas mais) para cada pixel de uma cena renderizada.

Sua geração é feita por um shader especializado, que recebe como input as geometrias dos objetos (vértices, texturas, mapeamentos uvs ...), e ao invés de renderizar uma imagem final (como normalmente fazemos), renderiza diversas imagens contendo cada uma as informações citadas anteriormente. Usando MRT, é possível em único passo (enviando uma única vez as geometrias para a GPU) gerar o GBuffer.

O GBuffer (contração usual para Geometric Buffer) guarda informações que serão utilizadas para o cálculo da iluminação. Mas que informações são estas? Esta resposta depende do modelo de iluminação utilizado, porém alguns parâmetros são comuns a todos os modelos e sempre terão que ser incluídos como os mostrados a seguir: (leia o artigo sobre Phong shading que está na lista citada anteriormente)

  • Albedo, contem a cor difusa de cada pixel renderizado.
  • Normais, representa as normais de cada um dos pixels renderizado.
  • Posição, contem a posição 3D (diretamente ou indiretamente) que originou cada pixel na tela.

Albedo

Conforme visto na figura, representa apenas cor difusa de cada pixel visto na tela, normalmente é armazenado em um buffer RGBA. Este componente não necessita de muita precisão, 8 bits para cada canal é mais do que suficiente.

Normal

Utilizado por praticamente todos os algoritmos de iluminação. O valor guardado neste buffer pode estar no espaço de câmera (eye Space) ou no espaço de mundo (podemos armazenar os seu valor antes ou depois de multiplicar pela matriz WorldView). Isto vai depender do algoritmo de iluminação utilizado. Os valores das normais (normalizados =P) podem variar no intervalo de [-1,1], desta forma, eles precisão sofrer uma mudança de extremos antes de serem armazenados na textura. Pode-se também usar uma compressão e armazenar a normal em apenas dois canais ao invés de três. Uma vez que as normais estão normalizadas, pode-se armazenar os valores de x e y e calcular o valor de z em tempo de execução. Este componente também não precisa de muita precisão, 8 bits para cada canal já é suficiente.

Posição

Este componente exige uma grande precisão. Existem varias opções de armazenamento, pode-se guardar as componentes x, y, z no espaço mundo, no de câmera ou no de projeção diretamente em uma textura. O inconveniente desta abordagem é que cada coordenada poderá conter apenas valores diferentes (supondo armazenamento em uma textura formato RGBA32), o que é muito pouco e a imprecisão será grande em se tratando de posição.

Uma escolha que se mostra bastante adequada quanto à precisão (apesar de requerer um processamento maior) é armazenar apenas o elemento Z/W da posição em espaço de projeção em uma textura do tipo SINGLE, que contem apenas um canal de 32 bits. Lembre-se que a posição em 3D é representada em coordenadas homogêneas (x,y,z,w)

A recuperação da posição 3D em espaço de mundo a partir deste valor (Z/W) é bastante simples. A seguir temos um código em HLSL que recupera a posicao 3D a partir do valor Z/W armazenado em uma texture

//ler depth (z/w)
float depthVal = tex2D(depthSampler,input.TexCoord).r;

//recuperar posicao (textura armazena valores entre 0 e 1, o z/w pode variar entre -1 e 1, entao realizamos uma conversao de dominio)
float4 position;

position.x = input.TexCoord.x * 2.0f - 1.0f;
position.y = -(input.TexCoord.y * 2.0f - 1.0f);
position.z = depthVal;
position.w = 1.0f;

//Converter para o espaco mundo
position = mul(position, InvertViewProjection);
position /= position.w;

Esta estratégia consegue uma precisão razoavelmente alta (apesar de sofrer com problemas clássicos como baixa precisão para objetos longe da câmera -> z-fight). O custo associado à recuperação da posição 3D a partir do z/w é aceitável. O Depth Buffer das placas de vídeo armazena a profundidade de cada pixel desta maneira.

Outro método um pouco menos intuitivo, porem mais rápido e com maior precisão (utilizados, por exemplo, na CryEngine) é armazenar o Z da posição em espaço de visão. Esta abordagem tem prós e contras, verificar Reconstructing Position From Depth para uma discussão mais profunda.

Outros Parâmetros do GBuffer

Estes outros parâmetros dependem do algoritmo de iluminação e podem variar desde fatores como Roughness/Specular para o caso de modelos de iluminação como o Cook/Torrence ou parâmetros como Specular Intensity e Specular Power para o clássico Blin-Phong.

Suporte de Hardware

É desejável que a placa de vídeo alvo tenha suporte a Multiple Render Targets (MRT), esta funcionalidade pode ser usada para reduzir o número de passos ao criar o G-Buffer. (sem ela esta técnica não pode ser usada em tempo real)

As ATI 9500 e superiores suportam até 4 RenderTargets simultâneos e cada um podem estar associados a uma superfície de até 16 bits por canal como a A16B16G16R16 o que possibilita a geração do G-Buffer em apenas um Passo. Placas de vídeo sem suporte a MRT são obrigadas a gerar o GBuffer em vários passos o que compromete seriamente o desempenho.

Funcionamento do Deferred Shading

Existem quatro fases distintas em deferred shading

  • Fase Geométrica (Geração do GBuffer)
  • Fase de Iluminação (Geração do LightMap a partir do GBUffer)
  • Fase de geração da imagem final (Combinar GBuffer com LightMap)
  • Fase de Post-Processing (não pertence de fato ao deferred rendering)

Cada fase usa um shader (com vertex e pixel shader), com propósito e entradas diferentes. Cada saída de uma fase torna-se entrada da próxima. Toda fase também tem acesso à saída da primeira (GBuffer).

As discussões feitas a seguir tem como base o DirectX 9c e o XNA 4.0 usando HLSL.

Fase de Geometria (criação do GBuffer)

O passo de Geometria é o único que de fato usa as informações dos modelos. Sua entrada (vértices, índices e texturas ...) é extremamente variada (alguns objetos podem usar Normal mapping, outros Specular mapping ...).

Normalmente existem diversos shaders que contribuem para a construção do GBuffer, cada um processa um objeto diferente e guarda as informações necessárias nos buffers do GBuffer

Um exemplo em pseudocódigo de um shader bastante simples para a geração do G-Buffer poderia ser (usando Multiple Render Target, repare que o pixel shader devolve varias informações):

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul( float4(input.Position,1), World);

    output.Position = mul(worldPosition, ViewProjection);
    output.TexCoord = input.TexCoord;
    output.Normal =mul(input.Normal,World);

    output.Depth.x = output.Position.z;
    output.Depth.y = output.Position.w;

    return output;
}

PixelShaderOutput PixelShaderFunction(VertexShaderOutput input)
{
    PixelShaderOutput output;

    output.Color = tex2D(diffuseSampler, input.TexCoord); //output Color
    output.Color.a = specularIntensity; //output SpecularIntensity

    output.Normal.rgb = 0.5f * (normalize(input.Normal) + 1.0f); //transform normal domain
    output.Normal.a = specularPower; //output SpecularPower

    output.Depth = input.Depth.x / input.Depth.y;

    return output;
}

O Vertex Shader recebe as informações dos vértices (como Posição, Normais em coordenadas de modelo, e coordenadas de textura) de um objeto e os transforma utilizando as matrizes de mundo, visão e projeção.

Nesta fase calcula-se a coordena Z da posição em coordenadas de projeção e envia-se o valor para o Pixel Shader.

O Pixel Shader apenas empacota os valores recebidos e mando-os para a saída. A estrutura de retorno do Pixel Shader pode ser definida da seguinte maneira:

struct PixelShaderOutput
{
    float4 Color : COLOR0;
    float4 Normal : COLOR1;
    float4 Depth : COLOR2;
};

Cada um dos ColorX está associado com um RenderTarget. Cada objeto da cena rodará um shader (que depende do material deste objeto) com uma estrutura bastante similar a mostrada anteriormente.

Os valores que saem do Pixel Shader irão passar pelo Depth Test antes de ir definitivamente para o GBuffer. (apenas os “pixels” mais próximos são de fato salvos)

Após esta fase, torna-se irrelevante saber quais técnicas geraram cada texel de cada uma das texturas do Gbuffer, objetos como partículas, billboards e modelos serão tratadas da mesma maneira em todas as fases subseqüentes.

Fase de Iluminação

O poder real do Deferred shading é a iluminação. A completa separação entre processamento da geometria e calculo da iluminação permite que cada uma destas fases seja tratada de maneira completamente diferente em relação aos renders tradicionais (Forward). Isto permite uma alta customização a respeito dos formatos das luzes e de como elas afetam as superfícies.

Os shaders desta fase têm acesso a todos os parâmetros do G-Buffer, toda a informação que o modelo de iluminação utilizar e que dependa do objeto processado (como taxa de refração) deve estar nestes buffers. Isto limita bastante a reutilização de shaders para modelos de iluminação diferentes. Por exemplo, o modelo de Phong utiliza como parâmetros extras apenas os fatores de Specular Intensity e Specular Power (dois parâmetros com tamanho de float), porém outros modelos como Cook/Torrance utilizam mais parâmetros por pixel. A conseqüência é que não podemos utilizar modelos de iluminação diferentes em uma mesma cena e a troca de uma modelo de iluminação (normalmente) acarreta em uma troca de todos os shaders geradores do GBuffer, uma vez que ele deverá ser populado com informações diferentes.

O processo de cálculo da etapa de iluminação consiste em gerar um LightMap (textura que “mostra” o quanto cada luz afeta cada objeto da cena). Independentemente de qual modelo de iluminação for utilizado, alguns procedimentos básicos terão de ser seguidos. REPARE que este passo NADA MAIS É do que um “exemplo” de processamento de imagem. (entra GBuffer + Shader Constantes e sai um LightMap)

  • Para cada Luz envia-se um Quad do tamanho EXATO da tela(quadrado com vértices [-1, -1] e [1,1] em espaço de projeção) com mapeamento de coordenadas de textura
  • O Vertex Shader simplesmente redireciona os valores para o Pixel Shader sem aplicar transformação alguma, uma vez que as posições já estão em coordenadas de tela.
  • O Rasterizador chama o pixel shader EXATAMENTE uma única vez para cada pixel gerado. (Veja tutorial sobre quad rendering)
  • No Pixel Shader que ocorre o cálculo real da iluminação, os principais passos são:
    • Recuperação dos valores do GBuffer (normal e parâmetros adicionais específicos do modelo de Iluminação)
      //get normal data from the normalMap
      float4 normalData = tex2D(normalSampler,input.TexCoord);
      
      //tranform normal back into [-1,1] range
      float3 normal = 2.0f * normalData.xyz - 1.0f;
      
      //get specular power, and get it into [0,255] range]
      float specularPower = normalData.a * 255;
      
      //get specular intensity from the colorMap
      float specularIntensity = tex2D(colorSampler, input.TexCoord).a;
    • Recuperação do valor de posição do objeto 3D que originou este pixel usando o GBuffer, conforme explicado anteriormente.
    • Cálculo da equação de Iluminação. Cada tipo de luz (Direcional, Spot e Point) terá parâmetros diferentes, e são processadas por shaders diferentes. Os parâmetros específicos de cada luz como posição e direção são enviados como Shader Constant para a GPU e são facilmente acessados. Veja este tutorial sobre phong shading (estou reforçando porque é importante mesmo =P)
    • Salvar as informações calculadas em uma textura que será chamada de LightMap.
  • Acumular (somar) o valor deste LightMap com o valor dos outros calculados para cada uma das luzes. (Utiliza-se normalmente Alpha Mapping aditivo para isso)
  • Repetir este processo (desde o passo 1) para cada uma das luzes.

Ao final do processamento de todas as luzes, ter-se-á uma textura com a contribuição de iluminação de toda a cena para o ponto de vista atual da câmera.

Existem diversas maneiras de otimizar este processo, como exemplo pode-se citar:

  • Ao Invés de enviar um Quad para a GPU, mandar uma forma geométrica (como esfera para Point Light) para a GPU, a fim de evitar o processamento de todos os pixels da tela a cada luz. Como exemplo, pode-se enviar uma esfera em espaço de modelo e aplicar transformações e levá-la ao espaço de projeção no Vertex Shader, desta forma, o Pixel Shader irá processar apenas os pixels que de fato contribuem para a iluminação desta luz, os outros que teriam contribuição zero nem serão processados.
  • Na CPU, verificar se determinada Luz de fato contribui para a cena, e caso não contribua efetuar um Culling. (Pode-se checar se alguma parte de um cone (que representa uma Spot Light) esta dentro do frustrum da câmera).
  • Em DirectX 10 pode-se utilizar o Geometric Shader para expandir as esferas de Point Lights com um custo bem menor, além de permitir um controle de qualidade adaptativo (o quanto expandir pode depender de variáveis como distância da câmera).
  • Alguns valores utilizados pelo modelo de iluminação podem estar pré-calculados e armazenados em texturas.

Fase de geração da imagem final

Em posse do GBuffer e do LightMap, pode-se calcular a imagem final. Os passos envolvidos são:

  • Envia-se um quad para a GPU
  • Recuperar os valores do GBuffer e do LightMap para cada texel e combiná-los. A seguir tem-se um exemplo deste processo para o modelo de Phong.
    float procces = tex2D(extraSampler,input.TexCoord).a;
    
    float3 diffuseColor = tex2D(colorSampler,input.TexCoord).rgba;
    
    float4 light = tex2D(lightSampler,input.TexCoord);
    
    float3 diffuseLight = light.rgb;
    
    float specularLight = light.a;
    
    return float4((diffuseColor * (diffuseLight + ambientColor)+ specularLight),0);
  • Salvar o valor da cor final em uma textura final.

A menos de Post Processing, a textura gerada pode ser enviada para tela. A imagem abaixo mostra em um frame, algumas imagens que descrevem a geração da imagem final. A esquerda acima temos a imagem final, a direita temos o albedo (componente do GBuffer), a esquerda abaixo temos o mapa de normais (componente do GBuffer) e a direita temos o LightMap.

image

Fase de Post Processing (Processamento de Imagem)

Consiste em aplicar efeitos sobre a imagem 2D final gerada no passo anterior. O shader que realizará o efeito pode acessar o GBuffer para ter acesso às informações de Normal, Posição 3D (da mesma maneira que na fase de iluminação) e de cor difusa. Esta fase não é exclusiva do Deferred Shading, a vantagem é que neste tipo de arquitetura os shaders poderão acessar parâmetros que não são possíveis em Forward. Desta maneira, alguns post Processing (como o caso do SSAO - Screen Space Ambient Occlusion) são muito mais facilmente implementados em deferred do que em forward.

Pode-se aplicar diversos post effects sobre a imagem final gerada no passo anterior, conforme mostra a figura abaixo:

image

Ao fim deste passo teremos a imagem final que irá para o monitor.

Pessoal, chega por hoje !!!! Os conceitos apresentados são razoavelmente pesados. É completamente normal ler o artigo e não entender nada. Qualquer duvida postem nos comentários abaixo.

No próximo artigo irei discutir algumas questões relacionadas as limitações e vantagem do deferred em relação as técnicas tradicionais.


Comentários (0)
Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter