Ponto V!

Home XNA Shaders Iluminação Básica – Point e Spot Light
Thiago Dias Pastor
Iluminação Básica – Point e Spot LightImprimir
Escrito por Thiago Dias Pastor

Olá pessoal! Dando continuidade ao tutorial anterior, hoje iremos implementar os dois últimos tipos de luzes básicas que faltam: Point e Spot Lights.

Seguirei a mesma linha de antes, uma introdução teórica inicial seguida de uma implementação usando shaders e Xna/PloobsEngine. O único pré requisito para acompanhar este tutorial é ter lido o anterior.

Teoria Point Lights

Uma Point Light é uma luz representada por um ponto que emite radiação em todas as direções, conforme a figura a seguir.

Figura 1: Representação Luz Pontual

Figura 1: Representação Luz Pontual

Um exemplo real deste tipo de luz é a nossa velha lâmpada de filamento:

 lâmpada de filamento

As principais diferenças (em termos de efeito) entre a Point Light a a luz direcional implementada no tutorial anterior são:

  • A luz direcional tem uma direção constante enquanto que a luz pontual tem uma direção variável (vetor entre o ponto de luz e ponto sendo iluminado – sai do centro da “lâmpada” e vai ao encontro dos objetos). Uma luz pontual muito distante pode ser considera como uma luz direcional em alguns casos (costumamos fazer essa simplificação ao modelar o sol).
  • A luz direcional afeta todos os objetos da cena com a mesma intensidade, enquanto que a pontual afeta apenas os objetos próximos a posição da luz (quanto mais afastado menor a intensidade)

Como vimos no post anterior a equação de Phong pode ser escrita da seguinte maneira:

capture

A luz pontual atua no parâmetro , a constante difusa. A fim de dar maior controle ao usuário costuma-se deixar o Kd intacto e criar um parâmetro multiplicativo a mais chamado atenuação (Att) e alterá-lo ao invés de alterar a “constante”, com esta modificação a equação fica:

capture

Algumas pessoas colocam este fator Att também multiplicando a parcela especular, eu prefiro não multiplicar =P (é difícil dizer que algo é certo ou errado ao falar de modelos de iluminação, A idéia é utilizar equação como uma base e ir alterando/inserindo parâmetros até conseguir o efeito desejado).

O parâmetro Att está multiplicando a parcela difusa da equação de phong, um valor maior que 1 fará com que a cor difusa fique mais intensa (intuitivamente a cor tenderia mais para o branco), um valor menor que 1 irá “escurecer” esta parcela.

Antes de mostrar como calculamos o Att para PointLight, vamos investigar qual deveria ser o seu comportamento para obtermos um efeito semelhante a uma lâmpada de filamento (estou fazendo isso para que vocês possam criar as suas próprias luzes customizadas no futuro).

  • Ele deve ter um valor próximo de 1 no centro da luz pontual.
  • Ele deve ter um decaimento não muito rápido (se não fica visualmente esquisito) e tenderá a 0 a partir de uma distância finita do centro da “lâmpada” (essa distância é chamada de raio da luz pontual).

Pelo comportamento esperado, vimos que precisamos de duas variáveis auxiliares, uma para descrever a posição da luz e outra para descrever o raio máximo de alcance dela. Vamos chamar estas variáveis de LightPosition e LightRadius

Uma possível função de atenuação para luz pontual é (em HLSL):

float3 LightDir = LightPosition - ObjectPositionRasterized;
float norm = saturate(length(LightDir)/(LightRadius));
float attenuation = pow(1.0f - norm , 2) ;

Observar que a Direção da luz varia a cada ponto processado conforme dito anteriormente. Seu valor é igual à diferença entre a posição da Luz e o ponto sendo processado (neste caso não normalizamos, pois estamos interessados na distância e não apenas na direção). Lembre-se, um vetor 3D pode ser definido como a diferença entre dois pontos

Observe a seguinte linha:

float norm = saturate(length(LightDir)/(LightRadius));

A instrução length(LightDir) calcula o tamanho do vetor LightDir(que corresponde a distância entre o centro da Point Light e ponto atual processado), em seguida dividimos esta distância pelo raio Point Light.

Intuitivamente interpretamos o resultado dessa linha como:

  • Próximo do centro da luz a length(LightDir) será algo muito pequeno e este algo ainda será dividido por uma constante lightRadius (logo o valor será próximo de 0).
  • Longe do centro da luz o length(LightDir) terá um valor maior, que será dividido por uma constante e diminuirá um pouco, mas com certeza será maior que o anterior (ou seja, length(LightDir) será próximo de 0 quando estamos perto da luz e Crescerá a medida que vamos distanciando dela ).

A instrução saturate (função intrínseca da HLSL) recebe um número como parâmetro e devolve 0 caso o valor passado seja menor que 0, 1 caso o numero passado seja maior que 1 e o próprio argumento caso ele esteja no intervalo entre 0 e 1. Neste caso ela é usada para garantir que a variável norm NUNCA tenha um valor maior que 1 (varie entre 0 e 1, sendo 0 no centro da luz e um quando atingimos a distância igual lightRadius). (se length(LightDir) > LightRadius logo length(LightDir)/(LightRadius) > 1, saturate garante que este valor será 1)

A linha:

float attenuation = pow(1.0f - norm , 2) ;

Calcula a atenuação de fato, a instrução pow (Power ou parâmetro 1 elevado ao parâmetro 2) corresponde a clip_image011. A interpretação intuitiva desta linha é:

  • Para pontos próximos da luz, a norm é pequena (próximo de 0), ao fazer 1 – norm e elevarmos o relsultado ao quadrado teremos um número próximo de 1
  • Para pontos longe da luz, a norm vale próximo de 1, logo 1 – norm é algo próximo de 0, que elevado ao quadrado é mais próximo de zero ainda =P.

Ou seja, os pontos próximos da luz tem atenuação próximo de 1 (como o fator é multiplicativo, quase não alteramos o valor da componente difusa) e pontos longe tem atenuação próximo de 0 (ou seja, estamos diminuindo (”escurecendo”) o efeito da parcela difusa na equação de Phong).

Essa maneira de calcular a atenuação é bastante clássica (atenuação quadrática) e muito usada, outra maneira mais rápida é não menos clássica é a linear:

attenuation = saturate(1.0f - (length(lightVector) )/(lightRadius )); 

Softwares de modelagem costumam usar uma atenuação mais rebuscada e mais lenta como a exponencial.

Teoria Spot Lights

Ao contrário das Point Lights, as Spots têm um feixe de direções de emissão de luz bem definido caracterizado por um cone.

A imagem a seguir ilustra muito bem o conceito:

Figura 2: Spot, Ponto que irradia luz em um formato cônico

Figura 2: Spot, Ponto que irradia luz em um formato cônico

Em termos de equação, a idéia é bastante semelhante a Point. Devemos manipular a variável Att da equação de Phong:

image

Novamente, tendo em mente o comportamento que queremos obter, vamos discutir o que o Att deve fazer:

  • clip_image015Temos um ponto que irradia luz apenas em um feixe de direções (cone), esse feixe pode ser descrito por um ângulo de abertura. (alpha da imagem a seguir) e por uma direção (linha vertical pontilhada) . Fora deste feixo a atenuação deve valer 0 (completamente atenuado, sem luz)
  • Dentro do feixe a atenuação deve ser forte (próximo de 0) nas beiradas e intenso no centro (um parâmetro (decaimento) controla o quanto a luz é atenuada dentro do cone).
  • Existem Modelos de Spots bem mais complexos que consideram diversas outras variáveis (como o implementado em Hardware em DirectX 8 e 9)

Pela descrição ficou claro que precisamos das variáveis auxiliares Posição da Luz, Ângulo de Abertura, Direção e Decaimento.

Para garantir que fora do feixe a atenuação seja 0, basta calcular o vetor direção entre o ponto que esta sendo processado e a posição da luz (igual ao LightDir da PointLight) e verificar o ângulo formado entre esse vetor e o vetor direção da luz (definido pelo usuário, a linha vertical pontilhada na última imagem, ele controla qual a direção do feixe).

Tentei fazer uma imagem para ilustrar o conceito (sim, ficou muito tosca =P)

clip_image017

O círculo azul é a posição da luz spot, e o raio Laranja é a sua direção (a definida pelo usuário, que controla a direção do feixe). Quando estivermos processando o ponto cima-esquerda do objeto (quadrado) calcularemos o ângulo entre os vetores laranja e amarelo, se ele for maior que o Ângulo de Abertura da luz (definido pelo usuário, controla a largura do feixe) a atenuação para este ponto vale zero. O mesmo vale para o ponto baixo-esquerda.

Em HLSL temos:

float3 LightDir = LightPosition - bjectPositionRasterized;
LightDir = normalize(LightDir);
float SdL = dot(lightDirection, -LightDir);
if(SdL > lightAngleCosine)
{

}

Calculamos o vetor direção, o normalizamos, calculamos o cosseno entre os vetores usando produto escalar e comparamos com o parâmetro lightAngleCosine, se for maior, a atenuação é diferente de zero . (Notar que o LightDir é negado, conforme discutido no tutorial anterior)

Comparamos o cosseno do ângulo (resultado do Produto Escalar entre dois vetores unitários) ao invés do ângulo em si, isto é uma “otimização”, já que calcular o ângulo exigiria mais uma operação lenta (arco-cosseno). Notar que a comparação é SdL > lightAngleCosine, se usássemos o ângulo seria SdL < lightAngle. (devido a natureza da função cosseno)

A segunda parte (calcular a atenuação dentro do cone) é exatamente igual à anterior (PointLight), usaremos desta vez a atenuação linear:

float3 LightDir = LightPosition - bjectPositionRasterized;
float attenuation = saturate(1.0f - length(LightDir) / lightRadius[i]);

Pronto !!! Temos a teoria para implementar os dois tipos de luzes.

Combinando Várias Luzes

Curto e grosso, combinar luzes equivale a somar suas contribuições. Em termos de equação fica:

capture

Calcule a contribuição de cada um e depois as some. Para uma explicação do porque isso funciona sugiro este livro Digital Image Synthesis. Falo levemente sobre isso na minha monografia

Implementação Point Light

Da mesma forma que no primeiro tutorial, usei a PloobsEngine para facilitar um pouco minha vida. Para este, implementei um aplicativo que permite que o usuário jogue luzes pontuais (até cinqüenta )em duas estátuas, estas luzes são representadas fisicamente por esferas que colidem com o cenário. (botão direito joga uma esfera de luz e o esquerdo posiciona uma Point Light na posição atual da câmera)

Toda luz na PloobsEngine extende a interface ILight (serve apenas para definir parâmetros de serialização e alguns outros detalhes internos). A Point Light é declarada assim: (omiti as partes não importantes).

public class LightPoint : ILight
{
    public LightPoint(Vector3 LightPosition, float Radius, Color LightColor,float Lightintensity)
    {
        this.lightPosition = LightPosition;
        this.lightRadius = Radius;
        this.lightColor = LightColor;
    }

    private Color lightColor;
    private Vector3 lightPosition;
    private float lightRadius;

    …

}

Esta classe contém apenas os dados específicos da luz (outros parâmetros como SpecularColor e Specular Intensity são propriedades do Material e estarão na classe que interage com o shader, mostrada mais a frente).

O shader que calcula a luz pontual (em HLSL) é mostrado a seguir:

#define MaxLights 50

///Constantes

float4x4 World;
float4x4 View;
float4x4 Projection;
int activeLights;
float3 LightPosition[MaxLights];
float4 LightColor[MaxLights];
float LightRadius[MaxLights];
float4 SpecularColor;
float SpecularPower;
float SpecularIntensity;
float4 AmbientColor;
float AmbientIntensity;
float DiffuseIntensity;
float3 camPosition;

///Textura e Samples para a textura Diffuse

texture DiffuseTexture;

sampler2D DiffuseSampler = sampler_state
{
    Texture = ;
    ADDRESSU = CLAMP;
    ADDRESSV = CLAMP;
    MAGFILTER = ANISOTROPIC;
    MINFILTER = ANISOTROPIC;
    MIPFILTER = LINEAR;
};

///Entrada do VertexShader

struct VertexShaderInput
{
    float3 Position : POSITION0;
    float3 Normal : Normal0;
    float2 TexCoord : TexCoord0;
};

///Saida do Vertex Shader

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 N : TEXCOORD0;
    float2 TextureCoord : TEXCOORD1;
    float3 V : TEXCOORD2;
    float3 WorldPosition : TEXCOORD3;
};

///VertexShader

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    ///Transforma os vertices
    float4 worldPosition = mul(float4(input.Position,1), World);
    float4 viewPosition = mul(worldPosition, View);

    output.Position = mul(viewPosition, Projection);
    output.N = mul(float4(input.Normal,0), World);

    output.TextureCoord = input.TexCoord;
    output.V = camPosition - worldPosition;
    output.WorldPosition = worldPosition;

    return output;
}

///Pixel Shader 

float4 PixelShaderAmbientFunction(VertexShaderOutput input) : COLOR0
{
    float4 finalColor = float4(0,0,0,0);
    float4 diffuseColor = tex2D(DiffuseSampler,input.TextureCoord);

    for(int i = 0 ; i < activeLights; i ++)
    {
        float3 Normal = normalize(input.N);
        float3 LightDir = LightPosition[i] - input.WorldPosition;

        float attenuation;
        float norm = saturate(length(LightDir)/(LightRadius[i]));
        attenuation = pow(1.0f - norm , 2) ;
        LightDir = normalize(LightDir);

        float3 ViewDir = normalize(input.V);
        float Diff = saturate(dot(Normal, LightDir));
        float3 Reflect = normalize(2 * Diff * Normal - LightDir);
        float Specular = pow(saturate(dot(Reflect, ViewDir)), SpecularPower); 

        finalColor += AmbientColor*AmbientIntensity + attenuation *LightColor[i] * DiffuseIntensity * diffuseColor * Diff + SpecularIntensity * SpecularColor * Specular; 
    }

    return finalColor;
}

O primeiro ponto a ser comentado é o fato de estarmos usando vetores para passar as informações da luzes para o shader. Estes vetores são estáticos e tem tamanho fixo pré alocado (definido pelo MaxLights), o exemplo suporta o máximo possível de luzes que o ShaderModel 3.0 consegue agüentar (cada Shader Model especifica diversas features disponíveis na GPU, entre elas a quantidade de registradores para armazenar variáveis Constantes, e os arrays passados são armazendos nestes registradores). O XNA 4.0 suporta Shader Model 2.0 ao 3.0, o DirectX 10 suporta até o 4.0 e o DirectX 11 suporta até o 5.0.

Um segundo ponto a ser levantado é que desta vez estamos enviando para o Rasterizador a posição em espaço Mundo (variável WorldPosition : TEXCOORD3 ) alem das outras variáveis comuns (nunca fizemos isso =P). A WorldPosition será usada para criar o vetor direção entre o ponto processado e a posição da luz.

O pixel shader apenas implementa as idéias discutidas anteriormente. Repare na somatória realizada para compor a cor final.

Conforme visto no tutorial passado, a parte XNA que interage com o shader deve implementar IShader, segue o código da implementação (meio grande):

public class PointLightShader : IShader
{

    public PointLightShader(float SpecularIntensity = 1, float SpecularPower = 50)
    {
        this.SpecularIntensity = 1;
        this.SpecularPower = 50;
        this.SpecularColor = Color.White;
        this.AmbientColor = Color.White;
        this.AmbientIntensity = 0;
        this.DiffuseIntensity = 1;
    }

    private Effect point;
    
    List LightPosition = new List();
    List LightColor = new List();
    List LightRadius = new List();

    public Color SpecularColor;
    public float SpecularPower;
    public float SpecularIntensity;
    public float AmbientIntensity;
    public Color AmbientColor;
    public float DiffuseIntensity;

    int activePointLights = 0;

    public int ActivePointLights
    {
        get { return activePointLights; }
    }

    public override void Update(GameTime gt, PloobsEngine.SceneControl.IObject ent, IList lights)
    {
        base.Update(gt, ent, lights);

        LightPosition.Clear();
        LightColor.Clear();
        LightRadius.Clear();

        activePointLights = 0;

        foreach (var item in lights)
        {
            if (item is LightPoint)
            {
                LightPoint lp = item as LightPoint;
                LightPosition.Add(lp.LightPosition);
                LightColor.Add(lp.LightColor.ToVector4());
                LightRadius.Add(lp.LightRadius);

                activePointLights++;

                if (activePointLights == 50)
                    break; ///maximo alcancado, descarta o resto
            }
        }

        point.Parameters["activeLights"].SetValue(activePointLights);
        point.Parameters["LightPosition"].SetValue(LightPosition.ToArray());
        point.Parameters["LightColor"].SetValue(LightColor.ToArray());
        point.Parameters["LightRadius"].SetValue(LightRadius.ToArray());
        point.Parameters["SpecularColor"].SetValue(SpecularColor.ToVector4());
        point.Parameters["SpecularPower"].SetValue(SpecularPower);
        point.Parameters["SpecularIntensity"].SetValue(SpecularIntensity);
        point.Parameters["AmbientColor"].SetValue(AmbientColor.ToVector4());
        point.Parameters["AmbientIntensity"].SetValue(AmbientIntensity);
        point.Parameters["DiffuseIntensity"].SetValue(DiffuseIntensity);
    }

    /// 
    /// Draws the object
    /// Chamado Todo frame
    /// 
    /// gametime
    /// object to be rendered
    /// The render.
    /// The cam.
    /// the lights
    public override void Draw(Microsoft.Xna.Framework.GameTime gt, PloobsEngine.SceneControl.IObject obj, PloobsEngine.SceneControl.RenderHelper render, PloobsEngine.Cameras.ICamera cam, IList lights)
    {
        IModelo modelo = obj.Modelo;

        ///Recupera a transformacao do Objecto 
        Matrix objTransform = obj.WorldMatrix;
        point.Parameters["camPosition"].SetValue(cam.Position);
        point.Parameters["DiffuseTexture"].SetValue(modelo.getTexture(TextureType.DIFFUSE));
        point.Parameters["View"].SetValue(cam.View);
        point.Parameters["Projection"].SetValue(cam.Projection);

        ///Desenha o IModelo,
        ///O IModelo contem diversos meshes, e cada mesh contem um array de BatchInformation
        for (int i = 0; i < modelo.MeshNumber; i++)
        {
            BatchInformation[] bi = modelo.GetBatchInformation(i);
            for (int j = 0; j < bi.Count(); j++)
            {
                ///Compoe a World Matrix do Objecto como uma Combinacao da Matriz LocalTransformation 
                ///do modelo (que vem dos softwares de modelagem) e a matrix objTransform do objeto (transformacao do objeto no espaco fisico)
                point.Parameters["World"].SetValue(bi[j].ModelLocalTransformation * objTransform);

                ///renderiza o modelo utilizando o efeito Phong
                render.RenderBatch(bi[j], point);
            }
        }
    }
}

Esta classe armazena as informações referentes ao material como SpecularPower e Specular intensity.

O método Update cria e popula os arrays com as informações das luzes, em seguida envia estas informações para o Shader. O Draw apenas carrega as informações das transformações e desenha o modelo na tela.

Implementação Spot Light

Bastante semelhante à Point light, não colocarei o código, pois não acrescenta muita coisa (e ocupa muito espaço =P), baixem o código fonte disponível no fim do artigo e dêem uma olhada.

Problemas com a abordagem utilizada para o cálculo da iluminação (SinglePass MultiLight)

Os principais problema da SinglePass MultiLight (um mesmo shader calcula a influência de todas as luzes) são :

  • Falta de Escalabilidade (temos um limite superior para o número de luzes)
  • Desempenho
    • Processamos objetos que depois não serão mostrados na tela (Depth Test)
    • Processamos luzes que não estão sendo vistas (o modelo faz um Loop por todas as luzes do ambiente)
    • Todo frame estamos atualizando as variáveis do Shader (tráfego CPU->GPU nunca é rapido)
  • Problemas com manutenção e expansibilidade:
    • Se você quiser usar Spot e Point na mesma cena, você terá que criar um novo shader, se quiser adicionar sombra terá que criar um novo ..... (essa combinação explode, existem sistemas especiais para cuidar disso, procure por uber shader)

Alguns dos problemas citados podem ser corrigidos mudando poucas coisas no código porém outros como a Falta de Escalabilidade não podem ser corrigidos facilmente (são limitações desta arquitetura).

Pretendo nos posts futuros falar um pouco mais sobre outras arquiteturas de iluminação como Deferred Shading (usado na PloobsEngine) e Inferred Shading.

Com a teórica vista até agora na serie de tutoriais, já é possível implementar outros modelos de iluminação além do Phong como o Cook Torrance.

É isso por hoje pessoal =P, no próximo tutorial pretendo falar um pouco sobre Normal Mapping e Specular Mapping. Como sempre, qualquer duvida deixe um comentário abaixo ou entre em contato com a gente pelo nosso fórum.

Estamos postando alguns artigos mais teóricos e iniciais no blog da equipe Ploobs, Confiram!!

Abraços =P

ScreenShots

Usei o mesmo modelo do tutorial anterior (estátua low poly e sem textura, experimentem modelos mais bem trabalhados =P, o efeito é bem legal)!

Preciso urgentemente de um modelo free (ate 200K triângulos mais ou menos) com bump e specular Map para os próximos tutoriais =P, se alguém quiser ajudar, entre em Este endereço de e-mail está protegido contra SpamBots. Você precisa ter o JavaScript habilitado para vê-lo. .

Figura 3: Varias Luzes Pontuais

Figura 3: Varias Luzes Pontuais

Figura 4: Varias Luzes Pontuais 2

Figura 4: Varias Luzes Pontuais 2

Figura 5: Uma Spot Light

Figura 5: Uma Spot Light

Figura 6: Point Light Concentradas

Figura 6: Point Light Concentradas

Figura 7: Uma Spot Light com grande abertura

Figura 7: Uma Spot Light com grande abertura

Código Fonte para Download

download


Comentários (4)
  • Michel  - MASSA mesmo
    avatar

    Olá pessoal.
    muito massa mesmo ....
    É muito bom ter tutoriais com um assunto tão "pesado" como shader e em português...
    A maioria é em inglês... da pra entender mas assim em portugues.... muito show a iniciativa :)
    Parabens mesmo pelo trabalho!!!

  • Thiago Dias Pastor
    avatar

    opa, vlw mesmo =P

  • Carlos Fabricio
    avatar

    Rapaz, ótimo tutorial. Só lavento ser direcionado para a PloobsEngine e não algo genérico que possa ser usado por todos, ficou limitado a engine infelizmente.

  • vinigodoy
    avatar

    Olá,

    O foco desse tutorial são os shaders, que são idênticos seja na engine, direto no XNA ou no DirectX.

    Sem a engine, haveria muito texto para mostrar como carregar os modelos, controlar a câmera, criar buffers, coisa que iria acabar tornando o tutorial mais longo e confuso. O autor ressalta em seu primeiro post que os recursos da engine utilizados não tem qualquer impacto sobre a implementação de shader.

    Quanto a limitação da máquina, a Engine também se baseia exclusivamente em XNA, portanto, pode ser usada nas mesmas plataformas que o XNA usa. A engine é de autoria do autor do artigo.

    Se você quiser seguir um bom tutorial sobre shaders em XNA, leia:
    http://www.riemers.net/eng/tutorials.php

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