
SSAO
O tópico iluminação de fundo foi abordado por nós em uma lição sobre os
princípios básicos da iluminação , mas apenas de passagem. Deixe-me lembrá-lo: o componente de fundo da iluminação é essencialmente um valor constante que é adicionado a todos os cálculos da iluminação da cena para simular o processo
de dispersão da luz . No mundo real, a luz sofre muitas reflexões com graus variados de intensidade, o que leva a uma iluminação igualmente desigual de partes da cena iluminadas indiretamente. Obviamente, flare com intensidade constante não é muito plausível.
Um tipo de cálculo aproximado do sombreamento da iluminação indireta é o algoritmo de
oclusão ambiental (AO ), que simula a atenuação da iluminação indireta nas proximidades de cantos, rugas e outras irregularidades da superfície. Tais elementos, em geral, são significativamente sobrepostos pela geometria adjacente e, portanto, deixam menos raios de luz escaparem do lado de fora, obscurecendo essas áreas.
Abaixo está uma comparação da renderização sem e usando o algoritmo AO. Preste atenção em como a intensidade da iluminação de fundo diminui nas proximidades dos cantos das paredes e em outras quebras acentuadas na superfície:
Embora o efeito não seja muito perceptível, a presença do efeito em toda a cena lhe adiciona realismo devido à ilusão adicional de profundidade criada por pequenos detalhes do efeito de sombreamento automático.
Vale a pena notar que os algoritmos para calcular a AO são bastante intensivos em recursos, pois requerem análise da geometria circundante. Em uma implementação ingênua, seria possível simplesmente emitir muitos raios em cada ponto da superfície e determinar o grau de sua sombra, mas essa abordagem atinge rapidamente o limite de uso intensivo de recursos aceitável para aplicativos interativos. Felizmente, em 2007, a Crytek publicou um artigo descrevendo sua própria abordagem para implementar o algoritmo de
oclusão ambiental de espaço de tela (SSAO ) usado na versão de lançamento do Crysis. A abordagem calculou o grau de sombreamento no espaço da tela, usando apenas o buffer de profundidade atual em vez de dados reais sobre a geometria circundante. Essa otimização acelerou radicalmente o algoritmo em comparação com a implementação de referência e, ao mesmo tempo, deu resultados quase plausíveis, o que tornou essa abordagem de cálculo aproximado do sombreamento de segundo plano uma indústria de fato padrão.
O princípio no qual o algoritmo se baseia é bastante simples: para cada fragmento de um quad de tela cheia, o
fator de oclusão é calculado com base nos valores de profundidade dos fragmentos circundantes. O coeficiente de sombreamento calculado é então usado para reduzir a intensidade da iluminação de fundo (até a exclusão completa). A obtenção de um coeficiente requer a coleta de dados de profundidade de uma pluralidade de amostras da região esférica ao redor do fragmento em questão e a comparação desses valores de profundidade com a profundidade do fragmento em questão. O número de amostras com profundidade maior que o fragmento atual determina diretamente o coeficiente de sombreamento. Veja este diagrama:
Aqui, cada ponto cinza fica dentro de um determinado objeto geométrico e, portanto, contribui para o valor do coeficiente de sombreamento. Quanto mais amostras estiverem dentro da geometria dos objetos circundantes, menor será a intensidade residual do sombreamento de fundo nessa área.
Obviamente, a qualidade e o realismo do efeito dependem diretamente do número de amostras colhidas. Com um pequeno número de amostras, a precisão do algoritmo diminui e leva ao aparecimento de um artefato de
faixas ou "
faixas " devido a transições abruptas entre regiões com coeficientes de sombreamento muito diferentes. Um grande número de amostras simplesmente mata o desempenho. A randomização do núcleo das amostras permite resultados um pouco semelhantes para reduzir levemente o número de amostras necessárias. Reorientação por rotação para um ângulo aleatório de um conjunto de vetores de amostra está implícita. No entanto, a introdução da aleatoriedade traz imediatamente um novo problema na forma de um padrão de ruído perceptível, que requer o uso de filtros de desfoque para suavizar o resultado. Abaixo está um exemplo do algoritmo (autor -
John Chapman ) e seus problemas típicos: padrão de faixas e ruído.
Como pode ser visto, uma banda perceptível devido ao pequeno número de amostras é bem removida através da introdução de randomização da orientação das amostras.
A implementação específica do SSAO da Crytek tinha um estilo visual reconhecível. Como os especialistas da Crytek usavam um núcleo esférico da amostra, isso afetava até superfícies planas, como paredes, tornando-as sombreadas - porque metade do volume do núcleo da amostra estava submersa sob a geometria. Abaixo está uma captura de tela de uma cena do Crysis mostrada em escala de cinza com base no valor do fator de sombreamento. Aqui o efeito de "cinza" é claramente visível:
Para evitar esse efeito, passaremos do núcleo esférico da amostra para um hemisfério orientado ao longo do normal para a superfície:
Ao coletar amostras desse
hemisfério hemisfério de orientação normal, não precisamos levar em consideração os fragmentos que estão sob a superfície da superfície adjacente no cálculo do coeficiente de sombreamento. Essa abordagem remove sombreamentos desnecessários, em geral, fornece resultados mais realistas. Esta lição usará a abordagem do hemisfério e um código um pouco mais refinado da brilhante lição da SSAO de
John Chapman .
Buffer de dados brutos
O processo de cálculo do fator de sombreamento em cada fragmento requer a disponibilidade de dados sobre a geometria circundante. Especificamente, precisamos dos seguintes dados:
- Vetor de posição para cada fragmento;
- Vetor normal para cada fragmento;
- Cor difusa para cada fragmento;
- O núcleo da amostra
- Um vetor de rotação aleatória para cada fragmento usado na reorientação do núcleo da amostra.
Usando dados sobre as coordenadas do fragmento no espaço de espécies, podemos orientar o hemisfério do núcleo da amostra ao longo do vetor normal especificado no espaço de espécies para o fragmento atual. Em seguida, o núcleo resultante é usado para fazer amostras com várias compensações de uma textura que armazena dados nas coordenadas dos fragmentos. Fazemos muitas amostras em cada fragmento e, para cada amostra que fazemos, comparamos seu valor de profundidade com o valor de profundidade do buffer de coordenadas do fragmento para estimar a quantidade de sombreamento. O valor resultante é então usado para limitar a contribuição do componente de plano de fundo no cálculo final da iluminação. Usando um vetor de rotação aleatória fragmentada, podemos reduzir significativamente o número necessário de amostras para obter um resultado decente, e isso será demonstrado.

Como o SSAO é um efeito realizado no espaço da tela, é possível realizar um cálculo direto renderizando um quad em tela cheia. Mas então não teremos dados sobre a geometria da cena. Para contornar essa restrição, renderizaremos todas as informações necessárias na textura, que serão posteriormente usadas no shader SSAO para acessar informações geométricas e outras informações sobre a cena. Se você seguiu cuidadosamente essas lições, já deve saber, na abordagem descrita, a aparência do algoritmo de sombreamento atrasado. É por isso que o efeito SSAO como nativo aparece na renderização com sombreamento adiado - afinal, as texturas que armazenam coordenadas e normais já estão disponíveis no buffer G.
Nesta lição, o efeito é implementado em cima de uma versão ligeiramente simplificada do código da lição sobre iluminação adiada . Se você ainda não se familiarizou com os princípios da iluminação diferida, recomendo fortemente que você volte para esta lição.
Como o acesso a informações sobre fragmentos sobre coordenadas e normais já deve estar disponível devido ao buffer G, o sombreador de fragmentos do estágio de processamento da geometria é bastante simples:
#version 330 core layout (location = 0) out vec4 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; void main() {
Como o algoritmo SSAO é um efeito no espaço da tela e o fator de sombreamento é calculado com base na área visível da cena, faz sentido realizar cálculos no espaço de visualização. Nesse caso, a variável
FragPos obtida do sombreador de vértice armazena a posição exatamente na viewport. Vale a pena garantir que as coordenadas e normais sejam armazenadas no buffer G no espaço de visualização, pois todos os cálculos adicionais serão realizados nele.
Existe a possibilidade de restaurar o vetor de posição com base apenas em uma profundidade de fragmento conhecida e em uma certa quantidade de mágica matemática, que é descrita, por exemplo, no blog de Matt Pettineo. Obviamente, isso exige um grande custo de cálculo, mas elimina a necessidade de armazenar dados de posição no buffer G, que consome muita memória de vídeo. No entanto, por uma questão de simplicidade, o código de exemplo deixará essa abordagem para estudo pessoal.
A textura do buffer de cores
gPosition está configurada da seguinte maneira:
glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
Essa textura armazena as coordenadas dos fragmentos e pode ser usada para obter dados de profundidade de cada ponto do núcleo das amostras. Observo que a textura usa um formato de dados de ponto flutuante - isso permitirá que as coordenadas dos fragmentos não sejam reduzidas ao intervalo [0., 1.]. Preste atenção também ao modo de repetição -
GL_CLAMP_TO_EDGE está definido. Isso é necessário para eliminar a possibilidade de não amostrar demais no espaço da tela de propósito. Ir além do intervalo principal de coordenadas de textura nos fornecerá dados incorretos de posição e profundidade.
A seguir, nos envolveremos na formação de um núcleo hemisférico das amostras e na criação de um método de orientação aleatória.
Criando um hemisfério de orientação normal
Portanto, a tarefa é criar um conjunto de pontos de amostra localizados dentro de um hemisfério orientado ao longo do normal para a superfície. Como a criação de um núcleo de amostra para todas as direções possíveis do normal é computacionalmente inatingível, usamos a transição para o
espaço tangente , onde o normal é sempre representado como um vetor na direção da semiaxe positiva
Z.Assumindo que o raio do hemisfério seja um processo único, a formação de um núcleo de uma amostra de 64 pontos se parece com isso:
Aqui, selecionamos aleatoriamente as coordenadas
x e
y no intervalo [-1., 1.] e a coordenada
z no intervalo [0., 1.] (se o intervalo for o mesmo que para
x e
y , obteríamos um núcleo esférico amostragem). Os vetores de amostra resultantes serão limitados aos hemisférios, uma vez que o núcleo da amostra será orientado ao longo do normal para a superfície.
No momento, todos os pontos da amostra são distribuídos aleatoriamente no interior do núcleo, mas, para garantir a qualidade do efeito, as amostras mais próximas da origem do núcleo devem dar uma contribuição maior ao cálculo do coeficiente de sombreamento. Isso pode ser realizado alterando a distribuição dos pontos de amostra formados, aumentando sua densidade perto da origem. Esta tarefa é facilmente realizada usando a função de interpolação de aceleração:
scale = lerp(0.1f, 1.0f, scale * scale); sample *= scale; ssaoKernel.push_back(sample); }
A função
lerp () é definida como:
float lerp(float a, float b, float f) { return a + f * (b - a); }
Esse truque nos dá uma distribuição modificada, onde a maioria dos pontos de amostra fica perto da origem do kernel.
Cada um dos vetores de amostra obtidos será usado para alterar a coordenada do fragmento no espaço de espécies para obter dados sobre a geometria circundante. Para obter resultados decentes ao trabalhar na viewport, você pode precisar de um número impressionante de amostras, o que inevitavelmente afetará o desempenho. No entanto, a introdução de ruído pseudo-aleatório ou rotação dos vetores de amostra em cada fragmento processado reduzirá significativamente o número necessário de amostras com qualidade comparável.
Rotação aleatória do núcleo da amostra
Portanto, a introdução da aleatoriedade na distribuição de pontos no núcleo da amostra pode reduzir significativamente a necessidade de o número desses pontos obter um efeito de qualidade decente. Seria possível criar um vetor de rotação aleatória para cada fragmento da cena, mas é muito caro de memória. É mais eficiente criar uma textura pequena contendo um conjunto de vetores de rotação aleatória e usá-la com o modo de repetição
GL_REPEAT definido .
Crie uma matriz 4x4 e preencha-a com vetores de rotação aleatória orientados ao longo do vetor normal no espaço tangente:
std::vector<glm::vec3> ssaoNoise; for (unsigned int i = 0; i < 16; i++) { glm::vec3 noise( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise); }
Como o núcleo está alinhado ao longo da semiaxe positiva
Z no espaço tangente, deixamos o componente
z igual a zero - isso garantirá a rotação apenas em torno do eixo
Z.Em seguida, crie uma textura 4x4 e preencha-a com nossa matriz de vetores de rotação. Certifique-se de usar o
modo de reprodução
GL_REPEAT para texturizar lado a lado:
unsigned int noiseTexture; glGenTextures(1, &noiseTexture); glBindTexture(GL_TEXTURE_2D, noiseTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
Bem, agora temos todos os dados necessários para a implementação direta do algoritmo SSAO!
Shader SSAO
Um sombreador de efeito será executado para cada fragmento de um quad de tela cheia, calculando o coeficiente de sombreamento em cada um deles. Como os resultados serão usados em outro estágio de renderização que cria a iluminação final, precisamos criar outro objeto buffer de estrutura para armazenar o resultado do shader:
unsigned int ssaoFBO; glGenFramebuffers(1, &ssaoFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); unsigned int ssaoColorBuffer; glGenTextures(1, &ssaoColorBuffer); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0);
Como o resultado do algoritmo é o único número real dentro de [0., 1.], para armazenamento, será suficiente criar uma textura com o único componente disponível. É por isso que
GL_RED é definido como o formato interno para o buffer de cores.
Em geral, o processo de renderização do estágio SSAO se parece com isso:
O
shaderSSAO aceita as texturas de buffer G necessárias como entrada, assim como a textura do ruído e o núcleo da amostra:
#version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D texNoise; uniform vec3 samples[64]; uniform mat4 projection;
Observe a variável
noiseScale . Nossa pequena textura com ruído deve estar lado a lado em toda a superfície da tela, mas como as coordenadas da
TexCoords estão dentro de [0., 1.], isso não acontecerá sem a nossa intervenção. Para esses fins, calculamos o fator para as coordenadas da textura, que é encontrado como a proporção do tamanho da tela e o tamanho da textura do ruído:
vec3 fragPos = texture(gPosition, TexCoords).xyz; vec3 normal = texture(gNormal, TexCoords).rgb; vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;
Como ao criar a textura de ruído
texNoise , definimos o modo de repetição como
GL_REPEAT , agora ele será repetido várias vezes na superfície da tela. Com
randomVec ,
fragPos e valores
normais disponíveis, podemos criar uma matriz de transformação TBN do tangente para o espaço da espécie:
vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal);
Usando o processo de Gram-Schmidt, criamos uma base ortogonal aleatoriamente inclinada em cada fragmento com base no valor aleatório
randomVec . Um ponto importante: como nesse caso, não importa para nós que a matriz TBN esteja exatamente orientada ao longo da superfície do triângulo (como no caso do mapeamento de paralaxe, aprox.
Em seguida, examinamos a matriz do núcleo da amostra, convertemos cada vetor de amostra do espaço tangente para o espaço da espécie e obtemos sua soma com a posição atual do fragmento. Em seguida, comparamos o valor da profundidade da quantidade resultante com o valor da profundidade obtido por amostragem da textura do buffer G correspondente.
Embora pareça confuso, vamos seguir as etapas:
float occlusion = 0.0; for(int i = 0; i < kernelSize; ++i) {
Aqui,
kernelSize e
radius são variáveis que controlam as características do efeito. Nesse caso, eles são 64 e 0,5, respectivamente. A cada iteração, traduzimos o vetor principal da amostra no espaço de espécies. Em seguida, adicionamos ao valor obtido o deslocamento da amostra no espaço de espécies o valor da posição do fragmento no espaço de espécies. Nesse caso, o valor do deslocamento é multiplicado pela variável radius, que controla o raio do núcleo da amostra de efeito SSAO.
Após essas etapas, devemos converter o vetor de
amostra resultante no espaço da tela, para que possamos selecionar a textura do buffer G que armazena as posições e profundidades dos fragmentos usando o valor projetado obtido. Como a
amostra está na viewport, precisamos da matriz de
projeção :
vec4 offset = vec4(sample, 1.0); offset = projection * offset;
Após a conversão no espaço do clipe, executamos manualmente a divisão da perspectiva, simplesmente dividindo os componentes
xyz pelo componente
w . O vetor resultante em coordenadas de dispositivo normalizadas (
NDC ) é convertido no intervalo de valores [0., 1.] para que possa ser usado como coordenadas de textura:
float sampleDepth = texture(gPosition, offset.xy).z;
Utilizamos os componentes
xy do vetor de
amostra para selecionar a partir da textura as posições do buffer G. Obtemos o valor de profundidade (componentes
z ) correspondente ao vetor de amostra quando visto da posição do observador (este é o primeiro fragmento visível não blindado). Se ao mesmo tempo a profundidade de amostragem obtida for maior que a profundidade armazenada, aumentamos o coeficiente de sombreamento:
occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0);
Observe o deslocamento da
polarização , que é adicionado à profundidade do fragmento original (definido no exemplo como 0,025). Esse deslocamento nem sempre é necessário, mas a presença de uma variável permite controlar a aparência do efeito SSAO e também, em determinadas situações, remove problemas com ondulações nas áreas sombreadas.
Mas isso não é tudo, pois essa implementação leva a artefatos visíveis. Ela se manifesta nos casos em que é considerado um fragmento próximo à borda de uma determinada superfície. Em tais situações, ao comparar as profundidades, o algoritmo captura inevitavelmente as profundidades das superfícies, que podem estar muito atrás do considerado. Nesses locais, o algoritmo aumenta erroneamente o grau de sombreamento, o que criará halos escuros perceptíveis nas bordas dos objetos. O artefato é tratado introduzindo uma verificação de distância adicional (um exemplo de
John Chapman ):
A verificação limitará a contribuição ao coeficiente de sombreamento apenas para valores de profundidade dentro do raio da amostra:
float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth)); occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0) * rangeCheck;
Também usamos a função GLSL
smoothstep () , que implementa a interpolação suave do terceiro parâmetro entre o primeiro e o segundo. Nesse caso, retorne 0 se o terceiro parâmetro for menor ou igual ao primeiro ou 1 se o terceiro parâmetro for maior que ou igual ao segundo. Se a diferença de profundidade estiver dentro do
raio , seu valor será suavizado no intervalo [0., 1.] de acordo com esta curva:
Se usássemos limites claros nas condições de verificação da profundidade, isso adicionaria artefatos na forma de limites nítidos nos locais em que os valores da diferença nas profundidades estão fora dos limites do
raio .
Com o toque final, normalizamos o valor do coeficiente de sombreamento usando o tamanho do núcleo da amostra e registramos o resultado. Também invertemos o valor final subtraindo-o da unidade, para que você possa usar o valor final diretamente para modular o componente de fundo da iluminação sem etapas adicionais:
} occlusion = 1.0 - (occlusion / kernelSize); FragColor = occlusion;
Para uma cena com um nanossuit mentiroso familiar para nós, executar o shader SSAO fornece a seguinte textura:
Como você pode ver, o efeito do sombreamento de fundo cria uma boa ilusão de profundidade. Somente a imagem de saída do shader já permite distinguir os detalhes da roupa e garantir que ela realmente fique no chão e não levite a alguma distância dela.
No entanto, o efeito está longe de ser ideal, uma vez que o padrão de ruído introduzido pela textura dos vetores de rotação aleatória é facilmente perceptível. Para suavizar o resultado do cálculo do SSAO, aplicamos um filtro de desfoque.
Desfocar sombreamento de fundo
Depois de criar o resultado do SSAO e antes da mistura final da iluminação, é necessário desfocar a textura que armazena dados no coeficiente de sombreamento. Para fazer isso, teremos outro buffer de quadro:
unsigned int ssaoBlurFBO, ssaoColorBufferBlur; glGenFramebuffers(1, &ssaoBlurFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO); glGenTextures(1, &ssaoColorBufferBlur); glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0);
A colocação de uma textura de ruído no espaço da tela fornece uma característica de aleatoriedade bem definida que você pode usar para sua vantagem ao criar um filtro de desfoque:
#version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D ssaoInput; void main() { vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0)); float result = 0.0; for (int x = -2; x < 2; ++x) { for (int y = -2; y < 2; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; result += texture(ssaoInput, TexCoords + offset).r; } } FragColor = result / (4.0 * 4.0); }
O sombreador simplesmente transita texels da textura SSAO com um deslocamento de -2 para +2, o que corresponde ao tamanho real da textura de ruído. O deslocamento é igual ao tamanho exato de um texel: a função textureSize
() é usada para o cálculo, que retorna
vec2 com as dimensões da textura especificada. T.O. O sombreador calcula a média dos resultados armazenados na textura, o que fornece um desfoque rápido e bastante eficaz:
No total, temos uma textura com dados de sombreamento de fundo para cada fragmento na tela - tudo está pronto para o estágio de redução final da imagem!
Aplicar sombreamento em segundo plano
A etapa de aplicação do coeficiente de sombreamento no cálculo final da iluminação é surpreendentemente simples: para cada fragmento, basta multiplicar o valor do componente de fundo da fonte de luz pelo coeficiente de sombreamento da textura preparada. Você pode pegar um shader pronto com o modelo Blinn-Fong da lição sobre
sombreamento adiado e corrigi-lo um pouco:
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D ssao; struct Light { vec3 Position; vec3 Color; float Linear; float Quadratic; float Radius; }; uniform Light light; void main() {
Existem apenas duas mudanças principais: a transição para os cálculos na viewport e a multiplicação do componente de iluminação de fundo pelo valor de
AmbientOcclusion . Um exemplo de cena com um único ponto de luz azul:
O código fonte completo está
aqui .
A manifestação do efeito SSAO depende fortemente de parâmetros como
kernelSize ,
raio e
viés , geralmente é necessário que você faça o ajuste fino deles para que o artista
elabore um local / cena específico. Não há combinações "melhores" e universais de parâmetros: para algumas cenas, um pequeno raio do núcleo da amostra é bom, enquanto outras se beneficiam do aumento do raio e do número de amostras. O exemplo usa 64 pontos de amostra, o que, francamente, é redundante, mas você sempre pode editar o código e ver o que acontece com um número menor de amostras.
Além dos uniformes listados, responsáveis por definir o efeito, existe a possibilidade de controlar explicitamente a gravidade do efeito de sombreamento em segundo plano. Para isso, basta elevar o coeficiente para um grau controlado por outro uniforme:
occlusion = 1.0 - (occlusion / kernelSize); FragColor = pow(occlusion, power);
Aconselho que você gaste algum tempo no jogo com as configurações, pois isso dará uma melhor compreensão da natureza das alterações na imagem final.
Para resumir, vale a pena dizer que, embora o efeito visual da aplicação do SSAO seja bastante sutil, em cenas com iluminação bem posicionada, inegavelmente acrescenta uma fração notável do realismo. Ter essa ferramenta em seu arsenal é certamente valioso.Recursos Adicionais
- Tutorial do SSAO : Um excelente artigo de lição de John Chapman, com base no qual o código para esta lição é criado.
- Conheça os artefatos do SSAO : um artigo muito valioso mostrando lucidamente não apenas os problemas mais prementes com a qualidade do SSAO, mas também maneiras de resolvê-los. Leitura recomendada.
- SSAO com reconstrução de profundidade : Adendo à lição principal do SSAO do OGLDev sobre uma técnica comumente usada para restaurar coordenadas de fragmentos com base na profundidade. A importância dessa abordagem se deve à economia significativa de memória devido à falta de necessidade de armazenar posições no buffer G. A abordagem é tão universal que se aplica ao SSAO na medida em que.
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!