Aprenda o OpenGL. Lição 6.3 - Iluminação Baseada em Imagem. Irradiação difusa

OGL3 A iluminação baseada na imagem ou IBL ( Iluminação Baseada em Imagem ) é uma categoria de métodos de iluminação baseados não na contabilização de fontes de luz analíticas (discutidas na lição anterior ), mas considerando todo o ambiente dos objetos iluminados como uma fonte de luz contínua. No caso geral, a base técnica de tais métodos reside no processamento de um mapa cúbico do ambiente (preparado no mundo real ou criado com base em uma cena tridimensional), para que os dados armazenados no mapa possam ser usados ​​diretamente nos cálculos de iluminação: de fato, todo texel do mapa cúbico é considerado uma fonte de luz . Em geral, isso permite capturar o efeito da iluminação global na cena, que é um componente importante que transmite o “tom” geral da cena atual e ajuda os objetos iluminados a serem melhor “incorporados” nela.

Como os algoritmos IBL levam em consideração a iluminação de um ambiente “global”, o resultado é considerado uma simulação mais precisa da iluminação de fundo ou até uma aproximação aproximada da iluminação global. Esse aspecto torna os métodos de IBL interessantes em termos de incorporação ao modelo PBR, pois o uso da luz ambiente no modelo de iluminação permite que os objetos pareçam muito mais fisicamente corretos.


Para incorporar a influência do IBL no sistema PBR já descrito, retornamos à equação de refletância familiar:

Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai


Conforme descrito anteriormente, o objetivo principal é calcular a integral para todas as direções de radiação recebidas wi hemisfério  Omega . Na última lição, o cálculo da integral não foi oneroso, pois sabíamos antecipadamente o número de fontes de luz e, portanto, todas as várias direções de incidência de luz que correspondiam a elas. Ao mesmo tempo, a integral não pode ser resolvida rapidamente: qualquer vetor em queda wi do ambiente pode levar brilho de energia diferente de zero. Como resultado, para a aplicabilidade prática do método, é necessário atender aos seguintes requisitos:
  • Você precisa encontrar uma maneira de obter o brilho de energia da cena para um vetor de direção arbitrário wi ;
  • É necessário que a solução da integral possa ocorrer em tempo real.

Bem, o primeiro ponto é resolvido por si só. Uma dica de solução já chegou aqui: um dos métodos para representar a irradiação de uma cena ou ambiente é um mapa cúbico que passou por um processamento especial. Cada texel desse mapa pode ser considerado uma fonte emissora separada. Amostrando a partir de tal mapa de acordo com um vetor arbitrário wi facilmente obtemos o brilho energético da cena nessa direção.

Então, obtemos o brilho de energia da cena para um vetor arbitrário wi :

vec3 radiance = texture(_cubemapEnvironment, w_i).rgb; 

Surpreendentemente, no entanto, resolver a integral exige que façamos amostras do mapa ambiental, não de uma direção, mas de todas as possíveis no hemisfério. E assim - para cada fragmento sombreado. Obviamente, para tarefas em tempo real, isso é praticamente impraticável. Um método mais eficaz seria calcular parte das operações do integrando antecipadamente, mesmo fora de nosso aplicativo. Mas, para isso, você terá que arregaçar as mangas e mergulhar mais fundo na essência da expressão da refletividade:

Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai



Pode-se observar que as partes da expressão relacionadas à difusa kd e espelho ks Os componentes BRDF são independentes. Você pode dividir a integral em duas partes:

Lo(p, omegao)= int limits Omega(kd fracc pi)Li(p, omegai)n cdot omegaid omegai+ int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai



Essa divisão em partes nos permitirá lidar com cada um deles individualmente, e nesta lição trataremos da parte responsável pela iluminação difusa.

Tendo analisado a forma da integral sobre o componente difuso, podemos concluir que o componente difuso de Lambert é essencialmente constante (cor s índice de refração kd e  pi são constantes nas condições do integrando) e não depende de outras variáveis. Diante desse fato, podemos colocar as constantes além do sinal da integral:

Lo(p, omegao)=kd fracc pi int limit OmegaLi(p, omegai)n cdot omegaid omegai



Então obtemos uma integral dependendo apenas da wi (assume-se que p corresponde ao centro do mapa cúbico do ambiente). Com base nessa fórmula, é possível calcular ou, melhor ainda, pré-calcular um novo mapa cúbico que armazena o resultado do cálculo da integral do componente difuso para cada direção da amostra (ou mapa texel) wo usando a operação de convolução.

Convolução é a operação de aplicar algum cálculo a cada elemento em um conjunto de dados, levando em consideração os dados de todos os outros elementos no conjunto. Nesse caso, esses dados são o brilho da energia da cena ou do mapa do ambiente. Assim, para calcular um valor em cada direção da amostra no mapa cúbico, teremos que levar em consideração os valores obtidos de todas as outras direções possíveis da amostra no hemisfério, ao redor do ponto de amostra.

Para envolver o mapa do ambiente, você precisa resolver a integral para cada direção resultante da amostra wo executando várias amostras discretas ao longo das instruções wi pertencente ao hemisfério  Omega e calculando a média do brilho total da energia. O hemisfério, com base no qual são tomadas as direções de amostragem wi orientado ao longo do vetor wo representando a direção de destino para a qual a convolução atual está sendo calculada. Veja a imagem para uma melhor compreensão:



Um mapa cúbico pré-calculado que armazena o resultado da integração para cada direção da amostra wo também pode ser considerado como armazenamento do resultado da soma de toda a iluminação difusa indireta na cena, incidente em uma determinada superfície orientada ao longo da direção wo . Em outras palavras, esses mapas cúbicos são chamados de mapas de irradiância, porque o mapa do ambiente cúbico pré-convolucional permite a amostra direta da magnitude da irradiação da cena, proveniente de uma direção arbitrária wo , sem cálculos adicionais.
A expressão que determina o brilho da energia também depende da posição do ponto de amostragem p que levamos bem no centro do mapa de irradiação. Essa suposição impõe uma limitação no sentido de que a fonte de toda iluminação indireta difusa também será um único mapa ambiental. Em cenas heterogêneas na iluminação, isso pode destruir a ilusão da realidade (especialmente em cenas internas). Os modernos mecanismos de renderização resolvem esse problema colocando objetos auxiliares especiais na sonda de reflexão de cena. Cada um desses objetos está envolvido em uma tarefa: ele forma seu próprio mapa de irradiação para o seu ambiente imediato. Com esta técnica, a irradiação (e brilho da energia) em um ponto arbitrário p será determinado por interpolação simples entre as amostras de reflexão mais próximas. Porém, para as tarefas atuais, concordamos que o mapa do ambiente é amostrado desde o centro e analisamos amostras de reflexão em outras lições.
Abaixo está um exemplo de um mapa cúbico do ambiente e um mapa de irradiação (baseado no mecanismo de ondas ) derivado dele, que calcula a média do brilho energético do ambiente para cada direção de saída wo .

Portanto, este cartão armazena o resultado da convolução em cada texel (correspondente à direção wo ) e externamente esse mapa parece armazenar a cor média do mapa do ambiente. Uma amostra em qualquer direção desse mapa retornará o valor da irradiação que emana dessa direção.

PBR e HDR


Na lição anterior , já foi observado brevemente que, para a operação correta do modelo de iluminação PBR, é extremamente importante levar em consideração a faixa de brilho HDR das fontes de luz presentes. Como o modelo PBR na entrada aceita parâmetros de uma maneira ou de outra com base em quantidades e características físicas muito específicas, é lógico exigir que o brilho da energia das fontes de luz corresponda aos seus protótipos reais. Não importa como justificamos o valor específico do fluxo de radiação para cada fonte: fazemos uma estimativa aproximada da engenharia ou recorremos a quantidades físicas - a diferença nas características entre uma lâmpada da sala e o sol será enorme em qualquer caso. Sem o uso da faixa HDR , será simplesmente impossível determinar com precisão o brilho relativo de uma variedade de fontes de luz.

Portanto, PBR e HDR são amigos para sempre, isso é compreensível, mas como esse fato se relaciona com os métodos de iluminação baseados em imagem? Na última lição, foi mostrado que a conversão de PBR para o intervalo de renderização HDR é fácil. Resta um "mas": como a iluminação indireta do ambiente é baseada em um mapa cúbico do ambiente, é necessário um caminho para preservar as características HDR dessa iluminação de fundo no mapa do ambiente.

Até agora, usamos mapas de ambiente criados no formato LDR (como caixas de passagem ). Usamos a amostra de cores deles na renderização como está e isso é bastante aceitável para o sombreamento direto dos objetos. E é completamente inadequado ao usar mapas do ambiente como fontes de medições fisicamente confiáveis.

RGBE - formato de imagem HDR


Familiarize-se com o formato de arquivo de imagem RGBE. Arquivos com a extensão " .hdr " são usados ​​para armazenar imagens com um amplo intervalo dinâmico, alocando um byte para cada elemento da tríade de cores e mais um byte para o expoente comum. O formato também permite armazenar mapas cúbicos do ambiente com um intervalo de intensidade de cores além do intervalo LDR [0., 1.]. Isso significa que as fontes de luz podem manter sua intensidade real, sendo representadas por um mapa do ambiente.

A rede possui muitos mapas de ambiente gratuitos no formato RGBE, gravados em várias condições reais. Aqui está um exemplo do site de arquivamento sIBL :


Você pode se surpreender com o que viu: afinal, essa imagem distorcida não se parece em nada com um mapa cúbico comum com sua quebra acentuada em seis faces. A explicação é simples: esse mapa do ambiente foi projetado de uma esfera para um plano - uma varredura retangular igual foi aplicada. Isso é feito para poder armazenar em um formato que não suporta o modo de armazenamento de cartões cúbicos como está. Obviamente, esse método de projeção tem suas desvantagens: a resolução horizontal é muito maior que a vertical. Na maioria dos casos de aplicação na renderização, essa é uma proporção aceitável, pois geralmente detalhes interessantes do ambiente e da iluminação estão localizados exatamente no plano horizontal, e não no vertical. Bem, além de tudo, precisamos do código de conversão de volta ao mapa cúbico.

Suporte para o formato RGBE em stb_image.h


O download deste formato de imagem por conta própria requer conhecimento da especificação do formato , o que não é difícil, mas ainda trabalhoso. Felizmente para nós , a biblioteca de carregamento de imagens stb_image.h , implementada em um único arquivo de cabeçalho, suporta o carregamento de arquivos RGBE, retornando uma matriz de números de ponto flutuante - o que precisamos para nossos propósitos! Adicionar uma biblioteca ao seu projeto, carregar dados de imagem é extremamente simples:

 #include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; } 

A biblioteca converte automaticamente valores do formato HDR interno em números reais reais de 32 bits, com três canais de cores por padrão. É suficiente salvar os dados da imagem HDR original em uma textura 2D de ponto flutuante normal.

Converter uma varredura de ângulo igual em um mapa cúbico


Uma varredura igualmente retangular pode ser usada para selecionar diretamente amostras do mapa do ambiente, no entanto, isso exigiria operações matemáticas caras, enquanto a busca em um mapa cúbico normal seria praticamente livre de desempenho. É precisamente a partir dessas considerações que nesta lição trataremos da conversão de uma imagem igualmente retangular em um mapa cúbico, que será usado posteriormente. No entanto, o método de amostragem direta de um mapa igualmente retangular usando um vetor tridimensional também será mostrado aqui, para que você possa escolher o método de trabalho mais adequado a você.

Para converter, você precisa desenhar um cubo do tamanho de uma unidade, observando-o por dentro, projetar um mapa retangular igual em suas faces e extrair seis imagens das faces como as faces do mapa cúbico. O shader de vértice desse estágio é bastante simples: ele simplesmente processa os vértices do cubo como está e também passa suas posições não reformadas para o shader de fragmento para uso como um vetor de amostra tridimensional:

 #version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); } 

No sombreador do fragmento, sombreamos cada face do cubo como se estivéssemos tentando envolver suavemente o cubo com uma folha com um mapa igualmente retangular. Para fazer isso, a direção da amostra transferida para o shader de fragmento é obtida, processada por mágica trigonométrica especial e, finalmente, a seleção é feita a partir de um mapa retangular igual a como se fosse realmente um mapa cúbico. O resultado da seleção é salvo diretamente como a cor do fragmento da face do cubo:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos   vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); } 

Se você realmente desenhar um cubo com esse shader e um mapa de ambiente HDR associado, obterá algo como isto:


I.e. pode-se ver que de fato projetamos uma textura retangular em um cubo. Ótimo, mas como isso nos ajudará a criar um mapa cúbico real? Para finalizar esta tarefa, é necessário renderizar o mesmo cubo 6 vezes com uma câmera olhando para cada um dos rostos, enquanto grava a saída em um objeto de buffer de quadro separado:

 unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO); 

Obviamente, não esqueceremos de organizar a memória para armazenar cada uma das seis faces do futuro mapa cúbico:

 unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) { //  ,     // 16     glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

Após essa preparação, resta apenas executar diretamente a transferência de partes de um mapa retangular igual à beira de um mapa cúbico.

Não entraremos em muitos detalhes, especialmente porque o código se repete muito visto nas lições sobre o buffer de quadros e sombras omnidirecionais . Em princípio, tudo se resume a preparar seis matrizes de vista separadas, orientando estritamente a câmera para cada uma das faces do cubo, bem como uma matriz de projeção especial com um ângulo de visão de 90 ° para capturar toda a face do cubo. Em seguida, apenas seis vezes, a renderização é executada e o resultado é salvo em um buffer de moldura de ponto flutuante:

 glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); glm::mat4 captureViews[] = { glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) }; //  HDR        equirectangularToCubemapShader.use(); equirectangularToCubemapShader.setInt("equirectangularMap", 0); equirectangularToCubemapShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrTexture); //         glViewport(0, 0, 512, 512); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { equirectangularToCubemapShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); //    } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Aqui, a cor do buffer do quadro é anexada e alterando alternadamente a face conectada do mapa cúbico, o que leva à saída direta da renderização para uma das faces do mapa do ambiente. Esse código precisa ser executado apenas uma vez, após o qual ainda teremos um mapa do ambiente envCubemap completo contendo o resultado da conversão da versão original retangular do mapa do ambiente HDR.

Testaremos o mapa cúbico resultante esboçando o shader mais simples do skybox:

 #version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; //         mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; } 

Preste atenção ao truque com os componentes do vetor clipPos : usamos o xyww tetrad ao gravar a coordenada transformada do vértice para garantir que todos os fragmentos da caixa do céu tenham uma profundidade máxima de 1,0 (a abordagem já foi usada na lição correspondente ). Não se esqueça de alterar a função de comparação para GL_LEQUAL :

 glDepthFunc(GL_LEQUAL); 

O shader de fragmento simplesmente seleciona em um mapa cúbico:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); } 

A seleção do mapa é baseada nas coordenadas locais interpoladas dos vértices do cubo, que é a direção correta da seleção neste caso (novamente, discutido na lição sobre caixas de céu, aprox. Por. ). Como os componentes de transporte na matriz da vista foram ignorados, a renderização da skybox não dependerá da posição do observador, criando a ilusão de um fundo infinitamente distante. Como aqui produzimos dados diretamente do cartão HDR para o buffer de quadros padrão, que é o receptor LDR, é necessário recuperar a compressão tonal. E, finalmente, quase todas as placas HDR são armazenadas no espaço linear, o que significa que a correção gama deve ser aplicada como acorde de processamento final.

Portanto, ao produzir a caixa skybox obtida, junto com a matriz já familiar de esferas, obtém-se algo semelhante:


Bem, muito esforço foi gasto, mas, no final, nos acostumamos com sucesso a ler o mapa do ambiente HDR, convertendo-o de um mapa equilátero para um mapa cúbico e exibindo o mapa cúbico do HDR como uma skybox na cena. Além disso, o código para converter em um mapa cúbico renderizando seis faces de um mapa cúbico é útil para nós na tarefa de convolução de um mapa do ambiente . O código para todo o processo de conversão está aqui .

Convolução de um cartão cúbico


Como foi dito no início da lição, nosso principal objetivo é resolver a integral para todas as direções possíveis da iluminação difusa indireta, levando em consideração a irradiação da cena dada na forma de um mapa cúbico do ambiente. Sabe-se que podemos obter o valor do brilho energético da cena L(p,wi) para direção arbitrária wi amostrando do HDR um mapa cúbico do ambiente nessa direção. Para resolver a integral, será necessário amostrar o brilho da cena em todas as direções possíveis no hemisfério.  Omega cada fragmento revisado.
Obviamente, a tarefa de amostrar a iluminação do ambiente de todas as direções possíveis no hemisfério  Omega é computacionalmente impraticável - há um número infinito dessas direções. No entanto, é possível aplicar a aproximação tomando um número finito de direções escolhidas aleatoriamente ou localizadas uniformemente dentro do hemisfério.Isso nos permitirá obter uma boa aproximação da irradiação verdadeira, resolvendo essencialmente a integral de interesse para nós na forma de uma soma finita.

Mas para tarefas em tempo real, mesmo essa abordagem ainda é incrivelmente imposta, porque as amostras são coletadas para cada fragmento e o número de amostras deve ser alto o suficiente para um resultado aceitável. Assim, seria bom preparar antecipadamente os dados para esta etapa, fora do processo de renderização. Como a orientação do hemisfério determina a partir de qual região do espaço captamos a irradiação, é possível calcular antecipadamente a irradiação para cada orientação possível do hemisfério com base em todas as direções de saída possíveisde w o :

L o ( p , ω o ) = k d c¸ohmsLi(p,ωi)nωidcoi



Como resultado, para um determinado vetor arbitrário w i , podemos amostrar a partir do mapa de irradiância calculado para obter a irradiação difusa nessa direção. Para determinar a magnitude da radiação difusa indireta no ponto do fragmento atual, tomamos a irradiação total do hemisfério orientada ao longo da superfície normal para a superfície do fragmento. Em outras palavras, obter a irradiação de uma cena se resume a uma seleção simples:

  vec3 irradiance = texture(irradianceMap, N); 

Além disso, para criar um mapa de irradiação, é necessário envolver o mapa do ambiente, convertido em um mapa cúbico. Sabemos que para cada fragmento seu hemisfério é considerado orientado ao longo do normal para a superfícieN . Nesse caso, a convolução do mapa cúbico é reduzida para calcular a quantidade média de brilho da energia de todas as direções w i no interior do hemisférioΩ orientado ao longo do normalN :


Felizmente, o demorado trabalho preliminar que fizemos no início da lição agora facilita a conversão do mapa do ambiente em um mapa cúbico em um shader de fragmento especial, cuja saída será usada para formar um novo mapa cúbico. Para isso, é útil a própria parte do código usada para converter um mapa de ambiente retangular retangular em um mapa cúbico.

Resta apenas usar outro shader de processamento:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { //       vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] //   FragColor = vec4(irradiance, 1.0); } 

Aqui, o amostrador environmentMap é um mapa cúbico HDR do ambiente anteriormente derivado de um equilátero.

Existem várias maneiras de envolver o mapa do ambiente.Nesse caso, para cada texel do mapa cúbico, geraremos vários vetores de amostra do hemisférioW , orientam ao longo da direcção da amostra, e a média dos resultados. O número de vetores de amostra será fixo e os próprios vetores serão distribuídos igualmente no hemisfério. Observo que o integrando é uma função contínua e uma estimativa discreta dessa função será apenas uma aproximação. E quanto mais vetores de amostragem tomarmos, mais próximos estaremos da solução analítica da integral. O integrando da expressão para refletividade depende do ângulo sólido

d w - valores com os quais não é muito conveniente trabalhar. Em vez de integrar sobre um ângulo sólidod w mudamos a expressão, levando à integração por coordenadas esféricasθ e ϕ :


O ângulo Phi representará o azimute no plano da base do hemisfério, variando de 0 a 2 π . Ângulo θ representará o ângulo de elevação, variando de 0 a12 π . A expressão modificada para refletividade nesses termos é a seguinte:

L o ( p , ϕ o , θ o ) = k d cπ 2 π ϕ = 0 12 πθ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ



A solução dessa integral exigirá a coleta de um número finito de amostras no hemisfério Aver e a média dos resultados. Conhecendo o número de amostrasn 1 e n 2 para cada uma das coordenadas esféricas, podemos traduzir a integral nasoma Riemanniana:

L o ( p , ϕ o , θ o ) = k d cπ 1n 1 n 2 n 1 ϕ=0 n 2 θ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ


Como as duas coordenadas esféricas variam discretamente, a cada momento, a amostragem é realizada com uma certa área média no hemisfério, como pode ser visto na figura acima. Devido à natureza da superfície esférica, o tamanho da área de amostragem discreta diminui inevitavelmente com o aumento do ângulo de elevaçãoθ e aproximando-se do zênite. Para compensar esse efeito de redução da área, adicionamos um coeficiente de peso à expressãos i n θ .

Como resultado, a implementação de amostragem discreta no hemisfério com base em coordenadas esféricas para cada fragmento na forma de código é a seguinte:

 vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { //   .   (  -) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); //      vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples)); 

A variável sampleDelta determina o tamanho da etapa discreta ao longo da superfície do hemisfério. Alterando esse valor, você pode aumentar ou diminuir a precisão do resultado.

Dentro de ambos os ciclos, um vetor de amostra tridimensional regular é formado a partir de coordenadas esféricas, transferidas da tangente para o espaço mundial e, em seguida, usadas para amostrar um mapa cúbico do ambiente a partir do HDR. O resultado das amostras é acumulado na variável irradiância , que ao final do processamento será dividida pelo número de amostras feitas para obter um valor médio de irradiação. Observe que o resultado da amostragem da textura é modulado em duas quantidades: cos (teta) - para levar em conta a atenuação da luz em grandes ângulos e pecado (teta)- compensar a redução na área da amostra ao se aproximar do zênite.

Resta apenas lidar com o código que renderiza e captura os resultados da convolução do mapa de ambiente envCubemap . Primeiro, crie um mapa cúbico para armazenar a irradiação (você precisará fazer isso uma vez, antes de entrar no ciclo de renderização principal):

 unsigned int irradianceMap; glGenTextures(1, &irradianceMap); glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

Como o mapa de irradiação é obtido pela média de amostras distribuídas uniformemente do brilho da energia do mapa do ambiente, ele praticamente não contém peças e elementos de alta frequência - uma textura de resolução bastante pequena (32x32 aqui) e filtragem linear ativada serão suficientes para armazená-lo.

Em seguida, defina o buffer de quadros de captura para esta resolução:

 glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32); 

O código para capturar os resultados da convolução é semelhante ao código para transferir um mapa do ambiente de um equilateral para um cúbico, apenas um shader de convolução é usado:

 irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); //        glViewport(0, 0, 32, 32); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { irradianceShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Após concluir esta etapa, teremos em mãos um mapa de irradiação pré-calculado que pode ser usado diretamente para calcular a iluminação difusa indireta. Para verificar como foi a convolução, tentaremos substituir a textura do skybox do mapa do ambiente pelo mapa de irradiação:


Se, como resultado, você viu algo que parecia um mapa muito desfocado do ambiente, provavelmente a convolução foi bem-sucedida.

PBR e iluminação indireta


O mapa de irradiação resultante é usado na parte difusa da expressão dividida de refletividade e representa a contribuição acumulada de todas as direções possíveis da iluminação indireta. Como neste caso a luz não provém de fontes específicas, mas do ambiente como um todo, consideramos a iluminação indireta difusa e espelhada como fundo ( ambiente ), substituindo o valor constante usado anteriormente.

Para começar, não se esqueça de adicionar um novo amostrador com um mapa de irradiação:

 uniform samplerCube irradianceMap; 

Ter um mapa de irradiação que armazena todas as informações sobre radiação difusa indireta da cena e normal na superfície, obtendo dados sobre a irradiação de um fragmento específico é tão simples quanto coletar uma amostra da textura:

 // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb; 

No entanto, como a radiação indireta contém dados para os componentes difuso e espelho (como vimos na versão componente da expressão de refletividade), precisamos modular o componente difuso de uma maneira especial. Como na lição anterior, usamos a expressão de Fresnel para determinar o grau de reflexão da luz para uma dada superfície, de onde obtemos o grau de refração da luz ou o coeficiente difuso:

 vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao; 

À medida que a iluminação de fundo cai de todas as direções no hemisfério, com base no normal na superfície N , é impossível determinar a única mediana (ameio caminho) para calcular o coeficiente de Fresnel. Para simular o efeito Fresnel nessas condições, é necessário calcular o coeficiente com base no ângulo entre o vetor normal e o vetor de observação. No entanto, anteriormente, como parâmetro para o cálculo do coeficiente de Fresnel, utilizamos o vetor mediano obtido com base no modelo de micro-superfícies e dependendo da rugosidade da superfície. Como neste caso a rugosidade não é incluída nos parâmetros de cálculo, o grau de reflexão da luz pela superfície será sempre superestimado. A iluminação indireta como um todo deve se comportar da mesma forma que a iluminação direta, ou seja, de superfícies rugosas, esperamos um menor grau de reflexão nas bordas. Mas como a rugosidade não é levada em consideração,então o grau de reflexão especular de acordo com Fresnel para iluminação indireta parece irreal em superfícies não metálicas ásperas (na imagem abaixo, o efeito descrito é exagerado para maior clareza):


Você pode contornar esse incômodo introduzindo aspereza na expressão de Fremlin-Schlick, um processo descrito por Sébastien Lagarde :

 vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); } 

Dada a rugosidade da superfície ao calcular o conjunto Fresnel, o código para calcular o componente de segundo plano assume a seguinte forma:

 vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao; 

Como se viu, o uso da iluminação baseada em imagem se resume a uma amostra de um mapa cúbico. Todas as dificuldades estão principalmente associadas à preparação e transferência preliminares do mapa ambiental para o mapa de irradiação.

Ao capturar uma cena familiar de uma lição sobre fontes de luz analíticas contendo uma série de esferas com metalicidade e rugosidade variadas e adicionar iluminação de fundo difusa do ambiente, você obtém algo como isto:


Ainda parece estranho, uma vez que materiais com um alto grau de metalicidade ainda requerem reflexão para realmente parecer, hmm, metal (afinal, os metais não refletem a iluminação difusa). E, neste caso, as únicas reflexões obtidas de fontes de luz analíticas pontuais. E, no entanto, agora podemos dizer que as esferas parecem mais imersas no ambiente (especialmente perceptíveis ao trocar mapas de ambiente), pois as superfícies agora respondem corretamente à iluminação de fundo do ambiente da cena.

O código fonte completo da lição está aqui.. Na próxima lição, finalmente trataremos da segunda metade da expressão da refletividade, responsável pela iluminação especular indireta. Após esta etapa, você sentirá realmente o poder da abordagem PBR na iluminação.

Materiais 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!

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


All Articles