Jogo 3D Shaders para Iniciantes

imagem

Deseja aprender como adicionar texturas, iluminação, sombras, mapas normais, objetos brilhantes, oclusão do ambiente e outros efeitos ao seu jogo em 3D? Ótimo! Este artigo apresenta um conjunto de técnicas de sombreamento que podem elevar o nível dos gráficos do seu jogo a novas alturas. Explico cada técnica de forma que você possa aplicar / portar essas informações em qualquer pilha de ferramentas, seja Godot, Unity ou qualquer outra coisa.

Como uma "cola" entre os shaders, decidi usar o magnífico motor de jogo Panda3D e o OpenGL Shading Language (GLSL). Se você usar a mesma pilha, obterá uma vantagem adicional - aprenderá como usar técnicas de sombreamento especificamente no Panda3D e OpenGL.

Preparação


Abaixo está o sistema que eu usei para desenvolver e testar o código de exemplo.

Quarta-feira


O código de amostra foi desenvolvido e testado no seguinte ambiente:

  • Manjaro Linux 4.9.135-1-MANJARO
  • Sequência do renderizador OpenGL: GeForce GTX 970 / PCIe / SSE2
  • Cadeia de versão do OpenGL: 4.6.0 NVIDIA 410.73
  • g ++ (GCC) 8.2.1 20180831
  • Panda3D 1.10.1-1

Materiais


Cada um dos materiais do Blender usados ​​para criar mill-scene.egg possui duas texturas.

A primeira textura é um mapa normal, a segunda é um mapa difuso. Se um objeto usa as normais de seus vértices, um mapa normal "azul claro" é usado. Devido ao fato de todos os modelos terem os mesmos cartões nas mesmas posições, os shaders podem ser generalizados e aplicados ao nó raiz do gráfico de cena.

Observe que o gráfico de cena é um recurso da implementação do mecanismo Panda3D.


Aqui está um mapa normal de uma cor contendo apenas a cor [red = 128, green = 128, blue = 255] .

Esta cor indica a unidade normal, indicando na direção positiva do eixo z [0, 0, 1] .

 [0, 0, 1] = [ round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ] = [128, 128, 255] = [ round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ] = [0, 0, 1] 

Aqui vemos a unidade normal [0, 0, 1] convertida em uma cor azul simples [128, 128, 255] e o azul sólido convertido em uma unidade normal.

Isso é descrito com mais detalhes na seção sobre técnicas normais de sobreposição de mapas.

Panda3d


Neste exemplo de código, o Panda3D é usado como a "cola" entre os shaders. Isso não afeta as técnicas descritas abaixo, ou seja, você pode usar as informações estudadas aqui em qualquer pilha ou mecanismo de jogo selecionado. O Panda3D oferece certas comodidades. No artigo, falei sobre eles, para que você possa encontrar o correspondente na pilha ou recriá-lo você mesmo se não estiver na pilha.

Vale considerar que o gl-coordinate-system default , textures-power-2 down e textures-auto-power-2 1 foram adicionados ao config.prc . Eles não estão contidos na configuração padrão do Panda3D .

Por padrão, o Panda3D usa um sistema de coordenadas destro com um eixo z para cima, enquanto o OpenGL usa um sistema de coordenadas destro com um eixo y para cima.

gl-coordinate-system default permite que você se livre das transformações entre dois sistemas de coordenadas dentro dos shaders.

textures-auto-power-2 1 nos permite usar tamanhos de textura que não são potências de dois, se o sistema os suportar.

Isso é conveniente ao executar o SSAO ou implementar outras técnicas em uma tela / janela, porque o tamanho da tela / janela geralmente não é uma potência de duas.

textures-power-2 down reduz o tamanho das texturas para uma potência de duas se o sistema suportar apenas texturas com tamanhos iguais à potência de duas.

Código de exemplo de compilação


Se você deseja executar o código de amostra, primeiro crie-o.

O Panda3D é executado no Linux, Mac e Windows.

Linux


Comece instalando o Panda3D SDK para sua distribuição.

Descubra onde estão os cabeçalhos e bibliotecas do Panda3D. Provavelmente, eles estão localizados em /usr/include/panda3d/ e em /usr/lib/panda3d/ .

Em seguida, clone este repositório e navegue para seu diretório.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Agora compile o código fonte em um arquivo de saída.

g++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-O2 \
-I/usr/include/python2.7/ \
-I/usr/include/panda3d/


Após criar o arquivo de saída, crie um arquivo executável associando o arquivo de saída às suas dependências.

g++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/usr/lib/panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread


Consulte o manual do Panda3D para mais informações.

Mac


Comece instalando o Panda3D SDK para Mac.

Descubra onde estão os cabeçalhos e as bibliotecas do Panda3D.

Em seguida, clone o repositório e navegue para seu diretório.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Agora compile o código fonte em um arquivo de saída. Você precisa descobrir onde estão os diretórios de inclusão no Python 2.7 e Panda3D.

clang++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-g \
-O2 \
-I/usr/include/python2.7/ \
-I/Developer/Panda3D/include/


Após criar o arquivo de saída, crie um arquivo executável associando o arquivo de saída às suas dependências.

Você precisa descobrir onde as bibliotecas do Panda3D estão localizadas.

clang++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/Developer/Panda3D/lib \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread


Consulte o manual do Panda3D para mais informações.

Windows


Comece instalando o SDK do Panda3D para Windows.

Descubra onde estão os cabeçalhos e bibliotecas do Panda3D.

Clone este repositório e navegue para seu diretório.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Consulte o manual do Panda3D para mais informações.

Iniciar demonstração


Depois de criar o código de amostra, você pode executar o arquivo executável ou demonstração. É assim que eles são executados no Linux ou Mac.

./3d-game-shaders-for-beginners

E assim eles rodam no Windows:

3d-game-shaders-for-beginners.exe

Controle de teclado


A demonstração possui um controle de teclado que permite mover a câmera e mudar o estado de vários efeitos.

Movimento


  • w - mova-se profundamente para a cena.
  • a - gire a cena no sentido horário.
  • s - afaste-se da cena.
  • d - gire a cena no sentido anti-horário.

Efeitos comutáveis


  • y - ative o SSAO.
  • Shift + y - desativa o SSAO.
  • u - inclusão de circuitos.
  • Shift + u - desativa contornos.
  • i - ativar a floração.
  • Shift + i - desativa o bloom.
  • o - ativar mapas normais.
  • Shift + o - desativa os mapas normais.
  • p - inclusão de nevoeiro.
  • Shift + p - desliga o nevoeiro.
  • h - a inclusão da profundidade de campo.
  • Shift + h - desativa a profundidade de campo.
  • j - habilitar a posterização.
  • Shift + j - desativar posterização
  • k - ativar pixelização.
  • Shift + k - desativa a pixelização.
  • l - afiar.
  • Shift + l - desativa a nitidez.
  • n inclusão de grão de filme.
  • Shift + n - desativa a granulação do filme.

Sistema de referência


Antes de começar a escrever shaders, você precisa se familiarizar com os seguintes sistemas de referência ou sistemas de coordenadas. Todos eles se resumem ao que as coordenadas atuais da origem da referência são obtidas (0, 0, 0) . Assim que descobrimos, podemos transformá-los usando algum tipo de matriz ou outro espaço vetorial. Normalmente, se a saída de um sombreador não parecer correta, a causa será confusa nos sistemas de coordenadas.

Modelo



O sistema de coordenadas do modelo ou objeto é relativo à origem do modelo. Nos programas de modelagem tridimensional, por exemplo, no Blender, ele geralmente é colocado no centro do modelo.

O mundo



O espaço do mundo é relativo à origem da cena / nível / universo que você criou.

Revisão



O espaço de coordenadas da vista é relativo à posição da câmera ativa.

Clipping



Espaço de recorte relativo ao centro do quadro da câmera. Todas as coordenadas nele são homogêneas e estão no intervalo (-1, 1) . X e y são paralelos ao filme da câmera e a coordenada z é a profundidade.


Todos os vértices que não estão dentro dos limites da pirâmide de visibilidade ou do volume de visibilidade da câmera são cortados ou descartados. Vemos como isso acontece com um cubo truncado para trás pelo plano mais distante da câmera e com um cubo localizado ao lado.

Ecrã



O espaço da tela é (geralmente) relativo ao canto inferior esquerdo da tela. X muda de zero para a largura da tela. Y muda de zero para a altura da tela.

GLSL


Em vez de trabalhar com um pipeline de funções fixas, usaremos um pipeline de renderização de GPU programável. Como é programável, nós mesmos devemos passar o código do programa na forma de shaders. Um sombreador é um programa (geralmente pequeno) criado com sintaxe semelhante à linguagem C. Um pipeline de renderização de GPU programável consiste em várias etapas que podem ser programadas usando sombreadores. Diferentes tipos de sombreadores incluem sombreadores de vértice, sombreamento de mosaico, sombreamento geométrico, de fragmento e computacional. Para usar as técnicas descritas no artigo, basta usar vértice e fragmento
estágios.

 #version 140 void main() {} 

Aqui está o shader GLSL mínimo, consistindo no número da versão do GLSL e na função principal.

 #version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } 

Aqui está o sombreador de vértice truncado GLSL, que transforma o vértice de entrada em espaço de recorte e exibe essa nova posição como uma posição uniforme de vértice.

O procedimento main não retorna nada, porque é void , e a variável gl_Position é a saída embutida.

Duas palavras-chave que vale a pena mencionar são: uniform e in .

A palavra-chave uniform significa que essa variável global é a mesma para todos os vértices. O próprio Panda3D define p3d_ModelViewProjectionMatrix e, para cada vértice, é a mesma matriz.

A palavra-chave in significa que essa variável global é passada para o shader. Um shader de vértice obtém cada vértice em que a geometria consiste, ao qual um shader de vértice está anexado.

 #version 140 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); } 

Aqui está o shader de fragmento GLSL aparado, exibindo verde opaco como a cor do fragmento.

Não esqueça que um fragmento afeta apenas um pixel da tela, mas vários fragmentos podem afetar um pixel.

Preste atenção à palavra-chave out.

A palavra out chave out significa que essa variável global é definida pelo shader.

O nome fragColor opcional, para que você possa escolher outro.


Aqui está a saída dos dois shaders mostrados acima.

Renderização de textura


Em vez de renderizar / desenhar diretamente na tela, o código de exemplo usa uma técnica para
o nome "renderizar para textura" (renderizar para textura). Para renderizar em uma textura, você precisa configurar o buffer de quadro e vincular a textura a ele. Você pode vincular várias texturas a um único buffer de quadro.

As texturas vinculadas ao buffer do quadro armazenam os vetores retornados pelo sombreador de fragmento. Geralmente esses vetores são vetores coloridos (r, g, b, a) , mas podem ser posições ou vetores normais (x, y, z, w) . Para cada textura vinculada, um shader de fragmento pode gerar um vetor separado. Por exemplo, podemos deduzir em uma passagem a posição e o normal do vértice.

A maior parte do código de exemplo que funciona com o Panda3D está relacionada à configuração das texturas do buffer de quadros . Para simplificar, cada sombreador de fragmento no código de exemplo tem apenas uma saída. No entanto, para garantir uma alta taxa de quadros (FPS), precisamos gerar o máximo de informações possível em cada passo de renderização.

Aqui estão duas estruturas de textura para o buffer de quadro do código de exemplo.


A primeira estrutura renderiza uma cena do moinho de água em uma textura de buffer de quadro usando uma variedade de shaders de vértice e fragmento. Essa estrutura passa por cada um dos vértices do palco com o moinho e ao longo dos fragmentos correspondentes.

Nessa estrutura, o código de exemplo funciona da seguinte maneira.

  • Salva os dados da geometria (por exemplo, posição ou vértice normal) para uso futuro.
  • Salva dados do material (por exemplo, cores difusas) para uso futuro.
  • Cria ligação UV de diferentes texturas (mapas difusos, normais, mapas de sombras, etc.).
  • Calcula a iluminação ambiente, difusa, refletida e emitida.
  • Torna neblina.


A segunda estrutura é uma câmera ortogonal voltada para um retângulo no formato de uma tela.
Essa estrutura percorre apenas quatro picos e seus fragmentos correspondentes.

Na segunda estrutura, o código de amostra executa as seguintes ações:

  • Processa a saída de outra textura de buffer de quadro.
  • Combina diferentes texturas de buffer de quadro em um.

No exemplo de código, podemos ver a saída de uma textura de buffer de quadro, configurando o quadro correspondente como true e false para todos os outros.

  // ... bool showPositionBuffer = false; bool showNormalBuffer = false; bool showSsaoBuffer = false; bool showSsaoBlurBuffer = false; bool showMaterialDiffuseBuffer = false; bool showOutlineBuffer = false; bool showBaseBuffer = false; bool showSharpenBuffer = false; bool showBloomBuffer = false; bool showCombineBuffer = false; bool showCombineBlurBuffer = false; bool showDepthOfFieldBuffer = false; bool showPosterizeBuffer = false; bool showPixelizeBuffer = false; bool showFilmGrainBuffer = true; // ... 

Texturização



Texturização é a ligação de uma cor ou outro vetor a um fragmento usando coordenadas UV. Os valores de U e V variam de zero a um. Cada vértice recebe uma coordenada UV e é exibida no sombreador de vértices.


O shader de fragmento obtém a coordenada UV interpolada. Interpolação significa que a coordenada UV do fragmento está em algum lugar entre as coordenadas UV dos vértices que compõem a face do triângulo.

Sombreador de vértice


 #version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } 

Aqui vemos que o sombreador de vértice gera a coordenada da textura para o sombreador de fragmento. Observe que este é um vetor bidimensional: um valor para U e outro para V.

Tonalizador de fragmentos


 #version 140 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; } 

Aqui vemos que o sombreador do fragmento procura a cor em sua coordenada UV e a exibe como a cor do fragmento.

Textura de preenchimento de tela


 #version 140 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; } 

Ao renderizar em uma textura, a malha é um retângulo plano com a mesma proporção da tela. Portanto, podemos calcular as coordenadas UV, sabendo apenas

A) a largura e a altura da textura com o tamanho da tela sobreposta no retângulo usando coordenadas UV, e
B) as coordenadas x e y do fragmento.

Para vincular x a U, é necessário dividir x pela largura da textura recebida. Da mesma forma, para vincular y a V, é necessário dividir y pela altura da textura recebida. Você verá que essa técnica é usada no código de exemplo.

Iluminação



Para determinar a iluminação, é necessário calcular e combinar aspectos da iluminação ambiente, difusa, refletida e emitida. O código de exemplo usa iluminação Phong.

Sombreador de vértice


 // ... uniform struct p3d_LightSourceParameters { vec4 color ; vec4 ambient ; vec4 diffuse ; vec4 specular ; vec4 position ; vec3 spotDirection ; float spotExponent ; float spotCutoff ; float spotCosCutoff ; float constantAttenuation ; float linearAttenuation ; float quadraticAttenuation ; vec3 attenuation ; sampler2DShadow shadowMap ; mat4 shadowViewMatrix ; } p3d_LightSource[NUMBER_OF_LIGHTS]; // ... 

Para cada fonte de luz, com exceção da luz ambiente, o Panda3D nos fornece uma estrutura conveniente disponível para shaders de vértice e de fragmento. O mais conveniente é um mapa de sombras e uma matriz para visualizar sombras para converter vértices em um espaço de sombras ou iluminação.

  // ... vertexPosition = p3d_ModelViewMatrix * p3d_Vertex; // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { vertexInShadowSpaces[i] = p3d_LightSource[i].shadowViewMatrix * vertexPosition; } // ... 

Começando com o sombreador de vértices, devemos transformar e remover o vértice do espaço de visualização na sombra ou no espaço de iluminação de cada fonte de luz na cena. Isso será útil no futuro para o shader de fragmento renderizar sombras. Um espaço de sombra ou iluminação é um espaço no qual cada coordenada é relativa à posição da fonte de luz (a origem é a fonte de luz).

Tonalizador de fragmentos


O sombreador de fragmento faz a maior parte do cálculo da iluminação.

Material


 // ... uniform struct { vec4 ambient ; vec4 diffuse ; vec4 emission ; vec3 specular ; float shininess ; } p3d_Material; // ... 

O Panda3D nos fornece material (na forma de uma estrutura) para a malha ou modelo que estamos processando atualmente.

Várias fontes de iluminação


  // ... vec4 diffuseSpecular = vec4(0.0, 0.0, 0.0, 0.0); // ... 

Antes de darmos uma olhada nas fontes de iluminação da cena, criaremos uma unidade que conterá cores difusas e refletidas.

  // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { // ... } // ... 

Agora podemos percorrer as fontes de luz em um ciclo, calculando as cores difusas e refletidas para cada uma.

Vetores relacionados de iluminação



Aqui estão quatro vetores básicos necessários para calcular as cores difusas e refletidas introduzidas por cada fonte de luz. O vetor de direção da iluminação é uma seta azul apontando para a fonte de luz. O vetor normal é uma seta verde apontando verticalmente para cima. O vetor de reflexão é uma seta azul que reflete o vetor de direção da luz. O vetor olho ou vista é a seta laranja apontando em direção à câmera.

  // ... vec3 lightDirection = p3d_LightSource[i].position.xyz - vertexPosition.xyz * p3d_LightSource[i].position.w; // ... 

A direção da iluminação é o vetor da posição do vértice para a posição da fonte de luz.

Se for uma iluminação direcional, o Panda3D definirá p3d_LightSource[i].position.w zero. A iluminação direcional não tem posição, apenas direção. Portanto, se essa é uma iluminação direcional, a direção da iluminação será uma direção negativa ou oposta à fonte, porque para iluminação direcional o Panda3D define p3d_LightSource[i].position.xyz como p3d_LightSource[i].position.xyz .

  // ... normal = normalize(vertexNormal); // ... 

O normal para o vértice deve ser um vetor de unidade. Os vetores unitários têm um valor igual a um.

  // ... vec3 unitLightDirection = normalize(lightDirection); vec3 eyeDirection = normalize(-vertexPosition.xyz); vec3 reflectedDirection = normalize(-reflect(unitLightDirection, normal)); // ... 

Em seguida, precisamos de mais três vetores.

Precisamos de um produto escalar com a participação da direção da iluminação, por isso é melhor normalizá-lo. Isso nos dá uma distância ou magnitude igual à unidade (vetor unitário).

A direção da visão é oposta à posição do vértice / fragmento, porque a posição do vértice / fragmento é relativa à posição da câmera. Não esqueça que a posição do vértice / fragmento está no espaço de visualização. Portanto, em vez de passar da câmera (olho) para o vértice / fragmento, passamos do vértice / fragmento para a câmera (olho).

O vetor de reflexão é um reflexo da direção da iluminação normal da superfície. Quando o "raio" da luz toca a superfície, é refletido no mesmo ângulo em que caiu. O ângulo entre o vetor de direção da iluminação e o normal é chamado de "ângulo de incidência". O ângulo entre o vetor de reflexão e o normal é chamado de "ângulo de reflexão".

Você precisa alterar o sinal do vetor de luz refletida, porque ele deve apontar na mesma direção que o vetor do olho. Não esqueça que a direção do olho vai do topo / fragmento para a posição da câmera. Usaremos o vetor de reflexão para calcular o brilho da luz refletida.

Iluminação difusa


  // ... float diffuseIntensity = max(dot(normal, unitLightDirection), 0.0); if (diffuseIntensity > 0) { // ... } // ... 

O brilho da iluminação difusa é o produto escalar do normal para a superfície e a direção da iluminação de um único vetor. O produto escalar pode variar de menos um a um. Se os dois vetores apontam na mesma direção, o brilho é a unidade. Em todos os outros casos, será menor que a unidade.


Se o vetor de iluminação se aproximar da mesma direção que o normal, o brilho da iluminação difusa tende a se unir.

Se o brilho da iluminação difusa for menor ou igual a zero, será necessário ir para a próxima fonte de luz.

  // ... vec4 diffuse = vec4 ( clamp ( diffuseTex.rgb * p3d_LightSource[i].diffuse.rgb * diffuseIntensity , 0 , 1 ) , 1 ); diffuse.r = clamp(diffuse.r, 0, diffuseTex.r); diffuse.g = clamp(diffuse.g, 0, diffuseTex.g); diffuse.b = clamp(diffuse.b, 0, diffuseTex.b); // ... 

Agora podemos calcular a cor difusa introduzida por esta fonte.Se o brilho da iluminação difusa for unificado, a cor difusa será uma mistura da cor da textura difusa e da cor da iluminação. Em qualquer outro brilho, a cor difusa será mais escura.

Observe que limito a cor difusa para que não fique mais brilhante que a cor da textura difusa. Isso evitará a superexposição da cena.

Luz refletida


Após a iluminação difusa, o refletido é calculado.


  // ... vec4 specular = clamp ( vec4(p3d_Material.specular, 1) * p3d_LightSource[i].specular * pow ( max(dot(reflectedDirection, eyeDirection), 0) , p3d_Material.shininess ) , 0 , 1 ); // ... 

O brilho da luz refletida é o produto escalar entre o vetor do olho e o vetor de reflexão. Como no caso do brilho da iluminação difusa, se dois vetores estão apontando na mesma direção, o brilho da iluminação refletida é igual à unidade. Qualquer outro brilho reduzirá a quantidade de cor refletida introduzida por esta fonte de luz.


O brilho do material determina quanto a iluminação da luz refletida será dispersa. Geralmente é definido em um programa de simulação, por exemplo no Blender. No Blender, isso é chamado de dureza especular.

Holofotes


  // ... float unitLightDirectionDelta = dot ( normalize(p3d_LightSource[i].spotDirection) , -unitLightDirection ); if (unitLightDirectionDelta >= p3d_LightSource[i].spotCosCutoff) { // ... } // ... } 

Este código não permite que a iluminação afete fragmentos fora do cone ou pirâmide do refletor. Felizmente, Panda3D pode definir spotDirection e spotCosCutofftrabalhar com luzes direcionais e spot. Os holofotes têm uma posição e uma direção. No entanto, a iluminação direcional tem apenas direção e as fontes pontuais têm apenas posição. No entanto, esse código funciona para todos os três tipos de iluminação sem a necessidade de declarações if confusas.

 spotCosCutoff = cosine(0.5 * spotlightLensFovAngle); 

Se no caso da projeção iluminar o produto escalar do vetor "fonte de iluminação de fragmento" e o vetor de direção do projetor for menor que o cosseno de metade do ângulo do campo de visão do
projetor, o sombreador não leva em consideração a influência dessa fonte.

Observe que você deve alterar o sinal unitLightDirection. unitLightDirectionvai do fragmento para o holofote e precisamos passar do holofote para o fragmento, porque spotDirectionvai diretamente para o centro da pirâmide do holofote a uma certa distância da posição do holofote.

No caso de iluminação direcional e pontual, o Panda3D define o spotCosCutoffvalor como -1. Lembre-se de que o produto escalar varia no intervalo de -1 a 1. Portanto, não importa qual será unitLightDirectionDelta, pois é sempre maior ou igual a -1.

  // ... diffuse *= pow(unitLightDirectionDelta, p3d_LightSource[i].spotExponent); // ... 

Como o código unitLightDirectionDelta, esse código também funciona para todos os três tipos de fontes de luz. No caso dos refletores, os fragmentos ficam mais brilhantes à medida que se aproxima do centro da pirâmide dos refletores. Para fontes direcionais e pontuais de luz spotExponenté zero. Lembre-se de que qualquer valor à potência de zero é igual à unidade, portanto a cor difusa é igual a si mesma, multiplicada por um, ou seja, não muda.

Sombras


  // ... float shadow = textureProj ( p3d_LightSource[i].shadowMap , vertexInShadowSpaces[i] ); diffuse.rgb *= shadow; specular.rgb *= shadow; // ... 

O Panda3D simplifica o uso de sombras porque cria um mapa de sombras e uma matriz de transformação de sombras para cada fonte de luz na cena. Para criar você mesmo uma matriz de transformação, colete uma matriz que converta as coordenadas do espaço de visualização no espaço de iluminação (as coordenadas são relativas à posição da fonte de luz). Para criar um mapa de sombras você mesmo, é necessário renderizar a cena do ponto de vista da fonte de luz na textura do buffer do quadro. A textura do buffer do quadro deve conter a distância da fonte de luz aos fragmentos. Isso é chamado de "mapa de profundidade". Finalmente, você precisa transferir manualmente para o shader seu mapa de profundidade caseiro como uniform sampler2DShadowe a matriz de transformação de sombra como uniform mat4. Então, vamos recriar o que o Panda3D faz automaticamente por nós.

O snippet de código mostrado é usado textureProj, diferente da função mostrada acima texture. textureProjprimeiro divide vertexInShadowSpaces[i].xyzpor vertexInShadowSpaces[i].w. Ela então o usa vertexInShadowSpaces[i].xypara encontrar a profundidade armazenada no mapa de sombras. Então, ela usa vertexInShadowSpaces[i].zpara comparar a profundidade do topo com a profundidade do mapa de sombras vertexInShadowSpaces[i].xy. Se a comparação for bem-sucedida, ela textureProjretornará um. Caso contrário, ele retornará zero. Zero significa que esse vértice / fragmento está na sombra e um significa que o vértice / fragmento não está na sombra.

Observe que textureProjele também pode retornar um valor de zero a um, dependendo de como o mapa de sombra está configurado. Neste exemplotextureProjExecuta vários testes de profundidade com base em profundidades adjacentes e retorna uma média ponderada. Essa média ponderada pode dar suavidade às sombras.

Atenuação



  // ... float lightDistance = length(lightDirection); float attenuation = 1 / ( p3d_LightSource[i].constantAttenuation + p3d_LightSource[i].linearAttenuation * lightDistance + p3d_LightSource[i].quadraticAttenuation * (lightDistance * lightDistance) ); diffuse.rgb *= attenuation; specular.rgb *= attenuation; // ... 

A distância para a fonte de luz é simplesmente a magnitude ou o comprimento do vetor de direção da iluminação. Observe que não usamos a direção normal da iluminação, porque essa distância seria igual à unidade.

A distância da fonte de luz é necessária para calcular a atenuação. Atenuação significa que o efeito da luz longe da fonte diminui.

Parâmetros constantAttenuation, linearAttenuatione quadraticAttenuationvocê pode definir quaisquer valores. Vale a pena começar com constantAttenuation = 1, linearAttenuation = 0e quadraticAttenuation = 1. Com esses parâmetros, na posição da fonte de luz, é igual à unidade e tende a zero ao se afastar dela.

Iluminação final a cores


  // ... diffuseSpecular += (diffuse + specular); // ... 

Para calcular a cor final da iluminação, é necessário adicionar a cor difusa e refletida. É necessário adicionar isso ao inversor em um ciclo de desvio das fontes de luz na cena.

Ambiente


 // ... uniform sampler2D p3d_Texture1; // ... uniform struct { vec4 ambient ; } p3d_LightModel; // ... in vec2 diffuseCoord; // ... vec4 diffuseTex = texture(p3d_Texture1, diffuseCoord); // ... vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex; // ... 

O componente de iluminação ambiente no modelo de iluminação é baseado na cor ambiente do material, na cor da iluminação ambiente e na cor da textura difusa.

Nunca deve haver mais de uma fonte de luz ambiente; portanto, esse cálculo deve ser realizado apenas uma vez, em contraste com os cálculos de cores difusas e refletidas acumuladas para cada fonte de luz.

Observe que a cor da luz ambiente é útil ao executar o SSAO.

Juntando tudo


  // ... vec4 outputColor = ambient + diffuseSpecular + p3d_Material.emission; // ... 

A cor final é a soma da cor ambiente, cor difusa, cor refletida e cor emitida.

Código fonte



Mapas normais



O uso de mapas normais permite adicionar novas peças à superfície sem geometria adicional. Normalmente, ao trabalhar em um programa de modelagem 3D, são criadas versões alta e baixa poli da malha. Em seguida, os normais dos vértices da malha poli alta são retirados e cozidos na textura. Essa textura é um mapa normal. Em seguida, dentro do shader de fragmento, substituímos as normais dos vértices da malha poli baixa pelas normais da malha poli alta inseridas no mapa normal. Devido a isso, ao iluminar uma malha, parece que ela tem mais polígonos do que realmente é. Isso permite que você mantenha alto FPS, enquanto transmite a maioria dos detalhes da versão high-poly.


Aqui vemos a transição de um modelo de alto poli para um modelo de baixo poli e, em seguida, para um modelo de baixo poli com um mapa normal sobreposto.


No entanto, não esqueça que a sobreposição de um mapa normal é apenas uma ilusão. Em um certo ângulo, a superfície começa a parecer plana novamente.

Sombreador de vértice


 // ... uniform mat3 p3d_NormalMatrix; // ... in vec3 p3d_Normal; // ... in vec3 p3d_Binormal; in vec3 p3d_Tangent; // ... vertexNormal = normalize(p3d_NormalMatrix * p3d_Normal); binormal = normalize(p3d_NormalMatrix * p3d_Binormal); tangent = normalize(p3d_NormalMatrix * p3d_Tangent); // ... 

Começando com o vertex shader, precisamos gerar o vetor normal, o vetor binormal e o tangente para o fragment shader. Esses vetores são usados ​​no sombreador de fragmentos para transformar o normal do mapa normal do espaço tangente para o espaço de visualização.

p3d_NormalMatrixconverte os vetores normais do vetor de vértice, binormal e tangente para o espaço de visualização. Não esqueça que no espaço de visualização todas as coordenadas são relativas à posição da câmera.

[p3d_NormalMatrix] são os principais elementos de transposição reversa 3x3 do ModelViewMatrix. Essa estrutura é usada para converter o vetor normal nas coordenadas do espaço de visualização.

Fonte

 // ... in vec2 p3d_MultiTexCoord0; // ... out vec2 normalCoord; // ... normalCoord = p3d_MultiTexCoord0; // ... 


Também precisamos enviar as coordenadas UV do mapa normal para o shader de fragmentos.

Tonalizador de fragmentos


Lembre-se de que o vértice normal foi usado para calcular a iluminação. No entanto, para calcular a iluminação, o mapa normal nos dá outras normais. No shader de fragmento, precisamos substituir as normais dos vértices pelas normais localizadas no mapa normal.

 // ... uniform sampler2D p3d_Texture0; // ... in vec2 normalCoord; // ... /* Find */ vec4 normalTex = texture(p3d_Texture0, normalCoord); // ... 

Usando as coordenadas do mapa normal transferidas pelo shader de vértice, extraímos o normal correspondente do mapa.

  // ... vec3 normal; // ... /* Unpack */ normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); // ... 

Acima, mostrei como as normais são convertidas em cores para criar mapas normais. Agora precisamos reverter esse processo para que possamos obter os normais originais no mapa.

 [ r, g, b] = [ r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z] 

Aqui está como é o processo de descompactar os normais do mapa normal.

  // ... /* Transform */ normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); // ... 

As normais obtidas no mapa normal geralmente estão no espaço tangente. No entanto, eles podem estar em outro espaço. Por exemplo, o Blender permite que você crie normais no espaço tangente, no espaço de objetos, no mundo e no espaço da câmera.


Para transferir a normal do mapa normal do espaço tangente para o espaço de visualização, crie uma matriz 3x3 com base no vetor tangente, vetores binormais e vértice normal. Multiplique o normal por esta matriz e normalize-o. É aqui que acabamos com os normais. Todos os outros cálculos de iluminação ainda são realizados.

Código fonte


Source: https://habr.com/ru/post/pt453300/


All Articles