Ponto V!

Home XNA Shaders Renderizando FullScreen Quads em XNA 4.0 (Princípios de processamento de Imagem na GPU)
Thiago Dias Pastor
Renderizando FullScreen Quads em XNA 4.0 (Princípios de processamento de Imagem na GPU)Imprimir
Escrito por Thiago Dias Pastor

Este artigo irá explorar um pouco dos conceitos envolvidos na criação de um processador de imagens na GPU. Apenas para se ter uma idéia, diversos efeitos visuais como Bloom, Blur, Fog, Night Vision e Chuva (às vezes =P) utilizam esta idéia. Sistemas de partículas e Deferred shading são outros bons exemplos que utilizam indiretamente os conceitos básicos abordados nesta técnica.

Antes de qualquer coisa, recomendo fortemente que o pessoal tenha pelo menos lido o primeiro artigo da serie (Introdução a Shaders). A matemática por de trás desta simples técnica é bastante simples e importante para aqueles que querem entender melhor como que funciona o pipeline gráfico da GPU ( especialmente a parte de transformações).

De uma maneira simplíssima, Processamento de imagem é qualquer forma de processamento de dados no qual a entrada e saída são imagens.

Pensando em termos da Pipeline Gráfica, uma maneira simples de implementar esta idéia é:

  • Enviar para a GPU um “FullScreen Quad” (retângulo que ocupa a “tela” inteira, formado por dois triângulos devidamente posicionados)
  • Enviar a imagem (textura) que será “processada” para a GPU
  • Projetar um Pixel Shader que irá transformar cada texel da textura acima.

O motivo do procedimento acima funcionar é o fato de construirmos um Quad perfeitamente alinhado com “as bordas” da nossa tela (em coordenadas de projeção), que quando rasterizado chama o pixel shader exatamente uma única vez para cada pixel da “tela”.

Antes de qualquer coisa, temos que esclarecer o significado da expressão: “da tela”. Em DirectX/Xna, temos um conceito chamado Render Target, que representa uma superfície na qual podemos renderizar algo (aonde que será salvo o resultado da aplicação do shader sobre uma geometria). Os tipos de render targets mais comuns são Texturas 2D (imagens comuns) e a nossa própria tela do monitor (na verdade um buffer interno que será enviado para o monitor, que normalmente é chamado de framebuffer) =P

Existem diversas maneiras de desenhar FullScreen Quads. Iremos usar uma que é simples de entender alem de ser bastante flexível (podemos facilmente alterá-la para desenhar quads de outros tamanhos).

A idéia é criar os vértices do quad na CPU já no espaço de Projeção e mandá-los para um by-pass Vertex Shader. (apenas redireciona os valores recebidos para o próximo estagio sem efetuar nenhuma alteração)

No espaço de projeção (após aplicarmos as matrizes de World, View e Projection) as “bordas da tela” (limites do retângulo que representa o render target) (usando a convenção DirectX/Xna para matrizes de projeção) para cada coordenada é: (este espaço às vezes é chamado de espaço homogêneo de projeção)

  • Em X os valores podem variar entre -1 e 1
  • Em Y os valores podem variar entre -1 e 1
  • Em Z os valores podem variar entre 0 e 1

Primitivas que tiverem valores X,Y,Z, após serem convertidas para o espaço de projeção, fora deste intervalo serão “removidas” (nem serão rasterizadas). Para quem quiser compreender um pouco mais a fundo estas transformações, sugiro conferirem estes posts (aqui e aqui)

Como exemplo, um ponto com coordenadas de projeção (-1, -1,0) estará em baixo à esquerda da sua tela (O valor de Z ser 0 diz que o ponto estava (em coordenadas World) muito próximo da câmera). Um ponto com coordenadas (0, 0,0) aparecera exatamente no meio da tela.

A imagem a seguir mostra o domínio do espaço das coordenadas de projeção (imagine a tela sendo o quadrado frontal do cubo). Lembre-se que em um momento posterior, o rasterizador irá converter este espaço para o espaço de tela real (dependente de resolução). Como curiosidade, o valor de Z visto acima é usado pela GPU para realizar o Depth Testing

DirectX Projection Coordinates space

Voltando a implementação =P:

Nós iremos criar dois triângulos (para completar o quad) cujos vértices já estarão no espaço de projeção, em seguida passaremos eles para o vertex shader que apenas os enviara para o rasterizador, o qual chamara o pixel shader uma vez para cada pixel gerado conforme explicado.

Cada vértice de cada triângulo conterá sua posição no espaço de projeção e uma coordenada de textura (de maneira alinhada, de forma a encaixar perfeitamente no quad gerado).

Como exemplo, o ponto cima- esquerda (tenha sua tela como referencia) terá a coordenada de textura (0,0), e o baixo-direita será (1,1).

O código a seguir mostra como isto é feito em XNA 4.0:

internal sealed class QuadRender    
{        
    private VertexPositionTexture[] verts;        
    private GraphicsDevice myDevice;        
    private short[] ib = null;         

    ///        
    /// Loads the quad.        
    ///        
    public QuadRender(GraphicsDevice device)        
    {             
        myDevice = device;                      

        ///criando os vertices dos “triangulos”            
        verts = new VertexPositionTexture[]                        
        {                            
            new VertexPositionTexture(                                
                new Vector3(0,0,0),                                
                new Vector2(1,1)),                            
            new VertexPositionTexture(
                new Vector3(0,0,0),
                new Vector2(0,1)),
            new VertexPositionTexture(
                new Vector3(0,0,0),
                new Vector2(0,0)),
            new VertexPositionTexture(
                new Vector3(0,0,0),
                new Vector2(1,0))                        
        };                  

        ///criando os indices dos triangulos             
        ib = new short[] { 0, 1, 2, 2, 3, 0 };         
    }                      

    ///        
    /// Draws the fullscreen quad.        
    ///        
    public void RenderFullScreenQuad(Effect effect)        
    {            
        effect.CurrentTechnique.Passes[0].Apply();            
        RenderQuad(Vector2.One * -1, Vector2.One);        
    }         

    public void RenderQuad(Vector2 v1, Vector2 v2, Effect effect)        
    {                      
        effect.CurrentTechnique.Passes[0].Apply();            
        verts[0].Position.X = v2.X;            
        verts[0].Position.Y = v1.Y;             
        verts[1].Position.X = v1.X;            
        verts[1].Position.Y = v1.Y;             
        verts[2].Position.X = v1.X;            
        verts[2].Position.Y = v2.Y;             
        verts[3].Position.X = v2.X;            
        verts[3].Position.Y = v2.Y;            

        ///desenhando na tela            
        myDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, verts, 0, 4, ib, 0, 2);        
    }    
}

Se você não quiser desenhar um fullscreen quad, basta mudar os valores V1 e V2 passados como parâmetro para a função RenderQuad.

O vertex Shader (by-pass) para processar este FullScreen quad é:

texture colorMap;

sampler colorSampler = sampler_state
{    
    Texture = (colorMap);    
    AddressU = CLAMP;    
    AddressV = CLAMP;    
    MagFilter = LINEAR;    
    MinFilter = LINEAR;    
    Mipfilter = LINEAR;
}; 

struct VertexShaderInput
{    
    float3 Position : POSITION0;    
    float2 TexCoord : TEXCOORD0;
}; 

struct VertexShaderOutput
{    
    float4 Position : POSITION0;    
    float2 TexCoord : TEXCOORD0_centroid;
}; 

float2 halfPixel;

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{    
    VertexShaderOutput output;    
    input.Position.x =  input.Position.x - PixelSize.x;    
    input.Position.y =  input.Position.y + PixelSize.y;    

    output.Position = float4(input.Position,1);    
    output.TexCoord = input.TexCoord ;    

    return output;
}

O XNA 4.0 funciona em cima do DirectX 9. Neste ambiente nos temos que realizar uma “estranha” correção nas posições passadas ao nosso VertexShader (adicionar/remover um PixelSize) quando trabalhamos com quads alinhados com a “tela”. A razão para isto é o funcionamento do algoritmo de rasterizacão usado (em DirectX > 9c ou Opengl, este procedimento não é necessário) (Para mais informações vejam este artigo).

Para calcular o valor da variável PixelSize usada acima basta usar o seguinte código (Calculado em XNA e passado para o shader como uma constante):

Vector2 PixelSize = new Vector2();

PixelSize.X = 1/ (float)GraphicsDevice.PresentationParameters.BackBufferWidth;
PixelSize.Y = 1/ (float)GraphicsDevice.PresentationParameters.BackBufferHeight;

Um Pixel shader de um simples processador de imagem está mostrado a seguir. Ele simplesmente multiplica o valor de cada texel da textura passada por 0.7 (escurece um pouco a imagem)

float4 PixelShaderFunctionNormal(VertexShaderOutput input) : COLOR0
{        
    float4 process = tex2D(colorSampler,input.TexCoord);        

    return process * 0.7f;
}

Por hoje é somente isso !!! Precisava deste post para esclarecer umas coisas que usarei no meu próximo artigo sobre Deferred Shading.

Provavelmente no futuro farei alguns artigos sobre Post Effects, por enquanto dêem uma olhada nestes exemplos que eu postei no blog do ploobs.

Ate mais !!!


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