|
Dando continuidade a série sobre shaders, neste post falarei uma pouco o uso de Normal Map. Como sempre, sugiro que o pessoal leia os artigos anteriores da serie para poder acompanhar as idéias apresentadas com mais facilidade. No fim do artigo disponibilizarei um demo contendo o que foi discutido.
Normal Mapping
Normal mapping é uma técnica de computação gráfica para simular imperfeições e rugosidades não presentes na geometria do modelo. Para isso, perturbamos as normais do objeto e a usamos no cálculo das equações de iluminação. A geometria do material não é alterada e isso implica que as silhuetas dos modelos não sofreram alterações, apenas nos interiores que teremos a tradicional ilusão de aumento de polígonos.
Muitas pessoas usam Bump Mapping e Normal Mapping como sinônimos, rigorosamente: o bump usa uma textura que define um mapa de alturas em escala de cinza enquanto que o normal map define uma mapa contendo a normal a ser utilizada (“codificada”). Ambos os efeitos tem a mesma finalidade. (aumentar o nível de detalhe de um modelo sem aumentar o numero de polígonos)
Um normal map costuma ser uma imagem RGB cujas coordenadas R, G, e B correspondem ao X,Y e Z da normal (ou algo que nos possibilite recuperar a normal) a ser utilizada no cálculo da iluminação do modelo.
A seguir temos um exemplo de Normal Map.
Figura 1: Normal Map gerado usando um plugin da Nvidia para o Photoshop
A grande sacada do Normal Mapping é utilizar as normais de um normal map (resolução por pixel) ao invés de usar as normais interpoladas dos vértices (resolução dependendo do número de triângulos do modelo) nas equações de phong.
Abaixo temos duas imagens de um modelo, o da direita não usa normal mapping e o da esquerda usa (renders do MAYA). (O exemplo abaixo esta BEM exagerado, a ideia é mostrar por contraste o funcionamento da técnica)
Teoria - Codificando as Normais em uma textura
Nesta seção falarei um pouquinho sobre como que as normais são codificadas no Normal Map. Aqueles que não querem saber como que a mágica funciona, podem pular sem problemas essa parte.
Serão necessárias alguns conceitos um pouco avançados de computação gráfica, aconselho ao pessoal ler a serie do Vinicius Godoy sobre Matrizes e transformações. (precisarei de alguns tópicos que ele ainda não chegou, citarei referencias nestes casos).
Tradicionalmente, as normais são salvas no Normal Map usando o que chamamos de espaço das tangentes. (em computação gráfica temos diversos “espaços” de coordenadas como o World Space, Model Space, Eye Space, cada um com suas vantagens). Esse site contém uma explicação bastante detalhada sobre TangentSpace. Gosto também deste artigo do Gamasutra.
Vou tentar justificar o motivo de se usar tangent space ao invés do Model Space (ou espaço objeto, para quem não está entendendo o que é este espaço, de uma olhada neste artigo que eu fiz há um tempo).
A primeira e mais óbvia tentativa é armazenar os X,Y,Z da normal no R,G,B da textura usando o Model Space (aquele que você usa quando esta editando um modelo no 3DS MAX, no qual o 0,0,0 está no centro e a posição dos vértices do seu objeto são mostrados em relação a ele). O maior problema desta abordagem (e também o principal motivo do pessoal preferir o tangent space) é o fato de precisarmos aplicar uma matriz de transformação especifica sobre cada uma das normais lidas do NormalMap, caso o modelo sofra algum tipo de deformação (a técnica clássica de animação por bones funciona deformando os vértices por exemplo).
Ao representar as normais no espaço tangente, esse tipo de cálculo não é necessário, uma vez que este espaço é invariante para este tipo de transformação (deformação da geometria).
Além desta imensa vantagem (desempenho), o tangent space também é mais compacto, pois permite que superfícies simétricas tenham a mesma representação de normal (economia de espaço na textura). Outro beneficio desta representação é o fato das normais serem unitárias, dessa forma precisamos guardar apenas o X e o Y (o Z sai por “pitágoras” =P ). Para uma discussão um pouco mais detalhada do assunto veja este artigo.
Convertendo as Normais para o espaço World
Como disse, as normais lidas do Normal Map estão no espaço Tangente, para usá-las precisamos convertê-las para o espaço Mundo (World). Os links que eu citei anteriormente contém uma demonstração matemática da matriz de conversão, não irei me aprofundar no assunto, farei apenas um passo-a-passo de como construí-la. (olhem o link do Gamasutra citado anteriormente).
Inicialmente, precisamos que os vértices do modelo tenham os atributos: Tangente, Bitangente e Normal. Caso o software de modelagem não tenha exportado estas informações o Xna pode gerá-las, basta setar a seguinte opção:
- Generate Tangents como true, conforme mostra a figura a seguir (clique no modelo uma vez e aperte F4 em seguida para aparecer as opções abaixo =P):
Para o pessoal que usa DirectX ou Opengl direto, eu recomendo FORTEMENTE a API ASSIMP para carregar modelos de diversos formatos, gerar normais, tangentes, binormais...
A matriz de conversão do espaço tangente para o espaço world tem a seguinte estrutura:
- Na primeira linha temos o vetor Tangente do vértice em questão em espaço Mundo
- Na segunda linha temos o vetor Binormal do vértice em questão em espaço Mundo
- Na terceira linha temos o vetor Normal do vértice em questão em espaço Mundo (sim, as normais originais do modelo são usadas !!!)
Como as normais não sofrem translação, essa matriz não tem a quarta linha (podemos colocar zeros se for necessário)
Ao multiplicar cada uma das normais recuperadas da textura Normal Map por essa matriz teremos a normal em espaço World. (que será utilizada no cálculo das equações de Phong para a iluminação)
Implementação em Shader
Inicialmente devemos definir a Entrada para o Vertex Shader. Iremos incluir os atributos Tangent, Binormal que não existiam nos exemplos anteriores.
///Entrada do VertexShader
struct VertexShaderInput
{
float3 Position : POSITION0;
float3 Normal : Normal0;
float2 TexCoord : TexCoord0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
};
Usamos as semânticas BINORMAL0 e TANGENT0 para a binormal e a tangente, uma vez que o VertexDeclaration está configurado para colocar esses dados nestes registradores (apesar de não ter dito antes, cada Semântica representa um tipo de registrador e cada numero colocado após ela representa qual dos registradores que estamos usando para colocar determinado dado).
Em seguida temos o VertexShader que utiliza as informações passadas para gerar a matriz TangentToWorld. (Conforme mostramos em outros tutoriais, nesta fase também calculamos alguns parâmetros que serão usados pela equação de Phong como o View Vector)
///Saida do Vertex Shader
struct VertexShaderOutput
{
float4 Position : POSITION0;
float3 N : TEXCOORD0;
float2 TextureCoord : TEXCOORD1;
float3 V : TEXCOORD2;
float3 WorldPosition : TEXCOORD3;
float3x3 tangentToWorld : TEXCOORD4;
};
///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;
output.tangentToWorld[0] = mul(input.Tangent, WorldInverseTranspose);
output.tangentToWorld[1] = mul(input.Binormal, WorldInverseTranspose);
output.tangentToWorld[2] = mul(input.Normal, WorldInverseTranspose );
return output;
}
Notar que ao invés de multiplicar as normais pela matriz World como de costume, estamos usando a WorldInverseTranspose.
O motivo é um pouco complexo e esta relacionado com o a natureza da matriz World:
- Se o objeto em questão sofre apenas rotações e translações, podemos transformar suas normais usando a parte 3c3 (esquerda/superior) da matriz World.
- Se o objeto sofre alterações de escala, nos devemos usar a matriz criada anteriormente, porém em seguida devemos re-normalizar a normal.
- Se o objeto sofre alteração de escala não uniforme (ou shear ou tape) (ex: vetor (1,2,3)) não podemos utilizar a matriz world (nem a parte 3x3). Para corrigir este problema usamos a famosa WorldInverseTranspose. (e depois renormalizamos o modelo)
A “matemágica” até que é simples e pode ser vista aqui.
Alguns renders simplesmente forçam o pessoal a não usar scale não uniforme, outros preferem deixar o shader mais simples/rápido e não se importam com os defeitos que apareceram na imagem final. No exemplo eu usei a abordagem mais robusta, porém na PloobsEngine eu optei pela abordagem mais rápida.
Em seguida temos o Pixel Shader que recuperará o valor do NormalMap, o converterá para o espaço world usando a matriz TangentToWorld e calculará a equação de Phong.
///Pixel Shader
float4 PixelShaderAmbientFunction(VertexShaderOutput input) : COLOR0
{
// Le a normal do normal map
float3 normalFromMap = tex2D(NormalSampler, input.TextureCoord);
//coloca no intervalo [-1,1], a textura esta no intervalo [0,1]
normalFromMap = 2.0f * normalFromMap - 1.0f;
//coloca no espaco World
normalFromMap = mul(normalFromMap, input.tangentToWorld);
float4 finalColor = float4(0,0,0,0);
float4 diffuseColor = tex2D(DiffuseSampler,input.TextureCoord);
for(int i = 0 ; i < activeLights; i ++)
{
float3 Normal = normalize(normalFromMap);
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;
}
As texturas armazenam valores entre 0 e 1 (em float, se pegarmos em bits, teremos valores de 0 a 255 no caso de RGBA32). As normais “normalizadas” têm seus valores entre -1 e 1, portanto devemos fazer uma simples conversão de domínio dos valores lidos (de [0,1] para [-1,1]). O resto do shader é igual ao usado no último artigo.
ScreenShots:
Os três screenshots são da mesma cena.
Figura 2: Com Bump e Specular Mapping
Figura 3: Sem Normal Mapiing e sem Specular Mapping, repare como que o leao fica “feio” =P
Figura 4: Com normal e com Specular
Conforme destacado no último tutorial, veja (no demo abaixo) o quão “não escalável” é o ForwardRendering para a adição de luzes (tente adicionar bastante luzes e veja a lentidão que fica =P).
Como sempre, utilizei a PloobsEngine como background =P, para aqueles curiosos, um dos nossos pacotes de demos contém um exemplo parecido usando Deferred Rendering, no qual conseguimos adicionar milhares de luzes sem lag algum !!!!
A cena mostrada foi feita no 3DS MAX, exportada para XML usando um plugin nosso e carregada na engine. O código está bastante comentado para ajudar o pessoal a entender a lógica. Qualquer coisa é só perguntar =P !!!
No demo eu adicionei Specular Mapping. Falarei mais sobre este efeito no próximo tutorial =P
Código Fonte para Download












Ótimo artigo. Se eu tivesse lido isso quando eu comecei a tentar fazer normal maps com certeza eu aprenderia mais rapido
Só uma dica pra galera do open-source.
Tem um plugin bem legal pra fazer normal map no GIMP:
http://code.google.com/p/gimp-normalmap/