
Mapeamento normal
Todas as cenas que usamos são compostas por polígonos, que por sua vez são constituídos por centenas, milhares de triângulos absolutamente planos. Já conseguimos aumentar um pouco o realismo das cenas devido a detalhes adicionais que fornecem a aplicação de texturas bidimensionais nesses triângulos planos. A texturização ajuda a esconder o fato de que todos os objetos na cena são apenas uma coleção de muitos pequenos triângulos. Uma ótima técnica, mas suas possibilidades não são ilimitadas: ao se aproximar de qualquer superfície, fica claro que se trata de superfícies planas. A maioria dos objetos reais não é completamente plana e mostra muitos detalhes de relevo.
Por exemplo, pegue alvenaria. Sua superfície é muito rugosa e, obviamente, não é representada por um plano: nela existem recessos com cimento e muitos pequenos detalhes, como buracos e rachaduras. Se analisarmos uma cena com imitação de alvenaria na presença de iluminação, a ilusão de relevo da superfície é facilmente destruída. A seguir, é apresentado um exemplo de uma cena contendo um plano com textura de alvenaria e fonte de luz de um ponto:
Como você pode ver, a iluminação não leva em conta os detalhes de relevo assumidos para essa superfície: todas as pequenas rachaduras e cavidades com cimento também são indistinguíveis do restante da superfície. Pode-se usar um mapa de brilho especular para limitar a iluminação de certos detalhes que estão nos recessos da superfície. Mas isso parece mais um hack sujo do que uma solução funcional. O que precisamos é de uma maneira de fornecer às equações de iluminação dados sobre o microrrelevo da superfície.
No contexto das equações de iluminação conhecidas por nós, considere esta questão: sob quais condições a superfície será iluminada como perfeitamente plana? A resposta está relacionada ao normal à superfície. Do ponto de vista do algoritmo de iluminação, as informações sobre a forma da superfície são transmitidas apenas através do vetor normal. Como o vetor normal é constante em toda parte da superfície apresentada acima, a iluminação também sai uniforme, correspondendo ao plano. Mas e se passarmos para o algoritmo de iluminação não a única constante normal de todos os fragmentos pertencentes ao objeto, mas a única normal de cada fragmento? Assim, o vetor normal mudará levemente com base na topografia da superfície, o que criará uma ilusão mais convincente da complexidade da superfície:

Através do uso de normais fragmentariamente diferentes, o algoritmo de iluminação considerará a superfície composta por muitos planos microscópicos perpendiculares ao seu vetor normal. Como resultado, isso adicionará significativamente textura ao objeto. A técnica de aplicar normais únicas a um fragmento, e não a toda a superfície - esse é o
mapeamento normal ou o
mapeamento de relevo . Como aplicado a uma cena já familiar:
Você pode ver o aumento impressionante na complexidade visual devido ao custo muito modesto do desempenho. Como todas as mudanças no modelo de iluminação estão apenas no fornecimento de um normal único em cada fragmento, nenhuma fórmula de cálculo é alterada. Somente na entrada, em vez do normal interpolado, o normal para o fragmento atual vem à superfície. Todas as mesmas equações de iluminação fazem o resto do trabalho para criar a ilusão de alívio.
Mapeamento normal
Portanto, precisamos fornecer ao algoritmo de iluminação normais normais para cada fragmento. Usaremos o método já familiar em texturas de reflexão difusa e especular e usaremos a textura 2D usual para armazenar dados normais em cada ponto da superfície. Não se surpreenda, as texturas também são ótimas para armazenar vetores normais. Depois, basta selecionar a textura, restaurar o vetor normal e realizar cálculos de iluminação.
À primeira vista, pode não estar muito claro como salvar dados vetoriais em uma textura regular, que normalmente é usada para armazenar informações de cores. Mas pense por um segundo: a tríade de cores RGB é essencialmente um vetor tridimensional. De maneira semelhante, você pode salvar os componentes do vetor normal XYZ nos componentes de cores correspondentes. Os valores dos componentes do vetor normal estão no intervalo [-1, 1] e, portanto, requerem conversão adicional no intervalo [0, 1]:
vec3 rgb_normal = normal * 0.5 + 0.5;
Essa redução do vetor normal ao espaço dos componentes de cores RGB nos permitirá salvar o vetor normal na textura, obtido com base no relevo real do objeto modelado e exclusivo para cada fragmento. Um exemplo dessa textura - mapas normais - para a mesma alvenaria:
É interessante notar o tom azul deste mapa normal (quase todos os mapas normais têm um tom semelhante). Isso acontece porque todas as normais são orientadas aproximadamente ao longo do eixo oZ, representado pela tripla coordenada (0, 0, 1), ou seja, sob a forma de uma tríade de cores - azul puro. Pequenas mudanças de tonalidade são uma consequência do desvio das normais do semi-eixo positivo oZ em algumas áreas, o que corresponde a terrenos irregulares. Assim, você pode ver que nas bordas superiores de cada tijolo, a textura assume uma tonalidade verde. E isso é lógico: nas faces superiores do tijolo, as normais devem ser orientadas mais para o eixo oY (0, 1, 0), que corresponde ao verde.
Para a cena de teste, pegue um plano orientado para o semi-eixo positivo oZ e use o seguinte
mapa difuso e
o mapa normal para ele.
Observe que o mapa normal no link e na imagem acima é diferente. No artigo, o autor mencionou casualmente as razões das diferenças, limitando-se a conselhos sobre a conversão de mapas normais para que o componente verde indique "inativo" em vez de "up" no sistema local para o plano de textura.
Se você olhar com mais detalhes, dois fatores interagem aqui:
- A diferença é como os texels são abordados na memória do cliente e na memória de textura OpenGL
- A presença de duas notações para mapas normais. Convencionalmente, dois campos: estilo DirectX e estilo OpenGL
Em relação às notações normais de mapas, historicamente familiares existem dois campos: DirectX e OpenGL.
Aparentemente, eles não são compatíveis. E com um pouco de reflexão, você pode entender que o DirectX considera o espaço tangente a ser canhoto e o OpenGL a destro. Deslizar o mapa X normal do nosso aplicativo sem nenhuma alteração resultará em iluminação incorreta e nem sempre é claro imediatamente se está incorreto. Mais notavelmente, as protuberâncias no formato OpenGL se tornam recuos para o DirectX e vice-versa.
Quanto ao endereçamento: carregando dados de um arquivo de textura na memória, assumimos que o primeiro texel é o texel superior esquerdo da imagem. Para representar dados de textura na memória do aplicativo, isso geralmente é verdade. Mas o OpenGL usa um sistema de coordenadas de textura diferente: para isso, o primeiro texel é o canto inferior esquerdo. Para texturizar corretamente, as imagens geralmente são invertidas ao longo do eixo Y no código de um ou outro carregador de arquivos de imagem. Para Stb_image usado nas lições, você precisa adicionar uma caixa de seleção
stbi_set_flip_vertically_on_load(1);
O engraçado é que duas opções são exibidas corretamente em termos de iluminação: um mapa normal na notação OpenGL com a reflexão Y ativada ou um mapa normal na notação DirectX com a reflexão Y desativada. A iluminação nos dois casos funciona corretamente, a diferença permanecerá apenas no inverso da textura ao longo do eixo Y.
Nota trans.
Portanto, carregue as duas texturas, vincule aos blocos de textura e renderize o plano preparado, levando em consideração as seguintes modificações no código do shader de fragmento:
uniform sampler2D normalMap; void main() {
Aqui aplicamos a transformação inversa do espaço de valor RGB em um vetor normal completo e, em seguida, simplesmente a usamos no conhecido modelo de iluminação Blinn-Fong.
Agora, se você mudar lentamente a posição da fonte de luz na cena, poderá sentir a ilusão de alívio da superfície fornecida pelo mapa normal:
Mas ainda existe um problema que reduz drasticamente o alcance possível do uso de mapas normais. Como já observado, o tom azul do mapa normal indicava que todos os vetores na textura são orientados em média ao longo do eixo positivo oZ. Em nossa cena, isso não criou problemas, porque o normal para a superfície do avião também estava alinhado com oZ. No entanto, o que acontece se mudarmos a posição do plano na cena para que o normal seja alinhado com o eixo positivo oY?

A iluminação acabou completamente errada! E a razão é simples: amostras de normais do mapa ainda retornam vetores orientados ao longo do meio eixo positivo oZ, embora neste caso devam ser orientados na direção do meio eixo positivo oY da superfície normal. Ao mesmo tempo, o cálculo da iluminação é como se as normais da superfície estivessem localizadas como se o plano ainda estivesse orientado em direção ao semi-eixo positivo oZ, o que dá um resultado incorreto. A figura abaixo mostra mais claramente a orientação das normais lidas no mapa em relação à superfície:
Pode-se observar que as normais estão geralmente alinhadas ao longo do semi-eixo positivo oZ, embora devam ter sido alinhadas ao longo do normal com a superfície que é direcionada ao longo do semi-eixo positivo oY.
Uma solução possível seria configurar um mapa normal separado para cada orientação da superfície em consideração. Para um cubo, seriam necessários seis mapas normais, mas para modelos mais complexos, o número de orientações possíveis pode ser muito alto e não adequado para implementação.
Existe outra abordagem matematicamente mais complicada, que oferece o cálculo da iluminação em um sistema de coordenadas diferente: de modo que os vetores normais nela sempre coincidam sempre aproximadamente com o meio eixo positivo oZ. Outros vetores necessários para os cálculos de iluminação são convertidos para esse sistema de coordenadas. Este método possibilita o uso de um mapa normal para qualquer orientação do objeto. E esse sistema de coordenadas específico é chamado de
espaço tangente ou
espaço tangente .
Espaço tangente
Deve-se notar que o vetor normal no mapa normal é expresso diretamente no espaço tangente, isto é, em tal sistema de coordenadas que o normal é sempre direcionado aproximadamente na direção do semi-eixo positivo oZ. O espaço tangente é definido como um sistema de coordenadas local ao plano do triângulo e cada vetor normal é definido dentro desse sistema de coordenadas. Você pode imaginar esse sistema como um sistema de coordenadas local para um mapa normal: todos os vetores nele são dados direcionados para o semi-eixo positivo oZ, independentemente da orientação final da superfície. Usando matrizes de transformação especialmente preparadas, é possível transformar vetores normais desse sistema de coordenadas tangentes locais em mundo ou visualizar coordenadas, orientando-os de acordo com a posição final das superfícies texturizadas.
Considere o exemplo anterior com o uso incorreto do mapeamento normal, em que o plano foi orientado ao longo do eixo positivo oY. Como o mapa de normais é definido no espaço tangente, uma das opções de ajuste é calcular a matriz de transição de normais do espaço tangente para que elas se tornem orientadas normais à superfície. Isso faria com que os normais ficassem alinhados ao longo do eixo positivo oY. Uma propriedade notável do espaço tangente é o fato de que, ao calcular essa matriz, podemos reorientar os normais para qualquer superfície e sua orientação.
Essa matriz é abreviada como
TBN , que é uma abreviação para o nome do triplo de vetores
Tangente ,
Bitangente e
Normal . Precisamos encontrar esses três vetores para formar essa matriz de mudança básica. Essa matriz faz a transição de um vetor do espaço tangente para outro e para a sua formação são necessários três vetores mutuamente perpendiculares, cuja orientação corresponde à orientação do plano normal do mapa. Este é um vetor de direção para cima, para a direita e para a frente, um conjunto familiar para nós da lição na
câmera virtual .
Com o vetor superior, tudo fica claro imediatamente - este é o nosso vetor normal. Os
vetores direito e direto são chamados tangente e
bitangente, respectivamente. A figura a seguir fornece uma idéia de sua posição relativa no plano:
O cálculo da tangente e da bi-tangente não é tão óbvio quanto o cálculo do vetor normal. Na figura, você pode ver que as direções da tangente e do mapa da tangente do normal estão alinhadas com os eixos que especificam as coordenadas de textura da superfície. Esse fato é a base para o cálculo desses dois vetores, o que exigirá alguma habilidade em matemática. Veja a foto:
Alterações nas coordenadas de textura ao longo da borda de um triângulo
designado como
e
expresso nas mesmas direções que os vetores tangentes
e tangente
. Com base nesse fato, você pode expressar as arestas de um triângulo
e
sob a forma de uma combinação linear de vetores tangentes e bi-tangentes:
Transformando em um registro bit a bit, obtemos:
é calculado como o vetor da diferença de dois vetores e
e
como a diferença nas coordenadas de textura. Resta encontrar duas incógnitas em duas equações: a tangente
e preconceito
. Se você se lembra das lições de álgebra, sabe que essas condições tornam possível resolver o sistema para
e para
.
A última forma de equações dada nos permite reescrevê-la na forma de multiplicação de matrizes:
\ begin {bmatrix} E_ {1x} e E_ {1y} e E_ {1z} \\ E_ {2x} e E_ {2y} e E_ {2z} \ end {bmatrix} = \ begin {bmatrix} \ Delta U_1 & \ Delta V_1 \\ \ Delta U_2 \ \ Delta V_2 \ end {bmatrix} \ begin {bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \ end {bmatrix}
Tente fazer a multiplicação da matriz em sua mente para garantir que o registro esteja correto. Escrever um sistema em forma de matriz facilita muito a compreensão da abordagem para encontrar
e
. Multiplique ambos os lados da equação pelo inverso de
:
\ begin {bmatrix} \ Delta U_1 e \ Delta V_1 \\ \ Delta U_2 e \ Delta V_2 \ end {bmatrix} ^ {- 1} \ begin {bmatrix} E_ {1x} e E_ {1y} & E_ {1z } \\ E_ {2x} e E_ {2y} e E_ {2z} \ end {bmatrix} = \ begin {bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \ end {bmatrix}
Temos uma decisão sobre
e
, que, no entanto, requer o cálculo da matriz inversa de alterações nas coordenadas de textura. Não entraremos em detalhes do cálculo de matrizes inversas - a expressão para a matriz inversa se parece com o produto do número inverso ao determinante da matriz original e da matriz adjacente:
\ begin {bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \ end {bmatrix} = \ frac {1} {\ Delta U_1 \ Delta V_2 - \ Delta U_2 \ Delta V_1} \ begin {bmatrix} \ Delta V_2 e - \ Delta V_1 \\ - \ Delta U_2 e \ Delta U_1 \ end {bmatrix} \ begin {bmatrix} E_ {1x} e E_ {1y} e E_ {1z} \\ E_ {2x} e E_ { 2y} & E_ {2z} \ end {bmatrix}
Esta expressão é a fórmula para calcular o vetor tangente
e tangente
com base nas coordenadas das faces do triângulo e nas coordenadas correspondentes da textura.
Não se preocupe se a essência dos cálculos matemáticos acima iludir você. Se você entende que obtemos a tangente e a tangente de viés com base nas coordenadas dos vértices do triângulo e em suas coordenadas de textura (já que as coordenadas da textura também pertencem ao espaço da tangente) - isso já é metade da batalha.
Cálculo de tangentes e bitangentes
No exemplo desta lição, pegamos um plano simples olhando em direção ao semi-eixo positivo oZ. Agora, tentaremos implementar o mapeamento normal usando o espaço tangente para poder orientar o plano no exemplo como quisermos, sem destruir o efeito de mapeamento normal. Usando o cálculo acima, encontramos manualmente a tangente e a bi-tangente à superfície em consideração.
Supomos que o plano seja composto pelos seguintes vértices com coordenadas de textura (dois triângulos são dados pelos vetores 1, 2, 3 e 1, 3, 4):
Primeiro, calculamos os vetores que descrevem as faces do triângulo, bem como os deltas das coordenadas da textura:
glm::vec3 edge1 = pos2 - pos1; glm::vec3 edge2 = pos3 - pos1; glm::vec2 deltaUV1 = uv2 - uv1; glm::vec2 deltaUV2 = uv3 - uv1;
Tendo os dados iniciais necessários em mãos, podemos começar a calcular a tangente e a bi-tangente diretamente pelas fórmulas da seção anterior:
float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y); tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x); tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y); tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z); tangent1 = glm::normalize(tangent1); bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x); bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y); bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z); bitangent1 = glm::normalize(bitangent1); [...]
Primeiro, retiramos o componente fracionário da expressão final em uma variável separada
f . Então, para cada componente dos vetores, executamos a parte correspondente da multiplicação da matriz e multiplicamos por
f . Comparando esse código com a fórmula de cálculo final, você pode ver que esse é seu arranjo literal. Não se esqueça de normalizar no final, para que os vetores encontrados sejam unitários.
Como o triângulo é uma figura plana, basta calcular a tangente e a bi-tangente uma vez por triângulo - elas serão iguais para todos os vértices. Vale ressaltar que a maioria das implementações de trabalho com modelos (como carregadeiras ou geradores de paisagem) usa essa organização de triângulos, onde eles compartilham vértices com outros triângulos. Nesses casos, os desenvolvedores geralmente recorrem à média de parâmetros em vértices comuns, como vetores normais, tangente e bi-tangente, para obter um resultado mais suave. Os triângulos que compõem nosso plano também compartilham vários vértices, mas como os dois estão no mesmo plano, a média não é necessária. No entanto, é útil lembrar a presença dessa abordagem em aplicativos e tarefas reais.
Os vetores tangentes e bi-tangentes resultantes devem ter valores (1, 0, 0) e (0, 1, 0), respectivamente. Que juntamente com o vetor normal (0, 0, 1) formam a matriz ortogonal TBN. Se você visualizar a base resultante com o plano, obterá a seguinte imagem:
Agora, tendo calculado vetores, você pode prosseguir para a implementação completa do mapeamento normal.
Mapeamento normal no espaço tangente
Primeiro, você precisa criar uma matriz TBN nos shaders. Para esse propósito, transferiremos os vetores tangentes e bi-tangentes pré-preparados para o sombreador de vértices através dos atributos de vértice:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; layout (location = 3) in vec3 aTangent; layout (location = 4) in vec3 aBitangent;
No próprio código do vertex shader, formamos a matriz diretamente:
void main() { [...] vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); vec3 B = normalize(vec3(model * vec4(aBitangent, 0.0))); vec3 N = normalize(vec3(model * vec4(aNormal, 0.0))); mat3 TBN = mat3(T, B, N) }
No código acima, primeiro convertemos todos os vetores da base do espaço tangente em um sistema de coordenadas no qual nos sentimos confortáveis trabalhando - nesse caso, este é o sistema de coordenadas do mundo e multiplicamos os vetores pelo modelo de matriz de
modelo . Em seguida, criamos a própria matriz TBN simplesmente passando todos os três vetores correspondentes para um construtor do tipo
mat3 . Observe que, para que a ordem seja completamente correta, é necessário multiplicar os vetores não pela matriz do modelo, mas pela matriz normal, pois estamos interessados apenas na orientação dos vetores, mas não em seu deslocamento ou escala.
A rigor, não é necessário transferir o vetor bi-tangente para o sombreador.
Como o triplo dos vetores TBN é mutuamente perpendicular, a bi-tangente pode ser encontrada no sombreador através da multiplicação do vetor:
vec3 B = cross(N, T)
Então, a matriz TBN é recebida, como a usamos? De fato, existem duas abordagens para seu uso no mapeamento normal:
- Use a matriz TBN para transformar todos os vetores necessários da tangente para o espaço do mundo. Transfira os resultados para o shader de fragmento, onde, também usando a matriz, transforma o vetor do mapa normal para o espaço do mundo. Como resultado, o vetor normal estará no espaço em que toda a iluminação é calculada.
- Pegue a matriz inversa em TBN e converta todos os vetores necessários do espaço mundial para tangente. I.e. use essa matriz para transformar os vetores envolvidos nos cálculos de iluminação em espaço tangente. O vetor normal neste caso também permanece no mesmo espaço que os outros participantes no cálculo da iluminação.
Vamos olhar para a primeira opção. O vetor normal da textura correspondente é especificado no espaço tangente, enquanto os outros vetores usados no cálculo da iluminação são definidos no espaço do mundo. Ao passar a matriz TBN para o shader de fragmento, poderíamos transformar o vetor normal obtido por amostragem da textura da tangente para o espaço do mundo, garantindo a unidade dos sistemas de coordenadas para todos os elementos do cálculo da iluminação. Nesse caso, todos os cálculos (especialmente multiplicações de vetores escalares) estarão corretos.
A transferência da matriz TBN é feita da maneira mais simples:
out VS_OUT { vec3 FragPos; vec2 TexCoords; mat3 TBN; } vs_out; void main() { [...] vs_out.TBN = mat3(T, B, N); }
No código do fragment shader, respectivamente, definimos uma variável de entrada do tipo mat3:
in VS_OUT { vec3 FragPos; vec2 TexCoords; mat3 TBN; } fs_in;
Tendo a matriz em mãos, é possível especificar o código para obter o normal pela expressão da tradução da tangente para o espaço do mundo:
normal = texture(normalMap, fs_in.TexCoords).rgb; normal = normalize(normal * 2.0 - 1.0); normal = normalize(fs_in.TBN * normal);
Como o normal resultante agora está definido no espaço do mundo, não há necessidade de alterar mais nada no código do sombreador. Os cálculos de iluminação e, portanto, assumem um vetor normal dado nas coordenadas do mundo.
Vamos também olhar para a segunda abordagem.
Isso exigirá a obtenção da matriz TBN inversa, bem como a transferência de todos os vetores envolvidos no cálculo da iluminação do sistema de coordenadas do mundo para aquele que corresponda aos vetores normais obtidos da textura - a tangente. Nesse caso, a formação da matriz TBN permanece inalterada, mas antes de passar para o shader de fragmento, precisamos obter a matriz inversa: vs_out.TBN = transpose(mat3(T, B, N));
Observe que a função transpose () é usada em vez de inversa () . Essa substituição é verdadeira, já que para matrizes ortogonais (onde todos os eixos são representados por vetores unitários mutuamente perpendiculares), a obtenção da matriz inversa fornece um resultado idêntico à transposição. E isso é muito útil, pois, no caso geral, calcular a matriz inversa é uma tarefa muito mais computacionalmente cara em comparação à transposição.No código do fragment shader, não converteremos o vetor normal; em vez disso, converteremos outros vetores importantes do sistema de coordenadas do mundo para a tangente, nomeadamente lightDir e viewDir. Essa solução também traz todos os elementos dos cálculos em um único sistema de coordenadas, desta vez a tangente. void main() { vec3 normal = texture(normalMap, fs_in.TexCoords).rgb; normal = normalize(normal * 2.0 - 1.0); vec3 lightDir = fs_in.TBN * normalize(lightPos - fs_in.FragPos); vec3 viewDir = fs_in.TBN * normalize(viewPos - fs_in.FragPos); [...] }
A segunda abordagem parece mais demorada e requer mais multiplicações de matrizes no sombreador de fragmentos (o que afeta muito o desempenho). Por que começamos a desmontá-lo?O fato é que traduzir vetores de coordenadas mundiais para tangentes fornece uma vantagem adicional: na verdade, podemos mover todo o código de transformação de fragmento para sombreador de vértice! Essa abordagem está funcionando porque lightPos e viewPos não mudam de fragmento para fragmento, e o valor é fs_in.FragPostambém podemos traduzir em espaço tangente no shader de vértice, o valor interpolado na entrada do shader de fragmento será bastante correto. Portanto, para a segunda abordagem, não há necessidade de converter todos esses vetores no espaço tangente no código do sombreador de fragmentos, enquanto o primeiro exige isso - porque o normal é único para cada fragmento.Como resultado, nos afastamos da transferência da matriz inversa ao TBN para o shader de fragmento e, em vez disso, passamos o vetor de posição do vértice, fonte de luz e observador no espaço tangente. Portanto, nos livraremos das caras multiplicações de matrizes no shader de fragmento, o que será uma otimização significativa, porque o shader de vértice é executado com muito menos frequência. É essa vantagem que coloca a segunda abordagem na categoria de uso preferido na maioria dos casos. out VS_OUT { vec3 FragPos; vec2 TexCoords; vec3 TangentLightPos; vec3 TangentViewPos; vec3 TangentFragPos; } vs_out; uniform vec3 lightPos; uniform vec3 viewPos; [...] void main() { [...] mat3 TBN = transpose(mat3(T, B, N)); vs_out.TangentLightPos = TBN * lightPos; vs_out.TangentViewPos = TBN * viewPos; vs_out.TangentFragPos = TBN * vec3(model * vec4(aPos, 0.0));
No shader de fragmento, passamos a usar novas variáveis de entrada nos cálculos de iluminação no espaço tangente. Como as normais são definidas condicionalmente neste espaço, todos os cálculos permanecem corretos.Agora que todos os cálculos normais de mapeamento são executados no espaço tangente, podemos alterar a orientação da superfície de teste no aplicativo como queremos e a iluminação permanecerá correta: glm::mat4 model(1.0f); model = glm::rotate(model, (float)glfwGetTime() * -10.0f, glm::normalize(glm::vec3(1.0, 0.0, 1.0))); shader.setMat4("model", model); RenderQuad();
De fato, exteriormente tudo parece como deveria:As fontes estão aqui .Objetos complexos
Assim, descobrimos como realizar o mapeamento normal no espaço tangente e como calcular independentemente os vetores tangentes tangentes e tendenciosos para isso. Felizmente, esse cálculo manual não costuma ser uma tarefa: na maioria das vezes, esse código é implementado por desenvolvedores em algum lugar nas entranhas do carregador de modelos. No nosso caso, isso é verdade para o carregador Assimp usado .O Assimp fornece um sinalizador de opção muito útil ao carregar modelos: aiProcess_CalcTangentSpace . Quando é passada para a função ReadFile () , a própria biblioteca calcula as linhas tangentes e bi-tangentes suaves de cada um dos vértices carregados - um processo semelhante ao discutido aqui. const aiScene *scene = importer.ReadFile( path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace );
Depois disso, você pode acessar diretamente as tangentes calculadas: vector.x = mesh->mTangents[i].x; vector.y = mesh->mTangents[i].y; vector.z = mesh->mTangents[i].z; vertex.Tangent = vector;
Você também precisará atualizar o código de download para levar em consideração o recebimento de mapas normais para modelos texturizados. O formato Wavefront Object (.obj) exporta mapas normais de forma que o sinalizador Assimp aiTextureType_NORMAL não garanta que esses mapas sejam carregados corretamente, enquanto tudo funciona corretamente com o sinalizador aiTextureType_HEIGHT . Portanto, pessoalmente, normalmente carrego mapas normais da seguinte maneira: vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
Obviamente, essa abordagem pode não ser adequada para outros formatos de descrição de modelo e tipos de arquivo. Também observo que a configuração do sinalizador aiProcess_CalcTangentSpace nem sempre funciona. Sabemos que o cálculo de tangentes é baseado em coordenadas de textura; no entanto, os autores de modelos geralmente aplicam vários truques às coordenadas de textura, o que interrompe o cálculo de tangentes. Portanto, uma imagem espelhada de coordenadas de textura é frequentemente usada para modelos com textura simétrica. Se o fato do espelhamento não for levado em consideração, o cálculo das tangentes estará incorreto. O Assimp não faz essa contabilidade. O modelo nanosuit familiar aqui não é adequado para demonstração, pois também usa espelhamento.Mas com um modelo com textura correta usando mapas normais e especulares, o aplicativo de teste fornece um resultado muito bom:Como você pode ver, o uso do mapeamento normal fornece um aumento tangível em detalhes e é barato em termos de custos de desempenho.Não esqueça que o uso do mapeamento normal pode ajudar a melhorar o desempenho de uma cena específica. Sem seu uso, é possível obter detalhes do modelo apenas através do aumento da densidade da malha poligonal. Mas essa técnica permite alcançar visualmente o mesmo nível de detalhe para malhas de baixo poli. Abaixo, você pode ver uma comparação dessas duas abordagens:O nível de detalhe no modelo de alto poli e baixo poli usando o mapeamento normal é praticamente indistinguível. Portanto, essa técnica é uma ótima maneira de substituir modelos high-poly na cena por modelos simplificados, praticamente sem perda na qualidade visual.Último comentário
Há outro detalhe técnico referente ao mapeamento normal, que melhora um pouco a qualidade com pouco ou nenhum custo adicional.Nos casos em que tangentes são calculadas para malhas grandes e complexas com um número significativo de vértices pertencentes a vários triângulos, os vetores tangentes são geralmente calculados para obter um resultado de mapeamento normal suave e visualmente agradável. No entanto, isso cria um problema: após a média, o triplo dos vetores TBN pode perder a perpendicularidade mútua, o que também significa a perda de ortogonalidade para a matriz TBN. No caso geral, o resultado do mapeamento normal obtido com base em uma matriz não ortogonal é apenas ligeiramente incorreto, mas ainda podemos melhorá-lo.Para fazer isso, basta aplicar um método matemático simples:Processo de Gram-Schmidt ou re-ortogonalização do nosso triplo de vetores TBN. No código do vertex shader: vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
Essa emenda, embora pequena, melhora a qualidade do mapeamento normal em troca de despesas gerais escassas. Se você estiver interessado nos detalhes deste procedimento, poderá assistir a última parte do vídeo Normal Mapping Mathematics, cujo link é fornecido abaixo.Recursos Adicionais
PS : Temos um telegrama conf para coordenação de transferências. Se você tem um desejo sério de ajudar com a tradução, é bem-vindo!